这一块内容放在进程间通信也没有问题,因为进程间通信研究的就是进程之间如何进行协同操作、如何交换数据进行对话的过程,上一篇文章介绍的几种机制都是用在一台机器上的进程互相进行通信的,而网络套接字这种机制则是用来为两台不同机器上的进程进行通信而设置的。
网络通信编程
跨主机传输要注意的问题
1、字节序问题
典型的存储方式:大端存储和小端存储。
在Linux网络编程中,字节序问题是一个非常重要的问题。字节序,或者称为端序,是指多字节数据在内存中的存储顺序。主要有两种类型:大端存储和小端存储。
大端存储(Big-Endian):
在大端存储方式中,多字节数据的高位字节存储在内存的低位地址,而低位字节则存储在内存的高位地址。这种存储方式也被称为高位优先(Most Significant Byte First)。采用大端存储方式的系统包括:PowerPC、Motorola 680x0、SPARC、IBM S390等。
大端模式,比如数字0x12 34 56 78在内存中的表示形式,它的低地址端存放的是0x78,而高地址端存放的是0x12。
小端存储(Little-Endian):
在小端存储方式中,多字节数据的低位字节存储在内存的低位地址,而高位字节则存储在内存的高位地址。这种存储方式也被称为低位优先(Least Significant Byte First)。采用小端存储方式的系统包括:x86、ARM、MIPS、DEC Alpha等。
小端模式,也以同样的数字0x12 34 56 78为例,这次低地址端存放的是0x12,而高地址端存放的是0x78。
在网络通信中,字节序问题显得尤为重要。因为不同的系统可能采用不同的字节序,如果两个系统之间进行通信而没有进行适当的字节序转换,就会导致数据解析错误。因此,在编写网络通信程序时,需要考虑到字节序的问题,并使用相应的函数进行转换。
为了简化问题,我们不去区分什么系统是什么字节序,统一看成两种:主机字节序host 和 网络字节序network。这两种字节序之间的转换都各自有函数可以实现。
在Linux系统中,可以使用以下函数来进行字节序转换(下面各个函数中的n表示network,h表示host,最后一个字母l表示long长一点的意思,为四个字节,s表示short短一点的意思,为两个字节的,to就不用说了,就是从什么字节序转换到什么字节序的意思):
htonl:将一个无符号长整型数从主机字节序转换为网络字节序(大端存储)。
htons:将一个无符号短整型数从主机字节序转换为网络字节序(大端存储)。
ntohl:将一个无符号长整型数从网络字节序转换为主机字节序(大端存储)。
ntohs:将一个无符号短整型数从网络字节序转换为主机字节序(大端存储)。
以上这些函数可以确保在不同系统之间进行通信时,数据的字节序能够正确地进行转换,避免出现数据解析错误。
2、内存对齐问题
这个在C语言学习结构体的时候提过,这里再复习一下。
如:
struct {
int i;
char ch;
float f;
};
理论上来说这个结构体应该占九个字节(int4字节、float4字节、char1字节),但实际上编译器在编译时会帮我们将结构体进行内存对齐,对齐的目的是为了加速、节省当前的取址周期。凡是参与网络通信的这种结构体我们一定要进行地址对齐,所以我们一定要指定告诉当前的编译器让该结构体不进行内存对齐,否则内存对齐之后大概率上这个结构体所占内存会比原来人为计算的要大一点(这就意味着存储地址长度会比原来要长一点,那么地址对齐就对不上了)。
比如上述这个结构体在32位环境下应该是占12个字节,比原来的要大3个字节,这就是因为内存对齐的原因。
用图来解释:
上图是一张内存的简化图示,从上往下地址从0开始编号(这里只是这么模拟可别被误导了,因为方便计算,实际情况中可用内存肯定不会从0开始),我们有一个公式来计算内存对齐后的整个结构体大小,就是使用要存储的起始地址addr % sizeof(type),这里假设0可以整除任何数。
从int i开始,要存储的地址是0,那么有 0%sizeof(int),32位环境下int占4个字节,所以有0%4 = 4,所以 i 变量会占据从0开始到3的内存地址,同理char类型的变量ch从4开始存,其占一个字节,所以 4%1 = 0,可以整除,所以其所占地址空间为从4到5,而float f 的起始存储地址位为5,且float类型占四个字节,所以有 5 % 4 = 0,不可以整除,6、7都不可以整除,而8可以整除,所以f的存储地址是从8到12,如下:
此时ch的存储空间中有三个存储空间没有被使用,但编译器为了方便运算,就自动对齐了,所以得到总共12个字节的空间。
需要注意的是,在实际情况中地址绝不可能从0开始嗷。
再比如下面这个结构体:
struct {
int i;
char ch1;
float f;
char ch;
};
该结构体会占16个字节(三十二位环境下),而下面这种:
struct {
int i;
float f;
char ch;
char ch1;
};
这种结构体则只占12个字节。
另外不同机器(如机器字长不同或者架构不同或者取址方式不同等)所采取的内存对齐方式都不一定一样,这也就意味着我们在不同主机之间进行网络通信传输结构体这种数据时,接收方未必能够正确解析发送方所传输过去的结构体数据,因此我们必须显式地告诉编译器不要帮我们进行内存对齐。
3、类型长度的问题
因为标准C当中并没有规定每种数据类型必须占多少字节,只是约定了一个规则,比如 int 必须大于 short 这种,那么这就意味着不同机器之间如果机器字长不一样的话数据传输过去依然会存在问题。
解决办法是使用一些比较通用的数据类型:
int32_t,表示32位的有符号整形;
uint32_t,表示32位的无符号整形;
int64_t,表示64位的有符号整形;
int8_t,表示8位的有符号整形(这个用来表示有符号char字符类型);
uint8_t,表示8位的无符号整形(这个用来表示无符号char字符类型);
4、socket是什么
在网络编程中,socket被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。这种约定允许一台计算机发送数据给另一台计算机,反之亦然。在编程层面,socket是一种抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作(这就意味着它也会有文件描述符!)。
套接字在TCP/IP协议栈的传输层上工作,其作用是实现不同虚拟机或不同计算机之间的通信。每个套接字都有一个与之关联的IP地址和端口号,这是网络套接字的组合。
在编程中,为了支持用户开发面向应用的通信程序,系统通常会提供基于TCP或UDP的应用程序编程接口(API),这些接口通常以一组函数的形式出现,这些函数被称为套接字函数。这些套接字函数可以创建、连接、读写和关闭套接字,从而支持网络通信。
总的来说,套接字是网络编程的核心概念之一,它是一种通信协议的抽象,使得不同计算机之间可以以标准化的方式进行数据交换和通信。
5、补充几个系统调用和函数
socket()
在Linux网络编程中,socket函数是用来创建套接字的,它是网络编程的基本组件之一。通过socket函数,可以在程序中创建一个套接字对象,该对象可以用于网络通信中的数据传输。
socket函数的原型如下:
int socket(int domain, int type, int protocol);
其中,domain表示地址域,type表示套接字类型,protocol表示协议。
地址域指定了套接字使用的网络协议,如IPv4、IPv6等。常见的地址域有:
AF_INET:使用IPv4地址协议
AF_INET6:使用IPv6地址协议
AF_UNIX:使用UNIX域协议,用于同一台计算机上的进程间通信
套接字类型指定了套接字的通信方式,如TCP、UDP等。常见的套接字类型有:
SOCK_STREAM:流式套接字,基于TCP协议
SOCK_DGRAM:数据报套接字,基于UDP协议
SOCK_RAW:原始套接字,可以访问底层的网络协议
协议指定了使用的传输协议,如TCP、UDP等。如果协议参数设置为0,则系统会根据指定的地址域和套接字类型自动选择合适的协议。
当socket函数成功创建套接字时,会返回一个非负整数,表示套接字的文件描述符。如果创建失败,则返回-1,并设置errno来指示具体的错误原因。
创建套接字后,可以使用其他函数如bind、listen、accept、connect等来进行网络通信操作。例如,bind函数可以将套接字绑定到一个本地地址和端口号上,listen函数可以使得套接字进入监听状态,accept函数可以接受来自客户端的连接请求,connect函数可以向服务器发起连接请求等。这些函数的使用方法可以参考相关的网络编程文档和教程。
inet_pton()
inet_pton函数是Linux系统中的一个网络编程函数,用于将点分十进制的IP地址转换为计算机可以识别的二进制形式。它是IPv4地址转换的标准方法之一。
函数原型如下:
int inet_pton(int family, const char *strptr, void *addrptr);
参数说明:
family:地址族,通常为AF_INET(IPv4)或AF_INET6(IPv6)。
strptr:一个字符串,表示要转换的IP地址。通常是点分十进制的形式,如"192.168.0.1"。
addrptr:一个指向存储转换后地址的缓冲区的指针。
返回值:成功时返回1,失败时返回0或负数。成功表示转换成功,失败表示转换失败。
inet_pton函数的作用是将字符串形式的IP地址转换为计算机可以识别的二进制形式。在网络通信中,计算机需要使用二进制形式的IP地址来进行通信。因此,将字符串形式的IP地址转换为二进制形式是必要的。
例如,当我们在程序中需要将字符串形式的IP地址转换为二进制形式时,可以使用inet_pton函数来完成这个任务。我们只需要将字符串形式的IP地址传递给inet_pton函数,它会返回一个表示二进制形式的IP地址的结构体或指针。然后,我们就可以使用这个结构体或指针来进行网络通信操作。
需要注意的是,在使用inet_pton函数时,需要确保传入的字符串形式的IP地址是正确的,否则会导致转换失败。同时,还需要注意处理错误情况,如当返回值为0或负数时,需要做出相应的错误处理。
inet_ntop()
inet_ntop函数是Linux系统中的一个网络编程函数,用于将计算机识别的二进制IP地址转换为点分十进制的字符串形式。它是IPv4地址转换的标准方法之一。
函数原型如下:
const char *inet_ntop(int family, const void *addrptr, char *strptr, socklen_t len);
参数说明:
family:地址族,通常为AF_INET(IPv4)或AF_INET6(IPv6)。
addrptr:一个指向存储二进制IP地址的缓冲区的指针。
strptr:一个指向存储转换后字符串的缓冲区的指针。
len:缓冲区的长度。
返回值:成功时返回一个指向转换后字符串的指针,失败时返回NULL。
inet_ntop函数的作用是将计算机识别的二进制IP地址转换为字符串形式。在网络通信中,通常使用字符串形式的IP地址来表示主机地址,以便于阅读和理解。因此,将二进制形式的IP地址转换为字符串形式是必要的。
例如,当我们在程序中需要将二进制形式的IP地址转换为字符串形式时,可以使用inet_ntop函数来完成这个任务。我们只需要将二进制形式的IP地址传递给inet_ntop函数,它会返回一个指向转换后字符串的指针。然后,我们就可以使用这个字符串来进行显示或记录等操作。
需要注意的是,在使用inet_ntop函数时,需要确保传入的二进制IP地址是有效的,否则会导致转换失败。同时,还需要注意处理错误情况,如当返回值为NULL时,需要做出相应的错误处理。此外,还需要注意缓冲区长度是否足够,否则会导致溢出等错误。
bind()
bind系统调用是Linux网络编程中的一个重要函数,它用于将套接字(socket)与本地地址和端口号绑定。bind函数将套接字对象与特定的IP地址和端口号关联起来,以便该套接字能够接收和发送网络数据。
bind系统调用的函数原型如下:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
其中,sockfd是套接字文件描述符,addr是一个指向sockaddr结构的指针,该结构包含了要绑定的本地地址和端口号,addrlen是地址结构的长度。
第二个结构体参数sockaddr在bind系统调用中用于指定套接字的本地地址和端口号。sockaddr结构体是套接字编程中的一个基本数据结构,它包含了套接字的本地和远程地址信息。
在bind函数中,第二个参数是一个指向sockaddr结构体的指针,该结构体包含了要绑定的本地地址和端口号。下面是sockaddr结构体的定义:
struct sockaddr {
sa_family_t sa_family; // 地址族,如AF_INET表示IPv4协议族
char sa_data[14]; // 地址数据,具体格式取决于地址族
};
sa_family_t是一个枚举类型,用于表示地址族,如AF_INET表示使用IPv4协议族。sa_data`是一个无符号短整型,用于存储具体的地址数据,其具体格式取决于地址族。
在实际使用中,通常会使用一个更加特定的结构体来替代sockaddr,如sockaddr_in(用于IPv4地址)或sockaddr_in6(用于IPv6地址)。这些结构体提供了更具体的字段来存储IP地址、端口号等信息。
下面是一个使用sockaddr_in结构体的示例:
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; // 地址族,如AF_INET表示IPv4协议族
in_port_t sin_port; // 端口号
struct in_addr in_addr; // IPv4地址结构体
};
//其中struct in_addr的定义如下
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
在使用bind函数时,可以通过创建相应的结构体对象来设置本地地址和端口号,并将其传递给bind函数作为参数。
在bind调用时,需要指定以下信息:
协议族(protocol family):指定使用的协议族,如AF_INET表示使用IPv4协议族。
本地地址(local address):指定要绑定的IP地址和端口号。可以使用INADDR_ANY表示绑定到所有本地IP地址,也可以指定具体的IP地址。
地址长度(address length):指定本地地址的长度。
bind系统调用在成功时返回0,失败时返回-1并设置errno来指示错误原因。如果bind调用失败,可以通过查看errno来获取错误信息。
bind系统调用的常见用途包括:
服务器端:在服务器端,bind函数用于将套接字绑定到特定的本地地址和端口号,以便监听网络上的客户端连接请求。
客户端:在客户端,bind函数可以用于指定要连接的服务器的IP地址和端口号。
多网卡环境:在多网卡环境下,bind函数可以用于将套接字绑定到特定的网卡接口上,以实现数据的网络接口选择性传输。
总之,bind系统调用是网络编程中非常重要的一个函数,它用于将套接字与本地地址和端口号绑定,以便实现网络数据的传输和接收。
getsockopt()
getsockopt函数是Linux网络编程中用于获取套接字选项的函数。它允许开发人员在程序中获取套接字的一些配置选项的值,以便进行监视、调试或控制。
函数原型如下:
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
参数说明:
sockfd:套接字文件描述符,即已连接套字的文件描述符。
level:选项的级别,可以是特定的协议级别或通用级别。
optname:要获取的选项名称。
optval:一个指向存储选项值的缓冲区的指针。
optlen:缓冲区的长度。
返回值:成功时返回0,失败时返回-1并设置相应的错误码。
getsockopt函数可以用于获取套接字的多种选项,如SO_RCVTIMEO(接收超时)、SO_SNDTIMEO(发送超时)、SO_KEEPALIVE(保持活动连接)等。通过获取这些选项的值,开发人员可以更好地了解和控制套接字的属性和行为。
例如,可以使用getsockopt函数获取SO_RCVTIMEO选项的值,以确定接收数据的超时时间。如果需要在程序中修改这个值,可以使用setsockopt函数进行设置。
需要注意的是,在使用getsockopt函数时,需要确保传入的参数和缓冲区长度是正确的,否则会导致错误。同时,还需要注意处理错误情况,如当返回值为-1时,需要使用perror或strerror函数来获取错误信息并进行相应的错误处理。
setsockopt()
setsockopt函数是Linux网络编程中用于设置套接字选项的函数。它允许开发人员在程序中设置套接字的某些配置选项的值,以便进行监视、调试或控制。
函数原型如下:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数说明:
sockfd:套接字文件描述符,即已连接套字的文件描述符。
level:选项的级别,可以是特定的协议级别或通用级别。
optname:要设置的选项名称。
optval:一个指向存储选项值的缓冲区的指针。
optlen:缓冲区的长度。
返回值:成功时返回0,失败时返回-1并设置相应的错误码。
setsockopt函数可以用于设置套接字的多种选项,如SO_RCVTIMEO(接收超时)、SO_SNDTIMEO(发送超时)、SO_KEEPALIVE(保持活动连接)等。通过设置这些选项的值,开发人员可以控制套接字的属性和行为以满足特定的需求。
例如,可以使用setsockopt函数设置SO_RCVTIMEO选项的值,以指定接收数据的超时时间。如果需要在程序中获取这个值,可以使用getsockopt函数进行获取。
需要注意的是,在使用setsockopt函数时,需要确保传入的参数和缓冲区长度是正确的,否则会导致错误。同时,还需要注意处理错误情况,如当返回值为-1时,需要使用perror或strerror函数来获取错误信息并进行相应的错误处理。
listen()
listen 函数是 Linux 系统编程中的一个重要函数,主要用于设置套接字的监听状态。当你想让一个套接字(socket)等待其他套接字的连接时,就会用到这个函数。
函数原型:
int listen(int sockfd, int backlog);
参数:
sockfd:这是一个已打开的套接字文件描述符。
backlog:这是系统在拒绝新的连接之前,可以排队的最大连接数量,也就是半连接池的大小,能够存储多少半连接状态的节点。
返回值:
如果成功,listen 函数返回0。如果出错,返回-1并设置 errno 来表示错误。
错误代码
EBADF:sockfd 不是一个有效的套接字文件描述符。
ENFILE:系统已达到打开文件的上限。
EMFILE:进程已达到打开文件的上限。
ENOSYS:系统不支持此功能。
ECONNABORTED:连接被中止。
其他错误代码…
accept()
accept函数在Linux系统中用于接受建立连接并处理来自客户端的请求。它是在网络编程中常用的一个函数,特别是在使用套接字(socket)进行服务器端编程时。
函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd:这是服务器套接字的文件描述符。服务器通常通过调用socket函数来创建一个套接字,并返回一个文件描述符。
addr:这是一个指向sockaddr结构的指针,用于存储与客户端连接的地址信息。这个结构体包含了客户端的IP地址和端口号。
addrlen:这是一个指向socklen_t类型的指针,用于存储addr参数指向的结构体的大小。
返回值:
如果成功,accept返回一个新的套接字文件描述符,该描述符与客户端建立连接。
如果失败,返回-1,并设置相应的错误代码。
接收端用到的有:send()、sendto()、sendmsg()
send()
send函数是Linux系统中的一个网络编程接口,用于向已连接的套接字发送数据。它是TCP/IP协议簇中一个重要的函数,用于实现网络数据的发送功能。
函数原型如下:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明:
sockfd:套接字文件描述符,即已连接套字的文件描述符。
buf:指向要发送的数据的缓冲区。
len:要发送的数据的长度。
flags:可选参数,用于控制函数的行为。常用的标志包括MSG_PEEK、MSG_WAITALL等。
返回值:成功时返回发送的字节数,失败时返回-1并设置相应的错误码。
send函数将数据从发送方的缓冲区中发送到已连接的套接字中。它通常与recv函数配合使用,recv函数用于从已连接的套接字接收数据。在网络通信中,数据的发送和接收通常是相互的,一方发送数据,另一方接收数据。
需要注意的是,在使用send函数之前,需要先创建一个已连接的套接字,并确保套接字已经连接到远程主机。同时,还需要注意数据的完整性和可靠性,以及异常情况下的处理和错误处理等问题。
与recv函数类似,send函数也有一些变体,如sendto函数用于未连接的套接字,sendmsg函数用于更高级的消息发送等。这些函数提供了不同的功能和参数,可以根据具体的需求进行选择。
sendto()
sendto函数是Linux系统中的一个网络编程接口,用于向未连接的套接字发送数据。它是UDP协议中一个重要的函数,用于实现网络数据的发送功能。
函数原型如下:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t dest_len);
参数说明:
sockfd:套接字文件描述符,即未连接套字的文件描述符。
buf:指向要发送的数据的缓冲区。
len:要发送的数据的长度。
flags:可选参数,用于控制函数的行为。常用的标志包括MSG_PEEK、MSG_WAITALL等。
dest_addr:指向目标地址结构的指针,用于指定接收方的地址信息。
dest_len:目标地址结构的长度。
返回值:成功时返回发送的字节数,失败时返回-1并设置相应的错误码。
sendto函数用于向未连接的套接字发送数据,它通常与recvfrom函数配合使用,recvfrom函数用于从未连接的套接字接收数据。在网络通信中,数据的发送和接收通常是相互的,一方发送数据,另一方接收数据。
与send函数不同的是,sendto函数需要指定接收方的地址信息,这是通过dest_addr参数来指定的。因此,在使用sendto函数之前,需要先创建一个未连接的套接字,并指定要发送到的目标地址信息。
需要注意的是,在使用sendto函数时,需要确保数据的完整性和可靠性,以及处理异常情况和错误处理等问题。同时,还需要注意一些细节问题,如目标地址结构的正确使用和填充等。
sendmsg()
sendmsg函数是Linux系统中的一个网络编程接口,用于发送消息。它是POSIX消息队列API的一部分,可以用于实现进程间通信(IPC)中的消息传递。
函数原型如下:
ssize_t sendmsg(int sockfd, const struct msghdr *msghdr, int flags);
参数说明:
sockfd:套接字文件描述符,即已连接套字的文件描述符。
msghdr:指向消息头结构的指针,用于存储要发送的消息的相关信息。
flags:可选参数,用于控制函数的行为。常用的标志包括MSG_PEEK、MSG_WAITALL等。
返回值:成功时返回发送的字节数,失败时返回-1并设置相应的错误码。
sendmsg函数可以发送一条或多条消息,并根据消息的类型、长度和标志位等信息进行处理。它提供了更高级的功能,如消息队列、控制信息等,适用于更复杂的网络通信场景。
在sendmsg函数中,msghdr参数是一个指向消息头结构的指针,该结构包含了要发送的消息的相关信息,如消息类型、目标地址、消息长度等。可以通过访问这些信息来处理发送的消息。
需要注意的是,在使用sendmsg函数之前,需要先创建一个已连接的套接字,并使用适当的recvmsg函数接收消息。同时,需要了解消息队列的相关知识,包括消息类型、消息队列的创建和使用等。此外,还需要注意数据的完整性和可靠性,以及异常情况下的处理和错误处理等问题。
与send和sendto函数相比,sendmsg函数提供了更高级的功能和更灵活的消息处理方式。它可以同时发送多个消息,并支持消息队列等高级特性。因此,在使用时需要根据具体的需求进行选择。
发送端能用到的有:recv()、recvfrom()、recvmsg()
recv()
recv函数是Linux系统中的一个网络编程接口,用于从已连接的套接字接收数据。它是TCP/IP协议簇中一个重要的函数,用于实现网络数据的接收功能。
函数原型如下:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明:
sockfd:套接字文件描述符,即已连接套字的文件描述符。
buf:指向接收缓冲区的指针,用于存储接收到的数据。
len:要接收的最大字节数。
flags:可选参数,用于控制函数的行为。常用的标志包括MSG_PEEK、MSG_WAITALL等。
返回值:成功时返回接收的字节数,失败时返回-1并设置相应的错误码。
recv函数通常与send函数配合使用,实现网络数据的发送和接收功能。在建立连接之后,可以使用send函数发送数据,然后使用recv函数接收来自对端的数据。recv函数将读取对端发送的数据,并将其存储到指定的缓冲区中。
需要注意的是,在使用recv函数之前,需要先创建一个已连接的套接字,通常使用socket函数创建套接字对象,并使用connect函数建立与对端的连接。然后,可以使用recv函数接收数据,并处理接收到的数据。
另外在多线程或多进程环境下,如果有多个线程或进程同时进行数据接收操作,需要使用合适的同步机制来避免竞争条件和数据冲突。此外,还需要注意数据的完整性和可靠性,以及异常情况下的处理和错误处理等问题。
recvfrom()
recvfrom函数是Linux系统中的一个网络编程接口,用于从未连接的套接字接收数据。它通常用于实现广播和多播功能。
函数原型如下:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
sockfd:套接字文件描述符,即未连接套字的文件描述符。
buf:指向接收缓冲区的指针,用于存储接收到的数据。
len:要接收的最大字节数。
flags:可选参数,用于控制函数的行为。常用的标志包括MSG_PEEK、MSG_WAITALL等。
src_addr:指向存储源地址的sockaddr结构体的指针。
addrlen:指向一个整型变量的指针,用于存储源地址的长度。
返回值:成功时返回接收的字节数,失败时返回-1并设置相应的错误码。
recvfrom函数与sendto函数配合使用,可以实现广播和多播功能。当使用recvfrom函数时,可以从任意一个对端接收数据,而不仅仅是从已连接的对端接收数据。此外,通过指定src_addr参数和addrlen参数,可以获取发送方的地址信息,从而实现基于源地址的路由和过滤等功能。
需要注意的是,在使用recvfrom函数之前,需要先创建一个未连接的套接字,通常使用socket函数创建套接字对象,并使用bind函数指定本地地址和端口号。然后,可以使用listen函数使未连接套接字进入监听状态,等待接收数据。在接收到数据后,可以使用recvfrom函数获取数据并获取发送方的地址信息。
recvmsg()
recvmsg函数是Linux系统中的一个网络编程接口,用于接收消息。它是POSIX消息队列API的一部分,可以用于实现进程间通信(IPC)中的消息传递。
函数原型如下:
ssize_t recvmsg(int sockfd, struct msghdr *msghdr, int flags);
参数说明:
sockfd:套接字文件描述符,即已连接套字的文件描述符。
msghdr:指向消息头结构的指针,用于存储接收到的消息的相关信息。
flags:可选参数,用于控制函数的行为。常用的标志包括MSG_PEEK、MSG_WAITALL等。
返回值:成功时返回接收的字节数,失败时返回-1并设置相应的错误码。
recvmsg函数可以接收一条或多条消息,并根据消息的类型、长度和标志位等信息进行处理。它提供了更高级的功能,如消息队列、控制信息等,适用于更复杂的网络通信场景。
在recvmsg函数中,msghdr参数是一个指向消息头结构的指针,该结构包含了接收到的消息的相关信息,如消息类型、发送方的地址、消息长度等。可以通过访问这些信息来处理接收到的消息。
需要注意的是,在使用recvmsg函数之前,需要先创建一个已连接的套接字,并使用适当的sendmsg函数发送消息。同时,需要了解消息队列的相关知识,包括消息类型、消息队列的创建和使用等。此外,还需要注意数据的完整性和可靠性,以及异常情况下的处理和错误处理等问题。
报式套接字
报式传输是指在网络中以报文的形式传输数据。在Linux网络编程中,报式传输通常指的是UDP协议。
UDP(User Datagram Protocol,用户数据报协议)是一种无连接的传输层协议,它提供了面向无连接的数据报传输服务。与TCP协议不同,UDP不建立连接,而是直接将数据封装在一个数据报中,并将其发送到网络中。
在报式传输中,数据被封装在一个数据报中,每个数据报都有一个报头,包含了源IP地址、目的IP地址、协议号、端口号等信息。发送端将数据报发送到网络中,接收端从网络中接收数据报。每个数据报都是一个独立的传输单位,它们可以单独处理和传输。
报式传输的特点是简单和快速。由于不需要建立连接,发送端可以直接将数据报发送到网络中,而不需要等待接收端的确认。这种传输方式适用于需要快速传输小块数据的应用场景,如实时音视频通信、在线游戏等。
在报式传输中,数据的传输是单向的。发送端将数据报发送到网络中后,接收端可以从网络中接收数据报。但是,接收端不能向发送端发送数据报。
报式传输是一种不可靠的数据传输方式,因为它不提供错误检测和纠正机制。当数据报在传输过程中出现错误时,接收端无法得知,也无法请求重新传输数据。因此,报式传输适用于那些可以容忍数据丢失的应用场景。
总之,报式传输是一种快速和简单的数据传输方式,适用于需要快速传输小块数据的应用场景。在Linux网络编程中,可以使用UDP协议来实现报式传输。
报式套接字响应过程
响应过程的逻辑:
借助C/S架构很好理解,被动端就是服务器端,肯定是需要先运行起来的,不然怎么为客户端提供服务呢?
而且只有客户端需要监听本地的某一个网络端口号,所以需要给socket取得地址(其实就是绑定一个端口号),而客户端是不用绑定的,因为客户端只需要对服务端的固定端口号发送请求即可,每次请求的客户端端口号甚至都是随机的。
接下来我们通过代码来实现一下这个逻辑。
实例
按照上面的逻辑,我们需要三个文件如下,
先定义头文件proto.h,封装数据格式:
#include <stdint.h>
#ifndef PROTO_H__
#define PROTO_H__
//定义接收网络数据的端口
//没有单位的数值没有意义,这里是老师的使用习惯
//到时候转成数字即可
#define RCVPORT "1989"
#define NAMESIZE 11
//约定发收双方的数格式
struct msg_st{
//数据类型采用标准形式
uint8_t name[NAMESIZE];
uint32_t math;
uint32_t chinese;
}__attribute__((packed));
//上面这一句是用来告诉编译器不要对该结构体进行内存对齐
#endif
然后再写我们的服务端rcver.c:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <unistd.h>
#include "proto.h"
#define IPSTRSIZE 40
//本程序为接收方,也就是服务端
int main(){
struct sockaddr_in laddr;//获得IPV4地址和地址长度
struct sockaddr_in raddr; //对端的地址和地址长度
socklen_t raddr_len; //对端地址长度的变量
char ipstr[IPSTRSIZE];//保存多端的点分十进制字符串形式
struct msg_st rbuf; //缓存数据的缓存区
//先取得一个socket
//第一个参数指定协议族(地址域),这里是IPV4
//第二个参数指定传输方式,这里是报式
//第三个参数指定具体的协议类型,填0则系统会自动分析写入
int sd = socket(AF_INET,SOCK_DGRAM,0);
if(sd < 0){
perror("socket()");
exit(1);
}
//然后绑定一个端口号
//这里强转成void *是因为我们用的sockaddr_in类型而不是原来的sockaddr类型
laddr.sin_family = AF_INET;//指定协议族
//atoi是将字符串转换成数字
//htons是将主机字节序转换成网络字节序,且将大小设置为2个字节
laddr.sin_port = htons(atoi(RCVPORT)); //指定端口号
//指定IP地址,因为我们日常用的ip地址是点分十进制的,但这是用来方便人类记忆的
//机器并不认识,所以需要进行转换,转成一个大整数给机器
//使用inet_pton这个函数可以帮我们完成这个转换
//第一个参数是指定协议族
//第二个参数是要绑定的IP地址的点分十进制形式。0.0.0.0表示广播
//第三个参数是转换后的大整数所存放的位置
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);
if(bind(sd,(void*)&laddr,sizeof(laddr)) < 0){
perror("bind()");
exit(1);
}
//下面这句话非常重要!!!
//我们必须初始化这个变量,并告知其所要接收的数据raddr到底有多大
//不然接收过来肯定有问题
raddr_len = sizeof(raddr);
while(1){
//接收数据
//第一个参数是已经绑定的socket
//第二个参数是拿到从网络中传输到的数据
//第三个长度是该数据的大小
//第四个参数是特殊要求
//第五个参数是对端网络(发送方)的ip地址
//第六个参数是对端网络的地址长度
recvfrom(sd,&rbuf,sizeof(rbuf),0,(void*)&raddr,&raddr_len);
//将接收到的数据打印在终端上
//不过要先将从网络传输来的大整数ip值转换成点分十进制
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
printf("------MESSAGE FROM %s:%d---\n",ipstr,ntohs(raddr.sin_port));
//name是单字节的,单字节数据网络传输不涉及大端小端的存储差异
printf("NAME = %s\n",rbuf.name);
printf("MATH = %d\n",ntohl(rbuf.math));
printf("CHINESE = %d\n",ntohl(rbuf.chinese));
}
//关闭socket
close(sd);
exit(0);
}
运行后使用 netstat -anu 命令可以查到udp协议的1989端口已经被占用啦:
最后写我们的客户端也就是seder.c:
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include "proto.h"
//本程序为客户端发送方
//main函数第一个参数是启动命令 第二个参数是指定对端的ip地址
int main(int argc, char** argv){
//待发送的数据
struct msg_st sbuf;
struct sockaddr_in raddr;//远端地址,是ipv4类型的
if(argc < 2){
fprintf(stderr, "Usage...\n");
exit(1);
}
//先取得socket
int sd = socket(AF_INET,SOCK_DGRAM,0);
if(sd < 0){
perror("socket()");
exit(1);
}
//封装数据
strcpy(sbuf.name,"Alan");
sbuf.math = htonl(rand()%100);
sbuf.chinese = htonl(rand()%100);
//封装远端的地址
raddr.sin_family = AF_INET;//对方所使用协议族,为ipv4
raddr.sin_port = htons(atoi(RCVPORT)); //对方端口号
//指定对端的ip地址,将点分十进制转换成大整数进行网络传输
inet_pton(AF_INET,argv[1],&raddr.sin_addr);
//采用的广播形式,所以走的是udp协议,使用sendto发送
//第一个参数指定要使用的socket
//第二个参数是要发送的数据
//第三个参数是设置特殊要求,为0表示没有
//第四个参数是发送数据的大小
//第五个是远端地址
//第六个参数是远端地址大小
if( sendto(sd,&sbuf,sizeof(sbuf),0,(void*)&raddr,sizeof(raddr)) < 0){
perror("sendto()");
exit(1);
}
puts("ok");
//关闭socket
close(sd);
exit(0);
}
运行结果:
先启动我们的服务端,也就是接收端:
然后启动我们的客户端,也就是发送端:
此时可以看见接收到了来自发送端所发送的数据:
这个程序中我们并没有绑定发送端的socket,所以上面所展示的接收端所打印的发送端的端口号是随机的,发送端会随机找一个没有被使用的端口给当前发送进程使用。
多点通讯:广播(分为全网广播,子网广播)与多播(也叫组播)
在Linux网络编程中,多点通讯是一种允许一台或多台主机(多播源)发送单一数据包到多台主机(一次的,同时的)的TCP/IP网络技术。它采用流媒体数据传输协议,能够高效传输大量数据。多点通讯的实现需要使用Linux套接字编程,其中套接字是一种网络编程接口,可以用于创建、连接、监听、接收和发送数据等操作。在Linux中,套接字可以用于实现多点通讯,包括广播和组播等。
广播是一种将数据包发送到局域网内所有主机的网络通信方式,而组播则是一种允许一台或多台主机发送单一数据包到多台主机的TCP/IP网络技术。在实现广播和组播时,需要使用Linux套接字编程中的一些函数和选项,如socket、bind、sendto等。
在进行多点通讯时,需要注意一些问题,如数据的完整性和可靠性、异常情况的处理和错误处理等。此外,还需要了解一些相关的网络协议和工具,如UDP协议、TCP协议、IP地址、端口号等。
总之,Linux网络编程中的多点通讯是一种可以实现高效数据传输的网络技术,可以用于广播和组播等应用场景。在进行多点通讯时,需要使用Linux套接字编程来实现,并注意一些相关的问题和协议。
广播是一种特殊的网络通信方式,用于将数据包发送到局域网内的所有主机。在进行广播时,同一网段的所有主机都能接收到数据包。广播通常用于在局域网内探测服务器。
组播则是一种允许一台或多台主机(多播源)发送单一数据包到多台主机(一次的,同时的)的TCP/IP网络技术。多播是IPv6数据包的3种基本目的地址类型之一。多播是一种一点对多点的通信,数据的收发仅仅在同一分组中进行,是节省网络带宽的有效方法之一。
广播实例(以全网广播为例)
这个实例我们在之前讲的代码上进行修改,先来看发送方,因为这个广播的重点就在发送方嘛(协议头文件不用修改):
seder.c:
#include <asm-generic/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include "proto.h"
//本程序为客户端发送方
//main函数第一个参数是启动命令 第二个参数是指定对端的ip地址
int main(int argc, char** argv){
//待发送的数据
struct msg_st sbuf;
struct sockaddr_in raddr;//远端地址,是ipv4类型的
//先取得socket
int sd = socket(AF_INET,SOCK_DGRAM,0);
if(sd < 0){
perror("socket()");
exit(1);
}
int val = 1;
//对 socket 套接字进行一些属性的设置
//第一个参数是选择要设置的socket
//第二个参数是设置选项的级别,这里是通用的套接字级别
//第三个参数是要设置的选项名称,表示是否允许在套接字上发送或接收广播信息
//第四个参数是一个指向存储选项值的缓冲区的指针,val通常是一个整数类型的变量
//用于表示选项的状态(通常是0或者1,表示关闭或者打开)
//第五个参数表示缓冲区的长度,这里因为传的val所以就sizeof(val)即可,而有的选项是传结构体嗷
/*
* 综合起来的意思就是为套接字sd设置一个选项,使其在套接字级别(SOL_SOCKET)上
* 允许或者禁止广播(SO_BROADCAST)。具体允许还是禁止取决于val,1允许0禁止。
* */
if(setsockopt(sd,SOL_SOCKET,SO_BROADCAST,&val,sizeof(val)) < 0){
perror("setsockopt()");
exit(1);
}
//封装数据
strcpy(sbuf.name,"Alan");
sbuf.math = htonl(rand()%100);
sbuf.chinese = htonl(rand()%100);
//封装远端的地址
raddr.sin_family = AF_INET;//对方所使用协议族,为ipv4
raddr.sin_port = htons(atoi(RCVPORT)); //对方端口号
//指定对端的ip地址,将点分十进制转换成大整数进行网络传输
//要实现广播,那么目标地址肯定是广播域啦
inet_pton(AF_INET,"255.255.255.255",&raddr.sin_addr);
//采用的广播形式,所以走的是udp协议,使用sendto发送
//第一个参数指定要使用的socket
//第二个参数是要发送的数据
//第三个参数是设置特殊要求,为0表示没有
//第四个参数是发送数据的大小
//第五个是远端地址
//第六个参数是远端地址大小
if( sendto(sd,&sbuf,sizeof(sbuf),0,(void*)&raddr,sizeof(raddr)) < 0){
perror("sendto()");
exit(1);
}
puts("ok");
//关闭socket
close(sd);
exit(0);
}
接收方rcver.c:
#include <asm-generic/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <unistd.h>
#include "proto.h"
#define IPSTRSIZE 40
//本程序为接收方,也就是服务端
int main(){
struct sockaddr_in laddr;//获得IPV4地址和地址长度
struct sockaddr_in raddr; //对端的地址和地址长度
socklen_t raddr_len; //对端地址长度的变量
char ipstr[IPSTRSIZE];//保存多端的点分十进制字符串形式
struct msg_st rbuf; //缓存数据的缓存区
//先取得一个socket
//第一个参数指定协议族(地址域),这里是IPV4
//第二个参数指定传输方式,这里是报式
//第三个参数指定具体的协议类型,填0则系统会自动分析写入
int sd = socket(AF_INET,SOCK_DGRAM,0);
if(sd < 0){
perror("socket()");
exit(1);
}
int val = 1;
if(setsockopt(sd,SOL_SOCKET,SO_BROADCAST,&val,sizeof(val)) < 0){
perror("setsockopt()");
exit(1);
}
//然后绑定一个端口号
//这里强转成void *是因为我们用的sockaddr_in类型而不是原来的sockaddr类型
laddr.sin_family = AF_INET;//指定协议族
//atoi是将字符串转换成数字
//htons是将主机字节序转换成网络字节序,且将大小设置为2个字节
laddr.sin_port = htons(atoi(RCVPORT)); //指定端口号
//指定IP地址,因为我们日常用的ip地址是点分十进制的,但这是用来方便人类记忆的
//机器并不认识,所以需要进行转换,转成一个大整数给机器
//使用inet_pton这个函数可以帮我们完成这个转换
//第一个参数是指定协议族
//第二个参数是要绑定的IP地址的点分十进制形式。0.0.0.0表示广播
//第三个参数是转换后的大整数所存放的位置
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);
if(bind(sd,(void*)&laddr,sizeof(laddr)) < 0){
perror("bind()");
exit(1);
}
//下面这句话非常重要!!!
//我们必须初始化这个变量,并告知其所要接收的数据raddr到底有多大
//不然接收过来肯定有问题
raddr_len = sizeof(raddr);
while(1){
//接收数据
//第一个参数是已经绑定的socket
//第二个参数是拿到从网络中传输到的数据
//第三个长度是该数据的大小
//第四个参数是特殊要求
//第五个参数是对端网络(发送方)的ip地址
//第六个参数是对端网络的地址长度
recvfrom(sd,&rbuf,sizeof(rbuf),0,(void*)&raddr,&raddr_len);
//将接收到的数据打印在终端上
//不过要先将从网络传输来的大整数ip值转换成点分十进制
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
printf("------MESSAGE FROM %s:%d---\n",ipstr,ntohs(raddr.sin_port));
//name是单字节的,单字节数据网络传输不涉及大端小端的存储差异
printf("NAME = %s\n",rbuf.name);
printf("MATH = %d\n",ntohl(rbuf.math));
printf("CHINESE = %d\n",ntohl(rbuf.chinese));
}
//关闭socket
close(sd);
exit(0);
}
运行结果(运行顺序不再赘述):
组播实例
在开始之前补充解释一个关于组播属性的一个结构体定义,这通过man 7 ip 的man手册中可以查找到相关内容:
在Linux网络编程中,struct ip_mreqn是一个用于表示IP多播(multicast)相关信息的结构体类型。它主要用于设置和操作套接字(socket)的多播选项。
struct ip_mreqn的定义通常如下所示:
struct ip_mreqn {
struct in_addr imr_multiaddr; /* 多播组地址 */
struct in_addr imr_address; /* 本地网络接口地址 */
int imr_ifindex; /* 本地网络接口索引,或叫网络接口序 */
};
下面是每个字段的详细解释:
imr_multiaddr:此字段表示多播组的IP地址。它是struct in_addr类型,用于存储IPv4地址。通过设置这个字段,可以指定要加入的多播组的地址。
imr_address:此字段表示本地接口的IP地址。它也是struct in_addr类型,用于存储IPv4地址。通过设置这个字段,可以指定将数据包发送到的本地接口的地址。
imr_ifindex:此字段表示本地接口的索引号。它是一个整数,通过调用if_nametoindex()函数可以将接口名称转换为索引号。这个索引号标识了使用的网络接口。
在Linux网络编程中,struct ip_mreqn常用于以下几个场景:
加入多播组:通过设置imr_multiaddr字段为多播组的IP地址,可以加入该多播组。然后可以使用套接字发送和接收该多播组的数据包。
设置多播接口:通过设置imr_address字段为本地接口的IP地址,可以指定将数据包发送到的接口。这对于在多个网络接口之间路由数据包非常有用。
获取本地接口索引:通过读取imr_ifindex字段,可以获取当前使用的本地接口的索引号。这对于后续操作(如绑定套接字到特定接口)非常有用。
总之,struct ip_mreqn是Linux网络编程中用于处理IP多播的重要结构体类型,它提供了设置和获取多播相关信息的接口,使得开发人员可以方便地进行多播通信。
下面正式开始代码的编写:
头文件需要改动,proto.h:
#include <stdint.h>
#ifndef PROTO_H__
#define PROTO_H__
//定义接收网络数据的端口
//没有单位的数值没有意义,这里是老师的使用习惯
//到时候转成数字即可
#define RCVPORT "1989"
//下面约定了多播组的组号
//这样发送方和接收方都可以知道组号
//就能加入同一个组进行组间通讯了
#define MTROUP "224.2.2.2"
#define NAMESIZE 11
//约定发收双方的数格式
struct msg_st{
//数据类型采用标准形式
uint8_t name[NAMESIZE];
uint32_t math;
uint32_t chinese;
}__attribute__((packed));
//上面这一句是用来告诉编译器不要对该结构体进行内存对齐
#endif
发送端seder.c:
#include <asm-generic/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include "proto.h"
//本程序为客户端发送方
//main函数第一个参数是启动命令 第二个参数是指定对端的ip地址
int main(int argc, char** argv){
//待发送的数据
struct msg_st sbuf;
struct sockaddr_in raddr;//远端地址,是ipv4类型的
//先取得socket
int sd = socket(AF_INET,SOCK_DGRAM,0);
if(sd < 0){
perror("socket()");
exit(1);
}
//这段代码的注释在代码的正下方进行解释
struct ip_mreqn mreq;
inet_pton(AF_INET,MTROUP,&mreq.imr_multiaddr);
inet_pton(AF_INET,"0.0.0.0",&mreq.imr_address);
mreq.imr_ifindex = if_nametoindex("eth0");
if(setsockopt(sd,IPPROTO_IP,IP_MULTICAST_IF,&mreq,sizeof(mreq)) < 0){
perror("setsockopt()");
exit(1);
}
//封装数据
strcpy(sbuf.name,"Alan");
sbuf.math = htonl(rand()%100);
sbuf.chinese = htonl(rand()%100);
//封装远端的地址
raddr.sin_family = AF_INET;//对方所使用协议族,为ipv4
raddr.sin_port = htons(atoi(RCVPORT)); //对方端口号
//指定对端的ip地址,将点分十进制转换成大整数进行网络传输
//要实现组播,那么目标地址肯定就是我们事先约定好的组ip地址号
inet_pton(AF_INET,MTROUP,&raddr.sin_addr);
//采用的广播形式,所以走的是udp协议,使用sendto发送
//第一个参数指定要使用的socket
//第二个参数是要发送的数据
//第三个参数是设置特殊要求,为0表示没有
//第四个参数是发送数据的大小
//第五个是远端地址
//第六个参数是远端地址大小
if( sendto(sd,&sbuf,sizeof(sbuf),0,(void*)&raddr,sizeof(raddr)) < 0){
perror("sendto()");
exit(1);
}
puts("ok");
//关闭socket
close(sd);
exit(0);
}
上面注释提到的内代码内容解释:
这段代码用于设置一个套接字(socket)的IP多播(multicast)选项。逐行解释:
1、struct ip_mreqn mreq;
这行代码定义了一个结构体变量mreq,这个结构体用于存储IP多播相关的信息。注意这里的struct ip_mreqn是一个特定于系统或库的结构体类型,它用于存储IP多播地址和相关的接口索引等信息(man 7 ip 手册可以进行查询)。
2、inet_pton(AF_INET,MTROUP,&mreq.imr_multiaddr);
这行代码调用了inet_pton函数,该函数用于将一个点分十进制的IP地址字符串转换为其二进制形式。这里的AF_INET表示这是一个IPv4地址。MTROUP是要转换的字符串,表示多播组地址。&mreq.imr_multiaddr是存储转换后结果的地址。
3、inet_pton(AF_INET,“0.0.0.0”,&mreq.imr_address);
这行代码再次调用了inet_pton函数,但这次是将字符串"0.0.0.0"转换为其二进制形式(本地网络接口填这个是表示任意网络接口的意思,比较万能),并存储在mreq.imr_address中。这通常表示任何IPv4地址。
4、mreq.imr_ifindex = if_nametoindex(“eth0”);
这行代码将接口名称"eth0"转换为其网络索引号,并存储在mreq.imr_ifindex中。这可以用于标识网络接口。
5、if(setsockopt(sd,IPPROTO_IP,IP_MULTICAST_IF,&mreq,sizeof(mreq)) < 0){
这行代码调用了setsockopt函数,该函数用于设置套接字的选项。这里,它被用来设置多播接口(multicast interface)。参数sd表示要设置的套接字描述符,IPPROTO_IP表示要设置的协议级别,IP_MULTICAST_IF表示要设置的选项名称。&mreq是存储设置选项的数据的地址,而sizeof(mreq)表示数据的长度。如果设置失败(返回值小于0),则会进入错误处理代码。
6、perror(“setsockopt()”);
如果上面的setsockopt函数调用失败,这行代码会打印出错误信息。perror函数会打印出与当前errno值相关的错误消息。
7、exit(1);
如果上述代码段中出现错误,这行代码会终止程序执行,并返回错误码1。
rcver.c:
#include <asm-generic/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <unistd.h>
#include "proto.h"
#define IPSTRSIZE 40
//本程序为接收方,也就是服务端
int main(){
struct sockaddr_in laddr;//获得IPV4地址和地址长度
struct sockaddr_in raddr; //对端的地址和地址长度
socklen_t raddr_len; //对端地址长度的变量
char ipstr[IPSTRSIZE];//保存多端的点分十进制字符串形式
struct msg_st rbuf; //缓存数据的缓存区
//先取得一个socket
//第一个参数指定协议族(地址域),这里是IPV4
//第二个参数指定传输方式,这里是报式
//第三个参数指定具体的协议类型,填0则系统会自动分析写入
int sd = socket(AF_INET,SOCK_DGRAM,0);
if(sd < 0){
perror("socket()");
exit(1);
}
//接收方加入多播组即可
//下面这段代码注释请参考前文所描述的发送方中的代码注释
struct ip_mreqn mreq;
inet_pton(AF_INET,MTROUP,&mreq.imr_multiaddr);
inet_pton(AF_INET,"0.0.0.0",&mreq.imr_address);
mreq.imr_ifindex = if_nametoindex("eth0");
if(setsockopt(sd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq)) < 0){
perror("setsockopt()");
exit(1);
}
//然后绑定一个端口号
//这里强转成void *是因为我们用的sockaddr_in类型而不是原来的sockaddr类型
laddr.sin_family = AF_INET;//指定协议族
//atoi是将字符串转换成数字
//htons是将主机字节序转换成网络字节序,且将大小设置为2个字节
laddr.sin_port = htons(atoi(RCVPORT)); //指定端口号
//指定IP地址,因为我们日常用的ip地址是点分十进制的,但这是用来方便人类记忆的
//机器并不认识,所以需要进行转换,转成一个大整数给机器
//使用inet_pton这个函数可以帮我们完成这个转换
//第一个参数是指定协议族
//第二个参数是要绑定的IP地址的点分十进制形式。0.0.0.0表示广播
//第三个参数是转换后的大整数所存放的位置
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);
if(bind(sd,(void*)&laddr,sizeof(laddr)) < 0){
perror("bind()");
exit(1);
}
//下面这句话非常重要!!!
//我们必须初始化这个变量,并告知其所要接收的数据raddr到底有多大
//不然接收过来肯定有问题
raddr_len = sizeof(raddr);
while(1){
//接收数据
//第一个参数是已经绑定的socket
//第二个参数是拿到从网络中传输到的数据
//第三个长度是该数据的大小
//第四个参数是特殊要求
//第五个参数是对端网络(发送方)的ip地址
//第六个参数是对端网络的地址长度
recvfrom(sd,&rbuf,sizeof(rbuf),0,(void*)&raddr,&raddr_len);
//将接收到的数据打印在终端上
//不过要先将从网络传输来的大整数ip值转换成点分十进制
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
printf("------MESSAGE FROM %s:%d---\n",ipstr,ntohs(raddr.sin_port));
//name是单字节的,单字节数据网络传输不涉及大端小端的存储差异
printf("NAME = %s\n",rbuf.name);
printf("MATH = %d\n",ntohl(rbuf.math));
printf("CHINESE = %d\n",ntohl(rbuf.chinese));
}
//关闭socket
close(sd);
exit(0);
}
编译运行:
流式套接字
流式传输是指在网络中以流的形式传输数据。在Linux网络编程中,流式传输通常指的是TCP/IP协议栈中的TCP协议。
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。它提供了一种可靠的数据传输服务,通过将数据分段为较小的数据包,并在发送端和接收端之间建立连接,以确保数据的正确传输。
在流式传输中,数据被视为一个连续的字节流,发送端将数据发送到网络中,接收端从网络中接收数据。流式传输的特点是连续性,即数据是连续不断地传输的。这种传输方式适用于需要长时间持续传输数据的应用场景,如文件传输、实时音视频通信等。
在流式传输中,发送端和接收端需要建立连接。在建立连接之前,发送端需要向接收端发送一个SYN报文,以请求建立连接。接收端收到SYN报文后,会向发送端发送一个SYN-ACK报文,以确认连接的建立。发送端收到SYN-ACK报文后,连接建立完成,可以开始传输数据。
在流式传输中,数据的传输是双向的。发送端和接收端都可以同时发送和接收数据。发送端将数据发送到网络中后,接收端可以从网络中接收数据。同样地,接收端也可以将数据发送到网络中,而发送端可以从网络中接收数据。
流式传输是一种可靠的数据传输方式,因为它提供了错误检测和纠正机制。当数据在传输过程中出现错误时,接收端可以向发送端发送一个RST报文,以请求重新传输数据。此外,TCP还提供了流量控制机制,以避免接收端因接收数据过快而导致的缓冲区溢出问题。
总之,流式传输是一种可靠的数据传输方式,适用于需要长时间持续传输数据的应用场景。在Linux网络编程中,可以使用TCP协议来实现流式传输。
流式套接字响应过程
实例
按照响应流程,我们可以知道依然有下面几个文件:
协议头文件,proto.c:
#ifndef PROTO_H__
#define PROTO_H__
//指定服务器端口
#define SERVERPORT "1989"
//每次有C端来跟S端连接的时候
//都把当前时戳发送给C端,\r\n是为了跨端兼容
#define FMT_STAMP "%lld\r\n"
#endif
服务端server.c:
#include <asm-generic/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <time.h>
#include <arpa/inet.h>
#include "proto.h"
#define IPSTRSIZE 40
#define BUFSIZE 1024
//服务端发送数据的函数
static void server_job(int sd){
char buf[BUFSIZE];
//发送数据
//数据就是一个时戳
int len = sprintf(buf,FMT_STAMP,(long long)time(NULL));
//第一个参数是要操作的socket
//第二个参数是要传输的数据
//第三个参数是数据的大小
//第四个参数是设置一些特殊内容
if(send(sd,buf,len,0) < 0){
perror("send()");
exit(1);
}
}
int main(){
struct sockaddr_in laddr;
struct sockaddr_in raddr;
char ipstr[IPSTRSIZE];
socklen_t raddr_len;
//先拿到socket
int sd = socket(AF_INET,SOCK_STREAM,0);
if(sd < 0){
perror("socket()");
exit(1);
}
//这一步的操作是为了避免服务器端进程被意外杀死后进入time_wait等待状态
//而导致无法迅速重启的问题
int val = 1;
if(setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &val,sizeof(val)) < 0){
perror("setsockopt()");
exit(1);
}
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);
//绑定本地的地址(是哪个端口还有IP地址)
if(bind(sd,(void*)&laddr,sizeof(laddr)) < 0){
perror("bind()");
exit(1);
}
//把socket设置成监听模式
//第一个参数要监听的socket
//第二个参数是加入半连接池的最大的C端节点数量
if(listen(sd,200) < 0){
perror("listen()");
exit(1);
}
//接受连接
//第一个参数是要接收的socket
//第二个参数是对端的地址信息
//第三个参数是地址结构体的大小
raddr_len = sizeof(raddr);
while(1){
int newsd = accept(sd,(void*)&raddr,&raddr_len);
if(newsd < 0){
perror("accept()");
exit(1);
}
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
printf("Client:%s:%d\n",ipstr,ntohs(raddr.sin_port));
//发送消息
server_job(newsd);
//关闭连接
close(newsd);
}
//关闭连接
close(sd);
exit(0);
}
客户端client.c:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include "proto.h"
int main(int argc,char** argv){
if(argc < 2){
fprintf(stderr,"Usage...\n");
exit(1);
}
struct sockaddr_in raddr;
long long stamp;
FILE* fp;
//获取socket
int sd = socket(AF_INET,SOCK_STREAM,0);
if(sd < 0){
perror("socket()");
exit(1);
}
//填充对端的地址信息
raddr.sin_family = AF_INET;
raddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET, argv[1], &raddr.sin_addr);
//发起连接
if(connect(sd,(void*)&raddr,sizeof(raddr)) < 0){
perror("connect()");
exit(1);
}
//接收数据
//rcve(); 用这个方法肯定没有问题,也简单
//但这里采用文件操作的方法,因为socket本身就是一个文件
fp = fdopen(sd,"r+");
if(fp == NULL){
perror("fdopen()");
exit(1);
}
//读取数据
if(fscanf(fp, FMT_STAMP, &stamp) < 1){
fprintf(stderr,"Bad format!\n");
}else{
fprintf(stdout,"stamp = %lld\n",stamp);
}
fclose(fp);
exit(1);
}
先启动服务端:
再启动客户端:
再查看服务端可知已经接收到请求并返还了数据:
但是这样的程序是存在效率问题的,这里因为服务端提供的服务只是打印几句话,所以多个请求进来轮转很快,但是假设我们让server_job睡眠一分钟,这意味着每一次请求该服务端都要为该请求进行长达一分钟的服务,那么这样后面的请求就请求不到东西了,这效率显然是很低的,所以我们应该让其并发操作才对。
改实例为并发版本
并发版本只有server.c的代码需要改变:
#include <asm-generic/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <time.h>
#include <arpa/inet.h>
#include "proto.h"
#define IPSTRSIZE 40
#define BUFSIZE 1024
//服务端发送数据的函数
static void server_job(int sd){
char buf[BUFSIZE];
//发送数据
//数据就是一个时戳
int len = sprintf(buf,FMT_STAMP,(long long)time(NULL));
//第一个参数是要操作的socket
//第二个参数是要传输的数据
//第三个参数是数据的大小
//第四个参数是设置一些特殊内容
if(send(sd,buf,len,0) < 0){
perror("send()");
exit(1);
}
sleep(5);
}
int main(){
struct sockaddr_in laddr;
struct sockaddr_in raddr;
char ipstr[IPSTRSIZE];
socklen_t raddr_len;
pid_t pid;
//先拿到socket
int sd = socket(AF_INET,SOCK_STREAM,0);
if(sd < 0){
perror("socket()");
exit(1);
}
//这一步的操作是为了避免服务器端进程被意外杀死后进入time_wait等待状态
//而导致无法迅速重启的问题
int val = 1;
if(setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &val,sizeof(val)) < 0){
perror("setsockopt()");
exit(1);
}
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);
//绑定本地的地址(是哪个端口还有IP地址)
if(bind(sd,(void*)&laddr,sizeof(laddr)) < 0){
perror("bind()");
exit(1);
}
//把socket设置成监听模式
//第一个参数要监听的socket
//第二个参数是加入半连接池的最大的C端节点数量
if(listen(sd,200) < 0){
perror("listen()");
exit(1);
}
//accept函数用来接受建立连接
//第一个参数是要接收的socket
//第二个参数是对端的地址信息
//第三个参数是地址结构体的大小
raddr_len = sizeof(raddr);
while(1){
int newsd = accept(sd,(void*)&raddr,&raddr_len);
if(newsd < 0){
perror("accept()");
exit(1);
}
//能执行到这说明已经没有发生报错,已经正确拿到连接的socket文件描述符
//那么我们就创建子进程进行并发操作
pid = fork();
if(pid < 0){
perror("fork()");
exit(1);
}
//如果为0,那么就是子进程,子进程就去干活
if(pid == 0){
//子进程并不需要用sd
close(sd);
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
printf("Client:%s:%d\n",ipstr,ntohs(raddr.sin_port));
//发送消息
server_job(newsd);
//关闭连接
close(newsd);
//子进程干完活,要退出嗷
exit(0);
}
//父进程不需要使用newsd
close(newsd);
//父进程就继续建立连接然后创建子进程
}
//关闭连接
close(sd);
exit(0);
}
此时多个客户端进行访问也没有问题。
静态进程池实现
既然提到进程池,那肯定就是服务端的代码需要改啦:
#include <asm-generic/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <time.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include "proto.h"
#define IPSTRSIZE 40
#define BUFSIZE 1024
#define PROCNUM 4 //定义进程池中的进程个数
static void server_loop(int sd);
//服务端发送数据的函数
static void server_job(int sd){
char buf[BUFSIZE];
//发送数据
//数据就是一个时戳
int len = sprintf(buf,FMT_STAMP,(long long)time(NULL));
//第一个参数是要操作的socket
//第二个参数是要传输的数据
//第三个参数是数据的大小
//第四个参数是设置一些特殊内容
if(send(sd,buf,len,0) < 0){
perror("send()");
exit(1);
}
}
int main(){
struct sockaddr_in laddr;
pid_t pid;
//先拿到socket
int sd = socket(AF_INET,SOCK_STREAM,0);
if(sd < 0){
perror("socket()");
exit(1);
}
//这一步的操作是为了避免服务器端进程被意外杀死后进入time_wait等待状态
//而导致无法迅速重启的问题
int val = 1;
if(setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &val,sizeof(val)) < 0){
perror("setsockopt()");
exit(1);
}
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);
//绑定本地的地址(是哪个端口还有IP地址)
if(bind(sd,(void*)&laddr,sizeof(laddr)) < 0){
perror("bind()");
exit(1);
}
//把socket设置成监听模式
//第一个参数要监听的socket
//第二个参数是加入半连接池的最大的C端节点数量
if(listen(sd,200) < 0){
perror("listen()");
exit(1);
}
for(int i=0;i<PROCNUM;i++){
pid = fork();
if(pid < 0){
perror("fork()");
exit(1);
}
if(pid == 0){
//子进程干活
server_loop(sd);
exit(0);
}
}
//给子进程收尸
for(int i=0; i<PROCNUM; i++){
wait(NULL);
}
//关闭sd
close(sd);
exit(0);
}
//子进程要做的事情:循环接收连接干活
static void server_loop (int sd){
struct sockaddr_in raddr;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
//接受连接
//第一个参数是要接收的socket
//第二个参数是对端的地址信息
//第三个参数是地址结构体的大小
raddr_len = sizeof(raddr);
while(1){
//accept函数本身就是有锁的,如果有四个进程同时要执行accept
//只会有一个进程能够执行,而其它的会阻塞
int newsd = accept(sd,(void*)&raddr,&raddr_len);
if(newsd < 0){
perror("accept()");
exit(1);
}
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
printf("[%d]Client:%s:%d\n",getpid(),ipstr,ntohs(raddr.sin_port));
//发送消息
server_job(newsd);
//关闭连接
close(newsd);
}
}
运行效果:
动态进程池
静态进程池的缺陷是,其进程池中进程的数量是写死的。那么这样在连接数量过大时它会无法适应,连接数量过少的时候又会资源浪费,所以我们需要一种可伸缩的、有弹性的进程池,可以依据连接数量的情况动态的增加或者减少进程的数量,这就是动态进程池。
下面就是一个比较规范的动态进程池写法了,需要好好消化:
server.c:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <errno.h>
#include <sys/mman.h>
#include <unistd.h>
#include <time.h>
#include <arpa/inet.h>
#include "proto.h"
//定义进程池中的进程个数上下限
#define MIN_SPARE_SERVER 5 //最小空闲个数5
#define MAX_SPARE_SERVER 10 //最大空闲个数10
#define MAX_CLIENTS 20 //支持最大数量的客户端上限,表示最多同时接收20个client连接
//这意味着最多会有同时二十个进程在跑
//SISUSER2是系统预留给我们的用来自定义行为的一个信号
#define SIG_NOTIFY SIGUSR2
#define IPSTRSIZE 40
#define LINEBUFSIZE 80
//server端状态枚举
enum{
STATE_IDEL=0,//空闲
STATE_BUSY //忙碌
};
//每个进程的抽象数据结构
struct server_st{
pid_t pid;
int state;
};
//采用堆上动态分配server空间的方式来创建每一个进程
static struct server_st* server_pool;
//全局计数器,记录空闲和忙碌的进程共有多少个
static int idle_count = 0,busy_count = 0;
//sd是所有进程共享的
static int sd;
//信号处理函数
static void usr2_handler(int s){
return;
}
//进程的任务函数
static void server_job(int pos){
int ppid;
struct sockaddr_in raddr;
socklen_t raddr_len;
int client_sd;
char ipstr[IPSTRSIZE];
char linebuf[LINEBUFSIZE];
time_t stamp;
int len;
//获得父进程的标识
ppid = getppid();
while(1){
server_pool[pos].state = STATE_IDEL;
//通知父进程确认当前进程的状态
kill(ppid,SIG_NOTIFY);
//接收连接
client_sd = accept(sd,(void*)&raddr,&raddr_len);
if(client_sd < 0){
//不是假错的话
if(errno != EINTR || errno != EAGAIN){
perror("accept()");
exit(1);
}
}
//将自己的状态设置为busy
server_pool[pos].state = STATE_BUSY;
//通知父进程自己的状态
kill(ppid,SIG_NOTIFY);
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
// printf("[%d]client:%s:%d\n",getpid(),ipstr,ntohs(raddr.sin_port));
stamp = time(NULL);
len = snprintf(linebuf,LINEBUFSIZE,FMT_STAMP,(long long)stamp);
//发送数据
send(client_sd,linebuf,len,0);
sleep(5);
//关闭客户端连接
close(client_sd);
}
}
//添加一个进程
static int add_1_server(void){
pid_t pid;
int slot;
//超过界限就报错
if(idle_count + busy_count >= MAX_CLIENTS){
return -1;
}
for(slot=0;slot<MAX_CLIENTS;slot++){
//因为pid初识就为-1,所以如果为-1则表示该进程还没激活
if(server_pool[slot].pid == -1){
break;
}
}
//然后将该进程设置为空闲态
server_pool[slot].state = STATE_IDEL;
pid = fork();
if(pid < 0){
perror("fork()");
exit(1);
}
if(pid == 0){ //child
server_job(slot);
exit(0);
}else{ //parent
server_pool[slot].pid = pid;
idle_count++;
}
return 0;
}
//删除一个进程
static int del_1_server(void){
if(idle_count == 0){
return -1;
}
for(int i=0; i<MAX_CLIENTS;i++){
//如果该进程为空闲进程且已经被激活了
if(server_pool[i].pid != -1 && server_pool[i].state == STATE_IDEL){
//那么就杀死该进程
kill(server_pool[i].pid,SIGTERM);
server_pool[i].pid = -1; //pid值要重新设置为-1
idle_count --; //空闲进程数--
break;
}
}
return 0;
}
//遍历当前的进程池
static int scan_poll(void){
int busy = 0,idle = 0;
for(int i=0; i < MAX_CLIENTS; i++){
if(server_pool[i].pid == -1){
continue;
}
//检查进程是否存在
//若不存在
if(kill(server_pool[i].pid,0)){
server_pool[i].pid = -1;
continue;
}
if(server_pool[i].state == STATE_IDEL){
idle++;
}else if(server_pool[i].state == STATE_BUSY){
busy++;
}else{
//说明出现了意料之外的错误
fprintf(stderr,"Unknown state.\n");
//杀掉当前进程并立即获得一个core dump文件
abort();
}
}
idle_count = idle;
busy_count = busy;
return 0;
}
int main(){
struct sigaction sa,osa;
struct sockaddr_in laddr;
sigset_t set,oset;
/*
* 下面这几句与信号相关的代码意思是:
* 不用等待主进程去进行收尸,让子进程结束的时候自行消亡;
* 我们对子进程进行了新行为的定义并将之前的旧行为放到了osa中
* */
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_NOCLDWAIT;
sigaction(SIGCHLD,&sa,&osa);
sa.sa_handler = usr2_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIG_NOTIFY,&sa,&osa);
sigemptyset(&set);
sigaddset(&set,SIG_NOTIFY);
sigprocmask(SIG_BLOCK, &set, &oset);
//使用mmap来进行内存分配,malloc也可
//此时相当于server_pool就是一个数组名
server_pool = mmap(NULL,sizeof(struct server_st)*MAX_CLIENTS,PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS,-1,0);
if(server_pool == MAP_SHARED){
perror("mmap()");
exit(1);
}
//初始化
for(int i=0; i<MAX_CLIENTS;i++){
server_pool[i].pid = -1;
}
sd = socket(AF_INET,SOCK_STREAM,0);
if(sd < 0){
perror("socket()");
exit(1);
}
//设置可重连的socket属性
int val = 1;
if(setsockopt(sd,SOL_SOCKET,SO_REUSEADDR,&val,sizeof(val)) < 0){
perror("setsockopt()");
exit(1);
}
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);
if(bind(sd,(void*)&laddr,sizeof(laddr)) < 0){
perror("bind()");
exit(1);
}
if(listen(sd,100) < 0){
perror("listen()");
exit(1);
}
for(int i=0; i<MIN_SPARE_SERVER; i++){
add_1_server();
}
while(1){
sigsuspend(&oset);
scan_poll();
//contrl the pool
//如果空闲进程太多,就杀掉一些
if(idle_count > MAX_SPARE_SERVER){
for(int i=0;i <(idle_count - MAX_SPARE_SERVER);i++){
del_1_server();
}
}
//如果空闲进程太少,那就增加一些
else if(idle_count < MIN_SPARE_SERVER){
for(int i=0; i<(MIN_SPARE_SERVER - idle_count);i++){
add_1_server();
}
}
//打印当前进程的状态
for(int i=0; i<MAX_CLIENTS; i++){
if(server_pool[i].pid == -1){
putchar(' ');
}else if(server_pool[i].state == STATE_IDEL){
putchar('.');
}else{
putchar('X');
}
}
putchar('\n');
}
sigprocmask(SIG_SETMASK, &oset, NULL);
exit(0);
}
当我们使用多个终端一直发送请求时:
可以看到此时正在疯狂响应,请求数量越多,X的数量就越多。
进程池的设计还是比较综合的,后续有时间还得好好研究一下这段代码。