主頁 > 軟體設計 > 網路編程基礎(TCP)

網路編程基礎(TCP)

2021-09-29 07:46:28 軟體設計

目錄

  • 一、網路發展和分層
      • 1、網路的體系結構
      • 2、網路的拆包和分包
  • 二、網路預備基礎知識
      • 1、socket
      • 2、IP 地址
      • 3、埠號
      • 4、位元組序
  • 三、系統呼叫分析
      • 1、分析 server 部分函式
        • (1)socket 函式的作用:
        • (2)bind 函式的作用:
        • (3)listen 函式:
        • (4)accept 函式(重要)
        • (5)撰寫程式:
      • 2、分析 client 部分
        • (1)connect 函式
        • (2)撰寫程式
        • (3)程式優化
  • 四、TCP并發服務器
      • 1、多執行緒
      • 2、多行程
      • 3、多路復用實作

大綱分類:

image-20210822171007473

一、網路發展和分層

Internet 的歷史:

1、最早是 ARPAnet (阿帕網)(1974 年)

  • 它可以使 不同作業系統不同型別的計算機之間進行資料傳輸,

  • 但是沒有糾錯功能, (TCP 協議擁有糾錯能力)

2、TCP/IP 協議

  • TCP :專門用來檢測 網路傳輸中 差錯的傳輸控制協議,(沒有絲毫差錯)
  • IP :針對于 不同網路進行互聯 的互聯網協議 IP,

1、網路的體系結構

分層的思想:

1、每一層實作不同的功能,對上層的資料做透明傳輸,

2、每次層向上層提供服務,同時又使用下層的服務,

一些術語:

兩層交換機:OSI 的資料鏈路層的交換機,(偏硬體)

三層交換機:OSI 的網路層的交換機,(偏軟體)

image-20210925213311581

  • 思考:為什么路由器只到了 IP 層?(分層,主機層面的 端到端)

image-20210925220355752

在分析之前,我們一定要反問自己兩個問題:

1、這一層對上一層提供了哪些功能

2、這一層實作了哪些功,解決了什么問題


網路介面和物理層:

  • Linux驅動:每個硬體都對應有一個驅動,通過這個驅動完成對這個硬體的操作
  • 這一層有:wifi、GPRS、3G、4G
  • 主要作用:屏蔽硬體的差異不管是什么硬體,都會給網路層提供一個統一的介面
  • net_device 結構是二層中一個非常重要的結構,其結構中成員很多,包含了硬體資訊,介面資訊,其他輔助資訊,以及設備操作函式等等

網路層:(IP 層)

  • 主要功能:實作 端到端傳輸的功能,(將資料傳輸到某臺主機上面)
  • 使用下層哪些功能:對于網路層來說,不管底層是什么的網路設備,操控都一樣,

傳輸層:

  • 情景:Linux 是一個多任務運行的機器,一臺手機發資料到 Linux 主機上面,那么這段資料到底應該發送到 Linux 的哪一個行程上呢

  • 作用:決定了資料包應該交給哪一個行程進行處理

  • 使用了網路層的什么功能:有了網路層,才能將資料傳輸范圍確定在某臺主機上面,

分析每一層的典型協議:

網路介面與物理層:

MAC 地址:48位的一個數字,網路設備的身份標識,

物理層只認識 MAC 地址,并不認識什么 IP 地址,什么埠號,

  • ARP:地址決議協議,( IP 轉換為 MAC)
  • RARP:逆向地址決議協議,(MAC 轉化為 IP)
  • PPP協議:撥號協議(GPRS/3G/4G)

網路層:

  • IP協議:Internet protocol :Internet 協議:實作端到端主機層面)的傳輸,盡力傳輸,
  • ICMP:Internet control manage :控制管理協議:ping 命令使用該協議
  • IGMP:Internet group manage :分組管理協議:廣播、組播

傳輸層:

  • TCP:Transfer Control 傳輸控制:提供可靠連接,出錯重新傳輸,
  • UDP:user Datagram 用戶資料報:提供盡力傳輸,(行程層面)

應用層:

  • HTTP/HTTPS :網頁訪問協議,
  • FTP:檔案輸出協議
  • Telnet/SSH:遠程登陸傳輸,
  • RTP/RTSP :用于傳輸音視頻協議,安防行業,(基于 TCP+UDP 來實作的)

2、網路的拆包和分包

情景:理解通過 FTP 協議,進行檔案傳輸的時候,通過了哪些步驟?

image-20210925222442937

首先:如何從應用層進入到內核:

通過系統呼叫:即 socket 的相關介面

image-20210925222501409

上圖我們要掌握兩點

MTU:Max Transfer Unit ,最大的傳輸單元

  • 和網路的型別有關!!!,不同的網路型別不同,
  • 以太網:1500位元組, 802.3 :1492 位元組,

封包:

  • 上層添加:TCP頭、IP 頭,

  • 物理層加的幀頭:根據硬體的不同添加的不同:以太網頭、WiFi 頭等等,

  • 驅動的作用:將封裝好的資料包,送到對應的硬體上面

  • 硬體層:會自動在資料包的尾部,添加 CRC 校驗(32位、4個位元組)

拆包:

  • 硬體層:拿到資料包之后,將以太網幀頭拆掉,交給網路層的時候最前面就是 IP 頭了,

二、網路預備基礎知識

1、socket

image-20210925224123246

什么是 socket?

  • 是一種編程的介面,是一種系統呼叫,是一種特殊的檔案描述符,是一種 IO 操作
  • 是一種行程間通信,不同兩臺主機上的兩個行程通信,(定位到行程,所以在傳輸層

socket 的分類:

1、SOCK_STREAM:TCP/IP ,面向連接型,(傳輸層)

2、SOCK_DGRAM:UDP,無連接服務,(傳輸層)

3、SOCK_RAW:IP、ICMP,直接與網路層進行通信,跨過了傳輸層,(網路層)

  • ping 命令就是直接利用 socket,直接跳過了傳輸層,直接和網路層互動,

2、IP 地址

1、表現形式

  • 日常表示:點分形式,192.168.1.141(字串)
  • 網路網路傳輸:32位整數

2、IP 地址的分類

名稱地址
局域網IP192.XXX.XXX.XXX 10.XXX.XXX.XXX
廣播 IPxxx.xxx.xxx.255 255.255.255.255(全網廣播)
組播IP

3、埠號

(1)作用:區分一臺主機接受到資料包之后,應該交給哪一個任務來進行處理,(任務包含:行程、執行緒)

(2)是一個 16 位的數字:1-65535

(3)TCP 埠號與 UDP 埠號相互獨立

(4)埠的分類

  • 縱所周知埠:1-1023 (FTP 21、SSH 22、HTTP 80、HTTPS 469)
  • 保留埠:1024-5000 (不建議使用)
  • 可以使用埠,

可以看出 UDP 、TCP 的資料被分為了兩路

image-20210925232159169


4、位元組序

x86、arm 采用小端模式,(怎么記憶小端模式:權重小的放到低地址

powerpc / mips:arm作為路由器、使用大端模式,

網路傳輸的時候采用大端模式,

問題:資料經過多個路由器轉化資料,最終位元組序是什么,到底被處理成了什么是一個問題

引入:本地位元組序、網路位元組序

本地位元組序 ——> 網路位元組序,

網路位元組序 ——> 本地位元組序,

位元組序轉換函式:

// 主機位元組序轉化為網路位元組序
nl: network long
ns: network short
   
u_long htonl(u_long hostlog);	// 4 位元組轉換
u_long htons(u_long hostlog);	// 2 位元組轉化

// 網路位元組序到主機位元組序
u_long ntohl(u_long hostlog);
u_long ntohs(u_long hostlog);

IP 地址的轉換:(字串 ——> int 型別)

inet_aton:

inet_addr:

  • 內部已經包含了位元組序的轉換,所以 ip 地址就不需要我們再次進行轉化了,
  • 僅僅適用于 ipv4 的地址轉換,
  • 當出錯的時候回傳 -1 ,這樣會產生一個局限性:
    • 計算機當中存放 -1, 以補碼的方式來進行存放,255 即 -1,
    • 所以,此函式不能用于 255.255.255.255 的轉換,(因為分不清楚到底是出錯,還是成功)

inet_ntoa:

inet_pton:

  • 適用于 ipv4、ipv6 的轉換
  • 能正確處理 255.255.255.255 的轉化問題
  • 引數: af:地址協議族、src:點分形式的IP地址 ,dst:轉化的結果
  • 回傳值:1 成功,0 有問題,

三、系統呼叫分析

1、socket 原來都是主動的,但是 經過 bind 、listen 的教育,就變成了被動連接,(其實是 listen 一個函式教育的)

2、主動的 socket 可以主動建立連接,被動的 socket 只能等待被連接

3、accept 函式回傳 ns, (回傳一個新的檔案描述符,new fs),拿到新的 fd ,那么資料通信就具體到了 兩個行程

4、三次握手發生在:connect 、accept 兩個函式之間,(listen 當中的 backlog 與三次握手有關),

5、connect 系結的是 服務器的 ip 和 埠號,那么自己客戶端的 ip 和埠號什么時候發送過去呢?

  • 在內核層面,傳輸層和網路層會自動為我們進行添加,

image-20210420114138217


1、分析 server 部分函式

(1)socket 函式的作用:

1、確定是 IPV4、還是 IPV6 的 IP地址,

2、確定是 TCP、還是 UDP 的通訊

3、回傳一個檔案描述符

int socket(int domain, int type, int protocol);
// 引數分析:
1、domain
	AF_LOCAL   			Local communication              unix(7)
	AF_INET             IPv4 Internet protocols          ip(7)       
	AF_INET6            IPv6 Internet protocols          ipv6(7)
2、type
	SOCK_STREAM :流式套接字,唯一對應 TCP(傳輸層)
	SOCK_DGRAM : 資料報套接字,唯一對應 UDP(傳輸層)
	SOCK_RAW   : 原始套接字,對應直接訪問網路層 IP、ICMP,例如 ping命令
3protocol    
	(1)因為資料報套接字和流式套接字都唯一對應著 TCP 與 UDP,所以使用這兩種type 的時候,一般填入0.
    (2)原始套接字,按照需要進行填充,
    
// 回傳值分析
    成功的時候回傳檔案描述符,失敗的時候回傳 -1

(2)bind 函式的作用:

1、確定 ip 地址(32位整數,不是點分形式)

2、確定 埠號

然后將:IP地址和埠號,與這個 socket 回傳的檔案描述符進行系結, (我們操作這個檔案描述符,就是對應某個IP地址和埠

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 引數分析:
1、sockfd:sockfd 回傳的檔案描述符
2const struct sockaddr *addr :結構體變數的首地址
3socklen_t addrlen:結構體變數的長度

如何填充結構體?

1、先填充 struct sockaddr_in 這個結構體,然后將他強制轉換為: struct sockaddr 結構體,

  • sin_family :網路域
  • sin_port:網路位元組序埠號,(使用 htons 來進行轉換,沒有點分形式的轉換,)
  • sin_addr:網路位元組序的 ip 地址,(可以使用 ip 地址的專用轉換函式: 點分形式 ——> int、本機位元組序 ——> 網路位元組序

image-20210420100538386

           struct sockaddr_in {
               sa_family_t    sin_family; /* address family: AF_INET */
               in_port_t      sin_port;   /* port in network byte order */
               struct in_addr sin_addr;   /* internet address */
           };

           /* Internet address. */
           struct in_addr {
               uint32_t       s_addr;     /* address in network byte order */
           };

(3)listen 函式:

作用:將主動套接字,變為被動套接字

int listen(int sockfd, int backlog);
// 引數分析:
sockfd :socket 回傳的檔案描述符
backlog :指同時允許幾路客戶端正在和服務器進行連接,(也就是正在三次握手程序),一般填入 5,ARM 最大為 8 

// 回傳值
成功:回傳 0,失敗回傳 -1 

內核服務器當中的套接字 fd 會維護兩個鏈表

1、正在三次握手的客戶端鏈表,(數量 = 2*backlog +1 )

2、已經建立好連接的客戶端鏈表,(已經完成 3 次握手分配好了 newfd)


(4)accept 函式(重要)

作用:

1、阻塞等待客戶端請求(阻塞函式:一般情況都是在阻塞,等待客戶端連接, 只有當客戶端連接之后,才能不阻塞,)

2、得到一個新的 fd,new fd, (舊的 fd ,可以回傳去繼續等待連接

3、得到客戶端的 IP 地址、和埠號

4、

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 引數分析
sockfd:經過前面 socket() 創建,bind(),listen() 修改過的 fd,
// 回傳值:
struct sockaddr *addr:拿到客戶端的 ip 地址、埠號
socklen_t *addrlen: 
    
    // 可以看出,connect 這一端發送的是輸入型引數
	int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

回傳值:
成功的時候:回傳建立好連接的,新的 fd ,
失敗的時候:回傳 -1

(5)撰寫程式:

頭檔案

#ifndef __NET_H_
#define __NET_H_

	#include <sys/types.h>          /* See NOTES */
	#include <sys/socket.h>
	#include <arpa/inet.h>
	#include <string.h>
	#include <unistd.h>
	#include <fcntl.h> 
	#include <error.h>
	#include <stdio.h>
	
	#define Server_Port  5001
	#define Server_IP  "172.25.58.104"
	#define QUIT	".quit"


#endif

server 端程式

#include "net.h"

int main(int argc, const char * argv[])
{
	int fd = -1;
	struct sockaddr_in sin;
	
	// 第一步:創建 socket 
	if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
	{
		perror("socket");
	}
	
	// 第二步:bind sever 的ip 和 埠號
	memset(&sin,0,sizeof(sin));
	
	sin.sin_family = AF_INET;
	sin.sin_port = htons(Server_Port);
		// 位元組序、點分形式的轉換
	if(inet_pton(AF_INET,Server_IP,(void *)&sin.sin_addr.s_addr) <= 0)
	{
		printf("inet_pton error");
	}
	
	if( bind(fd,(const struct sockaddr *)&sin,sizeof(sin)) < 0)
	{
		perror("bind");
	}
	// 第三步:listen 將socket 設定為被動的socket
	if(listen(fd,5) < 0)
	{
		perror("listen");
	}
	
	// 第四步:accept 阻塞等待連接
	int newfd = -1;
	if((newfd = accept(fd,NULL,NULL)) < 0)
	{
		perror("accept");
	}
	// 第五步:連接上之后,處理新的 fd
	char buf[256] = {0};
	int ret = -1;
	while(1)
	{
		do
		{
			ret = read(newfd,buf,sizeof(buf));
		}while(ret < 0); // 如果沒有讀取到,并且沒有中斷產生就一直阻塞
		
		if(ret == 0) // 對方關閉了 socket
		{
			printf("資料接收完畢\n");
            break ; // 跳出 while 1 回圈
		}
		
		printf("receive : %s \n",buf);
	 	
		if( !strncasecmp(buf,QUIT,strlen(QUIT)))
		{
			break;
		}
	}
	
	close(newfd);
	close(fd);
}

注意:

1、ip 地址埠號必須轉化為網路位元組序的格式,


2、分析 client 部分

(1)connect 函式

  • 作用:連接服務器,也是阻塞函式,(當連接上服務器之后,就不阻塞,函式繼續執行)
  • 系結的是 server 的地址 + 埠號,
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

這個函式和我們的 bind 函式相當類似,

(2)撰寫程式

client 端程式

#include "net.h"

int main(int argc,const char *argv[])
{
	int fd = -1;
	struct sockaddr_in sin;
	
	// 第一步:創建 socket 
	if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
	{
		perror("socket");
	}
	
	// 第二步:connect sever 的ip 和 埠號
	memset(&sin,0,sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons(Server_Port);
		// 位元組序、點分形式的轉換
	if(inet_pton(AF_INET,Server_IP,(void *)&sin.sin_addr.s_addr) <= 0)
	{
		printf("inet_pton error");
	}
	
	if( connect(fd,(const struct sockaddr *)&sin,sizeof(sin)) < 0)
	{
		perror("connect");
	}	
	// 開始發送資料
	char buf[256] = {0};
	while(1)
	{
		scanf("%s",buf);
		write(fd,buf,strlen(buf));
		
		if( !strncasecmp(buf, QUIT,strlen(QUIT)) ) // 用戶輸入了quit ,strncasecmp 表示不區別大小寫 
         {
            printf("Client is exiting!\n");
            break; //跳出回圈寫
         }
	}
	close(fd);
}

自己寫的 垃圾 makefile

all:server client

server:server.o net.h
    gcc server.o net.h -o server

client:client.o net.h
    gcc client.o net.h -o client

server.o:server.c
    gcc -c server.c -o server.o
client.o:client.c
    gcc -c client.c -o client.o

clean:
    rm -rf client server client.o server.o


(3)程式優化

之前 sever 端的問題:

1、bind 系結的 ip 地址是固定的,程式每換一臺電腦,就需要更改一下 ip 地址,

解決辦法:使用 INADDR_ANY 宏,

首先我們分析一下 bind 函式的作用:

  • 通過系結 ip 地址,來系結是哪一個網卡發來的資料,
  • 通過系結 埠號,來系結是哪一個任務接收資料,

所以:INADDY_ANY 宏的作用:系結本機的所有網卡(不管是虛擬的,還是物理的),只要是在當前 Linux 系統,就與此 socket 進行系結,

sin.sin_addr.s_addr = htonl(INADDR_ANY);

2、服務器端不知道客戶端的ip地址,因為我們 accept 當中直接填的NULL

修改:

// 函式原型 
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

struct sockaddr_in clinet_in;
socklen_t addrlen = sizeof(clinet_in);
int nfd = -1;
// 在 clinet_in 當中,就可以獲取到 client 的ip地址,但是是int型別的
if(nfd = accept(fd, (struct sockaddr *)&clinet_in, &addrlen) < 0); // 接收客戶端的IP地址和埠號
{
        perror("accept");
}

// 將int型別的地址,轉化為 點分形式的
// 函式源型 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

char ipv4_addr[16];  // 定義一個陣列接收 ip 地址的字串
inet_ntop(AF_INET, (const void *)&clinet_in.sin_addr.s_addr, ipv4_addr, sizeof(clinet_in));

printf("Clinet( %s:%d ) is connected!",ipv4_addr, ntohs(clinet_in.sin_port));

3、bind:Address already in use
問題分析:地址不能快速重用,必須等 2 分鐘才能繼續
解決問題:在 bind 之前加一段代碼:

int b_reuse = 1;
setsockopt(fd, SOL,SOCKET, SO_REUSEADDR, &b_reuse,sizof(int));


4、修改以后的 服務端

#include "dxdnet.h"

int main(int argc, const char * argv[])
{
	int fd = -1;
	struct sockaddr_in sin;
	
	// 第一步:創建 socket 
	if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
	{
		perror("socket");
	}
	// 優化3:允許地址快速重用
	int b_reuse = 1;
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse,sizeof(int));
	
	// 第二步:bind sever 的ip 和 埠號
	memset(&sin,0,sizeof(sin));
	
	sin.sin_family = AF_INET;
	sin.sin_port = htons(Server_Port);
	sin.sin_addr.s_addr = htonl(INADDR_ANY);
	
	if( bind(fd,(const struct sockaddr *)&sin,sizeof(sin)) < 0)
	{
		perror("bind");
	}
	
	// 第三步:listen 將socket 設定為被動的socket
	if(listen(fd,5) < 0)
	{
		perror("listen");
	}
	
	// 第四步:accept 阻塞等待連接
	int newfd = -1;
	struct sockaddr_in clinet_in;
	socklen_t addrlen = sizeof(struct sockaddr_in);
	
	newfd = accept(fd, (struct sockaddr *)&clinet_in, &addrlen); // 接收客戶端的IP地址和埠號  
	if(newfd < 0)   
	{
		printf("%d \n",newfd);
        perror("accept");
		return -1;
	}

	char ipv4_addr[16];  // 定義一個陣列接收 ip 地址的字串
	inet_ntop(AF_INET, (const void *)&clinet_in.sin_addr.s_addr, ipv4_addr, sizeof(clinet_in));
	printf("Clinet( %s:%d ) is connected! \n",ipv4_addr, ntohs(clinet_in.sin_port));
	
	// 第五步:連接上之后,處理新的 fd
	char buf[256] = {0};  
	int ret = -1;
	while(1)
	{
		do
		{
			ret = read(newfd,buf,sizeof(buf));
		}while(ret < 0); // 如果沒有讀取到,并且沒有中斷產生就一直阻塞
		
		if(ret == 0) // 對方關閉了 socket
		{
			printf("資料接收完畢\n");
            break ; // 跳出 while 1 回圈
		}
		
		printf("receive : %s \n",buf);
	 	
		if( !strncasecmp(buf,QUIT,strlen(QUIT)))
		{
			break;
		}
	}
	
	close(newfd);
	close(fd);
}

四、TCP并發服務器

之前 sever 端的問題:

1、accept 會進行阻塞、read 會進行阻塞,所以有多個客戶端連接,那么就沒有辦法進行解決,

解決辦法:多路復用、多執行緒

在這里插入圖片描述

1、多執行緒

(0)編譯檔案:
config.mk 檔案

CFLAGS = -c -g -Wall -I include
CC := gcc 
LDFLAGS = -lpthread 

makefile檔案

include config.mk

.PHONY:all clean

all:server client

server:server.o
    gcc server.o -o server -lpthread
client:client.o


server.o:server.c

client.o:client.c

clean:
    rm -rf *.o server client


(1)頭檔案

#ifndef __DXDNET_H_
#define __DXDNET_H_

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h> 
#include <error.h>
#include <stdio.h>
#include <pthread.h>

#define Server_Port  5001
#define Server_IP  "172.25.58.104"
#define QUIT	".quit"


typedef struct Client_Info
{
	int c_fd;
	char c_ipv4_addr[16];
	int c_port;
}C_Info;



#endif

(2)server 端

  • 主執行緒:死回圈、阻塞 accept ,來一個客戶端就開一個執行緒
  • 子執行緒:處理客戶端的程式,

缺點:

  • 沒有執行緒回識訓制,創建行程之后,資源無法回收,
  • 服務器端結束,客戶端還不知道,
  • 客戶端斷開連接,服務端也還不知道
#include "dxdnet.h"

void *clinet_hander(void *argc);

int main(int argc, const char * argv[])
{
	int fd = -1;
	int ret = -1;
	struct sockaddr_in sin;
	
	// 第一步:創建 socket 
	if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
	{
		perror("socket");
	}
	
	// 優化3:允許地址快速重用
	int b_reuse = 1;
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse,sizeof(int));
	
	// 第二步:bind sever 的ip 和 埠號
	memset(&sin,0,sizeof(sin));
	
	sin.sin_family = AF_INET;
	sin.sin_port = htons(Server_Port);
	sin.sin_addr.s_addr = htonl(INADDR_ANY);
	
	if( bind(fd,(const struct sockaddr *)&sin,sizeof(sin)) < 0)
	{
		perror("bind");
	}
	
	// 第三步:listen 將socket 設定為被動的socket
	if(listen(fd,5) < 0)
	{
		perror("listen");
	}
	
	// 第四步:accept 阻塞等待連接,并接收 客戶端的ip地址和埠號
	int newfd = -1;
	struct sockaddr_in clinet_in;
	socklen_t addrlen = sizeof(struct sockaddr_in);
	char ipv4_addr[16];	// 定義一個陣列接收 ip 地址的字串
	pthread_t pth = -1;
	
	C_Info c1; // 客戶端結構體,傳給子執行緒
	
	while(1)
	{
		newfd = accept(fd, (struct sockaddr *)&clinet_in, &addrlen); // 接收客戶端的IP地址和埠號  
		if(newfd < 0)   
		{
			printf("%d \n",newfd);
			perror("accept");
			return -1;
		}
		  
		inet_ntop(AF_INET, (const void *)&clinet_in.sin_addr.s_addr, ipv4_addr, sizeof(clinet_in));
		printf("Clinet( %s:%d ) is connected! \n",ipv4_addr, ntohs(clinet_in.sin_port));
		
		// 給子執行緒傳遞的變數賦值
		c1.c_fd = newfd;
		strcpy(c1.c_ipv4_addr,ipv4_addr);
		c1.c_port = ntohs(clinet_in.sin_port);
		
		
		// 增加一個執行緒來處理客戶端的資訊:
		ret = pthread_create(&pth,NULL,clinet_hander,&c1);
		if(0 != ret)
		{
			perror("pthread_create");
		}
	}
	close(newfd);
	close(fd);
}

void *clinet_hander(void *argc)
{
	// 第五步:連接上之后,處理新的 fd
	char buf[256] = {0};  
	int ret = -1;
	C_Info c1ient = *(C_Info *)argc;
	while(1)
	{
		do
		{
			ret = read(c1ient.c_fd,buf,sizeof(buf));
		}while(ret < 0); // 如果沒有讀取到,并且沒有中斷產生就一直阻塞
		
		if(ret == 0) // 對方關閉了 socket
		{
			printf("資料接收完畢\n");
            break ; // 跳出 while 1 回圈
		}
		
		printf("receive[ %s : %d ] : %s \n", c1ient.c_ipv4_addr, c1ient.c_port ,buf);
	 	
		if( !strncasecmp(buf,QUIT,strlen(QUIT)))
		{
			break;
		}
	}
	
	close(c1ient.c_fd);
	return NULL;
}



(3)client 端:

#include "dxdnet.h"

void *clinet_hander(void *argc);

int main(int argc, const char * argv[])
{
	int fd = -1;
	int ret = -1;
	struct sockaddr_in sin;
	
	// 第一步:創建 socket 
	if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
	{
		perror("socket");
	}
	
	// 第二步:bind sever 的ip 和 埠號
	memset(&sin,0,sizeof(sin));
	
	sin.sin_family = AF_INET;
	sin.sin_port = htons(Server_Port);
	sin.sin_addr.s_addr = htonl(INADDR_ANY);
	
	if( bind(fd,(const struct sockaddr *)&sin,sizeof(sin)) < 0)
	{
		perror("bind");
	}
	
	// 第三步:listen 將socket 設定為被動的socket
	if(listen(fd,5) < 0)
	{
		perror("listen");
	}
	
	// 第四步:accept 阻塞等待連接,并接收 客戶端的ip地址和埠號
	int newfd = -1;
	struct sockaddr_in clinet_in;
	socklen_t addrlen = sizeof(struct sockaddr_in);
	char ipv4_addr[16];	// 定義一個陣列接收 ip 地址的字串
	pthread_t pth = -1;
	
	C_Info c1; // 客戶端結構體,傳給子執行緒
	
	while(1)
	{
		newfd = accept(fd, (struct sockaddr *)&clinet_in, &addrlen); // 接收客戶端的IP地址和埠號  
		if(newfd < 0)   
		{
			printf("%d \n",newfd);
			perror("accept");
			return -1;
		}
		  
		inet_ntop(AF_INET, (const void *)&clinet_in.sin_addr.s_addr, ipv4_addr, sizeof(clinet_in));
		printf("Clinet( %s:%d ) is connected! \n",ipv4_addr, ntohs(clinet_in.sin_port));
		
		// 給子執行緒傳遞的變數賦值
		c1.c_fd = newfd;
		strcpy(c1.c_ipv4_addr,ipv4_addr);
		c1.c_port = ntohs(clinet_in.sin_port);
		
		
		// 增加一個執行緒來處理客戶端的資訊:
		ret = pthread_create(&pth,NULL,clinet_hander,&c1);
		if(0 != ret)
		{
			perror("pthread_create");
		}
	}
	close(newfd);
	close(fd);
}

void *clinet_hander(void *argc)
{
	// 第五步:連接上之后,處理新的 fd
	char buf[256] = {0};  
	int ret = -1;
	C_Info c1ient = *(C_Info *)argc;
	while(1)
	{
		do
		{
			ret = read(c1ient.c_fd,buf,sizeof(buf));
		}while(ret < 0); // 如果沒有讀取到,并且沒有中斷產生就一直阻塞
		
		if(ret == 0) // 對方關閉了 socket
		{
			printf("資料接收完畢\n");
            break ; // 跳出 while 1 回圈
		}
		
		printf("receive[ %s : %d ] : %s \n", c1ient.c_ipv4_addr, c1ient.c_port ,buf);
	 	
		if( !strncasecmp(buf,QUIT,strlen(QUIT)))
		{
			break;
		}
	}
	
	close(c1ient.c_fd);
	return NULL;
}

2、多行程

問題:

  • 行程間通信怎么解決?
  • 子行程退出之后,變成僵尸行程怎么辦?
  • 使用什么函式,對子行程進行回收呢?

分析:
1、fork() 的時候,子行程會繼承 父行程的一些東西, (fork 之前定義的變數,父行程和子行程都有)

2、子行程退出的時候,會給父行程發送信號,( SIGCHLD 信號)

typedef void (*sighandler_t)(int); // 規定了處理函式的源型
sighandler_t signal(int signum, sighandler_t handler);

signal(SIGCHLD,sig_clild_handle); // 系結信號的對應處理函式

3、使用 waitpid 函式來進行回收,可以放在系結的信號處理程式當中進行,

void sig_clild_handle(int signo)
{
	if(SIGCHLD == signo)
	{
		waitpid(-1,NULL, WNOHANG); // WNOHANG :函式不阻塞
	}
}

修改完畢:
注意:主行程執行完 else,就會繼續向下執行,重新被 accept 阻塞,

#include "dxdnet.h"

void *clinet_hander(void *argc);

void sig_child_handle(int signo)
{
	if(SIGCHLD == signo)
	{
		waitpid(SIGCHLD,NULL,WNOHANG);
	}
}

int main(int argc, const char * argv[])
{
	int fd = -1;
	int ret = -1;
	struct sockaddr_in sin;
	
	signal(SIGCHLD, sig_child_handle);  // 系結信號處理函式
	
	// 第一步:創建 socket 
	if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
	{
		perror("socket");
	}
	
	// 優化3:允許地址快速重用
	int b_reuse = 1;
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse,sizeof(int));
	
	// 第二步:bind sever 的ip 和 埠號
	memset(&sin,0,sizeof(sin));
	
	sin.sin_family = AF_INET;
	sin.sin_port = htons(Server_Port);
	sin.sin_addr.s_addr = htonl(INADDR_ANY);
	
	if( bind(fd,(const struct sockaddr *)&sin,sizeof(sin)) < 0)
	{
		perror("bind");
	}
	
	// 第三步:listen 將socket 設定為被動的socket
	if(listen(fd,5) < 0)
	{
		perror("listen");
	}
	
	// 第四步:accept 阻塞等待連接,并接收 客戶端的ip地址和埠號
	int newfd = -1;
	struct sockaddr_in clinet_in;
	socklen_t addrlen = sizeof(struct sockaddr_in);
	char ipv4_addr[16];	// 定義一個陣列接收 ip 地址的字串
	
	pid_t pid; // 執行緒 id
	
	C_Info c1; // 客戶端結構體,傳給子執行緒
	
	while(1)
	{
		newfd = accept(fd, (struct sockaddr *)&clinet_in, &addrlen); // 接收客戶端的IP地址和埠號  
		if(newfd < 0)   
		{
			printf("%d \n",newfd);
			perror("accept");
			return -1;
		}
		  
		// 開啟一個子行程
		pid = fork();
		if(pid < 0)
		{
			perror("fork");
		}
		if(pid == 0)
		{
			close(fd); // 關閉舊的檔案描述符
			
			inet_ntop(AF_INET, (const void *)&clinet_in.sin_addr.s_addr, ipv4_addr, sizeof(clinet_in));
			printf("Clinet( %s:%d ) is connected! \n",ipv4_addr, ntohs(clinet_in.sin_port));
			
			// 給子行程傳遞的變數賦值
			c1.c_fd = newfd;
			strcpy(c1.c_ipv4_addr,ipv4_addr);
			c1.c_port = ntohs(clinet_in.sin_port);
			
			clinet_hander(&c1);
			
			return 0;  // 子執行緒退出,并且給主執行緒發送信號
		}
		else
		{
			close(newfd); // 關閉新檔案描述符
			// 這里什么也不用管了,如果出現信號,會到相應的中斷處理函式當中執行,
			// sleep(10);
		}

	}
	close(newfd);
	close(fd);
}

void *clinet_hander(void *argc)
{
	// 第五步:連接上之后,處理新的 fd
	char buf[256] = {0};  
	int ret = -1;
	C_Info c1ient = *(C_Info *)argc;
	while(1)
	{
		do
		{
			ret = read(c1ient.c_fd,buf,sizeof(buf));
		}while(ret < 0); // 如果沒有讀取到,并且沒有中斷產生就一直阻塞
		
		if(ret == 0) // 對方關閉了 socket
		{
			printf("資料接收完畢\n");
            break ; // 跳出 while 1 回圈
		}
		
		printf("receive[ %s : %d ] : %s \n", c1ient.c_ipv4_addr, c1ient.c_port ,buf);
	 	
		if( !strncasecmp(buf,QUIT,strlen(QUIT)))
		{
			break;
		}
	}
	
	close(c1ient.c_fd);
	return NULL;
}


3、多路復用實作

貼一個github 上找的原始碼吧,懶得寫了,
https://github.com/yuanrw/tcp-server-client,

服務端注意的點:
當服務端 socket 被 listen 之后,有客戶端進行 connect 的話,服務端的 old fd 會回應,因此 select 會停止阻塞,從而去 accept 產生新的 newfd,再次被添加到 fd_set 當中,重新進行 select ,

		if(FD_ISSET(serverfd, &client_fdset))
		{
			struct sockaddr_in client_addr;  //新的客戶端連接
			size_t size = sizeof(struct sockaddr_in);
			int sock_client = accept(serverfd, (struct sockaddr*)(&client_addr), (unsigned int*)(&size));
			if(sock_client<0)
			{
				perror("accept error!\n");
				continue;
			}
			if(conn_amount<5)
			{
				client_sockfd[conn_amount++] = sock_client;
				bzero(buffer, 1024);
				strcpy(buffer, "this is a server! welcome!\n");
				send(sock_client, buffer, 1024, 0);    //把內容傳給新來的客戶端
				printf("new connection client[%d] %s:%d\n", conn_amount, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
				bzero(buffer, sizeof(buffer));
				ret = recv(sock_client, buffer, 1024, 0);
				if(ret<0)
				{
					perror("recv error!\n");
					close(serverfd);
					return -1;
				}
				printf("recv : %s\n", buffer);
				if(maxsock<sock_client)
					maxsock = sock_client;
				else
				{
					printf("max connections!!!quit!!\n");
					break;
				}
			}
		}

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/303849.html

標籤:其他

上一篇:Linux——最全面試題整理(僅此一篇)

下一篇:【歷史上的今天】9 月 28 日:“超級計算機之父”誕生;三星推出移動支付;LibreOffice 發布

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more