1.网络应用程序设计模式 1.1 C/S 模式 传统的网络应用设计模式,客户机(client)/ 服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。
优点:
协议选用更加灵活,可以自定义。
可以提前缓存大量数据,提高数据传输的效率。
缺点:
对用户主机构成威胁。
开发团队工作量大,客户端服务端联合调试困难。
1.2 B/S 模式 浏览器(browser)/ 服务器(server)模式。只需在一端部署服务器,而另外一端使用每台 PC 都默认配置的浏览器即可完成数据的传输。
优点:
不会安装软件,相对来说更安全。
不需要开发客户端,工作量相对较小。
跨平台。
缺点:
协议选择不灵活,必须完整的支持协议。
不能数据缓存。
2.socket 概念 socket 不是网络协议,只是传输层协议 TCP/UDP 的实现。
2.1 socket 是伪文件 在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。(类比有名管道)
socket 在数据传输中的位置:
2.2 socket 通信原理 所以 IP + port 就对应着一个 socket,想要连接的两个进程通过各自的 socket 来标识,这两个 socket 组成的 socket pair 就唯一标识一个连接。
2.3 socket 的内核实现
3.预备知识 3.1 网络字节序 数据包在传输的过程中,需要拆包封包,读取 IP + port,本地字节序是小端方式,而网络字节需是大端方式。
**重要:**因为 TCP/IP 协议规定,网络数据流应采用大端字节序 ;而 x86 机器都是小端字节序。
所以需要使用字节序转换函数。
h 表示 host,n 表示 network,l 表示 long 32 位长整型,s 表示 short 16位端整型。
1 2 3 4 5 6 #include <arpa/inet.h> unit32_t htonl (unit32_t hostlong) ; unit16_t htons (unit16_t hostshort) ; unit32_t ntohl (unit32_t netlong) ; unit16_t ntohs (unit16_t netshort) ;
PS:如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回 。
3.2 IP 地址转换 常见的点分十进制 IP 地址是字符串类型,我们需要将 string 类型,先手动转化为 unsigned int 类型,才能使用 htonl 等函数。于是有了更便捷的函数,直接传入点分十进制 IP 地址的字符串作为参数。
htonl:192.168.1.2 ==》 unsigned int ==》 网络字节序
inet_pton:192.168.1.2 ==》 网络字节序
1 2 #include <arpa/inet.h> int inet_pton (int af, const char *src, void *dst) ;
int af:用来表示 ipv4 还是 ipv6。
ipv4:af = AF_INET ipv6:af = AF_INT6 char *src:本地字节序。点分十进制的 IP 地址。
void *dst:网络字节序,传出参数。比如 server_addr.sin_addr
1 const char *inet_ntop (int af, const void *src, char *dst, socklen_t size) ;
3.3 sockaddr 数据结构 关于 struct sockaddr:
struct sockaddr 结构体诞生早于IPv4协议。
struct sockaddr 现在已经废弃,定义结构体时,应使用 sockaddr_in 或 sockaddr_in6。(分别表示 IPV4 和 IPV6)
【重要】但是 bind、accept、connect 等函数的参数,规定的还是 struct sockaddr *
。
所以需要强制转换,例如:bind( xxx, (struct sockaddr *) &addr );
关于 sockaddr_in:
在 netinet/in.h
文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 struct sockaddr_in { __kernel_sa_family_t sin_family; __be16 sin_port; struct in_addr sin_addr; unsigned char __pad[__SOCK_SIZE__ - sizeof (short int ) - sizeof (unsigned short int ) - sizeof (struct in_addr)]; };struct in_addr { __be32 s_addr; };
4.网络套接字函数 4.1 socket 函数 创建一个 socket,返回 socket 文件描述符。
1 2 3 4 #include <sys/types.h> #include <sys/socket.h> int socket (int domain, int type, int protocol) ;
4.2 bind 函数 绑定 IP + port 和 socket 文件描述符。使 sockfd 文件描述符监听 addr 所描述的地址和端口号。
1 2 3 4 5 #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
4.3 listen 函数 用来声明sockfd处于监听状态,并指定 同时链接 的个数上限。【不是指定链接上限数】
例如:最多只能有 400 个客户端和我同时建立链接。也就是说,处于三次握手过程中的链接数最多是 400 个。
1 2 3 4 #include <sys/types.h> #include <sys/socket.h> int listen (int sockfd, int backlog) ;
sockfd:socket 文件描述符。
backlog:处于三次握手过程中的链接数最。
返回值:成功返回 0,失败返回 -1。
查看系统默认 backlog:
1 cat /proc/sys/net/ipv4/tcp_max_syn_backlog
4.4 accept 函数 三方握手完成后,服务器 调用 accept() 接受连接。
如果服务器调用 accept() 时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
1 2 3 #include <sys/types.h> #include <sys/socket.h> int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen) ;
sockfd:socket 文件描述符。
addr:传出参数(value-result argumen),返回链接客户端的地址信息(IP + port)
addrlen:传入传出参数。【注意是指针 socklen_t*,bind 和 connect 函数的参数不是指针 socklen_t】
传入:addr 还没发生改变时,传入 sizeof(addr),表示传出参数容器的大小。 传出:函数执行后,addr 里面有了真正的数据,传出 sizeof(addr),表示真正接收到的 addr 的大小。 返回值:成功,返回一个新 的 socket 文件描述符,用于和客户端通信。失败返回 -1,设置 errno。
4.5 connect 函数 客户端 调用 connect() 连接服务器。
connect 和 bind 的参数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址。
1 2 3 4 #include <sys/types.h> #include <sys/socket.h> int connect (int sockfd, struct sockaddr *addr, socklen_t addrlen) ;
sockfd:socke 文件描述符。
addr:传入参数,指定服务器端的信息,IP + port。
addrlen:传入参数,sizeof(addr)。
返回值:成功返回 0,失败返回 -1,设置 neero。
5.socket 实例 5.1 CS模型的流程 客户端发送小写字母;服务端收到后,转换为大写字母,并回传给客户端。
5.2 代码实现 server.cc
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 #define SERVER_PORT 6666 int main () { int lfd = socket (AF_INET, SOCK_STREAM, 0 ); sockaddr_in server_addr; bzero (&server_addr, sizeof (server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons (SERVER_PORT); server_addr.sin_addr.s_addr = htonl (INADDR_ANY); bind (lfd, (sockaddr *)&server_addr, sizeof (server_addr)); listen (lfd, 32 ); sockaddr_in client_addr; socklen_t len = sizeof (client_addr); bzero (&client_addr, len); int cfd = accept (lfd, (sockaddr *)&client_addr, &len); if (cfd == -1 ) { std::cout << "fail" << std::endl; } char client_buf[BUFSIZ]; inet_ntop (AF_INET, &client_addr.sin_addr, client_buf, sizeof (client_buf)); int client_port = ntohs (client_addr.sin_port); std::cout << "Client IP: " << client_buf << "Client port: " << client_port << std::endl; while (1 ) { char buf[BUFSIZ]; int n = read (cfd, buf, sizeof (buf)); for (int i = 0 ; i < n; i++) { buf[i] = toupper (buf[i]); } write (cfd, buf, n); } close (lfd); return 0 ; }
client.cc
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 #define SERVER_IP "127.0.0.1" #define SERVER_PORT 6666 int main () { int cfd = socket (AF_INET, SOCK_STREAM, 0 ); sockaddr_in server_addr; memset (&server_addr, 0 , sizeof (server_addr)); server_addr.sin_family = AF_INET; inet_pton (AF_INET, SERVER_IP, &server_addr.sin_addr); server_addr.sin_port = htons (SERVER_PORT); int ret = connect (cfd, (sockaddr*)&server_addr, sizeof (server_addr)); if (ret == -1 ) { std::cout << "fail" << std::endl; } while (1 ) { char buf[BUFSIZ]; fgets (buf, sizeof (buf), stdin); std::cout << "buf = " << buf << std::endl; write (cfd, buf, strlen (buf)); int n = read (cfd, buf, sizeof (buf)); std::cout << "Response from server: " << std::endl; write (STDOUT_FILENO, buf, n); std::cout << std::endl; } close (cfd); return 0 ; }
5.3 遇到的问题 inet_pton 函数传出参数不是 &server_addr.sin_addr.s_addr
,而是 &server_addr.sin_addr
。
server 的 IP 一定不能错。
如果不使用 toupper 函数,自己实现的话,注意输入非法字符容易报错(中文、数字、大写字母)
【重要问题 】先关闭 server 后,四次挥手,server 进入 TIME_WAIT 状态,此时端口被占用,无法再次 connect。
检查端口 6666 是否占用。
1 netstat -apn | grep 6666
connect error: No route to host. 链接失败,没有主机路由。需要关闭服务器防火墙测试。
5.4 错误处理函数 什么是错误处理函数?
就是对 socket、bind、accept、connec 等函数进行二次封装,将返回值判断封装其中。
为什么要写错误处理函数?
对于 socket、bind、accept、connect 都应该判断返回值。但是为了保证主程序的逻辑清晰,将判断返回值的步骤放在错误处理函数中。
为什么封装后的函数名要大写?
socket 封装成 Socket,这样在 vim 中按 shift + k
依然可以打开 man 文档,因为打开 man 文档不区分函数名大小写。
wrap.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "include.hpp" #ifndef __WRAP_H_ #define __WRAP_H_ void perror_exit (const char *s) ;int Accept (int fd, struct sockaddr *sa, socklen_t *salenptr) ;void Bind (int fd, const struct sockaddr *sa, socklen_t salen) ;void Connect (int fd, const struct sockaddr *sa, socklen_t salen) ;void Listen (int fd, int backlog) ;int Socket (int family, int type, int protocol) ;ssize_t Read (int fd, void *ptr, size_t nbytes) ;ssize_t Write (int fd, const void *ptr, size_t nbytes) ;void Close (int fd) ;ssize_t Readn (int fd, void *vptr, size_t n) ;ssize_t Writen (int fd, const void *vptr, size_t n) ;static ssize_t my_read (int fd, char *ptr) ;ssize_t Readline (int fd, void *vptr, size_t maxlen) ;#endif
wrap.cc
【注意】if ( (n = accept(fd, sa, salenptr)) < 0) {
,一定要将 n = accept
用括号括起来,因为 < 的优先级高。
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 "wrap.hpp" void perror_exit (const char *s) { perror (s); exit (-1 ); }int Accept (int fd, sockaddr *sa, socklen_t *salenptr) { int n; again: if ( (n = accept (fd, sa, salenptr)) < 0 ) { if ((errno == ECONNABORTED) || (errno == EINTR)) { goto again; } else { perror_exit ("accept error" ); } } return n; }void Bind (int fd, const sockaddr *sa, socklen_t salen) { if (bind (fd, sa, salen) < 0 ) { perror_exit ("bind error" ); } }void Connect (int fd, const sockaddr *sa, socklen_t salen) { if (connect (fd, sa, salen) < 0 ) { perror_exit ("connect error" ); } }void Listen (int fd, int backlog) { if (listen (fd, backlog) < 0 ) { perror_exit ("listen error" ); } }int Socket (int family, int type, int protocol) { int n; if ( (n = socket (family, type, protocol)) < 0 ) { perror_exit ("socket error" ); } return n; }ssize_t Read (int fd, void *ptr, size_t nbytes) { ssize_t n; again: if ( (n = read (fd, ptr, nbytes)) == -1 ) { if (errno == EINTR) { goto again; } else { return -1 ; } } return n; }ssize_t Write (int fd, const void *ptr, size_t nbytes) { ssize_t n; again: if ( (n = write (fd, ptr, nbytes)) == -1 ) { if (errno == EINTR) { goto again; } else { return -1 ; } } return n; }void Close (int fd) { if (close (fd) == -1 ) { perror_exit ("close error" ); } }ssize_t Readn (int fd, void *vptr, size_t n) { size_t nleft; ssize_t nread; char *ptr; ptr = (char *)vptr; nleft = n; while (nleft > 0 ) { if ( (nread = read (fd, ptr, nleft)) < 0 ) { if (errno == EINTR) nread = 0 ; else return -1 ; } else if (nread == 0 ) break ; nleft -= nread; ptr += nread; } return n - nleft; }ssize_t Writen (int fd, const void *vptr, size_t n) { size_t nleft; ssize_t nwritten; const char *ptr; ptr = (char *)vptr; nleft = n; while (nleft > 0 ) { if ( (nwritten = write (fd, ptr, nleft)) <= 0 ) { if (nwritten < 0 && errno == EINTR) nwritten = 0 ; else return -1 ; } nleft -= nwritten; ptr += nwritten; } return n; }static ssize_t my_read (int fd, char *ptr) { static int read_cnt; static char *read_ptr; static char read_buf[100 ]; if (read_cnt <= 0 ) { again: if ( (read_cnt = read (fd, read_buf, sizeof (read_buf))) < 0 ) { if (errno == EINTR) goto again; return -1 ; } else if (read_cnt == 0 ) return 0 ; read_ptr = read_buf; } read_cnt--; *ptr = *read_ptr++; return 1 ; }ssize_t Readline (int fd, void *vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = (char *)vptr; for (n = 1 ; n < maxlen; n++) { if ( (rc = my_read (fd, &c)) == 1 ) { *ptr++ = c; if (c == '\n' ) break ; } else if (rc == 0 ) { *ptr = 0 ; return n - 1 ; } else return -1 ; } *ptr = 0 ; return n; }
6.多进程并发服务器 6.1 思路 每一个客户端,都由一个子进程负责通信。
Server–父进程:
监听连接部分:
创建 socket,将监听端口绑定到 lfd。 等待 Client 连接,并返回用于通信的 cfd。 管理子进程部分:
一旦有新 Client 连接,则创建子进程,让子进程负责通信。 捕捉信号(子进程死亡 SIGCHLD),回收子进程。 Server–子进程:
Client:
6.2 代码实现 server.cc
要注意的地方:
父进程不负责通信,所以必须关掉 cfd,以防文件描述符不够用。
子进程不负责连接和创建子进程,需要退出循环并关掉 lfd。
子进程通信时,如果客户端关闭写端,此时 read 的返回值 n == 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 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 void wait_child (int signo) { pid_t wait_pid = 0 ; while ((wait_pid = waitpid (0 , NULL , WNOHANG)) > 0 ) { std::cout << "child_pid = " << wait_pid << " is end!" << std::endl; } }#define SERVER_PORT 8888 int main () { int lfd = Socket (AF_INET, SOCK_STREAM, 0 ); sockaddr_in server_addr; bzero (&server_addr, sizeof (server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons (SERVER_PORT); server_addr.sin_addr.s_addr = htonl (INADDR_ANY); Bind (lfd, (sockaddr*)&server_addr, sizeof (server_addr)); Listen (lfd, 128 ); int cfd; pid_t pid; while (1 ) { sockaddr_in client_addr; bzero (&client_addr, sizeof (client_addr)); socklen_t client_addr_len = sizeof (client_addr); cfd = Accept (lfd, (sockaddr*)&client_addr, &client_addr_len); char client_buf[BUFSIZ]; inet_ntop (AF_INET, &client_addr.sin_addr, client_buf, sizeof (client_buf)); int client_port = ntohs (client_addr.sin_port); std::cout << "Client IP: " << client_buf << "Client port: " << client_port << std::endl; pid = fork(); if (pid < 0 ) { perror_exit ("fork error" ); } else if (pid == 0 ) { close (lfd); break ; } else { close (cfd); signal (SIGCHLD, wait_child); } } if (pid == 0 ) { while (1 ) { char read_buf[BUFSIZ] = {0 }; int n = Read (cfd, read_buf, sizeof (read_buf)); if (n == 0 ) { close (cfd); return 0 ; } else { for (int i = 0 ; i < n; i++) { read_buf[i] = toupper (read_buf[i]); } Write (cfd, read_buf, n); } } } return 0 ; }
client.cc
和 5.2 中的 client 相同。
因为客户端的逻辑并没有改变,还是连接服务器 + 发送请求。
7.多线程并发服务器 7.1 思路 和多线程并发思路一致,只是将进程换成线程。
每一个客户端,都由一个子线程负责通信。
Server–父线程:
监听连接部分:
创建 socket,将监听端口绑定到 lfd。 等待 Client 连接,并返回用于通信的 cfd。 管理子线程部分:
一旦有新 Client 连接,则创建子线程,让子线程负责通信。 并设置线程分离。 Server–子线程:
Client:
7.2 代码实现 server.cc
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 struct socket_info { sockaddr_in client_addr; int cfd; };void *communicate (void *arg) { socket_info *s_info = (socket_info *)arg; char client_buf[BUFSIZ] = {0 }; inet_ntop (AF_INET, &s_info->client_addr.sin_addr, client_buf, sizeof (client_buf)); int client_port = ntohs (s_info->client_addr.sin_port); std::cout << "Client IP: " << client_buf << " Client port: " << client_port << std::endl; while (1 ) { char read_buf[BUFSIZ] = {0 }; int n = Read (s_info->cfd, read_buf, sizeof (read_buf)); if (n == 0 ) { close (s_info->cfd); break ; } else { for (int i = 0 ; i < n; i++) { read_buf[i] = toupper (read_buf[i]); } Write (s_info->cfd, read_buf, n); } } close (s_info->cfd); return (void *)0 ; }#define SERVER_PORT 8888 int main () { int lfd = Socket (AF_INET, SOCK_STREAM, 0 ); sockaddr_in server_addr; bzero (&server_addr, sizeof (server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons (SERVER_PORT); server_addr.sin_addr.s_addr = htonl (INADDR_ANY); Bind (lfd, (sockaddr*)&server_addr, sizeof (server_addr)); Listen (lfd, 128 ); int cfd; pid_t pid; socket_info s_info[256 ]; int i = 0 ; pthread_t pth_id; while (1 ) { socklen_t client_addr_len = sizeof (s_info[i].client_addr); bzero (&s_info[i].client_addr, client_addr_len); s_info[i].cfd = Accept (lfd, (sockaddr*)&s_info[i].client_addr, &client_addr_len); pthread_create (&pth_id, NULL , communicate, (void *)&s_info[i]); pthread_detach (pth_id); i++; } return 0 ; }
client.cc
和 5.2 中的 client 相同。
因为客户端的逻辑并没有改变,还是连接服务器 + 发送请求。
8.socket 与 TCP 8.1 TCP 状态转换 详见 计算机网络笔记
8.2 socket 与 TCP 对应
###8.3 TIME_WAIT 状态
先关闭 server 后,四次挥手,server 进入 TIME_WAIT 状态,此时端口被占用,无法再次 connect。
检查端口 6666 是否占用。
1 netstat -apn | grep 6666
8.4 FIN_WAIT_2 状态 可以使用 API 实现半关闭状态。
1 2 #include <sys/stocket.h> int shutdown (int sockfd, int how) ;
sockfd:要关闭的文件描述符。
how:关闭的方式。
SHUT_RD(0):关闭sockfd上的读功能,该套接字不再接收数据 ,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。 SHUT_WR(1):关闭sockfd的写功能,进程不能在对此套接字发出写操作。 SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是 SHUT_RD,然后是SHUT_WR。 PS:shutdown 和 close 的区别
举个例子:
多个进程共享一个套接字,close 被调用一次,计数减一,直到所有进程均调用 close,套接字才被释放。(套接字就是内核中的两个缓冲区)
在多进程中,如果一个进程调用了shutdown(sfd, SHUT_RDWR)后,其它的进程将无法进行通信。但如果一个进程close(sfd)将不会影响到其它进程。
8.5 端口复用 函数原型:
该函数用法极其复杂,记住咱们设置端口复用即可。
1 2 3 4 #include <sys/types.h> #include <sys/socket.h> int setsockopt (int sockfd, int level, int optname, const void *optval, socklen_t optlen) ;
sockfd:套接字文件描述符
level:级别
optname:选项名
optval:选项值
optlen:选项的字节长度
设置端口复用:
在 bind 之前,设置。
1 2 int opt = 1 ;setsockopt (listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt));
SOL_SOCKET:级别
SO_REUSEADDR:允许重用本地端口
9.多路 IO 转接服务器 也叫多任务 IO 服务器。
9.1 思想 不再由应用程序自己监听客户端连接,取而代之由内核替应用程序监视文件。
下面主要介绍三种方式:
以 select 监听为例(poll、epoll 同理):
lfd 请求建立连接。
举个例子:select 发现 lfd 有连接请求,会让 server 调用 accept 函数,和客户端建立建立连接。(因为是有连接请求后,才调用的 accept 函数,所以 accept 不会阻塞 。)
cfd 读、写、异常事件。
举个例子:当 select 监听到有 cfd_d 有可读事件时,会让 server 调用 read 函数,读取 cfd_d 中的数据。(因为是 cfd 中有数据后,才调用的 read 函数,所以 read 不会阻塞 。)
9.2 问题 虽然不需要服务器程序自己监听,但是连接客户端、处理请求都需要服务器程序来处理。
**问题:**一旦某个客户端的请求处理的时间过长,其他的客户端就无法及时连接。
**解决:**线程池。
10.select 10.1 函数原型 1 2 3 4 5 6 7 8 9 10 11 12 #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) ;struct timeval { long tv_sec; long tv_usec; };
nfds :所监听的所有文件描述符中,最大的文件描述符 + 1。
意义:此参数会告诉内核检测前多少个文件描述符的状态。
比如:进程打开了序号为 0 - 400 的文件描述符,nfds 则为 401。
fd_set :位图,用二进制来标记文件描述符是否在集合中。
readfds :监听文件描述符集合 readfds 中,是否有可读事件。传入传出参数。
writefds :监听文件描述符集合 writefds 中,是否有可写事件。传入传出参数。
exceptfds :监听文件描述符集合 exceptfds 中,是否有异常事件。传入传出参数。
【传入传出】举个例子 :
调用 select 之前,我们想监听 4、5 文件描述符是否有可读事件。
readfds 集合中,4、5 文件描述符对应的值为 1,其他的为 0。
调用 select 之后,只有 4 文件描述符有可读事件。
readfds 集合中,4 文件描述符对应的值为 1,其他的为 0。
10.2 设置 fd_set 类似设置未决信号集、阻塞信号集(类型 sigset_t)
【全部清除】将 set 所有位置 0。
1 void FD_ZERO (fd_set *set) ;
【清除 fd】将 set 中对应的 fd 置 0。
1 void FD_CLR (int fd, fd_set *set) ;
【添加 fd】将 set 中对应的 fd 置 1。
1 void FD_SET (int fd, fd_set *set) ;
【判断 fd 是否在 set 中】判读 set 中对应的 fd 是否为 1。
1 int FD_ISSET (int fd, fd_set *set) ;
10.3 select 的局限性 文件描述符上限:1024。
也就是说同时监听文件描述符 1024 个。
select 返回值只返回总数,如果需要知道具体的哪一个文件描述符,需要 for 循环判断 0 - 1023 个文件描述符。
**解决方案:**自定义结构体(数组),将要监听的文件描述符加入数组。for 循环时,只循环判断数组中的文件描述符。
监听集合、满足监听条件的集合是同一个集合。
所以需要保存原有的监听集合 ,否则返回后监听集合就被破坏了。
10.4 思路 储存文件描述符和返回结果的思路。
传入三个位图,传出三个位图,在传出的三个位图中,寻找满足条件的事件进行处理。
10.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 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 #define SERVER_PORT 8888 int main () { int lfd = Socket (AF_INET, SOCK_STREAM, 0 ); int opt = 1 ; setsockopt (lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); sockaddr_in server_addr; socklen_t server_addr_len = sizeof (server_addr); bzero (&server_addr, server_addr_len); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl (INADDR_ANY); server_addr.sin_port = htons (SERVER_PORT); Bind (lfd, (sockaddr*)&server_addr, server_addr_len); Listen (lfd, 128 ); int max_fd = lfd; int client[FD_SETSIZE]; int max_i = -1 ; for (int i = 0 ; i < FD_SETSIZE; i++) { client[i] = -1 ; } fd_set allset; FD_ZERO (&allset); FD_SET (lfd, &allset); while (1 ) { fd_set rset = allset; int n_ready = select (max_fd + 1 , &rset, NULL , NULL , NULL ); if (n_ready < 0 ) { perror_exit ("select error" ); } std::cout << "n_ready = " << n_ready << std::endl; if (FD_ISSET (lfd, &rset)) { sockaddr_in client_addr; socklen_t client_addr_size = sizeof (client_addr); bzero (&client_addr, client_addr_size); int cfd = Accept (lfd, (sockaddr *)&client_addr, &client_addr_size); char client_ip[BUFSIZ] = {0 }; inet_ntop (AF_INET, &client_addr.sin_addr, client_ip, sizeof (client_ip)); int client_port = ntohs (client_addr.sin_port); std::cout << "Client IP: " << client_ip << "Client port: " << client_port << std::endl; int i = 0 ; for (i = 0 ; i < FD_SETSIZE; i++) { if (client[i] < 0 ) { client[i] = cfd; break ; } } if (i == FD_SETSIZE) { std::cout << "too many client" << std::endl; exit (1 ); } FD_SET (cfd, &allset); if (max_fd < cfd) { max_fd = cfd; } if (i > max_i) { max_i = i; } if (-- n_ready == 0 ) { continue ; } } for (int i = 0 ; i <= max_i; i++) { int sockfd = client[i]; if (sockfd < 0 ) { continue ; } if (FD_ISSET (sockfd, &rset)) { int n; char read_buf[BUFSIZ] = {0 }; if ( (n = Read (sockfd, read_buf, sizeof (read_buf))) == 0 ) { close (sockfd); FD_CLR (sockfd, &allset); client[i] = -1 ; } else if (n > 0 ) { for (int j = 0 ; j < n; j++) { read_buf[j] = toupper (read_buf[j]); } Write (sockfd, read_buf, n); } if (--n_ready == 0 ) { break ; } } } } return 0 ; }
11.poll 10.1 函数原型 1 2 3 4 5 6 7 8 9 #include <poll.h> int poll (struct pollfd *fds, nfds_t nfds, int timeout) ;struct pollfd { int fd; short events; short revents; };
struct pollfd 结构体
10.2 poll 的优势 和 select 相比。
修改配置文件 file descriptors 的值,可以改变文件描述符 1024 的上限。
查看文件描述符个数:ulimit -a
或者 ulimit -n
1 2 3 4 5 6 7 8 9 10 cat //proc/sys/fs/file-max vi /etc/security/limits.conf * soft nofile 65535 * hard nofile 10000
分离了监听集合和返回集合
在 select 中,文件描述符集合是传入传出参数。
FD_ISSET 搜索的范围变小。
select 需要从 0 - 1024 遍历。(解决方案:自己维护一个用于遍历监听文件描述符的数组)
poll 监听的文件描述符在 pollfd 结构体数组中,直接遍历 pollfd 结构体数组。
10.3 思路 储存文件描述符和返回结果的思路。
用结构体数组储存 需要监听的文件描述符、需要监听的事件、监听到的事件。
10.4 代码实现
server.cc
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 #include "include.hpp" #include "wrap.hpp" #define SERVER_PORT 8888 #define OPEN_MAX 1024 int main () { int lfd = Socket (AF_INET, SOCK_STREAM, 0 ); int opt = 1 ; setsockopt (lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); sockaddr_in server_addr; socklen_t server_addr_len = sizeof (server_addr); bzero (&server_addr, server_addr_len); server_addr.sin_family = AF_INET; server_addr.sin_port = htons (SERVER_PORT); server_addr.sin_addr.s_addr = htonl (INADDR_ANY); Bind (lfd, (sockaddr *)&server_addr, server_addr_len); Listen (lfd, 128 ); pollfd client[OPEN_MAX]; for (int i = 0 ; i < OPEN_MAX; i++) { client[i].fd = -1 ; } int maxi = 0 ; client[0 ].fd = lfd; client[0 ].events = POLLIN; maxi = 1 ; while (1 ) { int n_ready = poll (client, maxi + 1 , -1 ); std::cout << "n_ready = " << n_ready <<std::endl; if (client[0 ].revents & POLLIN) { sockaddr_in client_addr; socklen_t client_addr_len = sizeof (client_addr); bzero (&client_addr, client_addr_len); int cfd = Accept (lfd, (sockaddr *)&client_addr, &client_addr_len); char IP[BUFSIZ] = {0 }; std::cout << "IP: " << inet_ntop (AF_INET, &client_addr.sin_addr, IP, sizeof (IP)) << " Port: " << ntohs (client_addr.sin_port) << std::endl; int i = 0 ; for (i = 0 ; i < OPEN_MAX; i++) { if (client[i].fd < 0 ) { client[i].fd = cfd; client[i].events = POLLIN; std::cout << "i = " << i <<std::endl; break ; } } if (i == OPEN_MAX) { perror_exit ("Too many client" ); } if (maxi < i) { maxi = i; } if (--n_ready <= 0 ) { continue ; } } for (int i = 1 ; i <= maxi; i++) { if (client[i].fd < 0 ) { continue ; } std::cout << "11 i = " << i <<std::endl; if (client[i].revents & POLLIN) { char read_buf[BUFSIZ] = {0 }; int n = 0 ; if ( (n = Read (client[i].fd, read_buf, sizeof (read_buf))) < 0 ) { if (errno == ECONNRESET) { std::cout << "client[" << i << "] aborted connection." << std::endl; Close (client[i].fd); client[i].fd = -1 ; } else { perror_exit ("read error" ); } } else if (n == 0 ) { std::cout << "client[" << i << "] closed connection." << std::endl; Close (client[i].fd); client[i].fd = -1 ; } else { for (int j = 0 ; j < n; j++) { read_buf[j] = toupper (read_buf[j]); } Write (client[i].fd, read_buf, n); } if (--n_ready <= 0 ) { break ; } } } } return 0 ; }
12.epoll 12.1 函数原型 (1)创建一个 epoll 句柄
1 2 3 #include <sys/epoll.h> int epoll_create (int size) ;
(2)控制 epoll 监控的某个文件描述符上的事件:注册、修改、删除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <sys/epoll.h> int epoll_ctl (int epfd, int op, int fd, struct epoll_event *event) ;struct epoll_event { __uint32_t events; epoll_data_t data; };typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t ;
epfd:epoll_create 函数返回的红黑树根节点。
op:表示动作,用三个宏表示 注册、修改、删除。
EPOLL_CTL_ADD (注册新的fd到epfd)
EPOLL_CTL_MOD (修改已经注册的fd的监听事件)
EPOLL_CTL_DEL (从epfd删除一个fd)
fd:所监听的文件描述符。
event:结构体,两个变量:events 和 data。【传入参数】
EPOLLERR :表示对应的文件描述符发生错误
返回值:成功:0;失败:-1,设置相应的 errno。
(3)监听
1 2 3 #include <sys/epoll.h> int epoll_wait (int epfd, struct epoll_event *events, int maxevents, int timeout) ;
epfd:同 epoll_ctl 第一个参数 epfd,红黑树的根节点。
events:与 epoll_ctl 最后一个参数不同,此处表示 event 的数组。【传出参数】
maxevents:events 数组容量。
timeout:
-1:阻塞。
0:立即返回,非阻塞。
>0:指定毫秒。
返回值:成功返回有多少文件描述符有监听到事件。时间到时返回 0,出错返回 -1。
12.2 特点 epoll 能显著提高 程序在大量并发连接中只有少量活跃 的情况下的系统CPU利用率。
因为 epoll 会复用文件描述符集合来传递结果,而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll 除了提供 select/poll 那种 IO 事件的水平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存 IO 状态,减少 epoll_wait/epoll_pwait 的调用,提高应用程序效率。
epoll 是 Linux 大规模并发网络程序中的热门首选模型。
与 poll 相同,通过 /etc/security/limits.conf
更改文件描述符个数。
12.3 思路 储存文件描述符和返回结果的思路。
12.4 代码实现 和 poll 实现流程相似,poll 的监听结构体数组被 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 #include "include.hpp" #include "wrap.hpp" #define SERVER_PORT 8888 #define OPEN_MAX 1024 int main () { int lfd = Socket (AF_INET, SOCK_STREAM, 0 ); int opt = 1 ; setsockopt (lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); sockaddr_in server_addr; socklen_t server_addr_len = sizeof (server_addr); bzero (&server_addr, server_addr_len); server_addr.sin_family = AF_INET; server_addr.sin_port = htons (SERVER_PORT); server_addr.sin_addr.s_addr = htonl (INADDR_ANY); Bind (lfd, (sockaddr *)&server_addr, server_addr_len); Listen (lfd, 128 ); int epfd = epoll_create (OPEN_MAX); if (epfd == -1 ) { perror_exit ("epoll_create error" ); } epoll_event fd_event; fd_event.events = EPOLLIN; fd_event.data.fd = lfd; int res = epoll_ctl (epfd, EPOLL_CTL_ADD, lfd, &fd_event); if (res == -1 ) { perror_exit ("epoll_ctl error" ); } epoll_event fd_event_arr[OPEN_MAX]; while (1 ) { int nready = epoll_wait (epfd, fd_event_arr, OPEN_MAX, -1 ); if (nready == -1 ) { perror_exit ("epoll_wait error" ); } for (int i = 0 ; i < nready; i++) { if (!fd_event_arr[i].events & EPOLLIN) { continue ; } if (fd_event_arr[i].data.fd == lfd) { sockaddr_in client_addr; socklen_t client_addr_len = sizeof (client_addr); bzero (&client_addr, client_addr_len); int cfd = Accept (lfd, (sockaddr *)&client_addr, &client_addr_len); char ip_buf[1024 ] = {0 }; inet_ntop (AF_INET, &client_addr.sin_addr, ip_buf, sizeof (ip_buf)); std::cout << "client connect successfully." << " IP: " << ip_buf << " Port: " << ntohs (client_addr.sin_port) << std::endl; fd_event.events = EPOLLIN; fd_event.data.fd = cfd; int res = epoll_ctl (epfd, EPOLL_CTL_ADD, cfd, &fd_event); if (res == -1 ) { perror_exit ("epoll_ctl error" ); } } else { int cfd = fd_event_arr[i].data.fd; char read_buf[BUFSIZ]; int n = Read (cfd, read_buf, sizeof (read_buf)); if (n == 0 ) { int res = epoll_ctl (epfd, EPOLL_CTL_DEL, cfd, NULL ); if (res == -1 ) { perror_exit ("epoll_ctl error" ); } Close (cfd); std::cout << "client closed connection" << std::endl; } else if (n < 0 ) { perror ("read n < 0 error" ); int res = epoll_ctl (epfd, EPOLL_CTL_DEL, cfd, NULL ); Close (cfd); } else { for (int i = 0 ; i < n; i++) { read_buf[i] = toupper (read_buf[i]); } Write (cfd, read_buf, n); } } } } return 0 ; }
12.5 水平触发和边沿触发 两种触发模式作用:减少对 epoll_wait 的调用次数。
(1)epoll 事件模型
epoll 事件模型有两种:
举一个形象的例子:
边缘触发就像上升沿、下降沿,只有高低电平发生变化时,才会触发。
水平触发就像水平沿,只要是高电平都会触发。
(2)ET 模式
举个例子:
[1]:客户端写入 2KB 数据。
[2]:服务端调用 epoll_wait,返回 cfd 有可读事件。
[3]:服务端读取 1KB 数据。(readn 函数)
[4]:服务端调用 epoll_wait。
在 ET 模式中,此时尽管缓冲区还有 1KB 数据没有读取,如果客户端没有新的数据写给服务端,epoll_wait 会阻塞在这里。
【问题】一旦客户端在等待服务端对于这 2KB 的数据反馈,服务端在等待客户端发送新的数据。一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
【解决】使用非阻塞的函数。no-block。
ET是高速工作方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。
然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。
(3)LT 模式
LT 是缺省 的工作方式,并且同时支持 block 和 no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的 select/poll 都是这种模型的代表。
(4)实现方式
epoll 的触发方式,通过结构体 struct epoll_event 中,成员变量 events 的监听事件来确定。
1 2 3 4 5 event.events = EPOLLIN; event.events = EPOLLIN | EPOLLET;
12.6 ET 模式的实例 epoll 也可以监听管道的文件描述符。
下面以管道为例,实现 ET 模式的 epoll 监听。