5种IO模型分别如下:
1、阻塞IO模型
当上层应用app1调用recv系统调用时,如果对等方没有发送数据(缓冲区没有数据),上层app1将阻塞(默认行为,被linux内核阻塞)。
当对等方发送了数据,linux内核recv端缓冲区有数据后,内核会把数据copy给用户空间。然后上层应用app1解除阻塞,执行下一步操作。
2、非阻塞IO模型
上层应用程序app2将套接字设置成非阻塞模式。
上层应用程序app2轮询调用recv函数,接收数据,若缓冲区没有数据,上层程序app2不会阻塞,recv返回值-1,错误码是EWOULDBLOCK。
上层应用程序不断轮询有没有数据到来,会造成上层应用忙等待,大量的消耗CPU时间,很少直接用,应用范围小,一般和select IO复用配合使用。
阻塞IO和非阻塞IO是两个极端,一个是没有数据就死等,另一个极端是轮询。
有没有这样一种机制去管理n个文件描述符,当文件描述符状态发生变化了,会通知应用程序呢?这就是IO复用技术,也叫作多路复用。
3、IO复用模型
上层应用程序app3调用select机制(该机制由linux内核支持,避免了app3忙等待),进行轮询文件描述符的状态变化。
当select管理的文件描述符没有数据(或者状态没有变化时),上层应用程序app3也会阻塞。
好处是select会管理多个文件描述符。
select可以看成一个管理者,用select来管理多个IO。
一旦检测到一个IO或者多个IO有我们感兴趣的事件发生,select函数将返回,返回值为检测到的事件个数,继而可以利用select相关api函数,具体操作事件。
select函数可以设置等待时间,避免了上层应用程序app3长期僵死。
和阻塞IO模型相比,select IO复用模型相当于提前阻塞了,等到有数据到来时再调用recv就不会发生阻塞。
4、信号驱动模型:
上层应用程序app4建立SIGIO信号处理程序,当缓冲区有数据到来时,内核会发送信号告诉上层应用程序app4。
上层应用程序app4接收到信号后,调用recv函数,因缓冲区有数据,recv函数一般不会阻塞。
这种模型用的也比较少,属于典型的“拉模式”,即上层应用程序app4需要调用recv函数把数据拉进来。
应用程序接收到信号有一个时间延迟,处理这个信号可能还有一个时间延迟,如果这些延迟比较长,内核空间数据有可能发生变化。存在隐患。因此,这种IO模型用的不多。
5、异步IO模型
上层应用app5调用aio_read函数,同时提交一个应用层的缓冲区buf,调用完毕后不会阻塞,上层应用程序app5可以进行其他任务。
当TCP IP协议缓冲区有数据时,linux内核主动把内核缓冲区中的数据copy到用户空间,然后再给上层应用app5发送信号,告诉app5数据有了,赶快处理吧。
这是典型的推模式。
效率最高的一种形式,上层应用app5有异步处理的能力(在linux内核的支持下,言外之意:处理其他任务的同时,也可支持IO通讯)。
上层应用app5也可以在干别的活儿的同时接收数据。与信号驱动IO模型相比,上层应用程序app5不需要调用recv函数。
这种IO模型效率非常高,是高性能、高并发服务器常用的模型。
IO复用模型和异步IO模型是两种常用的模型。
重要概念:
阻塞IO:
数据没有准备好,读操作就会阻塞。
数据不能立即被收时,写操作就会阻塞。
打开文件时阻塞,直到某些条件发生。
非阻塞IO:
立即返回,并用错误值来表示当前的状态。
指定非阻塞方式:
打开时指定O_NONBLOCK标志。
使用fcntl打开或者关闭非阻塞方式
网络编程时,使用非阻塞,用轮询方式发送
使用多线程可以避免使用非阻塞IO,但是同步开销大
多路IO:
当程序需要同时从两个输入读数据时
使用多进程、多线程,同步复杂,进程线程开销
使用非阻塞IO,交替轮询
通过信号使用异步IO,无法判断哪个IO完成
多路IO:把关心的IO放入一个列表,调用多路函数
多路IO函数阻塞,直到有一个数据准备好后返回
返回后告诉调用者哪个描述符准备好了
IO复用模型分析:
select实现说明:
调用select时通过参数告诉内核用户感兴趣的IO描述符
关心的IO状态:输入、输出或者错误
调用者等待时间
返回之后内核告诉调用者多个描述符准备好了
哪些描述符发生了变化
调用返回后对准备好的描述符调用读写操作
不关心的描述符集传NULL
select函数原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
监视readfs来查看是否read的时候会被阻塞,注意:即使到了end-of-file,fd也是可读的
监视writefds看写的时候会不会阻塞
监视excepted是否出现了异常,主要用来读取OOB数据,异常并不是指出错
注意当一个套接口出错时,它会变得既可读又可写
如果有了状态改变,会将其他fd清零,只有那些发生改变了的fd保持置位,以用来指示set中的哪一个改变了状态
参数n是所有set里的所有fd里面,具有最大值的那个fd的值加1
以下四个宏用来对fd_set进行操作:
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形式,select用来监视标准输入描述符和套接字,服务器端程序还是用普通的多进程模型。只给出客户端程序如下:
1 #include <sys/types.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 #include <string.h> 6 #include <errno.h> 7 #include <signal.h> 8 #include <arpa/inet.h> 9 #include <sys/socket.h> 10 #include <netinet/in.h> 11 #include <sys/socket.h> 12 #include <netinet/ip.h> /* superset of previous */ 13 14 void handler(int num) 15 { 16 printf("signal num : %d\n", num); 17 } 18 19 ssize_t readn(int fd, void *buf, size_t count) 20 { 21 size_t nleft = count; 22 ssize_t nread; 23 24 char *bufp = (char*)buf; 25 26 while(nleft > 0) 27 { 28 if( (nread = read(fd, bufp, nleft)) < 0 ) 29 { 30 if(errno == EINTR) 31 { 32 continue; 33 } 34 35 return -1; 36 } 37 else if(nread == 0) 38 { 39 return count - nleft; 40 } 41 42 bufp += nread; 43 nleft -= nread; 44 } 45 46 return count; 47 } 48 49 ssize_t writen(int fd, const void *buf, size_t count) 50 { 51 size_t nleft = count; 52 ssize_t nwritten; 53 54 char *bufp = (char*)buf; 55 56 while(nleft > 0) 57 { 58 if( (nwritten = write(fd, bufp, nleft)) < 0) 59 { 60 if(errno == EINTR) 61 { 62 continue; 63 } 64 65 return -1; 66 } 67 else if(nwritten == 0) 68 { 69 continue; 70 } 71 72 bufp += nwritten; 73 nleft -= nwritten; 74 } 75 76 return count; 77 } 78 79 ssize_t recv_peek(int sockfd, void *buf, size_t len) 80 { 81 while(1) 82 { 83 int ret = recv(sockfd, buf, len, MSG_PEEK); 84 if(ret == -1 && errno == EINTR) 85 continue; 86 return ret; 87 } 88 } 89 90 ssize_t readline(int sockfd, void *buf, size_t maxline) 91 { 92 int ret; 93 int nread; 94 char *bufp = (char*)buf; 95 int nleft = maxline; 96 97 while(1) 98 { 99 ret = recv_peek(sockfd, bufp, nleft); 100 if(ret < 0) 101 { 102 return ret; 103 } 104 else if(ret == 0) 105 { 106 return ret; 107 } 108 109 nread = ret; 110 int i; 111 for(i = 0; i < nread; i++) 112 { 113 if(bufp[i] == '\n') 114 { 115 ret = readn(sockfd, bufp, i+1); 116 if(ret != i+1) 117 { 118 perror("readn error"); 119 exit(0); 120 } 121 122 return ret; 123 } 124 } 125 126 if(nread > nleft) 127 { 128 perror("FAILURE"); 129 exit(0); 130 } 131 132 nleft -= nread; 133 ret = readn(sockfd, bufp, nread); 134 if(ret != nread) 135 { 136 perror("readn error"); 137 exit(0); 138 } 139 bufp += nread; 140 } 141 142 return -1; 143 } 144 145 146 void echo_cli(int sock) 147 { 148 fd_set rset; 149 FD_ZERO(&rset); 150 151 int nready; 152 int maxfd; 153 int fd_stdin = fileno(stdin); 154 if(fd_stdin > sock) 155 maxfd = fd_stdin; 156 else 157 maxfd = sock; 158 159 char sendbuf[1024] = {0}; 160 char recvbuf[1024] = {0}; 161 162 int stdineof = 0; 163 164 while(1) 165 { 166 if (stdineof == 0) 167 FD_SET(fd_stdin, &rset); 168 FD_SET(sock, &rset); 169 170 nready = select(maxfd + 1, &rset, NULL, NULL, NULL); 171 172 if(nready == -1) 173 { 174 perror("select error"); 175 exit(0); 176 } 177 178 if(nready == 0) 179 { 180 continue; 181 } 182 183 if(FD_ISSET(sock, &rset)) 184 { 185 int ret = readline(sock, recvbuf, sizeof(recvbuf)); 186 if(ret == -1) 187 { 188 perror("readline error"); 189 exit(0); 190 } 191 else if(ret == 0) 192 { 193 printf("server closed\n"); 194 break; 195 } 196 197 fputs(recvbuf, stdout); 198 memset(recvbuf, 0, sizeof(recvbuf)); 199 } 200 201 if(FD_ISSET(fd_stdin, &rset)) 202 { 203 if(fgets(sendbuf, sizeof(sendbuf), stdin) == NULL) 204 { 205 printf("ctrl+d result fgets return\n"); 206 stdineof = 1; 207 close(sock); 208 } 209 else 210 { 211 writen(sock, sendbuf, strlen(sendbuf)); 212 memset(sendbuf, 0, sizeof(sendbuf)); 213 } 214 } 215 216 } 217 } 218 219 int main() 220 { 221 int sockfd; 222 223 signal(SIGPIPE, handler); 224 225 sockfd = socket(AF_INET, SOCK_STREAM, 0); 226 227 struct sockaddr_in addr; 228 addr.sin_family = AF_INET; 229 addr.sin_port = htons(8001); 230 inet_aton("192.168.31.128", &addr.sin_addr); 231 //addr.sin_addr.s_addr = inet_addr("192.168.31.128"); 232 233 if( connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1 ) 234 { 235 perror("connect error"); 236 exit(0); 237 } 238 239 struct sockaddr_in localaddr; 240 socklen_t addrlen = sizeof(localaddr); 241 if(getsockname(sockfd, (struct sockaddr*)&localaddr, &addrlen) < 0) 242 { 243 perror("getsockname error"); 244 exit(0); 245 } 246 247 printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port)); 248 249 echo_cli(sockfd); 250 251 return 0; 252 }
核心函数为echo_cli,其中调用了select函数,167、168将两个描述符加入到监视器中。fgets发生错误或者读到文件末尾时会返回NULL。读标准输入时ctrl+d代表文件末尾。
启动服务器和客户端,执行结果如下:
connect连接时,会阻塞进程,在公网上默认会等待75秒超时,但是一般应用不想等这么久,因此,需要优化一下。
网络模型中常用的操作:
1 #ifndef _SCK_UTIL_H_ 2 #define _SCK_UTIL_H_ 3 4 #include <unistd.h> 5 #include <sys/types.h> 6 #include <fcntl.h> 7 #include <sys/select.h> 8 #include <sys/time.h> 9 #include <sys/socket.h> 10 #include <netinet/in.h> 11 #include <arpa/inet.h> 12 #include <netdb.h> 13 14 #include <stdio.h> 15 #include <errno.h> 16 #include <string.h> 17 #include <stdlib.h> 18 19 #define ERR_EXIT(m) \ 20 do \ 21 { \ 22 perror(m); \ 23 exit(EXIT_FAILURE); \ 24 } \ 25 while (0) 26 27 void activate_nonblock(int fd); 28 void deactivate_nonblock(int fd); 29 30 int read_timeout(int fd, unsigned int wait_seconds); 31 int write_timeout(int fd, unsigned int wait_seconds); 32 int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds); 33 int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds); 34 35 36 ssize_t readn(int fd, void *buf, size_t count); 37 ssize_t writen(int fd, const void *buf, size_t count); 38 ssize_t recv_peek(int sockfd, void *buf, size_t len); 39 ssize_t readline(int sockfd, void *buf, size_t maxline); 40 41 #endif /* _SYS_UTIL_H_ */
1 #include "sckutil.h" 2 3 /* read函数的调用方法 4 int ret; 5 ret = read_timeout(fd, 5); 6 if (ret == 0) 7 { 8 read(fd, ...); 9 } 10 else if (ret == -1 && errno == ETIMEDOUT) 11 { 12 timeout.... 13 } 14 else 15 { 16 ERR_EXIT("read_timeout"); 17 } 18 */ 19 20 /** 21 * read_timeout - 读超时检测函数,不含读操作 22 * @fd: 文件描述符 23 * @wait_seconds: 等待超时秒数,如果为0表示不检测超时 24 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT 25 */ 26 int read_timeout(int fd, unsigned int wait_seconds) 27 { 28 int ret = 0; 29 if (wait_seconds > 0) 30 { 31 fd_set read_fdset; 32 struct timeval timeout; 33 34 FD_ZERO(&read_fdset); 35 FD_SET(fd, &read_fdset); 36 37 timeout.tv_sec = wait_seconds; 38 timeout.tv_usec = 0; 39 40 //select返回值三态 41 //1 若timeout时间到(超时),没有检测到读事件 ret返回=0 42 //2 若ret返回<0 && errno == EINTR 说明select的过程中被别的信号中断(可中断睡眠原理) 43 //2-1 若返回-1,select出错 44 //3 若ret返回值>0 表示有read事件发生,返回事件发生的个数 45 46 do 47 { 48 ret = select(fd + 1, &read_fdset, NULL, NULL, &timeout); 49 } while (ret < 0 && errno == EINTR); 50 51 if (ret == 0) 52 { 53 ret = -1; 54 errno = ETIMEDOUT; 55 } 56 else if (ret == 1) 57 ret = 0; 58 } 59 60 return ret; 61 } 62 63 /** 64 * write_timeout - 写超时检测函数,不含写操作 65 * @fd: 文件描述符 66 * @wait_seconds: 等待超时秒数,如果为0表示不检测超时 67 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT 68 */ 69 int write_timeout(int fd, unsigned int wait_seconds) 70 { 71 int ret = 0; 72 if (wait_seconds > 0) 73 { 74 fd_set write_fdset; 75 struct timeval timeout; 76 77 FD_ZERO(&write_fdset); 78 FD_SET(fd, &write_fdset); 79 80 timeout.tv_sec = wait_seconds; 81 timeout.tv_usec = 0; 82 do 83 { 84 ret = select(fd + 1, NULL, &write_fdset, NULL, &timeout); 85 } while (ret < 0 && errno == EINTR); 86 87 if (ret == 0) 88 { 89 ret = -1; 90 errno = ETIMEDOUT; 91 } 92 else if (ret == 1) 93 ret = 0; 94 } 95 96 return ret; 97 } 98 99 /** 100 * accept_timeout - 带超时的accept 101 * @fd: 套接字 102 * @addr: 输出参数,返回对方地址 103 * @wait_seconds: 等待超时秒数,如果为0表示正常模式 104 * 成功(未超时)返回已连接套接字,超时返回-1并且errno = ETIMEDOUT 105 */ 106 int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) 107 { 108 int ret; 109 socklen_t addrlen = sizeof(struct sockaddr_in); 110 111 if (wait_seconds > 0) 112 { 113 fd_set accept_fdset; 114 struct timeval timeout; 115 FD_ZERO(&accept_fdset); 116 FD_SET(fd, &accept_fdset); 117 timeout.tv_sec = wait_seconds; 118 timeout.tv_usec = 0; 119 do 120 { 121 ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout); 122 } while (ret < 0 && errno == EINTR); 123 if (ret == -1) 124 return -1; 125 else if (ret == 0) 126 { 127 errno = ETIMEDOUT; 128 return -1; 129 } 130 } 131 132 //一但检测出 有select事件发生,表示对等方完成了三次握手,客户端有新连接建立 133 //此时再调用accept将不会堵塞 134 if (addr != NULL) 135 ret = accept(fd, (struct sockaddr*)addr, &addrlen); //返回已连接套接字 136 else 137 ret = accept(fd, NULL, NULL); 138 if (ret == -1) 139 ERR_EXIT("accept"); 140 141 return ret; 142 } 143 144 /** 145 * activate_noblock - 设置I/O为非阻塞模式 146 * @fd: 文件描符符 147 */ 148 void activate_nonblock(int fd) 149 { 150 int ret; 151 int flags = fcntl(fd, F_GETFL); 152 if (flags == -1) 153 ERR_EXIT("fcntl"); 154 155 flags |= O_NONBLOCK; 156 ret = fcntl(fd, F_SETFL, flags); 157 if (ret == -1) 158 ERR_EXIT("fcntl"); 159 } 160 161 /** 162 * deactivate_nonblock - 设置I/O为阻塞模式 163 * @fd: 文件描符符 164 */ 165 void deactivate_nonblock(int fd) 166 { 167 int ret; 168 int flags = fcntl(fd, F_GETFL); 169 if (flags == -1) 170 ERR_EXIT("fcntl"); 171 172 flags &= ~O_NONBLOCK; 173 ret = fcntl(fd, F_SETFL, flags); 174 if (ret == -1) 175 ERR_EXIT("fcntl"); 176 } 177 178 179 /** 180 * connect_timeout - connect 181 * @fd: 套接字 182 * @addr: 要连接的对方地址 183 * @wait_seconds: 等待超时秒数,如果为0表示正常模式 184 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT 185 */ 186 int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) 187 { 188 int ret; 189 socklen_t addrlen = sizeof(struct sockaddr_in); 190 191 if (wait_seconds > 0) 192 activate_nonblock(fd); 193 194 ret = connect(fd, (struct sockaddr*)addr, addrlen); 195 if (ret < 0 && errno == EINPROGRESS) 196 { 197 //printf("11111111111111111111\n"); 198 fd_set connect_fdset; 199 struct timeval timeout; 200 FD_ZERO(&connect_fdset); 201 FD_SET(fd, &connect_fdset); 202 timeout.tv_sec = wait_seconds; 203 timeout.tv_usec = 0; 204 do 205 { 206 // 一但连接建立,则套接字就可写 所以connect_fdset放在了写集合中 207 ret = select(fd + 1, NULL, &connect_fdset, NULL, &timeout); 208 } while (ret < 0 && errno == EINTR); 209 if (ret == 0) 210 { 211 ret = -1; 212 errno = ETIMEDOUT; 213 } 214 else if (ret < 0) 215 return -1; 216 else if (ret == 1) 217 { 218 //printf("22222222222222222\n"); 219 /* ret返回为1(表示套接字可写),可能有两种情况,一种是连接建立成功,一种是套接字产生错误,*/ 220 /* 此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取。 */ 221 int err; 222 socklen_t socklen = sizeof(err); 223 int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen); 224 if (sockoptret == -1) 225 { 226 return -1; 227 } 228 if (err == 0) 229 { 230 //printf("3333333333333\n"); 231 ret = 0; 232 } 233 else 234 { 235 //printf("4444444444444444:%d\n", err); 236 errno = err; 237 ret = -1; 238 } 239 } 240 } 241 if (wait_seconds > 0) 242 { 243 deactivate_nonblock(fd); 244 } 245 return ret; 246 } 247 248 /** 249 * readn - 读取固定字节数 250 * @fd: 文件描述符 251 * @buf: 接收缓冲区 252 * @count: 要读取的字节数 253 * 成功返回count,失败返回-1,读到EOF返回<count 254 */ 255 ssize_t readn(int fd, void *buf, size_t count) 256 { 257 size_t nleft = count; 258 ssize_t nread; 259 char *bufp = (char*)buf; 260 261 while (nleft > 0) 262 { 263 if ((nread = read(fd, bufp, nleft)) < 0) 264 { 265 if (errno == EINTR) 266 continue; 267 return -1; 268 } 269 else if (nread == 0) 270 return count - nleft; 271 272 bufp += nread; 273 nleft -= nread; 274 } 275 276 return count; 277 } 278 279 /** 280 * writen - 发送固定字节数 281 * @fd: 文件描述符 282 * @buf: 发送缓冲区 283 * @count: 要读取的字节数 284 * 成功返回count,失败返回-1 285 */ 286 ssize_t writen(int fd, const void *buf, size_t count) 287 { 288 size_t nleft = count; 289 ssize_t nwritten; 290 char *bufp = (char*)buf; 291 292 while (nleft > 0) 293 { 294 if ((nwritten = write(fd, bufp, nleft)) < 0) 295 { 296 if (errno == EINTR) 297 continue; 298 return -1; 299 } 300 else if (nwritten == 0) 301 continue; 302 303 bufp += nwritten; 304 nleft -= nwritten; 305 } 306 307 return count; 308 } 309 310 /** 311 * recv_peek - 仅仅查看套接字缓冲区数据,但不移除数据 312 * @sockfd: 套接字 313 * @buf: 接收缓冲区 314 * @len: 长度 315 * 成功返回>=0,失败返回-1 316 */ 317 ssize_t recv_peek(int sockfd, void *buf, size_t len) 318 { 319 while (1) 320 { 321 int ret = recv(sockfd, buf, len, MSG_PEEK); 322 if (ret == -1 && errno == EINTR) 323 continue; 324 return ret; 325 } 326 } 327 328 /** 329 * readline - 按行读取数据 330 * @sockfd: 套接字 331 * @buf: 接收缓冲区 332 * @maxline: 每行最大长度 333 * 成功返回>=0,失败返回-1 334 */ 335 ssize_t readline(int sockfd, void *buf, size_t maxline) 336 { 337 int ret; 338 int nread; 339 char *bufp = buf; 340 int nleft = maxline; 341 while (1) 342 { 343 ret = recv_peek(sockfd, bufp, nleft); 344 if (ret < 0) 345 return ret; 346 else if (ret == 0) 347 return ret; 348 349 nread = ret; 350 int i; 351 for (i=0; i<nread; i++) 352 { 353 if (bufp[i] == '\n') 354 { 355 ret = readn(sockfd, bufp, i+1); 356 if (ret != i+1) 357 exit(EXIT_FAILURE); 358 359 return ret; 360 } 361 } 362 363 if (nread > nleft) 364 exit(EXIT_FAILURE); 365 366 nleft -= nread; 367 ret = readn(sockfd, bufp, nread); 368 if (ret != nread) 369 exit(EXIT_FAILURE); 370 371 bufp += nread; 372 } 373 374 return -1; 375 }
上述带超时的函数,具有一定的健壮性,将IO设置为阻塞或者非阻塞,主要是给connect_timeout函数用。先将fd设置为非阻塞,这样connect就可以立即返回,如果返回时fd还没有连接好,即errno == EINPROGRESS,则调用select去监控这个fd,要么指定时间内fd可用,要么超时。虽然connect立即返回了,但是三次握手内核还是会做的。fd可读可有两种可能,一种是连接建立了,真正可读了,另一种是发生错误了,这时候fd也可读了。因此,
我们需要调用getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen)来获取错误码,根据错误码进行进一步的判断。
connect_timeout总结如下图:
优化网络应用就是优化连接,可以通过在客户端建立连接池提供优化。TCP IP三次握手很浪费时间。
select三个应用场景:
1、用select封装超时(connect、accept、read、write)
2、用select优化客户端(stdin、confd)
3、用select优化服务器(用一个单进程去支持多个客户端)
select是一个管理机制,管理了多个IO,用单进程轮询的方式去查询n个IO是否发生了变化。