网络编程(TCP编程)

news2025/7/27 21:40:05

思维导图

1.基础流程

流程图中是TCP连接的基础步骤,其他操作都是在此基础上进行添加修改。

2.函数接口

2.1 创建套接字(socket)

int socket(int domain, int type, int protocol);

头文件:#include <sys/types.h>        
              #include <sys/socket.h>

功能:创建套接字

参数:

        domain:协议族

                AF_UNIX, AF_LOCAL 本地通信

                AF_INET ipv4

                AF_INET6 ipv6

        type:套接字类型

                SOCK_STREAM:流式套接字

                SOCK_DGRAM:数据报套接字

                SOCK_RAW:原始套接字

        protocol:协议

                0:自动匹配底层 ,根据type,系统默认自动帮助匹配对应协议

                传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP

                网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)

返回值:

        成功 文件描述符

        失败 -1,更新errno

2.2 绑定套接字(bind)

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

头文件:#include <sys/types.h>        
              #include <sys/socket.h>

功能:绑定

参数:

        socket:套接字

        addr:用于通信结构体 (提供的是通用结构体,需要根据选择通信方式,填充对应结构体-通信当时socket第一个参数确定)

        addrlen:结构体大小

返回值:成功 0

               失败-1,更新errno

通用结构体:

struct sockaddr {

        sa_family_t sa_family;

        char sa_data[14];

}

ipv4通信结构体:

struct sockaddr_in {

        sa_family_t sin_family;

        in_port_t sin_port;

        struct in_addr sin_addr;

};

struct in_addr {

        uint32_t s_addr;

};

2.3 启动监听(listen)

int listen(int sockfd, int backlog);

头文件:#include <sys/types.h>        
              #include <sys/socket.h>

功能:监听,将主动套接字变为被动套接字

参数:

        sockfd:套接字

        backlog:同时响应客户端请求链接的最大个数,不能写0.

                        不同平台可同时链接的数不同,一般写6-8个

                        (队列1:保存正在连接)

                        (队列2,连接上的客户端)

返回值:成功 0

              失败-1,更新errno

2.4 接受连接请求(accept)

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

头文件:#include <sys/types.h>        
              #include <sys/socket.h>

使用:accept(sockfd,NULL,NULL);

功能:1.阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接,

           2.则accept()函数返回,返回一个用于通信的套接字文件描述符;

参数:

Sockfd :套接字

addr: 链接客户端的ip和端口号

如果不需要关心具体是哪一个客户端,那么可以填NULL;

addrlen:结构体的大小

如果不需要关心具体是哪一个客户端,那么可以填NULL;

返回值:

成功:文件描述符; //用于通信

失败:-1,更新errno

2.5 读取数据(recv)

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

头文件:#include <sys/types.h>        
              #include <sys/socket.h>

功能: 接收数据

参数:

        sockfd:acceptfd ;

        buf:存放位置

        len:大小

        flags:一般填0,相当于read()函数

                   MSG_DONTWAIT 非阻塞

返回值:

        < 0:失败出错 更新errno

        =0:表示客户端退出

        >0:成功接收的字节个数

2.6 请求链接(connect)

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

头文件:#include <sys/types.h>        
              #include <sys/socket.h>

功能:用于连接服务器;

参数:

        sockfd:socket函数的返回值

        addr:填充的结构体是服务器端的;

        addrlen:结构体的大小

返回值:

        失败:-1,更新errno

        正确:0

2.7 发送数据(send)

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

头文件:#include <sys/types.h>        
              #include <sys/socket.h>

功能:发送数据

参数:

        sockfd:socket函数的返回值

        buf:发送内容存放的地址

        len:发送内存的长度

        flags:如果填0,相当于write();

2.8 其他函数接口(htonl(),htons(),inet_addr(),inet_ntoa())

主机字节序转换为网络字节序 (小端序->大端序)

头文件:#include <arpa/inet.h>

u_long htonl (u_long hostlong);   //host to network long

u_short htons (u_short short);     //掌握这个(用的多)

网络字节序转换为主机字节序(大端序->小端序)

头文件:#include <arpa/inet.h>

u_long ntohl (u_long hostlong);

u_short ntohs (u_short short);

主机字节序转换为网络字节序 (小端序->大端序)

in_addr_t inet_addr(const char *strptr); //该参数是字符串 typedef uint32_t in_addr_t;

头文件:#include <sys/socket.h>
              #include <netinet/in.h>
              #include <arpa/inet.h>

功能: 主机字节序转为网络字节序

参数: const char *strptr: 字符串

返回值: 返回一个无符号长整型数(无符号32位整数用十六进制表示), 否则NULL

             struct in_addr {

                     in_addr_t s_addr;

             };

网络字节序转换为主机字节序(大端序->小端序)

char *inet_ntoa(stuct in_addr inaddr);

头文件:#include <sys/socket.h>
              #include <netinet/in.h>
              #include <arpa/inet.h>

功能: 将网络字节序二进制地址转换成主机字节序。

参数: stuct in_addr in addr : 只需传入一个结构体变量

返回值: 返回一个字符指针, 否则NULL;

练习

1.初级版:

初级版本只包含流程图中的功能

服务器端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>

int main(int argc, const char *argv[])
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);		//创建套接字
	if(sockfd < 0){
		perror("socket err");
		return -1;
	}
	
	struct sockaddr_in server_addr;					//服务器配置
	server_addr.sin_family = AF_INET;				//地址族选择ipv4
	server_addr.sin_port = htons(8880); 			//配置端口号
    server_addr.sin_addr.s_addr = inet_addr("192.168.0.108");       //配置服务器地址
	
	//绑定套接字
	if(bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0){
		perror("bind err");
		close(sockfd);
		return -1;
	}
	
	//监听
	if(listen(sockfd,6) < 0){
		perror("listen err");
		close(sockfd);
		return -1;
	}
		
	//阻塞函数,等待客户端的连接请求,连接成功生成一个用于通信的文件描述符
	int acceptfd = accept(sockfd,NULL,NULL);		//返回文件描述符,用于后续通信
	if(acceptfd < 0){
		perror("accept err");
		close(sockfd);
		return -1;
	}
	
	char buf[32] = "";
	while(1){
		//接收数据
		ssize_t rd = recv(acceptfd,buf,sizeof(buf),0);
		if(rd < 0){	    		//接收失败
			perror("recv err");
			close(sockfd);
			return -1;
		}
		else if(rd == 0){		//当接收函数返回值是零的时候,连接的客户端退出连接
			printf("TCP2 exit\n");
			return 0;
		}
		else        			//返回值大于零的时候是正确接收到数据
			printf("%s\n",buf);
		memset(buf,0,sizeof(buf));		//清空buf数组
	}
	close(acceptfd);			//关闭用于通信的文件描述符
	close(sockfd);				//关闭socket文件描述符号
	return 0;
}
客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>

int main(int argc, const char *argv[])
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);		//创建套接字
	if(sockfd < 0){
		perror("socket err");
		return -1;
	}
	struct sockaddr_in server_addr;					//服务器配置
	server_addr.sin_family = AF_INET;				//地址族选择ipv4
	server_addr.sin_port = htons(8880);				//配置连接目标的服务器的端口号
	server_addr.sin_addr.s_addr = inet_addr("192.168.0.108");	//配置服务器地址
	
	//连接服务器
	if(connect(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0){
		perror("connect err");
		close(sockfd);
		return -1;
	}
	char buf[32] = "";
	
	//循环输入数据
	while(1){
		fgets(buf,sizeof(buf),stdin);				//从终端输入流获取字符串更安全,超出字符串长度不会段错误
		if(buf[strlen(buf)-1] = '\n')				//去除字符串尾部添加的\n
			buf[strlen(buf)-1] = '\0';
		if(send(sockfd,buf,strlen(buf),0) < 0){
			perror("send err");
			close(sockfd);
			return -1;
		}
		memset(buf,0,sizeof(buf));
	}
	close(sockfd);
	return 0;
}
效果:

服务器循环接收并输出:

客户端循环输入:

2.进阶版

1.优化服务器代码,客户端链接成功后,可以循环多次通信,优化客户端代码,当客户端输入quit时,客户端退出。

2.优化服务器代码客户端输入quit退出后,服务器不退出,等待下一个客户端连接

3.优化客户端代码,地址和端口都通过文件描述符参数传入

4.优化服务器端代码,自动获取本机地址,端口号用文件描述符参数传入

5.增加来电显示功能(显示连接服务器的客户端的IP与端口号(通过accept函数的参数))

服务器端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>

int main(int argc, const char *argv[])
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);		//创建套接字
	if(sockfd < 0){
		perror("socket err");
		return -1;
	}
	
	struct sockaddr_in server_addr;					//服务器配置
	server_addr.sin_family = AF_INET;				//地址族选择ipv4
	//server_addr.sin_port = htons(8880);			//配置端口号
	server_addr.sin_port = htons(atoi(argv[1]));	//通过终端命令行参数获取端口号(终端输入的都是字符类型,要把其转换为int类型)
    
	//server_addr.sin_addr.s_addr = inet_addr(argv[1]);			//配置连接目标的服务器的地址
    //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0");       //0.0.0.0地址代表会自动匹配主机的IP地址,换网后不需要再更改代码
    server_addr.sin_addr.s_addr = INADDR_ANY;                   //C语言内自带的宏定义,代表0.0.0.0地址
	
	//绑定套接字
	if(bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0){
		perror("bind err");
		close(sockfd);
		return -1;
	}
	
	//监听
	if(listen(sockfd,6) < 0){
		perror("listen err");
		close(sockfd);
		return -1;
	}

	//循环实现:当客户端退出连接,会继续等待连接新的客户端来连接服务器通信
		while(1){
		struct sockaddr_in clientaddr;
		int a = sizeof(clientaddr);
		
		//阻塞函数,等待客户端的连接请求,连接成功生成一个用于通信的文件描述符
		int acceptfd = accept(sockfd,(struct sockaddr *)&clientaddr,&a);		//返回文件描述符,用于后续通信
		if(acceptfd < 0){
			perror("accept err");
			close(sockfd);
			return -1;
		}
		printf("接入设备的ip地址:%s\n该设备的端口号:%d\n",inet_ntoa(clientaddr.sin_addr),clientaddr.sin_port);
		
		char buf[32] = "";
		while(1){
			//接收数据
			ssize_t rd = recv(acceptfd,buf,sizeof(buf),0);
			if(strcmp(buf,"quit") == 0)			//当客户端输入quit,断开连接连接下一个客户端
				break;
			if(strcmp(buf,"quit all") == 0){	//当客户端输入quit all,服务器退出
				close(sockfd);
				close(acceptfd);
				return 0;
			}
			else if(rd < 0){			//接收失败
				perror("recv err");
				close(sockfd);
				return -1;
			}
			else{        	        	//当接收函数返回值是零的时候,连接的客户端退出连接
				printf("TCP2 exit\n");
				return 0;
			}
			if(rd > 0)			//返回值大于零的时候是正确接收到数据
				printf("%s\n",buf);
			memset(buf,0,sizeof(buf));
		}
		close(acceptfd);		//关闭用于通信的文件描述符
	}
	close(sockfd);				//关闭socket文件描述符号
	return 0;
}
 客户端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>

int main(int argc, const char *argv[])
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);		//创建套接字
	if(sockfd < 0){
		perror("socket err");
		return -1;
	}

	struct sockaddr_in server_addr;					//服务器配置
	server_addr.sin_family = AF_INET;				//地址族选择ipv4
	server_addr.sin_port = htons(atoi(argv[2]));				//配置连接目标的服务器的端口号
	//server_addr.sin_addr.s_addr = inet_addr("192.168.0.108");			//配置服务器地址
	server_addr.sin_addr.s_addr = inet_addr(argv[1]);	//通过终端命令行参数获取ip地址作为服务器地址

	//连接服务器
	if(connect(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0){
		perror("connect err");
		close(sockfd);
		return -1;
	}
	char buf[32] = "";
	
	//循环输入数据
	while(1){
		fgets(buf,sizeof(buf),stdin);				//从终端输入流获取字符串更安全,超出字符串长度不会段错误
		if(buf[strlen(buf)-1] = '\n')				//去除字符串尾部添加的\n
			buf[strlen(buf)-1] = '\0';
		if(send(sockfd,buf,strlen(buf),0) < 0){
			perror("send err");
			close(sockfd);
			return -1;
		}
		if( ( strcmp(buf,"quit") == 0 ) || ( strcmp(buf,"quit all") == 0 ) )	//当输入quit,退出客户端;输入quit all,退出客户端和服务端
			break;
		memset(buf,0,sizeof(buf));
	}
	close(sockfd);
	return 0;
}
效果: 

建立连接

输入quit,服务器不退出,客户端断开连接

当客户端输入quit all,客户端与服务器都退出 

3.TCP粘包,拆包

tcp粘包

  • 定义:多个应用层数据包被组合成一个 TCP 段发送

  • 现象:接收方一次读取操作获取了多个应用层消息

tcp拆包

  • 定义:单个应用层数据包被拆分成多个 TCP 段发送

  • 现象:接收方需要多次读取才能获取完整的应用层消息

TCP粘包、拆包发生原因:

发生TCP粘包或拆包有很多原因,常见的几点:

1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包

2、待发送数据大于MSS(传输层的最大报文长度),将进行拆包.

3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包

4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包

粘包解决办法:

解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下:

1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

2、发送端将每个数据包封装为固定长度,这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

4、延时发送

4.数据的封装与传递过程

5.以太网帧完整帧

对于网络层最大数据帧长度是1500字节

对于链路层最大数据长度是1518字节(1500+14+CRC)

发送时候,IP层协议栈程序检测到发送数据和包头总长度超过1500字节时候,会进行自动分包处理,接收端在IP层进行包重组,然后才继续往上传递

6.三次握手四次挥手

6.1.三次握手

第一次握手都由客户端发起

类比打电话的过程:

第一次握手:喂,能听见我说话吧?

第二次握手:能听见你说话,你能听见我说话不?

第三次握手:能听见

开始通话

  1. 第一次握手(客户端 → 服务器)
    • 客户端发送SYN报文段(SYN=1,seq=x),其中x为客户端选择的初始序列号(ISN)。
    • 目的:客户端告知服务器自己的初始序列号,请求建立连接,
    • 此时客户端进入SYN_SEND状态。
  2. 第二次握手(服务器 → 客户端)
    • 服务器收到SYN后,发送SYN+ACK报文段(SYN=1,ACK=1,seq=y,ack=x+1)。
    • seq=y:服务器选择的初始序列号(ISN)。
    • ack=x+1:确认客户端的SYN(表示已收到序列号x,期望下一个字节为x+1)。
    • 此时服务器进入SYN_RECV状态。
  3. 第三次握手(客户端 → 服务器)
    • 客户端收到SYN+ACK后,发送ACK报文段(ACK=1,seq=x+1,ack=y+1)。
    • ack=y+1:确认服务器的SYN(表示已收到序列号y,期望下一个字节为y+1)。
    • 连接建立:双方通过seq同步(都发送完确认后),进入ESTABLISHED状态。

6.2.四次挥手 

类比挂电话的过程:

第一次挥手:我说完了,我要挂了

第二次挥手:好的,我知道了,但是你先别急,等我把话说完

第三次挥手:好了,我说完了,咱们可以挂电话了

第四次挥手:好的,挂了吧

  1. 第一次挥手(客户端 → 服务器)
    • 客户端发送FIN报文段(FIN=1,seq=u),表示不再发送数据。
    • 进入状态:TIME_WAIT
  2. 第二次挥手(服务器 → 客户端)
    • 服务器收到FIN后,发送ACK报文段(ACK=1,seq=v,ack=u+1)。
    • ack=u+1:确认客户端的关闭请求(表示已收到序列号u,期望下一个字节为u+1)。
  3. 第三次挥手(服务器 → 客户端)
    • 服务器发送FIN报文段(FIN=1,seq=w),表示不再发送数据。
  4. 第四次挥手(客户端 → 服务器)
    • 客户端收到FIN后,发送ACK报文段(ACK=1,seq=u+1,ack=w+1)。
    • ack=w+1:确认服务器的关闭请求(表示已收到序列号w,期望下一个字节为w+1)。
    • 连接关闭完成,双方进入CLOSED状态。

补充:1MSL:数据包在系统内的最大存活时间

 常见问题

1.为什么一定是三次握手,不能是两次握手

主要是为了防止已经失效的连接请求报文突然又传送到了服务器,从而导致不必要的错误和资源的浪费。

两次握手只能保证单向连接是畅通的。因为TCP是一个双向传输协议,只有经过第三次握手,才能确保双向都可以接收到对方的发送的数据。

2.第二次挥手与第三次挥手之间有一段时间间隔是为什么?

等待第二次挥手发送的ACK报文发送完毕

3.第四次挥手之后主动断开方会等待一段时间再关闭,这个等待的时间是多少?为什么要等待?

等待时间为2MSL,1MSL是报文在系统内的最大存活时间,等待2MSL的时间是为了确保ACK包成功到达被动断开方

在第一个MSL时间内,是被动断开方等待主动断开方ACK报文,若没有在1msl的时间内收到,会超时重传FIN报文,主动断开方在剩余的1MSL时间,接收到被动断开方发送的新的FIN

7.服务器模型

7.1.循环服务器模型

一个服务器可以连接多个客户端,但是同一时间只能连接一个处理一个客户端的请求,等到while循环结束才可以连接下一个客户端。

流程图:

socket();

//填充结构体

bind();

listen();

while(1)

        accept();

        while(1)

                recv();

基础服务器实现:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>
 
int main(int argc, const char *argv[])
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);		//创建套接字
	if(sockfd < 0){
		perror("socket err");
		return -1;
	}
	
	struct sockaddr_in server_addr;					//服务器配置
	server_addr.sin_family = AF_INET;				//地址族选择ipv4
	server_addr.sin_port = htons(8880); 			//配置端口号
    server_addr.sin_addr.s_addr = inet_addr("192.168.0.108");       //配置服务器地址
	
	//绑定套接字
	if(bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0){
		perror("bind err");
		close(sockfd);
		return -1;
	}
	
	//监听
	if(listen(sockfd,6) < 0){
		perror("listen err");
		close(sockfd);
		return -1;
	}
		
	//阻塞函数,等待客户端的连接请求,连接成功生成一个用于通信的文件描述符
	int acceptfd = accept(sockfd,NULL,NULL);		//返回文件描述符,用于后续通信
	if(acceptfd < 0){
		perror("accept err");
		close(sockfd);
		return -1;
	}
	
	char buf[32] = "";
	while(1){
		//接收数据
		ssize_t rd = recv(acceptfd,buf,sizeof(buf),0);
		if(rd < 0){	    		//接收失败
			perror("recv err");
			close(sockfd);
			return -1;
		}
		else if(rd == 0){		//当接收函数返回值是零的时候,连接的客户端退出连接
			printf("TCP2 exit\n");
			return 0;
		}
		else        			//返回值大于零的时候是正确接收到数据
			printf("%s\n",buf);
		memset(buf,0,sizeof(buf));		//清空buf数组
	}
	close(acceptfd);			//关闭用于通信的文件描述符
	close(sockfd);				//关闭socket文件描述符号
	return 0;
}


7.2.并发服务器

一个服务器可以同时处理多个客户端请求

(1)多线程

流程图:

每有一个客户端连接就创建一个新的线程通信,相当于线程和客户端连接,然后在线程执行接收数据等操作。

代码实现多线程客户端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <pthread.h>

void *pthread(void *arg){
	int rd = 0;
	char buf[32] = "";
	int *acceptfd = (int *)arg;
	while(1){
		//接收数据
		rd = recv(*acceptfd,buf,sizeof(buf),0);
		if(strcmp(buf,"quit") == 0)			//当客户端输入quit,断开连接连接下一个客户端
			break;
		if(rd < 0){					//接收失败
			perror("recv err");
			return NULL;
		}
		else if(rd == 0){			//当接收函数返回值是零的时候,连接的客户端退出连接
			printf("TCP2 exit\n");
			break;
		}
		else          				//返回值大于零的时候是正确接收到数据
			printf("buf:%s\n",buf);
		memset(buf,0,sizeof(buf));
	}
	close(*acceptfd);		//关闭用于通信的文件描述符
}

int main(int argc, const char *argv[])
{
	int acceptfd;
	pthread_t tid;
	int sockfd = socket(AF_INET,SOCK_STREAM,0);		//创建套接字
	if(sockfd < 0){
		perror("socket err");
		return -1;
	}
	
	struct sockaddr_in server_addr,clientaddr;		//服务器配置
	server_addr.sin_family = AF_INET;				//地址族选择ipv4
	server_addr.sin_port = htons(atoi(argv[1]));	//通过终端命令行参数获取端口号(终端输入的都是字符类型,要把其转换为int类型)
    server_addr.sin_addr.s_addr = INADDR_ANY;                   //C语言内自带的宏定义,代表0.0.0.0地址
	
	//绑定套接字
	if(bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0){
		perror("bind err");
		close(sockfd);
		return -1;
	}
	
	//监听
	if(listen(sockfd,6) < 0){
		perror("listen err");
		close(sockfd);
		return -1;
	}

		//循环实现:当客户端退出连接,会继续等待连接新的客户端来连接服务器通信
		while(1){
		int a = sizeof(clientaddr);
		
		//阻塞函数,等待客户端的连接请求,连接成功生成一个用于通信的文件描述符
		acceptfd = accept(sockfd,(struct sockaddr *)&clientaddr,&a);		//返回文件描述符,用于后续通信
		if(acceptfd < 0){
			perror("accept err");
			close(sockfd);
			return -1;
		}
		printf("接入设备的ip地址:%s\n该设备的端口号:%d\n",inet_ntoa(clientaddr.sin_addr),clientaddr.sin_port);
		pthread_create(&tid,NULL,pthread,&acceptfd);        //创建线程
		pthread_detach(tid);                                //不阻塞进程,让线程结束自动回收资源
	}
	close(sockfd);				//关闭socket文件描述符号
	return 0;
}

(2)多进程

为什么要用多进程?        解决通信问题

什么时间创建进程?        连接之后创建进程

子进程:通信

父进程:循环等待下个客户端连接

父回收子进程

SIGCHLD:当子进程结束会给父进程发送此信号

父进程就可以捕捉信号并进程子进程回收处理
缺点:资源使用太多,一般不用

(3)使用IO多路复用(select)

流程图:

关键在于什么时候使用select,应该在连接前,监听sockid(被动套接字符)是否有响应来确定是否连接。建立连接之后再循环对通信通信字符进行监听,和哪个客户端有通信信号响应,就去处理有响应的客户端的数据。

IO多路复用服务器实现:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <unistd.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>

int main(int argc, const char *argv[])
{
	int ret = 0;
	char buf[64] = "";
	fd_set readfd,tempfd;					//建立读表和临时表
	FD_ZERO(&readfd);
	FD_ZERO(&tempfd);
	//创建套接字
	int sockid = socket(AF_INET,SOCK_STREAM,0);
	if(sockid == -1){
		perror("socket err");
		return -1;
	}
	//配置网络信息
	struct sockaddr_in serveraddr,clientaddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(atoi(argv[1]));
	serveraddr.sin_addr.s_addr = INADDR_ANY;
	int len = sizeof(clientaddr);
	//绑定套接字
	if(bind(sockid,(struct sockaddr*)&serveraddr,sizeof(serveraddr)) == -1){
		perror("bind err");
		return -1;
	}
	printf("sockid:%d\n",sockid);
	//监听,将主动套接字变为被动套接字
	if(listen(sockid,6) == -1){
		perror("linten err");
		return -1;
	}
	FD_SET(sockid,&readfd);					//把套接字文件符加入读表
	int max = sockid;
	while(1){
		tempfd = readfd;
		//select监听是否有设备请求连接
		ret = select(max + 1,&tempfd,NULL,NULL,NULL);
		if(ret == -1){
			perror("select err");
			return -1;
		}
		else if(ret == 0){
			printf("please link\n");
			continue;
		}
		if(FD_ISSET(sockid,&tempfd)){
			//建立连接
			int acceptid = accept(sockid,(struct sockaddr*)&clientaddr,&len);		//阻塞进程,直到有客户端建立连接
			if(acceptid == -1){
				perror("accept err");
				return -1;
			}
			printf("acceptid:%d\nip:%s port:%d\n",acceptid,inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
			if(max < acceptid)
				max = acceptid;
			//把通信文件符添加入读表
			FD_SET(acceptid,&readfd);
		}
		//对读取表内的已经连接的设备进行依次判断,哪个设备有发送了消息
		for(int i = sockid + 1 ; i <= max ; i++){
			if(FD_ISSET(i,&tempfd)){
				ret = recv(i,buf,sizeof(buf),0);
				if(ret == -1){				//接收失败
					perror("recv err");
					return -1;
				}
				else if(ret == 0){			//客户端退出
					printf("client %d quit\n",i);
					close(i);				//如果建立连接的设备退出连接,关闭通信描述符
					FD_CLR(i,&readfd);		//再从读表中删除该文件描述符
					while(!FD_ISSET(max,&readfd))
						max--;
				}
				else						//成功接收到消息
					printf("client %d send buf:%s\n",i,buf);
				}
			}
		memset(buf,0,sizeof(buf));
	}
	close(sockid);							//关闭套接字文件描述符
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2405488.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

热成像实例分割电力设备数据集(3类,838张)

在现代电力系统的运维管理中&#xff0c;红外热成像已经成为检测设备隐患、预防故障的重要手段。相比传统可见光图像&#xff0c;红外图像可揭示设备温度分布&#xff0c;从而更直观地反映过热、老化等问题。而在AI赋能下&#xff0c;通过实例分割技术对热成像中的电力设备进行…

用电脑通过USB总线连接控制keysight示波器

通过USB总线控制示波器的优势 在上篇文章我介绍了如何通过网线远程连接keysight示波器&#xff0c;如果连接的距离不是很远&#xff0c;也可以通过USB线将示波器与电脑连接起来&#xff0c;实现对示波器的控制和截图。 在KEYSIGHT示波器DSOX1204A的后端&#xff0c;除了有网口…

uni-app学习笔记二十四--showLoading和showModal的用法

showLoading(OBJECT) 显示 loading 提示框, 需主动调用 uni.hideLoading 才能关闭提示框。 OBJECT参数说明 参数类型必填说明平台差异说明titleString是提示的文字内容&#xff0c;显示在loading的下方maskBoolean否是否显示透明蒙层&#xff0c;防止触摸穿透&#xff0c;默…

【Linux】centos软件安装

目录 Linux下安装软件的办法什么是yum使用yum试着安装软件查看yum源配置额外的第三方库 Linux下安装软件的办法 做为一个操作系统&#xff0c;与win和mac一样&#xff0c;安装软件无可厚非。那Linux下安装软件有哪些办法呢&#xff1f;第一种是直接下载源代码本地编译安装&…

基于Vue3.0的在线工具网站

文章目录 1、初始化项目1.1 创建项目1.2 安装vue路由1.3 安装UI库2、首页搭建2.0 页面布局2.1 页头2.2 侧边栏2.3 内容显示区域3、字符串加密解密功能实现3.1 页面构建3.2 实现加密/解密4、Json工具4.1 Json格式化4.1.1 搭建页面4.1.2 实现Json格式化4.2 Json转XML4.1.1 搭建页…

STM32H562----------串口通信(UART)

1、串口介绍 1.1、 数据通信概念 在单片机中我们常用的通信方式有 USART、IIC、SPI、CAN、USB 等; 1、数据通信方式 根据数据通信方式可分为串行通信和并行通信两种,如下图: 串行通信基本特征是数据逐位顺序依次传输,优点:传输线少成本低,抗干扰能力强可用于远距离传…

webpack其余配置

webpack搭建本地服务器 首先是要安装一个webpack-dev-server npm install webpack-dev-server -D 安装后在package.json中添加&#xff1a; {"name": "babel_core_demo","version": "1.0.0","main": "index.js"…

【CUDA 】第5章 共享内存和常量内存——5.3减少全局内存访问(2)

CUDA C编程笔记 第五章 共享内存和常量内存5.3 减少全局内存访问5.3.2 使用展开的并行规约思路reduceSmemUnroll4&#xff08;共享内存&#xff09;具体代码&#xff1a;运行结果意外发现书上全局加载事务和全局存储事务和ncu中这两个值相同 5.3.3 动态共享内存的并行规约reduc…

Python 训练营打卡 Day 46

通道注意力 一、什么是注意力 注意力机制是一种让模型学会「选择性关注重要信息」的特征提取器&#xff0c;就像人类视觉会自动忽略背景&#xff0c;聚焦于图片中的主体&#xff08;如猫、汽车&#xff09;。 transformer中的叫做自注意力机制&#xff0c;他是一种自己学习自…

Rust学习(1)

声明&#xff1a;学习来源于 《Rust 圣经》 变量的绑定和解构 变量绑定 let a "hello world":这个过程称之为变量绑定。绑定就是把这个对象绑定给一个变量&#xff0c;让这个变量成为它的主人。 变量可变性 Rust 变量默认情况下不可变&#xff0c;可以通过 mut …

鸿蒙仓颉语言开发实战教程:商城应用个人中心页面

又到了高考的日子&#xff0c;幽蓝君在这里祝各位考生朋友冷静答题&#xff0c;超常发挥。 今天要分享的内容是仓颉语言商城应用的个人中心页面&#xff0c;先看效果图&#xff1a; 下面介绍下这个页面的实现过程。 我们可以先分析下整个页面的布局结构。可以看出它是纵向的布…

智能生成完整 Java 后端架构,告别手动编写 ControllerServiceDao

在 Java 后端开发的漫长征途上&#xff0c;开发者们常常深陷繁琐的基础代码编写泥潭。尤其是 Controller、Service、Dao 这三层代码的手动编写&#xff0c;堪称开发效率的 “拦路虎”。从搭建项目骨架到填充业务逻辑&#xff0c;每一个环节都需要开发者投入大量精力&#xff0c…

Python----目标检测(yolov5-7.0安装及训练细胞)

一、下载项目代码 yolov5代码源 GitHub - ultralytics/yolov5: YOLOv5 &#x1f680; in PyTorch > ONNX > CoreML > TFLite yolov5-7.0代码源 Release v7.0 - YOLOv5 SOTA Realtime Instance Segmentation ultralytics/yolov5 GitHub 二、创建虚拟环境 创建一个3.8…

【Linux】文件赋权(指定文件所有者、所属组)、挂载光驱(图文教程)

文章目录 文件赋权创建文件 testChmod查看文件的当前权限使用 chmod 命令修改权限验证权限关键命令总结答案汇总 光驱挂载确认文件是否存在打包压缩压缩验证创建 work 目录将压缩文件复制到 work 目录新建挂载点 /MNT/CDROM 并挂载光驱答案汇总 更多相关内容可查看 此篇用以解决…

第22讲、Odoo18 QWeb 模板引擎详解

Odoo QWeb 模板引擎详解与实战 Odoo 的 QWeb 是其自研的模板引擎&#xff0c;广泛应用于 HTML、XML、PDF 等内容的生成&#xff0c;支撑了前端页面渲染、报表输出、门户页面、邮件模板等多种场景。本文将系统介绍 QWeb 的核心用法、工作原理&#xff0c;并通过实战案例演示如何…

【原理解析】为什么显示器Fliker dB值越大,闪烁程度越轻?

显示器Fliker 1 显示器闪烁现象说明2 Fliker量测方法2.1 FMA法2.2 JEITA法问题答疑&#xff1a;为什么显示器Fliker dB值越大&#xff0c;闪烁程度越轻&#xff1f; 3 参考文献 1 显示器闪烁现象说明 当一个光源闪烁超过每秒10次以上就可在人眼中产生视觉残留&#xff0c;此时…

Bootstrap Table开源的企业级数据表格集成

Bootstrap Table 是什么 ‌Bootstrap Table 是一个基于 Bootstrap 框架的开源插件&#xff0c;专为快速构建功能丰富、响应式的数据表格而设计。‌ 它支持排序、分页、搜索、导出等核心功能&#xff0c;并兼容多种 CSS 框架&#xff08;如 Semantic UI、Material Design 等&am…

vue3表格使用Switch 开关

本示例基于vue3 element-plus 注&#xff1a;表格数据返回状态值为0、1。开关使用 v-model"scope.row.state 0" 会报错 故需要对写法做些修改&#xff0c;效果图如下 <el-table-column prop"state" label"入学状态" width"180" …

【11408学习记录】考研写作双核引擎:感谢信+建议信复合结构高分模板(附16年真题精讲)

感谢信建议信 英语写作2016年考研英语&#xff08;二&#xff09;真题小作文题目分析写作思路第一段第二段锦囊妙句9&#xff1a;锦囊妙句12&#xff1a;锦囊妙句13&#xff1a;锦囊妙句18&#xff1a; 第三段 妙句成文 每日一句词汇第一步&#xff1a;找谓语第二步&#xff1a…

一套个人知识储备库构建方案

写文章的初心是做知识沉淀。 好记性不如烂笔头&#xff0c;将阶段性的经验总结成文章&#xff0c;下次遇到相同的问题时&#xff0c;查起来比再次去搜集资料快得多。 然而&#xff0c;当文章越来越多时&#xff0c;有一个问题逐渐开始变得“严峻”起来。 比如&#xff0c;我…