本文介绍网络编程的基础知识,socket 的库函数,网络通讯的原理,TCP缓存,I/O 复用的模型(select、poll、epoll),非阻塞的I/O
第一个网络通讯程序
客户/服务器
网络通讯是指两台计算机中的程序进行传输数据的过程
客户程序(端):指主动发起通讯的程序
服务程序(端/器):指被动的等待,然后为向它发起通讯的客户端提供服务
客户端必须提前知道服务端的 IP 地址和通讯端口
服务端不需要知道客户端的 IP 地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std;int main (int argc, char * argv[]) { if (argc != 3 ) { cout << "Using: ./demo1 服务端的 IP 服务端的端口 \nExample: ./demo1 192.168.112.131 5005\n\n" ; return -1 ; } int sockfd = socket (AF_INET, SOCK_STREAM, 0 ); if (sockfd == -1 ) { perror ("socket" ); return -1 ; } struct hostent * h; if ((h = gethostbyname (argv[1 ])) == 0 ) { cout << "gethostbyname failed.\n" << endl; close (sockfd); return -1 ; } struct sockaddr_in servaddr; memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (atoi (argv[2 ])); memcpy (&servaddr.sin_addr, h->h_addr, h->h_length); if (connect (sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) != 0 ) { perror ("connect" ); close (sockfd); return -1 ; } char buffer[1024 ]; for (int ii=0 ; ii < 3 ; ii++) { int iret; memset (buffer, 0 , sizeof (buffer)); sprintf (buffer, "这是第 %d 个报文,编号 %03d" , ii + 1 , ii + 1 ); if ((iret=write (sockfd, buffer, strlen (buffer))) <= 0 ) { perror ("send" ); break ; } cout << "发送:" << buffer << endl; memset (buffer, 0 , sizeof (buffer)); if ((iret=read (sockfd, buffer, sizeof (buffer))) <= 0 ) { cout << "iret=" << iret << endl; break ; } cout << "接收:" << buffer << endl; sleep (1 ); } close (sockfd); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std;int main (int argc, char * argv[]) { if (argc != 2 ) { cout << "Using: ./demo2 通讯端口 \nExample: ./demo2 5005\n\n" ; cout << "注意:运行服务端程序的 Linux 系统的防火墙必须要开通5005端口\n" ; cout << " 如果是云服务器,还要开通云平台的访问策略\n\n" ; } int listenfd = socket (AF_INET, SOCK_STREAM, 0 ); if (listenfd == -1 ) { perror ("socket" ); return -1 ; } struct sockaddr_in servaddr; memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl (INADDR_ANY); servaddr.sin_port = htons (atoi (argv[1 ])); if (bind (listenfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) != 0 ) { perror ("bind" ); close (listenfd); return -1 ; } if (listen (listenfd, 5 ) != 0 ) { perror ("listen" ); close (listenfd); return -1 ; } int clientfd = accept (listenfd, 0 , 0 ); if (clientfd == -1 ) { perror ("accept" ); close (listenfd); return -1 ; } cout << "客户端已连接\n" ; char buffer[1024 ]; while (true ) { int iret; memset (buffer, 0 , sizeof (buffer)); if ((iret = recv (clientfd, buffer, sizeof (buffer), 0 )) <= 0 ) { cout << "iret=" << iret << endl; break ; } cout << "接收:" << buffer << endl; strcpy (buffer, "ok" ); if ((iret=send (clientfd, buffer, strlen (buffer), 0 )) <= 0 ) { perror ("send" ); break ; } cout << "发送:" << buffer << endl; } close (listenfd); close (clientfd); }
1 2 3 4 5 6 7 8 // 客户端 终端 [zxc@study CppNetworkProgramming]$ ./demo1 192.168.112.131 5005 发送:这是第 1 个报文,编号 001 接收:ok 发送:这是第 2 个报文,编号 002 接收:ok 发送:这是第 3 个报文,编号 003 接收:ok
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // 服务端 终端 [zxc@study CppNetworkProgramming]$ ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 00:0c:29:43:d5:19 brd ff:ff:ff:ff:ff:ff inet 192.168.112.131/24 brd 192.168.112.255 scope global noprefixroute dynamic ens33 valid_lft 1323sec preferred_lft 1323sec inet6 fe80::9ed2:1872:20e3:d79a/64 scope link noprefixroute valid_lft forever preferred_lft forever 3: virbr0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000 link/ether 52:54:00:d1:15:4a brd ff:ff:ff:ff:ff:ff inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0 valid_lft forever preferred_lft forever 4: virbr0-nic: <BROADCAST,MULTICAST> mtu 1500 qdisc pfifo_fast master virbr0 state DOWN group default qlen 1000 link/ether 52:54:00:d1:15:4a brd ff:ff:ff:ff:ff:ff [zxc@study CppNetworkProgramming]$ ./demo2 5005 客户端已连接 接收:这是第 1 个报文,编号 001 发送:ok 接收:这是第 2 个报文,编号 002 发送:ok 接收:这是第 3 个报文,编号 003 发送:ok iret=0
客户端和服务端程序可以运行在不同的虚拟机上,也可以运行在同一个虚拟机上,网络通信的客户端和服务端是逻辑的概念,与物理计算机和操作系统没有关系
基于 Linux 的文件操作
对 Linux 来说,socket 操作与文件操作没有区别
在网络传输数据的过程中,可以使用文件的 I/O 函数
文件描述符是 Linux 分配给文件或 socket 的整数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> int main () { int fd; fd = open ("data.txt" , O_CREAT|O_RDWR|O_TRUNC); if (fd == -1 ) { perror ("open(data.txt)" ); return -1 ; } printf ("文件描述符 fd=%d\n" , fd); char buffer[1024 ]; strcpy (buffer, "我是渺如星辰\n" ); if (write (fd, buffer, strlen (buffer)) == -1 ) { perror ("write()" ); return -1 ; } close (fd); }
1 2 3 4 5 6 7 8 // 终端 [zxc@study CppNetworkProgramming]$ g++ -g -o demo3 demo3.cpp [zxc@study CppNetworkProgramming]$ ./demo3 文件描述符 fd=3 [zxc@study CppNetworkProgramming]$ ls data.txt demo1 demo1.cpp demo2 demo2.cpp demo3 demo3.cpp demo4.cpp [zxc@study CppNetworkProgramming]$ cat data.txt 我是渺如星辰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> int main () { int fd; fd = open ("data.txt" , O_RDONLY); if (fd==-1 ) { perror ("open(data.txt)" ); return -1 ; } printf ("文件描述符 fd=%d\n" , fd); char buffer[1024 ]; memset (buffer, 0 , sizeof (buffer)); if (read (fd, buffer, sizeof (buffer)) == -1 ) { perror ("write()" ); return -1 ; } printf ("%s" , buffer); close (fd); }
1 2 3 4 5 // 终端 [zxc@study CppNetworkProgramming]$ g++ -g -o demo4 demo4.cpp [zxc@study CppNetworkProgramming]$ ./demo4 文件描述符 fd=3 我是渺如星辰
文件描述符的分配规则
/proc/进程id/fd目录中,存放了每个进程打开的 fd
Linux 进程默认打开了三个文件描述符:0-标准输入(键盘),1-标准输出(显示器),2-标准错误(显示器) cin cout cerr
文件描述符的分配规则是:找到最小的,没有被占用的文件描述符
socket 函数详解 协议 协议是网络通讯的规则,是约定
创建 socket 1 2 3 4 5 int socket (int domain, int type, int protocol) ;
domain 通讯的协议家族 PF_INET IPv4 互联网协议族PF_INET6 IPv6 互联网协议族PF_LOCAL 本地通信的协议族PF_PACKET 内核底层的协议族PF_IPX IPX Novell 协议族 IPv6 尚未普及,其它的不常用
type 数据传输的类型 SOCK_STREAM 面向连接的 socket:数据不会丢失;数据的顺序不会错乱;双向通道SOCK_DGRAM 无连接的 socket:数据可能会丢失;数据的顺序可能会错乱;传输的效率更高
protocol 最终使用的协议 在 IPv4 网络协议家族中,数据传输方式为 SOCK_STREAM 的协议只有 IPPROTO_TCP ,数据传输方式为 SOCK_DGRAM 的协议只有 IPPROTO_UDP 本参数也可以填0
1 2 socket (PF_INET, SOCK_STREAM, IPPROTO_TCP); socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP);
TCP和UDP
TCP 和 UDP 的区别 TCP TCP 面向连接,通过三次握手建立连接,四次挥手断开连接 TCP 是可靠的通信方式,通过超时重传、数据校验等方式来确保数据无差错,不丢失,不重复,并且按序到达 TCP 把数据当成字节流,当网络出现波动时,连接可能出现响应延迟的问题 TCP 只支持点对点通信 TCP 报文的首部较大,为20字节 TCP 是全双工的可靠信道UDP UDP 是无连接的,即发送数据之前不需要建立连接,这种方式为 UDP 带来了高效的传输效率,但也导致无法确保数据的发送成功 UDP 以最大的速率进行传输,但不保证可靠交付,会出现丢失、重复等等问题 UDP 没有拥塞控制,当网络出现拥塞时,发送方不会降低发送速率 UDP 支持一对一,一对多,多对一和多对多的通信 UDP 报文的首部比较小,只有8字节 UDP 是不可靠信道
TCP 保证自身可靠的方式 1)数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组 2)到达确认:接收端接收到分片数据时,根据分片的序号向对端回复一个确认包 3)超时重发:发送方在发送分片后开始计时,若超时却没有收到对端的确认包,将会重发分片 4)滑动窗口:TCP 中采用滑动窗口来进行传输控制,发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为0时,发送方不会再发送数据 5)失序处理:TCP 的接收端会把接收到的数据重新排序 6)重复处理:如果传输的分片出现重复,TCP 的接收端会丢弃重复的数据 7)数据校验:TCP 通过数据的校验和来判断数据在传输过程中是否正确
UDP 不可靠的原因 没有上述 TCP 的机制,如果校验和出错,UDP 会将该报文丢弃
TCP 和 UDP 使用场景 TCP 使用场景 TCP 实现了数据传输过程中的各种控制,适合对可靠性有要求的场景UDP 使用场景 可以容忍数据丢失的场景: 1)视频、音频等多媒体通信(即时通信) 2)广播信息
UDP 能实现可靠传输吗 如果用 UDP 实现可靠传输,应用程序必须实现重传和排序等功能,非常麻烦,还不如直接用TCP
主机字节序与网络字节序 大端序/小端序 如果数据类型占用的内存空间大于1字节,CPU 把数据存放在内存中的方式有两种 大端序(Big Endian):低位字节存放在高位,高位字节存放在低位 小端序(Little Endian):低位字节存放在地位,高位字节存放在高位 假设从内存地址 0x00000001 处开始存储十六进制数 0x12345678,那么 Big-Endian(按原来顺序存储) 0x00000001 0x12 0x00000002 0x34 0x00000003 0x56 0x00000004 0x78 Little-Endian(颠倒顺序存储) 0x00000001 0x78 0x00000002 0x56 0x00000003 0x34 0x00000004 0x12 Intel 系列的 CPU 以小端序方式保存数据,其它型号的 CPU 不一定 操作文件的本质是把内存中的数据写入磁盘,在网络编程中,传输数据的本质也是把数据写入文件(socket 也是文件描述符) 这样的话,字节序不同的计算机之间传输数据,可能会出现问题
网络字节序 为了解决不同字节序的计算机之间传输数据的问题,约定采用网络字节序(大端序) C 语言提供了四个库函数,用于在主机字节序和网络字节序之间转换
1 2 3 4 5 6 7 8 9 uint16_t htons (uint16_t hostshort) ; uint32_t htonl (uint32_t hostlong) ; uint16_t ntohs (uint16_t netshort) ;uint32_t ntohl (uint32_t netlong) ;
IP 地址和通讯端口 在计算机中,IPv4 的地址用4字节的整数存放,通讯端口用2字节的整数(0-65535)存放
如何处理大小端序 在网络编程中,数据收发的时候中有自动转换机制,不需要程序员手动转换,只有向 sockaddr_in 结构体成员变量填充数据时,才需要考虑字节序的问题
sockaddr 结构体 sockaddr 结构体 存放协议族、端口和地址信息,客户端的 connect() 函数和服务端的 bind() 函数需要这个结构体
1 2 3 4 struct sockaddr { unsigned short sa_family; unsigned char sa_data[14 ]; };
sockaddr_in 结构体 sockaddr 结构体是为了统一地址结构的表示方法,统一接口函数,但是操作不方便,所以定义了等价的 sockaddr_in 结构体,它的大小与 sockaddr 相同,可以强制转换成 sockaddr
1 2 3 4 5 6 7 8 9 10 struct sockaddr_in { unsigned short sin_family; unsigned short sin_port; struct in_addr sin_addr; unsigned char sin_zero[8 ]; }; struct in_addr { unsigned int s_addr; };
gethostbyname 函数 根据域名/主机名/字符串 IP 获取大端序 IP,用于网络通讯的客户端程序中
1 2 3 4 5 6 7 8 9 struct hostent *gethostbyname (const char *name);struct hostent { char *h_name; char **h_aliases; short h_addrtype; short h_length; char **h_addr_list; }; #define h_addr h_addr_list[0]
转换后,用以下代码把大端序的地址复制到 sockaddr_in 结构体的 sin_addr 成员中
1 memcpy (&servaddr.sin_addr, h->h_addr, h->h_length);
字符串 IP 与大端序 IP 的转换 C语言提供了几个库函数,用于字符串格式的 IP 和大端序 IP 的互相转换,用于网络通讯的服务端程序中
1 2 3 4 5 6 7 8 9 10 typedef unsigned int in_addr_t ; in_addr_t inet_addr (const char *cp) ;int inet_aton (const char *cp, struct in_addr *inp) ;char *inet_ntoa (struct in_addr in) ;
封装 socket
网络编程涉及到多个数据结构和函数,使用起来不方便
把客户端程序用到的数据结构和函数封装成 ctcpclient 类
把服务端程序用到的数据结构和函数封装成 ctcpserver 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std;class ctcpclient { private : int m_clientfd; string m_ip; unsigned short m_port; public : ctcpclient () : m_clientfd (-1 ) {} bool connect (const string &in_ip, const unsigned short in_port) { if (m_clientfd != -1 ) return false ; m_ip = in_ip; m_port = in_port; if ((m_clientfd = socket (AF_INET, SOCK_STREAM, 0 )) == -1 ) { return false ; } struct sockaddr_in servaddr; memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (m_port); struct hostent * h; if ((h = gethostbyname (m_ip.c_str ())) == nullptr ) { ::close (m_clientfd); m_clientfd = -1 ; return false ; } memcpy (&servaddr.sin_addr, h->h_addr, h->h_length); if (::connect (m_clientfd, (struct sockaddr *)&servaddr, sizeof (servaddr)) == -1 ) { ::close (m_clientfd); m_clientfd = -1 ; return false ; } return true ; } bool send (const string &buffer) { if (m_clientfd == -1 ) return false ; if ((::send (m_clientfd, buffer.data (), buffer.size (), 0 )) <= 0 ) return false ; return true ; } bool recv (string &buffer, const size_t maxlen) { buffer.clear (); buffer.resize (maxlen); int readn = ::recv (m_clientfd, &buffer[0 ], buffer.size (), 0 ); if (readn <= 0 ) { buffer.clear (); return false ; } buffer.resize (readn); return true ; } bool close () { if (m_clientfd == -1 ) return false ; ::close (m_clientfd); m_clientfd = -1 ; return true ; } ~ctcpclient () { close (); } }; int main (int argc, char * argv[]) { if (argc != 3 ) { cout << "Using: ./demo5 服务端的 IP 服务端的端口 \nExample: ./demo5 192.168.112.131 5005\n\n" ; return -1 ; } ctcpclient tcpclient; if (tcpclient.connect (argv[1 ], atoi (argv[2 ])) == false ) { perror ("connect()" ); return -1 ; } string buffer; for (int ii = 0 ; ii < 3 ; ii++) { buffer = "这是第" + to_string (ii + 1 ) + "个报文,编号" + to_string (ii + 1 ); if (tcpclient.send (buffer) == false ) { perror ("send" ); break ; } cout << "发送:" << buffer << endl; if (tcpclient.recv (buffer, 1024 ) == false ) { perror ("recv()" ); break ; } cout << "接收:" << buffer << endl; sleep (1 ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std;class ctcpserver { private : int m_listenfd; int m_clientfd; string m_clientip; unsigned short m_port; public : ctcpserver () : m_listenfd (-1 ), m_clientfd (-1 ) {} bool initserver (const unsigned short in_port) { if ((m_listenfd = socket (AF_INET, SOCK_STREAM, 0 )) == -1 ) return false ; m_port = in_port; struct sockaddr_in servaddr; memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (m_port); servaddr.sin_addr.s_addr = htonl (INADDR_ANY); if (bind (m_listenfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) == -1 ) { close (m_listenfd); m_listenfd = -1 ; return false ; } if (listen (m_listenfd, 5 ) == -1 ) { close (m_listenfd); m_listenfd = -1 ; return false ; } return true ; } bool accept () { struct sockaddr_in caddr; socklen_t addrlen = sizeof (caddr); if ((m_clientfd = ::accept (m_listenfd, 0 , 0 )) == -1 ) return false ; m_clientip = inet_ntoa (caddr.sin_addr); return true ; } const string& clientip () const { return m_clientip; } bool send (const string& buffer) { if (m_clientfd == -1 ) return false ; if ((::send (m_clientfd, buffer.data (), buffer.size (), 0 )) <= 0 ) return false ; return true ; } bool recv (string& buffer, const size_t maxlen) { buffer.clear (); buffer.resize (maxlen); int readn = ::recv (m_clientfd, &buffer[0 ], buffer.size (), 0 ); if (readn <= 0 ) { buffer.clear (); return false ; } buffer.resize (readn); return true ; } bool closelisten () { if (m_listenfd == -1 ) return false ; close (m_listenfd); m_listenfd = -1 ; return true ; } bool closeclient () { if (m_clientfd == -1 ) return false ; close (m_clientfd); m_clientfd = -1 ; return true ; } ~ctcpserver () { closelisten (); closeclient (); } }; int main (int argc, char * argv[]) { if (argc != 2 ) { cout << "Using: ./demo2 通讯端口 \nExample: ./demo2 5005\n\n" ; cout << "注意:运行服务端程序的 Linux 系统的防火墙必须要开通5005端口\n" ; cout << " 如果是云服务器,还要开通云平台的访问策略\n\n" ; return -1 ; } ctcpserver tcpserver; if (tcpserver.initserver (atoi (argv[1 ])) == false ) { perror ("initserver()" ); return -1 ; } if (tcpserver.accept () == false ) { perror ("accept()" ); return -1 ; } cout << "客户端已连接(" <<tcpserver.clientip () << ")\n" ; string buffer; while (true ) { if (tcpserver.recv (buffer, 1024 ) == false ) { perror ("recv()" ); break ; } cout << "接收:" << buffer << endl; buffer = "ok" ; if (tcpserver.send (buffer) == false ) { perror ("send" ); break ; } cout << "发送:" << buffer << endl; } }
多进程的服务端 在多进程的服务程序中,如果子进程收到退出信号,子进程自行退出,如果父进程收到退出信号,则应该先向全部的子进程发送退出信号,然后自己再退出 父进程只受理客户端的连接,不与客户端通讯 子进程负责与客户端通讯,不受理客户端的连接fork 之后,父进程不需要 tcpserver.m_clientfd ,子进程不需要 tcpserver.m_listenfd
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <signal.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std;class ctcpserver { private : int m_listenfd; int m_clientfd; string m_clientip; unsigned short m_port; public : ctcpserver () : m_listenfd (-1 ), m_clientfd (-1 ) {} bool initserver (const unsigned short in_port) { if ((m_listenfd = socket (AF_INET, SOCK_STREAM, 0 )) == -1 ) return false ; m_port = in_port; struct sockaddr_in servaddr; memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (m_port); servaddr.sin_addr.s_addr = htonl (INADDR_ANY); if (bind (m_listenfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) == -1 ) { close (m_listenfd); m_listenfd = -1 ; return false ; } if (listen (m_listenfd, 5 ) == -1 ) { close (m_listenfd); m_listenfd = -1 ; return false ; } return true ; } bool accept () { struct sockaddr_in caddr; socklen_t addrlen = sizeof (caddr); if ((m_clientfd = ::accept (m_listenfd, 0 , 0 )) == -1 ) return false ; m_clientip = inet_ntoa (caddr.sin_addr); return true ; } const string& clientip () const { return m_clientip; } bool send (const string& buffer) { if (m_clientfd == -1 ) return false ; if ((::send (m_clientfd, buffer.data (), buffer.size (), 0 )) <= 0 ) return false ; return true ; } bool recv (string& buffer, const size_t maxlen) { buffer.clear (); buffer.resize (maxlen); int readn = ::recv (m_clientfd, &buffer[0 ], buffer.size (), 0 ); if (readn <= 0 ) { buffer.clear (); return false ; } buffer.resize (readn); return true ; } bool closelisten () { if (m_listenfd == -1 ) return false ; close (m_listenfd); m_listenfd = -1 ; return true ; } bool closeclient () { if (m_clientfd == -1 ) return false ; close (m_clientfd); m_clientfd = -1 ; return true ; } ~ctcpserver () { closelisten (); closeclient (); } }; ctcpserver tcpserver; void FathEXIT (int sig) ; void ChldEXIT (int sig) ; int main (int argc, char * argv[]) { if (argc != 2 ) { cout << "Using: ./demo7 通讯端口 \nExample: ./demo7 5005\n\n" ; cout << "注意:运行服务端程序的 Linux 系统的防火墙必须要开通5005端口\n" ; cout << " 如果是云服务器,还要开通云平台的访问策略\n\n" ; return -1 ; } for (int ii = 1 ; ii <= 64 ; ii++) signal (ii, SIG_IGN); signal (SIGTERM, FathEXIT); signal (SIGINT, FathEXIT); if (tcpserver.initserver (atoi (argv[1 ])) == false ) { perror ("initserver()" ); return -1 ; } while (true ) { if (tcpserver.accept () == false ) { perror ("accept()" ); return -1 ; } int pid = fork(); if (pid == -1 ) { perror ("fork()" ); return -1 ; } if (pid > 0 ) { tcpserver.closeclient (); continue ; } tcpserver.closelisten (); signal (SIGTERM, ChldEXIT); signal (SIGINT, SIG_IGN); cout << "客户端已连接(" <<tcpserver.clientip () << ")\n" ; string buffer; while (true ) { if (tcpserver.recv (buffer, 1024 ) == false ) { perror ("recv()" ); break ; } cout << "接收:" << buffer << endl; buffer = "ok" ; if (tcpserver.send (buffer) == false ) { perror ("send" ); break ; } cout << "发送:" << buffer << endl; } return 0 ; } } void FathEXIT (int sig) { signal (SIGINT, SIG_IGN); signal (SIGTERM, SIG_IGN); cout << "父进程退出,sig=" << sig << endl; kill (0 , SIGTERM); tcpserver.closelisten (); exit (0 ); } void ChldEXIT (int sig) { signal (SIGINT, SIG_IGN); signal (SIGTERM, SIG_IGN); cout << "子进程" << getpid () << "退出,sig=" << sig << endl; tcpserver.closeclient (); exit (0 ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 终端 父进程关闭客户端连接的 socket,子进程关闭监听的 socket [zxc@study CppNetworkProgramming]$ ps -ef|grep demo7 zxc 40134 38359 0 23:43 pts/1 00:00:00 ./demo7 5005 zxc 40136 40134 0 23:43 pts/1 00:00:00 ./demo7 5005 zxc 40145 39974 0 23:43 pts/3 00:00:00 grep --color=auto demo7 [zxc@study CppNetworkProgramming]$ ls /proc/40134/fd 0 1 2 3 [zxc@study CppNetworkProgramming]$ ls /proc/40136/fd 0 1 2 4 [zxc@study CppNetworkProgramming]$ ps -ef|grep demo7 zxc 40134 38359 0 23:43 pts/1 00:00:00 ./demo7 5005 zxc 40136 40134 0 23:43 pts/1 00:00:00 ./demo7 5005 zxc 40176 40134 0 23:44 pts/1 00:00:00 ./demo7 5005 zxc 40178 39974 0 23:44 pts/3 00:00:00 grep --color=auto demo7 [zxc@study CppNetworkProgramming]$ ls /proc/40134/fd 0 1 2 3 [zxc@study CppNetworkProgramming]$ ls /proc/40136/fd 0 1 2 4 [zxc@study CppNetworkProgramming]$ ls /proc/40176/fd 0 1 2 4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 终端 父进程不关闭客户端连接的 socket,子进程不关闭监听的 socket [zxc@study CppNetworkProgramming]$ ps -ef|grep demo7 zxc 2774 2717 0 23:53 pts/0 00:00:00 ./demo7 5005 zxc 2945 2774 0 23:54 pts/0 00:00:00 ./demo7 5005 zxc 2948 2822 0 23:55 pts/2 00:00:00 grep --color=auto demo7 [zxc@study CppNetworkProgramming]$ ls /proc/2774/fd 0 1 2 3 4 [zxc@study CppNetworkProgramming]$ ls /proc/2945/fd 0 1 2 3 4 [zxc@study CppNetworkProgramming]$ ps -ef|grep demo7 zxc 2774 2717 0 23:53 pts/0 00:00:00 ./demo7 5005 zxc 2945 2774 0 23:54 pts/0 00:00:00 ./demo7 5005 zxc 2977 2774 0 23:55 pts/0 00:00:00 ./demo7 5005 zxc 2979 2822 0 23:55 pts/2 00:00:00 grep --color=auto demo7 [zxc@study CppNetworkProgramming]$ ls /proc/2774/fd 0 1 2 3 4 5 [zxc@study CppNetworkProgramming]$ ls /proc/2945/fd 0 1 2 3 4 [zxc@study CppNetworkProgramming]$ ls /proc/2977/fd 0 1 2 3 4 5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 // 终端 部分 // 服务端 [zxc@study CppNetworkProgramming]$ ./demo7 5005 客户端已连接(141.19.0.0) 接收:这是第1个报文,编号1 发送:ok 客户端已连接(0.0.0.0) 接收:这是第1个报文,编号1 发送:ok 接收:这是第2个报文,编号2 发送:ok 接收:这是第2个报文,编号2 发送:ok 接收:这是第3个报文,编号3 发送:ok 接收:这是第3个报文,编号3 发送:ok 接收:这是第4个报文,编号4 发送:ok 接收:这是第4个报文,编号4 发送:ok 接收:这是第5个报文,编号5 发送:ok 接收:这是第5个报文,编号5 发送:ok 接收:这是第6个报文,编号6 发送:ok 接收:这是第6个报文,编号6 发送:ok 接收:这是第7个报文,编号7 发送:ok 接收:这是第7个报文,编号7 发送:ok ^C父进程退出,sig=2 子进程40256退出,sig=15 子进程40254退出,sig=15 // 1号客户端 [zxc@study CppNetworkProgramming]$ ./demo5 192.168.112.131 5005 发送:这是第1个报文,编号1 接收:ok 发送:这是第2个报文,编号2 接收:ok 发送:这是第3个报文,编号3 接收:ok 发送:这是第4个报文,编号4 接收:ok 发送:这是第5个报文,编号5 接收:ok 发送:这是第6个报文,编号6 接收:ok 发送:这是第7个报文,编号7 接收:ok 发送:这是第8个报文,编号8 recv(): Success // 2号客户端 [zxc@study CppNetworkProgramming]$ ./demo5 192.168.112.131 5005 发送:这是第1个报文,编号1 接收:ok 发送:这是第2个报文,编号2 接收:ok 发送:这是第3个报文,编号3 接收:ok 发送:这是第4个报文,编号4 接收:ok 发送:这是第5个报文,编号5 接收:ok 发送:这是第6个报文,编号6 接收:ok 发送:这是第7个报文,编号7 接收:ok 发送:这是第8个报文,编号8 recv(): Success
实现文件传输功能 采用 socket 通讯,客户端把指定文件传输给服务端(支持文本文件和二进制文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 #include <iostream> #include <fstream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std;class ctcpclient { private : int m_clientfd; string m_ip; unsigned short m_port; public : ctcpclient () : m_clientfd (-1 ) {} bool connect (const string &in_ip, const unsigned short in_port) { if (m_clientfd != -1 ) return false ; m_ip = in_ip; m_port = in_port; if ((m_clientfd = socket (AF_INET, SOCK_STREAM, 0 )) == -1 ) { return false ; } struct sockaddr_in servaddr; memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (m_port); struct hostent * h; if ((h = gethostbyname (m_ip.c_str ())) == nullptr ) { ::close (m_clientfd); m_clientfd = -1 ; return false ; } memcpy (&servaddr.sin_addr, h->h_addr, h->h_length); if (::connect (m_clientfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) == -1 ) { ::close (m_clientfd); m_clientfd = -1 ; return false ; } return true ; } bool send (const string& buffer) { if (m_clientfd == -1 ) return false ; if ((::send (m_clientfd, buffer.data (), buffer.size (), 0 )) <= 0 ) return false ; return true ; } bool send (void * buffer, const size_t size) { if (m_clientfd == -1 ) return false ; if ((::send (m_clientfd, buffer, size, 0 )) <= 0 ) return false ; return true ; } bool recv (string& buffer, const size_t maxlen) { buffer.clear (); buffer.resize (maxlen); int readn = ::recv (m_clientfd, &buffer[0 ], buffer.size (), 0 ); if (readn <= 0 ) { buffer.clear (); return false ; } buffer.resize (readn); return true ; } bool close () { if (m_clientfd == -1 ) return false ; ::close (m_clientfd); m_clientfd = -1 ; return true ; } bool sendfile (const string& filename, const size_t filesize) { ifstream fin (filename, ios::binary) ; if (fin.is_open () == false ) { cout << "打开文件" << filename << "失败\n" ; return false ; } int onread = 0 ; int totalbytes = 0 ; char buffer[4096 ]; while (true ) { memset (buffer, 0 , sizeof (buffer)); if (filesize - totalbytes > 4096 ) onread = 4096 ; else onread = filesize - totalbytes; fin.read (buffer, onread); if (send (buffer, onread) == false ) return false ; totalbytes = totalbytes + onread; if (totalbytes == filesize) break ; } return true ; } ~ctcpclient () { close (); } }; int main (int argc, char * argv[]) { if (argc != 5 ) { cout << "Using: ./demo8 服务端的 IP 服务端的端口 文件名 文件大小\n" ; cout << "Example: ./demo8 192.168.112.131 5005 aaa.txt 2424\n\n" ; return -1 ; } ctcpclient tcpclient; if (tcpclient.connect (argv[1 ], atoi (argv[2 ])) == false ) { perror ("connect()" ); return -1 ; } struct st_fileinfo { char filename[256 ]; int filesize; }fileinfo; memset (&fileinfo, 0 , sizeof (fileinfo)); strcpy (fileinfo.filename, argv[3 ]); fileinfo.filesize = atoi (argv[4 ]); if (tcpclient.send (&fileinfo, sizeof (fileinfo)) == false ) { perror ("send" ); return -1 ; } cout << "发送文件信息的结构体" << fileinfo.filename << "(" << fileinfo.filesize << ")" << endl; string buffer; if (tcpclient.recv (buffer, 2 ) == false ) { perror ("recv()" ); return -1 ; } if (buffer != "ok" ) { cout << "服务端没有回复ok\n" ; return -1 ; } if (tcpclient.sendfile (fileinfo.filename, fileinfo.filesize) == false ) { perror ("sendfile()" ); return -1 ; } if (tcpclient.recv (buffer,2 )==false ) { perror ("recv()" ); return -1 ; } if (buffer!="ok" ) { cout << "发送文件内容失败\n" ; return -1 ; } cout << "发送文件内容成功\n" ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 #include <iostream> #include <fstream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <signal.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std;class ctcpserver { private : int m_listenfd; int m_clientfd; string m_clientip; unsigned short m_port; public : ctcpserver () : m_listenfd (-1 ), m_clientfd (-1 ) {} bool initserver (const unsigned short in_port) { if ((m_listenfd = socket (AF_INET, SOCK_STREAM, 0 )) == -1 ) return false ; m_port = in_port; struct sockaddr_in servaddr; memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (m_port); servaddr.sin_addr.s_addr = htonl (INADDR_ANY); if (bind (m_listenfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) == -1 ) { close (m_listenfd); m_listenfd = -1 ; return false ; } if (listen (m_listenfd, 5 ) == -1 ) { close (m_listenfd); m_listenfd = -1 ; return false ; } return true ; } bool accept () { struct sockaddr_in caddr; socklen_t addrlen = sizeof (caddr); if ((m_clientfd = ::accept (m_listenfd, (struct sockaddr*)&caddr, &addrlen)) == -1 ) return false ; m_clientip = inet_ntoa (caddr.sin_addr); return true ; } const string& clientip () const { return m_clientip; } bool send (const string& buffer) { if (m_clientfd == -1 ) return false ; if ((::send (m_clientfd, buffer.data (), buffer.size (), 0 )) <= 0 ) return false ; return true ; } bool recv (string& buffer, const size_t maxlen) { buffer.clear (); buffer.resize (maxlen); int readn = ::recv (m_clientfd, &buffer[0 ], buffer.size (), 0 ); if (readn <= 0 ) { buffer.clear (); return false ; } buffer.resize (readn); return true ; } bool recv (void * buffer, const size_t size) { if (::recv (m_clientfd, buffer, size, 0 ) <= 0 ) { return false ; } return true ; } bool closelisten () { if (m_listenfd == -1 ) return false ; close (m_listenfd); m_listenfd = -1 ; return true ; } bool closeclient () { if (m_clientfd == -1 ) return false ; close (m_clientfd); m_clientfd = -1 ; return true ; } bool recvfile (const string& filename, const size_t filesize) { ofstream fout; fout.open (filename, ios::binary); if (fout.is_open () == false ) { cout << "打开文件" << filename << "失败\n" ; return false ; } int totalbytes=0 ; int onread=0 ; char buffer[4096 ]; while (true ) { if (filesize - totalbytes > 4096 ) onread = 4096 ; else onread = filesize - totalbytes; if (recv (buffer ,onread) == false ) return false ; fout.write (buffer, onread); totalbytes = totalbytes + onread; if (totalbytes == filesize) break ; } return true ; } ~ctcpserver () { closelisten (); closeclient (); } }; ctcpserver tcpserver; void FathEXIT (int sig) ; void ChldEXIT (int sig) ; int main (int argc, char * argv[]) { if (argc != 3 ) { cout << "Using: ./demo9 通讯端口 文件存放的目录\n" ; cout << "Example: ./demo9 5005 /tmp\n\n" ; cout << "注意:运行服务端程序的 Linux 系统的防火墙必须要开通5005端口\n" ; cout << " 如果是云服务器,还要开通云平台的访问策略\n\n" ; return -1 ; } for (int ii = 1 ; ii <= 64 ; ii++) signal (ii, SIG_IGN); signal (SIGTERM, FathEXIT); signal (SIGINT, FathEXIT); if (tcpserver.initserver (atoi (argv[1 ])) == false ) { perror ("initserver()" ); return -1 ; } while (true ) { if (tcpserver.accept () == false ) { perror ("accept()" ); return -1 ; } int pid = fork(); if (pid == -1 ) { perror ("fork()" ); return -1 ; } if (pid > 0 ) { tcpserver.closeclient (); continue ; } tcpserver.closelisten (); signal (SIGTERM, ChldEXIT); signal (SIGINT, SIG_IGN); cout << "客户端已连接(" <<tcpserver.clientip () << ")\n" ; struct st_fileinfo { char filename[256 ]; int filesize; }fileinfo; memset (&fileinfo, 0 , sizeof (fileinfo)); if (tcpserver.recv (&fileinfo, sizeof (fileinfo)) == false ) { perror ("recv()" ); return -1 ; } cout << "文件信息结构体" << fileinfo.filename << "(" << fileinfo.filesize << ")" << endl; if (tcpserver.send ("ok" ) == false ) { perror ("send" ); break ; } if (tcpserver.recvfile (string (argv[2 ]) + "/" + fileinfo.filename, fileinfo.filesize) == false ) { cout << "接收文件内容失败\n" ; return -1 ; } cout << "接收文件内容成功\n" ; tcpserver.send ("ok" ); return 0 ; } } void FathEXIT (int sig) { signal (SIGINT, SIG_IGN); signal (SIGTERM, SIG_IGN); cout << "父进程退出,sig=" << sig << endl; kill (0 , SIGTERM); tcpserver.closelisten (); exit (0 ); } void ChldEXIT (int sig) { signal (SIGINT, SIG_IGN); signal (SIGTERM, SIG_IGN); cout << "子进程" << getpid () << "退出,sig=" << sig << endl; tcpserver.closeclient (); exit (0 ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // 终端 // 服务端 [zxc@study CppNetworkProgramming]$ dd if=/dev/zero of=aaa.txt bs=2424 count=1 记录了1+0 的读入 记录了1+0 的写出 2424字节(2.4 kB)已复制,0.00107924 秒,2.2 MB/秒 [zxc@study CppNetworkProgramming]$ ll aaa.txt -rw-rw-r--. 1 zxc zxc 2424 10月 13 12:35 aaa.txt [zxc@study CppNetworkProgramming]$ ./demo9 5005 Using: ./demo9 通讯端口 文件存放的目录 Example: ./demo9 5005 /tmp 注意:运行服务端程序的 Linux 系统的防火墙必须要开通5005端口 如果是云服务器,还要开通云平台的访问策略 [zxc@study CppNetworkProgramming]$ ./demo9 5005 /tmp 客户端已连接(192.168.112.131) 文件信息结构体aaa.txt(2424) 接收文件内容成功 ^C父进程退出,sig=2 // 客户端 [zxc@study CppNetworkProgramming]$ ./demo8 192.168.112.131 5005 aaa.txt 2424 发送文件信息的结构体aaa.txt(2424) 发送文件内容成功 [zxc@study CppNetworkProgramming]$ ll /tmp/aaa.txt -rw-rw-r--. 1 zxc zxc 2424 10月 13 12:36 /tmp/aaa.txt
三次握手与四次挥手 TCP 是面向连接的、可靠的协议,建立 TCP 连接需要三次对话(三次握手),拆除 TCP 连接需要四次对话(四次握/挥手)
三次握手 服务端调用 listen() 函数后进入监听(等待连接)状态,这时候,客户端就可以调用 connect() 函数发起 TCP 连接请求 客户端调用 connect() 函数的时候会触发三次握手 三次握手完成后,客户端与服务端将建立一个双向的传输通道
客户端的 socket 也有端口号,对程序员来说,不必关心客户端 socket 的端口号,因为系统随机分配(socket 通讯中的地址包括 ip 和端口号,但是,习惯中的地址仅指 ip 地址)
服务端的 bind() 函数,普通用户只能使用1024以上的端口,root 用户可以使用任意端口
listen() 函数的第二个参数+1为已连接队列(ESTABLISHED状态,三次握手已完成但是没有被 accept() 的 socket,只存在于服务端)的大小(在高并发的服务程序中,该参数应该调大一些)
SYN_RECV 状态的连接也称为半连接
CLOSED 是假想状态,实际上不存在
四次挥手(握手) 断开一个 TCP 连接时,需要客户端和服务端相互总共发送四个包以确认连接的断开 在 socket 编程中,这一过程由客户端或服务端任一方执行 close() 函数触发
主动断开的端在四次挥手后,socket 的状态为 TIME_WAIT,该状态将持续 2MSL(30秒/1分钟/2分钟) MSL(Maximum Segment Lifetime)报文在网络上存在的最长时间,超过这个时间报文将被丢弃
如果是客户端主动断开,TIME_WAIT 状态的 socket 几乎不会造成危害 1)客户端程序的 socket 很少,服务端程序很多(成千上万) 2)客户端的端口是随机分配的,不存在重用的问题
如果是服务端主动断开,有两方面危害 1)socket 没有立即释放 2)端口号只能在 2MSL 后才能重用 在服务端程序中,用 setsockopt() 函数设置 socket 的属性(一定要放在 bind() 之前)
1 2 int opt = 1 ;setsockopt (m_listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt));
TCP 缓存 系统为每个 socket 创建了发送缓冲区和接收缓冲区 应用程序调用 send()/write() 函数发送数据的时候,内核把数据从应用进程拷贝到 socket 的发送缓冲区中 应用程序调用 recv()/read() 函数接收数据的时候,内核把数据从 socket 的接收缓冲区拷贝到应用进程中 发送数据即把数据放入发送缓冲区 接收数据即从接收缓冲区中去数据
查看 socket 缓存的大小
1 2 3 4 5 6 7 8 int bufsize = 0 ;socklen_t optlen = sizeof (bufsize);getsockopt (sockfd, SOL_SOCKET, SO_SNDBUF, &bufsize, &optlen); cout << "send bufsize=" << bufsize << endl; getsockopt (sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, &optlen); cout << "recv bufsize=" << bufsize << endl;
send() 函数有可能会阻塞吗 如果自己的发送缓冲区和对端的接收缓冲区都满了,会阻塞
向 socket 中写入数据后,如果关闭了 socket,对端还能接收到数据吗 对端能够接收到数据
Nagle 算法 在 TCP 协议中,无论发送多少数据,都要在数据前面加上协议头,同时,对方收到数据后,也需要回复 ACK 表示确认 为了尽可能的利用网络带宽,TCP 希望每次都能够以 MSS(Maximum Segment Size,最大报文长度)的数据块来发送数据 Nagle 算法就是为了尽可能发送大块的数据,避免网络中充斥着小数据块 Nagle 算法的定义是:任意时刻,最多只能有一个未被确认的小段,小段是指小于 MSS 的数据块,未被确认是指一个数据块发送出去后,没有收到对端回复的 ACK 例如,发送端调用 send() 函数将一个 int 型数据(称之为 A 数据块)写入到 socket 中,A 数据块会被马上发送到接收端,接着,发送端又调用 send() 函数写入一个 int 型数据(称之为 B 数据块),这时候,A 块的 ACK 没有返回(已经存在了一个未被确认的小段),所以 B 块不会立即被发送,而是等 A 块的 ACK 返回之后(大概40ms)才发送 TCP协议中不仅仅有 Nagle 算法,还有一个 ACK 延迟机制:当接收端收到数据之后,并不会马上向发送端回复 ACK,而是延迟40ms后再回复,它希望在40ms内接收端会向发送端回复应答数据,这样 ACK 就可以和应答数据一起发送,把 ACK 捎带过去 如果 TCP 连接的一端启用了 Nagle 算法,另一端启用了 ACK 延时机制,而发送的数据包又比较小,则可能出现这样的情况:发送端在等待上一个包的 ACK,而接收端正好延迟了此 ACK,那么这个正要被发送的包就会延迟 40ms 解决方案 开启 TCP_NODELAY 选项,这个选项的作用就是禁用 Nagle 算法
1 2 3 4 #include <netinet/tcp.h> int opt = 1 ;setsockopt (sockfd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof (opt));
对时效要求很高的系统,例如联机游戏、证券交易,一般会禁用 Nagle 算法
I/O 多路复用
用一个进/线程处理多个 TCP 连接,减少系统开销
三种模型:select(1024)、poll(数千)和 epoll(百万)
网络通讯-写事件 发送缓冲区没有满,可以写入数据(可以向对端发送报文)
网络通讯-读事件
已连接队列中有已经准备好的 socket(有新的客户端连上来)
接收缓存中有数据可以读(对端发送的报文已到达)
TCP 连接已断开(对端调用 close() 函数关闭了连接)
select 模型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) ;void FD_CLR (int fd, fd_set *set) ; int FD_ISSET (int fd, fd_set *set) ; void FD_SET (int fd, fd_set *set) ; void FD_ZERO (fd_set *set) ;
select 模型-性能测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/fcntl.h> int initserver (int port) ;int main (int argc, char * argv[]) { if (argc != 2 ) { printf ("usage: ./tcpselect port\n" ); return -1 ; } int listensock = initserver (atoi (argv[1 ])); printf ("listensock=%d\n" , listensock); if (listensock < 0 ) { printf ("initserver() failed\n" ); return -1 ; } fd_set readfds; FD_ZERO (&readfds); FD_SET (listensock, &readfds); int maxfd = listensock; while (true ) { struct timeval timeout; timeout.tv_sec = 10 ; timeout.tv_usec = 0 ; fd_set tmpfds = readfds; int infds = select (maxfd + 1 , &tmpfds, NULL , NULL , &timeout); if (infds < 0 ) { perror ("select() failed" ); break ; } if (infds == 0 ) { printf ("select() timeout\n" ); continue ; } for (int eventfd = 0 ; eventfd <= maxfd; eventfd++) { if (FD_ISSET (eventfd, &tmpfds) == 0 ) continue ; if (eventfd == listensock) { struct sockaddr_in client; socklen_t len = sizeof (client); int clientsock = accept (listensock, (struct sockaddr*)&client, &len); if (clientsock < 0 ) { perror ("accept() failed" ); continue ; } printf ("accept client(socket=%d) ok\n" , clientsock); FD_SET (clientsock, &readfds); if (maxfd < clientsock) maxfd = clientsock; }else { char buffer[1024 ]; memset (buffer, 0 , sizeof (buffer)); if (recv (eventfd, buffer, sizeof (buffer), 0 ) <= 0 ) { printf ("client(eventfd=%d) disconnected\n" , eventfd); close (eventfd); FD_CLR (eventfd, &readfds); if (eventfd == maxfd) { for (int ii = maxfd; ii > 0 ; ii--) { if (FD_ISSET (ii, &readfds)) { maxfd = ii; break ; } } } }else { printf ("recv(eventfd=%d):%s\n" , eventfd, buffer); send (eventfd, buffer, strlen (buffer), 0 ); } } } } return 0 ; } int initserver (int port) { int sock = socket (AF_INET, SOCK_STREAM, 0 ); if (sock < 0 ) { perror ("socket() failed" ); return -1 ; } int opt = 1 ; unsigned int len = sizeof (opt); setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &opt, len); struct sockaddr_in servaddr; servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl (INADDR_ANY); servaddr.sin_port = htons (port); if (bind (sock, (struct sockaddr*)&servaddr, sizeof (servaddr)) < 0 ) { perror ("bind() failed" ); close (sock); return -1 ; } if (listen (sock, 5 ) != 0 ) { perror ("listen() failed" ); close (sock); return -1 ; } return sock; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <netinet/in.h> #include <sys/socket.h> #include <arpa/inet.h> int main (int argc, char * argv[]) { if (argc != 3 ) { printf ("usage: ./client ip port\n" ); return -1 ; } int sockfd; struct sockaddr_in servaddr; char buf[1024 ]; if ((sockfd = socket (AF_INET, SOCK_STREAM, 0 )) < 0 ) { printf ("socket() failed\n" ); return -1 ; } memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (atoi (argv[2 ])); servaddr.sin_addr.s_addr = inet_addr (argv[1 ]); if (connect (sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) != 0 ) { printf ("connect(%s:%s) failed\n" , argv[1 ], argv[2 ]); close (sockfd); return -1 ; } printf ("connect ok\n" ); for (int ii = 0 ; ii < 1000000 ; ii++) { memset (buf, 0 , sizeof (buf)); printf ("please input:" ); scanf ("%s" , buf); if (send (sockfd, buf, strlen (buf), 0 ) <= 0 ) { printf ("write() failed\n" ); close (sockfd); return -1 ; } memset (buf, 0 , sizeof (buf)); if (recv (sockfd, buf, sizeof (buf), 0 ) <= 0 ) { printf ("read() failed\n" ); close (sockfd); return -1 ; } printf ("recv:%s\n" , buf); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // 终端 // 服务端 [zxc@study CppNetworkProgramming]$ g++ -std=c++11 -g -o tcpselect tcpselect.cpp [zxc@study CppNetworkProgramming]$ ./tcpselect 5008 listensock=3 select() timeout // 每隔10s超时 accept client(socket=4) ok recv(eventfd=4):aaaaa select() timeout select() timeout accept client(socket=5) ok recv(eventfd=5):bbbbbb client(eventfd=4) disconnected client(eventfd=5) disconnected // 1号客户端 [zxc@study CppNetworkProgramming]$ g++ -std=c++11 -g -o client client.cpp [zxc@study CppNetworkProgramming]$ ./client 192.168.112.132 5008 connect ok please input:aaaaa recv:aaaaa please input:^C // 2号客户端 [zxc@study CppNetworkProgramming]$ ./client 192.168.112.132 5008 connect ok please input:bbbbbb recv:bbbbbb please input:^C
select 模型-写事件
如果 TCP 的发送缓冲区没有满,那么,socket 连接是可写的
一般来说,发送缓冲区不容易被填满
如果发送的数据量太大,或网络带宽不够,发送缓冲区有填满的可能
select 监视写事件,如果发送缓冲区没有满,select 函数会立即返回
select 模型-水平触发
select() 监视的 socket 如果发生了事件,select() 会返回(通知应用程序处理事件)
如果事件没有被处理,再次调用 select() 的时候会立即再通知
select 模型-存在的问题
采用轮询方式扫描 bitmap,性能会随着 socket 数量增多而下降
select() 会修改 bitmap,每次调用 select() 前,需要拷贝一份 bitmap,程序运行在用户态,网络通信在内核,调用 select() 的时候,要把 bitmap 从用户态拷贝一份到内核态,bitmap 总共需要拷贝两次
bitmap 的大小(单个进/线程打开的 socket 数量)由 FD_SETSIZE 宏设置,默认是1024个,可以修改,但是效率将更低
poll 模型 1 2 3 4 5 6 7 8 9 10 11 12 13 struct pollfd { int fd; short events; short revents; }; POLLIN There is data to read. POLLPRI There is urgent data to read (e.g., out-of-band data on TCP socket; pseudoterminal master in packet mode has seen state change in slave) . POLLOUT Writing now will not block.
poll模型-性能测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <poll.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/fcntl.h> int initserver (int port) ;int main (int argc, char * argv[]) { if (argc != 2 ) { printf ("usage: ./tcppoll port\n" ); return -1 ; } int listensock = initserver (atoi (argv[1 ])); printf ("listensock=%d\n" , listensock); if (listensock < 0 ) { printf ("initserver() failed\n" ); return -1 ; } pollfd fds[2048 ]; for (int ii = 0 ; ii < 2048 ; ii++) fds[ii].fd = -1 ; fds[listensock].fd = listensock; fds[listensock].events = POLLIN; int maxfd = listensock; while (true ) { int infds = poll (fds, maxfd+1 , 10000 ); if (infds < 0 ) { perror ("poll() failed" ); break ; } if (infds == 0 ) { printf ("poll() timeout\n" ); continue ; } for (int eventfd = 0 ; eventfd <= maxfd; eventfd++) { if (fds[eventfd].fd < 0 ) continue ; if ((fds[eventfd].revents&POLLIN) == 0 ) continue ; if (eventfd == listensock) { struct sockaddr_in client; socklen_t len = sizeof (client); int clientsock = accept (listensock, (struct sockaddr*)&client, &len); if (clientsock < 0 ) { perror ("accept() failed" ); continue ; } printf ("accept client(socket=%d) ok\n" , clientsock); fds[clientsock].fd = clientsock; fds[clientsock].events = POLLIN; if (maxfd < clientsock) maxfd = clientsock; }else { char buffer[1024 ]; memset (buffer, 0 , sizeof (buffer)); if (recv (eventfd, buffer, sizeof (buffer), 0 ) <= 0 ) { printf ("client(eventfd=%d) disconnected\n" , eventfd); close (eventfd); fds[eventfd].fd = -1 ; if (eventfd == maxfd) { for (int ii = maxfd; ii > 0 ; ii--) { if (fds[ii].fd != -1 ) { maxfd = ii; break ; } } } }else { printf ("recv(eventfd=%d):%s\n" , eventfd, buffer); send (eventfd, buffer, strlen (buffer), 0 ); } } } } return 0 ; } int initserver (int port) { int sock = socket (AF_INET, SOCK_STREAM, 0 ); if (sock < 0 ) { perror ("socket() failed" ); return -1 ; } int opt = 1 ; unsigned int len = sizeof (opt); setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &opt, len); struct sockaddr_in servaddr; servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl (INADDR_ANY); servaddr.sin_port = htons (port); if (bind (sock, (struct sockaddr*)&servaddr, sizeof (servaddr)) < 0 ) { perror ("bind() failed" ); close (sock); return -1 ; } if (listen (sock, 5 ) != 0 ) { perror ("listen() failed" ); close (sock); return -1 ; } return sock; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <netinet/in.h> #include <sys/socket.h> #include <arpa/inet.h> int main (int argc, char * argv[]) { if (argc != 3 ) { printf ("usage: ./client ip port\n" ); return -1 ; } int sockfd; struct sockaddr_in servaddr; char buf[1024 ]; if ((sockfd = socket (AF_INET, SOCK_STREAM, 0 )) < 0 ) { printf ("socket() failed\n" ); return -1 ; } memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (atoi (argv[2 ])); servaddr.sin_addr.s_addr = inet_addr (argv[1 ]); if (connect (sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) != 0 ) { printf ("connect(%s:%s) failed\n" , argv[1 ], argv[2 ]); close (sockfd); return -1 ; } printf ("connect ok\n" ); for (int ii = 0 ; ii < 1000000 ; ii++) { memset (buf, 0 , sizeof (buf)); printf ("please input:" ); scanf ("%s" , buf); if (send (sockfd, buf, strlen (buf), 0 ) <= 0 ) { printf ("write() failed\n" ); close (sockfd); return -1 ; } memset (buf, 0 , sizeof (buf)); if (recv (sockfd, buf, sizeof (buf), 0 ) <= 0 ) { printf ("read() failed\n" ); close (sockfd); return -1 ; } printf ("recv:%s\n" , buf); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // 终端 // 服务端 [zxc@study CppNetworkProgramming]$ g++ -std=c++11 -g -o tcppoll tcppoll.cpp [zxc@study CppNetworkProgramming]$ ./tcppoll 5008 listensock=3 poll() timeout poll() timeout accept client(socket=4) ok recv(eventfd=4):bbb accept client(socket=5) ok recv(eventfd=5):cccc client(eventfd=5) disconnected client(eventfd=4) disconnected ^C // 1号客户端 [zxc@study CppNetworkProgramming]$ ./client 192.168.112.132 5008 connect ok please input:bbb recv:bbb please input:^C // 2号客户端 [zxc@study CppNetworkProgramming]$ ./client 192.168.112.132 5008 connect ok please input:cccc recv:cccc please input:^C
poll 模型-写事件
如果 TCP 的发送缓冲区没有满,那么,socket 连接是可写的
一般来说,发送缓冲区不容易被填满
如果发送的数据量太大,或网络带宽不够,发送缓冲区有填满的可能
poll 模型-水平触发
poll() 监视的 socket 如果发生了事件,poll() 会返回(通知应用程序处理事件)
如果事件没有被处理,再次调用 poll() 的时候会立即再通知
poll 模型-存在的问题
在程序中,poll 的数据结构是数组,传入内核后转换成了链表
每调用一次 select() 需要拷贝两次 bitmap,poll 拷贝一次结构体数组
poll 监视的连接数没有1024的限制,但是也是遍历的方法,监视的 socket 越多,效率越低
epoll 模型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int epoll_create (int size) ;typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t ; struct epoll_event { uint32_t events; epoll_data_t data; };
epoll 模型-性能测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <sys/fcntl.h> #include <sys/epoll.h> int initserver (int port) ;int main (int argc, char * argv[]) { if (argc != 2 ) { printf ("usage: ./tcpepoll port\n" ); return -1 ; } int listensock = initserver (atoi (argv[1 ])); printf ("listensock=%d\n" , listensock); if (listensock < 0 ) { printf ("initserver() failed\n" ); return -1 ; } int epollfd = epoll_create (1 ); epoll_event ev; ev.data.fd = listensock; ev.events = EPOLLIN; epoll_ctl (epollfd, EPOLL_CTL_ADD, listensock, &ev); epoll_event evs[10 ]; while (true ) { int infds = epoll_wait (epollfd, evs, 10 , -1 ); if (infds < 0 ) { perror ("epoll() failed" ); break ; } if (infds == 0 ) { printf ("epoll() timeout\n" ); continue ; } for (int ii = 0 ; ii < infds; ii++) { if (evs[ii].data.fd == listensock) { struct sockaddr_in client; socklen_t len = sizeof (client); int clientsock = accept (listensock, (struct sockaddr*)&client, &len); printf ("accept client(socket=%d) ok\n" , clientsock); ev.data.fd = clientsock; ev.events = EPOLLIN; epoll_ctl (epollfd, EPOLL_CTL_ADD, clientsock, &ev); }else { char buffer[1024 ]; memset (buffer, 0 , sizeof (buffer)); if (recv (evs[ii].data.fd, buffer, sizeof (buffer), 0 ) <= 0 ) { printf ("client(eventfd=%d) disconnected\n" , evs[ii].data.fd); close (evs[ii].data.fd); }else { printf ("recv(eventfd=%d):%s\n" , evs[ii].data.fd, buffer); send (evs[ii].data.fd, buffer, strlen (buffer), 0 ); } } } } return 0 ; } int initserver (int port) { int sock = socket (AF_INET, SOCK_STREAM, 0 ); if (sock < 0 ) { perror ("socket() failed" ); return -1 ; } int opt = 1 ; unsigned int len = sizeof (opt); setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &opt, len); struct sockaddr_in servaddr; servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl (INADDR_ANY); servaddr.sin_port = htons (port); if (bind (sock, (struct sockaddr*)&servaddr, sizeof (servaddr)) < 0 ) { perror ("bind() failed" ); close (sock); return -1 ; } if (listen (sock, 5 ) != 0 ) { perror ("listen() failed" ); close (sock); return -1 ; } return sock; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <netinet/in.h> #include <sys/socket.h> #include <arpa/inet.h> int main (int argc, char * argv[]) { if (argc != 3 ) { printf ("usage: ./client ip port\n" ); return -1 ; } int sockfd; struct sockaddr_in servaddr; char buf[1024 ]; if ((sockfd = socket (AF_INET, SOCK_STREAM, 0 )) < 0 ) { printf ("socket() failed\n" ); return -1 ; } memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (atoi (argv[2 ])); servaddr.sin_addr.s_addr = inet_addr (argv[1 ]); if (connect (sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) != 0 ) { printf ("connect(%s:%s) failed\n" , argv[1 ], argv[2 ]); close (sockfd); return -1 ; } printf ("connect ok\n" ); for (int ii = 0 ; ii < 1000000 ; ii++) { memset (buf, 0 , sizeof (buf)); printf ("please input:" ); scanf ("%s" , buf); if (send (sockfd, buf, strlen (buf), 0 ) <= 0 ) { printf ("write() failed\n" ); close (sockfd); return -1 ; } memset (buf, 0 , sizeof (buf)); if (recv (sockfd, buf, sizeof (buf), 0 ) <= 0 ) { printf ("read() failed\n" ); close (sockfd); return -1 ; } printf ("recv:%s\n" , buf); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // 终端 // 服务端 [zxc@study CppNetworkProgramming]$ g++ -std=c++11 -g -o tcpepoll tcpepoll.cpp [zxc@study CppNetworkProgramming]$ ./tcpepoll 5008 listensock=3 accept client(socket=5) ok recv(eventfd=5):aaa accept client(socket=6) ok recv(eventfd=6):bbbbb client(eventfd=6) disconnected client(eventfd=5) disconnected accept client(socket=5) ok recv(eventfd=5):ccc ^C // 1号客户端 [zxc@study CppNetworkProgramming]$ ./client 192.168.112.132 5008 connect ok please input:aaa recv:aaa please input:^C // 2号客户端 [zxc@study CppNetworkProgramming]$ ./client 192.168.112.132 5008 connect ok please input:bbbbb recv:bbbbb please input:^C // 3号客户端 [zxc@study CppNetworkProgramming]$ ./client 192.168.112.132 5008 connect ok please input:ccc recv:ccc
阻塞&非阻塞的 I/O
阻塞:在进/线程中,发起一个调用时,在调用返回之前,进/线程会被阻塞等待,等待中的进/线程让出CPU的使用权
非阻塞:在进/线程中,发起一个调用时,会立即返回
会阻塞的四个函数:connect()、accept()、send()、recv()
在传统的网络服务端程序中(每连接每线/进程),采用阻塞 I/O
在 I/O 复用的模型中,事件循环不能被阻塞在任何环节,所以应该采用非阻塞 I/O
非阻塞 I/O - connect()
对非阻塞的 I/O 调用 connect() 函数,不管是否能连接成功,connect() 都会立即返回失败,errno == EINPROGRESS
对非阻塞的 I/O 调用 connect() 函数,如果 socket 的状态是可写的,证明连接是成功的,否则是失败的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <netinet/in.h> #include <sys/socket.h> #include <arpa/inet.h> #include <poll.h> #include <fcntl.h> int setnonblocking (int fd) { int flags; if ((flags = fcntl (fd, F_GETFL, 0 )) == -1 ) flags = 0 ; return fcntl (fd, F_SETFL, flags | O_NONBLOCK); } int main (int argc, char * argv[]) { if (argc != 3 ) { printf ("usage: ./client1 ip port\n" ); return -1 ; } int sockfd; struct sockaddr_in servaddr; char buf[1024 ]; if ((sockfd = socket (AF_INET, SOCK_STREAM, 0 )) < 0 ) { printf ("socket() failed\n" ); return -1 ; } setnonblocking (sockfd); memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (atoi (argv[2 ])); servaddr.sin_addr.s_addr = inet_addr (argv[1 ]); if (connect (sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) != 0 ) { if (errno != EINPROGRESS) { printf ("connect(%s:%s) failed\n" , argv[1 ], argv[2 ]); close (sockfd); return -1 ; } } pollfd fds; fds.fd = sockfd; fds.events = POLLOUT; poll (&fds, 1 , -1 ); if (fds.revents == POLLOUT) printf ("connect ok\n" ); else printf ("connect failed\n" ); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 // 终端 // 服务端 [zxc@study CppNetworkProgramming]$ ./tcpepoll 5008 listensock=3 accept client(socket=5) ok client(eventfd=5) disconnected // 客户端 [zxc@study CppNetworkProgramming]$ ./client1 192.168.112.132 5008 connect ok [zxc@study CppNetworkProgramming]$ ./client1 19.168.112.132 5008 connect failed
非阻塞 I/O - accept()
对非阻塞的 I/O 调用 accept(),如果已连接队列中没有 socket,函数立即返回失败,errno == EAGAIN
非阻塞 I/O - recv()
对非阻塞的 I/O 调用 recv(),如果没数据可读(接收缓冲区为空),函数立即返回失败,errno == EAGAIN
非阻塞 I/O - send()
对非阻塞的 I/O 调用 send(),如果 socket 不可写(发送缓冲区已满),函数立即返回失败,errno == EAGAIN
水平触发&边缘触发
select 和 poll 采用水平触发
epoll 有水平触发和边缘触发两种机制,缺省是水平触发
水平触发
读事件:如果 epoll_wait 触发了读事件,表示有数据可读,如果程序没有把数据读完,再次调用 epoll_wait 的时候,将立即再次触发读事件
写事件:如果发送缓冲区没有满,表示可以写入数据,只要缓冲区没有被写满,再次调用 epoll_wait 的时候,将立即再次触发写事件
边缘触发
读事件:epoll_wait 触发读事件后,不管程序有没有处理读事件,epoll_wait 都不会再触发读事件,只有当新的数据到达时,才再次触发读事件
写事件:epoll_wait 触发写事件之后,如果发送缓冲区仍可以写(发送缓冲区没有满),epoll_wait 不会再次触发写事件,只有当发送缓冲区由满变成不满时,才再次触发写事件
水平触发的代码不能用于边缘触发,但是边缘触发的代码可以用于水平触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <sys/fcntl.h> #include <sys/epoll.h> int setnonblocking (int fd) { int flags; if ((flags = fcntl (fd, F_GETFL, 0 )) == -1 ) flags = 0 ; return fcntl (fd, F_SETFL, flags | O_NONBLOCK); } int initserver (int port) ;int main (int argc,char * argv[]) { if (argc != 2 ) { printf ("usage: ./tcpepoll2 port\n" ); return -1 ; } int listensock = initserver (atoi (argv[1 ])); printf ("listensock=%d\n" , listensock); if (listensock < 0 ) { printf ("initserver() failed.\n" ); return -1 ; } setnonblocking (listensock); int epollfd = epoll_create (1 ); epoll_event ev; ev.data.fd = listensock; ev.events = EPOLLIN | EPOLLET; epoll_ctl (epollfd, EPOLL_CTL_ADD, listensock, &ev); epoll_event evs[10 ]; while (true ) { int infds = epoll_wait (epollfd, evs, 10 , -1 ); if (infds < 0 ) { perror ("epoll() failed" ); break ; } if (infds == 0 ) { printf ("epoll() timeout\n" ); continue ; } for (int ii = 0 ; ii < infds; ii++) { if (evs[ii].data.fd == listensock) { while (true ) { struct sockaddr_in client; socklen_t len = sizeof (client); int clientsock = accept (listensock, (struct sockaddr*)&client, &len); if ((clientsock < 0 ) && (errno == EAGAIN)) break ; printf ("accept client(socket=%d) ok.\n" , clientsock); setnonblocking (clientsock); ev.data.fd = clientsock; ev.events = EPOLLIN | EPOLLET; epoll_ctl (epollfd, EPOLL_CTL_ADD, clientsock, &ev); } } else { char buffer[1024 ]; memset (buffer, 0 , sizeof (buffer)); int readn; char * ptr = buffer; while (true ) { if ((readn = recv (evs[ii].data.fd, ptr, 5 , 0 )) <= 0 ) { if ((readn < 0 ) && (errno == EAGAIN)) { send (evs[ii].data.fd, buffer, strlen (buffer), 0 ); printf ("recv(eventfd=%d):%s\n" , evs[ii].data.fd, buffer); } else { printf ("client(eventfd=%d) disconnected\n" , evs[ii].data.fd); close (evs[ii].data.fd); } break ; }else ptr = ptr + readn; } } } } return 0 ; } int initserver (int port) { int sock = socket (AF_INET, SOCK_STREAM, 0 ); if (sock < 0 ) { perror ("socket() failed" ); return -1 ; } int opt = 1 ; unsigned int len = sizeof (opt); setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &opt, len); struct sockaddr_in servaddr; servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl (INADDR_ANY); servaddr.sin_port = htons (port); if (bind (sock, (struct sockaddr*)&servaddr, sizeof (servaddr)) < 0 ) { perror ("bind() failed" ); close (sock); return -1 ; } if (listen (sock,5 ) != 0 ) { perror ("listen() failed" ); close (sock); return -1 ; } return sock; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <netinet/in.h> #include <sys/socket.h> #include <arpa/inet.h> #include <time.h> int main (int argc, char * argv[]) { if (argc != 3 ) { printf ("usage:./client2 ip port\n" ); return -1 ; } int sockfd; struct sockaddr_in servaddr; char buf[1024 ]; if ((sockfd = socket (AF_INET, SOCK_STREAM, 0 )) < 0 ) { printf ("socket() failed\n" ); return -1 ; } memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (atoi (argv[2 ])); servaddr.sin_addr.s_addr = inet_addr (argv[1 ]); if (connect (sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) != 0 ) { printf ("connect(%s:%s) failed\n" , argv[1 ], argv[2 ]); close (sockfd); return -1 ; } printf ("connect ok\n" ); for (int ii = 0 ; ii < 200000 ; ii++) { memset (buf,0 ,sizeof (buf)); printf ("please input:" ); scanf ("%s" ,buf); if (send (sockfd, buf, strlen (buf), 0 ) <= 0 ) { printf ("write() failed\n" ); close (sockfd); return -1 ; } memset (buf,0 ,sizeof (buf)); if (recv (sockfd, buf, sizeof (buf), 0 ) <= 0 ) { printf ("read() failed\n" ); close (sockfd); return -1 ; } printf ("recv:%s\n" ,buf); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // 终端 // 服务端 [zxc@study CppNetworkProgramming]$ g++ -std=c++11 -g -o tcpepoll2 tcpepoll2.cpp [zxc@study CppNetworkProgramming]$ ./tcpepoll2 5008 listensock=3 accept client(socket=5) ok. recv(eventfd=5):aaaaaaaaaa accept client(socket=6) ok. recv(eventfd=6):bbbbbbbbbb client(eventfd=6) disconnected client(eventfd=5) disconnected ^C // 1号客户端 [zxc@study CppNetworkProgramming]$ g++ -std=c++11 -g -o client2 client2.cpp [zxc@study CppNetworkProgramming]$ ./client2 192.168.112.132 5008 connect ok please input:aaaaaaaaaa recv:aaaaaaaaaa please input:^C // 2号客户端 [zxc@study CppNetworkProgramming]$ ./client2 192.168.112.132 5008 connect ok please input:bbbbbbbbbb recv:bbbbbbbbbb please input:^C
epoll 的原理 Epoll原理解析_epoll底层原理-CSDN博客
转载记录:防止文章丢失
本文核心思想是:要让读者清晰明白 Epoll 为什么性能好
文章会从网卡接收数据的流程讲起,串联起 CPU 中断、操作系统进程调度等知识; 再一步步分析阻塞接收数据、Select 到 Epoll 的进化过程; 最后探究 Epoll 的实现细节
从网卡接收数据说起
下边是一个典型的计算机结构图,计算机由 CPU、存储器(内存)与网络接口等部件组成,了解 Epoll 本质的第一步,要从硬件的角度看计算机怎样接收网络数据
计算机结构图(图片来源:Linux 内核完全注释之微型计算机组成结构)
下图展示了网卡接收数据的过程:
在 1 阶段,网卡收到网线传来的数据
经过 2 阶段的硬件电路的传输
最终 3 阶段将数据写入到内存中的某个地址上
这个过程涉及到 DMA 传输、IO 通路选择等硬件有关的知识,但我们只需知道:网卡会把接收到的数据写入内存
网卡接收数据的过程
通过硬件传输,网卡接收的数据存放到内存中,操作系统就可以去读取它们
如何知道接收了数据?
了解 Epoll 本质的第二步,要从 CPU 的角度来看数据接收。理解这个问题,要先了解一个概念:中断
计算机执行程序时,会有优先级的需求。比如,当计算机收到断电信号时,它应立即去保存数据,保存数据的程序具有较高的优先级(电容可以保存少许电量,供 CPU 运行很短的一小段时间)
一般而言,由硬件产生的信号需要 CPU 立马做出回应,不然数据可能就丢失了,所以它的优先级很高
CPU 理应中断掉正在执行的程序,去做出响应; 当 CPU 完成对硬件的响应后,再重新执行用户程序
中断的过程如下图,它和函数调用差不多,只不过函数调用是事先定好位置,而中断的位置由“信号”决定
中断程序调用
以键盘为例,当用户按下键盘某个按键时,键盘会给 CPU 的中断引脚发出一个高电平,CPU 能够捕获这个信号,然后执行键盘中断程序
下图展示了各种硬件通过中断与 CPU 交互的过程:
CPU 中断(图片来源:net.pku.edu.cn)
现在可以回答 “如何知道接收了数据?” 这个问题了:当网卡把数据写入到内存后,网卡向 CPU 发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据
进程阻塞为什么不占用 CPU 资源?
了解 Epoll 本质的第三步,要从操作系统进程调度的角度来看数据接收。阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)发生之前的等待状态,Recv、Select 和 Epoll 都是阻塞方法
下边分析一下进程阻塞为什么不占用 CPU 资源?为简单起见,我们从普通的 Recv 接收开始分析,先看看下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 int s = socket (AF_INET, SOCK_STREAM, 0 ); bind (s, ...) listen (s, ...) int c = accept (s, ...) recv (c, ...); printf (...)
这是一段最基础的网络编程代码,先新建 Socket 对象,依次调用 Bind、Listen 与 Accept,最后调用 Recv 接收数据
Recv 是个阻塞方法,当程序运行到 Recv 时,它会一直等待,直到接收到数据才往下执行。那么阻塞的原理是什么
工作队列
操作系统为了支持多任务,实现了进程调度的功能,会把进程分为 “运行” 和 “等待” 等几种状态
运行状态是进程获得 CPU 使用权,正在执行代码的状态; 等待状态是阻塞状态,比如上述程序运行到 Recv 时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态
操作系统会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务
下图的计算机中运行着 A、B 与 C 三个进程,其中进程 A 执行着上述基础网络程序,一开始,这 3 个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行
工作队列中有 A、B 和 C 三个进程
等待队列
当进程 A 执行到创建 Socket 的语句时,操作系统会创建一个由文件系统管理的 Socket 对象(如下图)
创建 Socket
这个 Socket 对象包含了发送缓冲区、接收缓冲区与等待队列等成员。等待队列是个非常重要的结构,它指向所有需要等待该 Socket 事件的进程
当程序执行到 Recv 时,操作系统会将进程 A 从工作队列移动到该 Socket 的等待队列中(如下图)
Socket 的等待队列
由于工作队列只剩下了进程 B 和 C,依据进程调度,CPU 会轮流执行这两个进程的程序,不会执行进程 A 的程序。所以进程 A 被阻塞,不会往下执行代码,也不会占用 CPU 资源
注:操作系统添加等待队列只是添加了对这个 “等待中” 进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下。上图为了方便说明,直接将进程挂到等待队列之下
唤醒进程
当 Socket 接收到数据后,操作系统将该 Socket 等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码
同时由于 Socket 的接收缓冲区已经有了数据,Recv 可以返回接收到的数据
内核接收网络数据全过程
这一步,贯穿网卡、中断与进程调度的知识,叙述阻塞 Recv 下,内核接收数据的全过程
内核接收数据全过程
如上图所示,进程在 Recv 阻塞期间:
计算机收到了对端传送的数据(步骤 ①)
数据经由网卡传送到内存(步骤 ②)
然后网卡通过中断信号通知 CPU 有数据到达,CPU 执行中断程序(步骤 ③)
此处的中断程序主要有两项功能,先将网络数据写入到对应 Socket 的接收缓冲区里面(步骤 ④),再唤醒进程 A(步骤 ⑤),重新将进程 A 放入工作队列中
唤醒进程的过程如下图所示:
唤醒进程
以上是内核接收数据全过程,这里我们可能会思考两个问题:
操作系统如何知道网络数据对应于哪个 Socket
如何同时监视多个 Socket 的数据
第一个问题:因为一个 Socket 对应着一个端口号,而网络数据包中包含了 IP 和端口的信息,内核可以通过端口号找到对应的 Socket
当然,为了提高处理速度,操作系统会维护端口号到 Socket 的索引结构,以快速读取
第二个问题是多路复用的重中之重,也正是本文后半部分的重点
同时监视多个 Socket 的简单方法
服务端需要管理多个客户端连接,而 Recv 只能监视单个 Socket,这种矛盾下,人们开始寻找监视多个 Socket 的方法。Epoll 的要义就是高效地监视多个 Socket
从历史发展角度看,必然先出现一种不太高效的方法,人们再加以改进,正如 Select 之于 Epoll。先理解不太高效的 Select,才能够更好地理解 Epoll 的本质
假如能够预先传入一个 Socket 列表,如果列表中的 Socket 都没有数据,挂起进程,直到有一个 Socket 收到数据,唤醒进程。这种方法很直接,也是 Select 的设计思想
为方便理解,我们先复习 Select 的用法。在下边的代码中,先准备一个数组 FDS,让 FDS 存放着所有需要监视的 Socket
然后调用 Select,如果 FDS 中的所有 Socket 都没有数据,Select 会阻塞,直到有一个 Socket 接收到数据,Select 返回,唤醒进程
用户可以遍历 FDS,通过 FD_ISSET 判断具体哪个 Socket 收到数据,然后做出处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int s = socket(AF_INET, SOCK_STREAM, 0); bind(s, ...) listen(s, ...) int fds[] = 存放需要监听的socket while(1){ int n = select(..., fds, ...) for(int i=0; i < fds.count; i++){ if(FD_ISSET(fds[i], ...)){ //fds[i]的数据处理 } } }
Select 的流程
Select 的实现思路很直接,假如程序同时监视如下图的 Sock1、Sock2 和 Sock3 三个 Socket,那么在调用 Select 之后,操作系统把进程 A 分别加入这三个 Socket 的等待队列中
当任何一个 Socket 收到数据后,中断程序将唤起进程。下图展示了 Sock2 接收到了数据的处理流程:
Sock2 接收到了数据,中断程序唤起进程 A
注:Recv 和 Select 的中断回调可以设置成不同的内容
所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面,如下图所示:
将进程 A 从所有等待队列中移除,再加入到工作队列里面
经由这些步骤,当进程 A 被唤醒后,它知道至少有一个 Socket 接收了数据。程序只需遍历一遍 Socket 列表,就可以得到就绪的 Socket
这种简单方式行之有效,在几乎所有操作系统都有对应的实现。但是简单的方法往往有缺点,主要是:
每次调用 Select 都需要将进程加入到所有监视 Socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个 FDS 列表传递给内核,有一定的开销
正是因为遍历操作开销大,出于效率的考量,才会规定 Select 的最大监视数量,默认只能监视 1024 个 Socket
进程被唤醒后,程序并不知道哪些 Socket 收到数据,还需要遍历一次。
那么,有没有减少遍历的方法?有没有保存就绪 Socket 的方法?这两个问题便是 Epoll 技术要解决的
补充说明:本节只解释了 Select 的一种情形。当程序调用 Select 时,内核会先遍历一遍 Socket,如果有一个以上的 Socket 接收缓冲区有数据,那么 Select 直接返回,不会阻塞
这也是为什么 Select 的返回值有可能大于 1 的原因之一。如果没有 Socket 有数据,进程才会阻塞
Epoll 的设计思路
Epoll 是在 Select 出现 N 多年后才被发明的,是 Select 和 Poll (Poll 和 Select 基本一样,有少量改进)的增强版本。Epoll 通过以下一些措施来改进效率:
措施一:功能分离
Select 低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一
相比 Select,Epoll 拆分了功能
如上图所示,每次调用 Select 都需要这两步操作,然而大多数应用场景中,需要监视的 Socket 相对固定,并不需要每次都修改
Epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见地,效率就能得到提升
为方便理解后续的内容,我们先了解一下 Epoll 的用法。如下的代码中,先用 epoll_create 创建一个 Epoll 对象 Epfd,再通过 epoll_ctl 将需要监视的 Socket 添加到 Epfd 中,最后调用 epoll_wait 等待数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 int s = socket (AF_INET, SOCK_STREAM, 0 ); bind (s, ...) listen (s, ...) int epfd = epoll_create(...); epoll_ctl(epfd, ...); // 将所有需要监听的socket 添加到epfd中 while (1 ){ int n = epoll_wait(...) for (接收到数据的socket ){ // 处理 } }
功能分离,使得 Epoll 有了优化的可能
措施二:就绪列表
Select 低效的另一个原因在于程序不知道哪些 Socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 Socket,就能避免遍历
就绪列表示意图
如上图所示,计算机共有三个 Socket,收到数据的 Sock2 和 Sock3 被就绪列表 Rdlist 所引用
当进程被唤醒后,只要获取 Rdlist 的内容,就能够知道哪些 Socket 收到数据
Epoll 的原理与工作流程
本节会以示例和图表来讲解 Epoll 的原理和工作流程
创建 Epoll 对象
如下图所示,当某个进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象(也就是程序中 Epfd 所代表的对象)
内核创建 eventpoll 对象
eventpoll 对象也是文件系统中的一员,和 Socket 一样,它也会有等待队列
创建一个代表该 Epoll 的 eventpoll 对象是必须的,因为内核要维护“就绪列表”等数据,“就绪列表”可以作为 eventpoll 的成员
维护监视列表
创建 Epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 Socket。以添加 Socket 为例
添加所要监听的 Socket
如上图,如果通过 epoll_ctl 添加 Sock1、Sock2 和 Sock3 的监视,内核会将 eventpoll 添加到这三个 Socket 的等待队列中
当 Socket 收到数据后,中断程序会操作 eventpoll 对象,而不是直接操作进程
接收数据
当 Socket 收到数据后,中断程序会给 eventpoll 的 “就绪列表” 添加 Socket 引用
给就绪列表添加引用
如上图展示的是 Sock2 和 Sock3 收到数据后,中断程序让 Rdlist 引用这两个 Socket
eventpoll 对象相当于 Socket 和进程之间的中介,Socket 的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态
当程序执行到 epoll_wait 时,如果 Rdlist 已经引用了 Socket,那么 epoll_wait 直接返回,如果 Rdlist 为空,阻塞进程
阻塞和唤醒进程
假设计算机中正在运行进程 A 和进程 B,在某时刻进程 A 运行到了 epoll_wait 语句
epoll_wait 阻塞进程
如上图所示,内核会将进程 A 放入 eventpoll 的等待队列中,阻塞进程
当 Socket 接收到数据,中断程序一方面修改 Rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态(如下图)
Epoll 唤醒进程
也因为 Rdlist 的存在,进程 A 可以知道哪些 Socket 发生了变化
Epoll 的实现细节
至此,相信读者对 Epoll 的本质已经有一定的了解。但我们还需要知道 eventpoll 的数据结构是什么样子
此外,就绪队列应该使用什么数据结构?eventpoll 应使用什么数据结构来管理通过 epoll_ctl 添加或删除的 Socket
Epoll 原理示意图,图片来源:《深入理解 Nginx:模块开发与架构解析(第二版)》,陶辉
如上图所示,eventpoll 包含了 Lock、MTX、WQ(等待队列)与 Rdlist 等成员,其中 Rdlist 和 RBR 是我们所关心的
就绪列表的数据结构
就绪列表引用着就绪的 Socket,所以它应能够快速的插入数据。程序可能随时调用 epoll_ctl 添加监视 Socket,也可能随时删除
当删除时,若该 Socket 已经存放在就绪列表中,它也应该被移除。所以就绪列表应是一种能够快速插入和删除的数据结构
双向链表就是这样一种数据结构,Epoll 使用双向链表来实现就绪队列(对应上图的 Rdlist)
索引结构
既然 Epoll 将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视的 Socket,至少要方便地添加和移除,还要便于搜索,以避免重复添加
红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是 O(log(N)),效率较好,Epoll 使用了红黑树作为索引结构(对应上图的 RBR)
注:因为操作系统要兼顾多种功能,以及有更多需要保存的数据,Rdlist 并非直接引用 Socket,而是通过 Epitem 间接引用,红黑树的节点也是 Epitem 对象
同样,文件系统也并非直接引用着 Socket。为方便理解,本文中省略了一些间接结构
总结
Epoll 在 Select 和 Poll 的基础上引入了 eventpoll 作为中间层,使用了先进的数据结构,是一种高效的多路复用技术
这里也以表格形式简单对比一下 Select、Poll 与 Epoll,结束此文。希望读者能有所收获
图片来源《Linux 高性能服务器编程》