前言
早期操作系统通常将进程中可创建的线程数限制在一个较低的阈值,大约几百个。因此, 操作系统会提供一些高效的方法来实现多路IO,例如Unix的select和poll。现代操作系统中,线程数已经得到了极大的提升,如NPTL线程软件包可支持数十万的线程。
I/O多路复用
select
select 允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或指定时间后返回它。
select函数原型
#include <sys/select.h> #include <sys/time.h> int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);
返回值:监听到有事件发生的文件描述符的个数,超时为0,错误为 -1.
1.当监视的相应的文件描述符集中满足条件时,比如说读文件描述符集中有数据到来时,内核(I/O)根据状态修改文件描述符集,并返回一个大于0的数。
2.当没有满足条件的文件描述符,且设置的timeval监控时间超时时,select函数会返回一个为0的值。
3.当select返回负值时,发生错误。
参数:
maxfd:是需要监视的最大的文件描述符值+1;
rdset、wrset、exset:是传入传出参数,fd_set类型,分别对应于需要检测的可读文件描述符的集合、可写文件描述符的集合、异常文件描述符的集合。若对其中任何参数条件不感兴趣,则可将其设为NULL。
timeout:设置超时时间,指定select在返回前没有接收事件时应该等待的时间。
timeval 结构体
struct timeval{ long tv_sec; // 秒 long tv_usec; // 微秒 }
timeout参数的三种可能:
NULL:永远等待下去,仅在有描述符就绪时才返回
正常时间:在不超过timeout设置的时间内,在有描述符就绪时返回
0:检查描述符的每位后立即返回(轮询)
描述符集合fd_set操作函数
系统提供了4个宏对描述符集进行操作:
#include <sys/select.h> #include <sys/time.h> void FD_SET(int fd, fd_set *fdset); // 设置文件描述符集fdset中对应于文件描述符fd的位(设置为1) void FD_CLR(int fd, fd_set *fdset); // 清除文件描述符集fdset中对应于文件描述符fd的位(设置为0) void FD_ISSET(int fd, fd_set *fdset); // 检测文件描述符集fdset中对应于文件描述符fd的位是否被设置 void FD_ZERO(fd_set *fdset); // 清除文件描述符集fdset中的所有位(既把所有位都设置为0)
理解select模型
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set,FD_ZERO(&set),则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set),后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
select模型的描述符集合,内部实现是位图,这些参数指明了我们关心哪些描述符,和需要满足什么条件(可写,可读,异常)。fd_set类型变量每一位代表了一个描述符。我们也可以认为它只是一个由很多二进制位构成的数组。
select的第一个参数是最大的描述符+1,表示描述符大小的范围,此时仅将描述符当做一个数看待,它会遍历从0到maxfd+1个位置
将感兴趣的描述符加入对应的集合中,调用select,它会遍历maxfd+1个描述符,如果有条件满足,内核(I/O)根据状态修改文件描述符集,并返回有事件发生的描述符的个数。此时描述符集合fd_set中的描述符被修改了,集合中都是有事件发生的。
select模型特点
基于上面的讨论,可以轻松得出select模型的特点
(1)可监控的文件描述符个数取决与fd_set的值。一般为1024,每bit表示一个文件描述符,则支持的最大文件描述符是1024。可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
(2)每次调用 select(),都需要把描述符集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大,同时每次调用 select() 都需要在内核中轮询最大描述符数+1个描述符,这个开销在 fd 很多时也很大。将文件描述符fd加入到事件集合中,还需要用一个数组,将文件描述符fd保存起来。一是,用于在select返回之后,fd_set参数中已经被修改为都是有事件发生的文件描述符位,这个数组中的文件描述符可以用FD_ISSET来轮询对发生事件后的集合中的描述符判断;二是,select返回后会把以前加入的但并无事件发生的fd的位清空,下一次开始 select 前要重新从数组中取得文件描述符逐个加入到 fd_set 中( FD_ZERO 最先),扫描数组的同时取得文件描述符的最大值 maxfd ,用于 select 的第一个参数。
(3)返回后的集合,需要轮询数组中保存的描述符的每一个与集合中进行FD_ISSET操作,排查当文件描述符个数很多时,效率很低。
select模型实例
#define MAXLINE 80 #define SERV_PORT 6666 int main(int argc, char *argv[]) { int i, maxi, maxfd, listenfd, connfd, sockfd; int nready, client[FD_SETSIZE]; // FD_SETSIZE 默认为 1024 ssize_t n; fd_set rset, allset; //fd_set类型的rset、allset char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; socklen_t cliaddr_len; struct sockaddr_in cliaddr, servaddr; listenfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(listenfd, 20); maxfd = listenfd; maxi = -1; for (i = 0; i < FD_SETSIZE; i++) client[i] = -1; FD_ZERO(&allset); //将所有集合清零 FD_SET(listenfd, &allset); // 将监听套接字加入到集合中 for (;;) { rset = allset; // 每次循环时都从新设置select监听集合 nready = select(maxfd + 1, &rset, NULL, NULL, NULL); //nready是发生事件的描述符的个数 if (nready < 0) perr_exit("select error"); if (FD_ISSET(listenfd, &rset)) //如果listenfd在返回的rset中 { //新客户端请求连接 cliaddr_len = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); //conn接收客户端的套接字描述符 printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); for (i = 0; i < FD_SETSIZE; i++) { if (client[i] < 0) { client[i] = connfd; //保存接收客户端的描述符到client[]里 break; } } //达到select能监控的文件个数上限 1024 if (i == FD_SETSIZE) { fputs("too many clients\n", stderr); exit(1); } FD_SET(connfd, &allset); // 添加接受的客户端的描述符到监听集合中 if (connfd > maxfd) //是否更新maxfd maxfd = connfd; if (i > maxi) //是否更新client[]的最大下标 maxi = i; if (--nready == 0) continue; // 如果没有更多的就绪文件描述符继续回到上面select阻塞监听(仅监听到新客户端的连接) } for (i = 0; i <= maxi; i++) { if ((sockfd = client[i]) < 0) //取出client[]中>0的描述符 continue; if (FD_ISSET(sockfd, &rset)) //检测clients[]中哪个描述符在返回的rset可读事件集合中 { if ((n = Read(sockfd, buf, MAXLINE)) == 0) { Close(sockfd); /* 当client关闭链接时,服务器端也关闭对应链接 */ FD_CLR(sockfd, &allset); /* 解除select监控此文件描述符 */ client[i] = -1; } else { int j; for (j = 0; j < n; j++) buf[j] = toupper(buf[j]); Write(sockfd, buf, n); } if (--nready == 0) break; } } } close(listenfd); return 0; }
poll
poll 和 select 系统调用的本质一样,poll 的机制与 select 类似,与 select 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理。
poll函数原型
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);
返回值:
返回值 < 0,表示出错
返回值 == 0,表示poll函数等待超时
返回值 > 0,表示poll由于监听的文件描述符就绪返回,并且返回结果就是就绪的文件描述符的个数。
参数:
fds:一个结构数组,struct pollfd结构如下:
struct pollfd{ int fd; //要监听的文件描述符 short events; //需要监听的事件(读、写、异常) short revents; //调用poll后的结果事件,内核在调用返回时设置这个事件 };
events & revents的取值如下:
常量 | 说明 |
---|---|
POLLIN | 普通或优先级带数据可读 |
POLLRDNORM | 普通数据可读 |
POLLRDBAND | 优先级带数据可读 |
POLLPRI | 高优先级数据可读 |
POLLOUT | 普通数据可写 |
POLLWRNORM | 普通数据可写 |
POLLWRBAND | 优先级带数据可写 |
POLLERR | 发生错误 |
POLLHUP | 发生挂起 |
POLLNVAL | 描述字不是一个打开的文件 |
nfds:要监视的描述符的数目。经过测试,如果监听了两个fd,但是nfds==1的情况下,只有fdarray0.fd能被监听到。
timeout:用毫秒表示的时间,是指定poll在返回前没有接收事件时应该等待的时间。
timeout值 | 说明 |
---|---|
-1 | 永远等待 |
0 | 立即返回,不阻塞进程 |
>0 | 等待指定数目的毫秒数 |
poll模型的特点
(1)poll没有最大连接数的限制,原因是它是基于链表来存储的。在select中,被监听集合和返回集合是一个集合,在poll中将监听和返回的事件都在结构体中不同的成员中,它们互补干扰,poll 中将有事件发生的文件描述符设置其结构体的revents,不需要向select一样用一个数组存储原来的文件描述符。
(2)poll函数中fds数组中元素是pollfd结构体,该结构体保存描述符的信息,每增加一个文件描述符就向数组中结构体加入一个描述符,结构体只需要拷贝一次到内核态。poll解决了select重复初始化的问题。但轮寻检查事件发生的问题仍然未解决。
(3)与select一样,poll返回后,需要轮询每个pollfd结构体的revents来获取就绪的描述符,这样会使性能下降 ,poll会遍历到数组已使用的最大下标,如果同时连接的大量客户端在一时刻可能只有很少的就绪状态,就是最大下标很大,而只有几个描述符发生事件,因此随着监视的描述符数量的增长,其效率也会线性下降。
poll模型实例
#define MAXLINE 80 #define SERV_PORT 8000 #define OPEN_MAX 1024 int main(int argc, char *argv[]) { int i, j, maxi, listenfd, connfd, sockfd; int nready; //接收poll返回值, 记录发生事件的fd个数 ssize_t n; char buf[MAXLINE], str[INET_ADDRSTRLEN]; socklen_t clilen; struct pollfd client[OPEN_MAX]; //client[]中存放pollfd类型结构体 struct sockaddr_in cliaddr, servaddr; listenfd = Socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(listenfd, 128); client[0].fd = listenfd; // 要监听的第一个文件描述符 存入client[0] client[0].events = POLLIN; // listenfd监听读事件 for (i = 1; i < OPEN_MAX; i++) client[i].fd = -1; // 用-1初始化client[]里剩下元素,0也是文件描述符,不能用 maxi = 0; // client[]数组有效元素中最大元素下标 for (;;) { nready = poll(client, maxi + 1, -1); // 阻塞监听 if (client[0].revents & POLLIN) //有客户端连接 { clilen = sizeof(cliaddr); connfd = Accept(client[0].fd, (struct sockaddr *)&cliaddr, &clilen); /* 接收客户端请求 Accept 不会阻塞 */ printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); for (i = 1; i < OPEN_MAX; i++) //找到client[]中空闲的位置存放接收客户端的套接字描述符 if (client[i].fd < 0) { client[i].fd = connfd; break; } if (i == OPEN_MAX) // 达到了最大客户端数 perr_exit("too many clients"); client[i].events = POLLIN; // 将接收客户端的套接字描述符的事件设置位读 if (i > maxi) maxi = i; // 更新client[]中最大元素下标 if (--nready <= 0) //是否仅有客户端连接一个事件 continue; } for (i = 1; i <= maxi; i++)//有多个事件发生,轮询检测client[] 看是那个connfd就绪 { if ((sockfd = client[i].fd) < 0) //每个sockfd是否有效>0 continue; if (client[i].revents & POLLIN) //每个结构体的revents是否是可读事件 { if ((n = Read(sockfd, buf, MAXLINE)) < 0) { if (errno == ECONNRESET) { // 收到RST标志 printf("client[%d] aborted connection\n", i); Close(sockfd); client[i].fd = -1; // poll中不监控该文件描述符,直接置为-1即可,不用像select中那样移除 } else perr_exit("read error"); } else if (n == 0) { // 说明客户端先关闭链接 printf("client[%d] closed connection\n", i); Close(sockfd); client[i].fd = -1; } else { for (j = 0; j < n; j++) buf[j] = toupper(buf[j]); Writen(sockfd, buf, n); } if (--nready <= 0) break; } } } return 0; }
epoll
epoll 是之前的 select 和 poll 的增强版本。相对于 select 和 poll 来说,epoll更加灵活,没有描述符限制。epoll使用一个epoll句柄管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll相关函数
int epoll_create(int size);
创建一个epoll文件描述符,相当于一个句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
返回值:返回一个文件描述符fd,可以理解为指向内核中的一颗红黑树的树根,size就是创建红黑树的大小。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它在这里注册要监听的事件类型。参数 epfd 是epoll_create()的返回值的描述符;参数 op 表示动作,用三个宏来表示,控制某个epoll监听的文件描述符上的事件:添加、修改、删除。相当于在红黑树上操作。参数 fd 是需要监听事件的文件描述符,参数 event 是告诉内核需要监听什么事件。
返回值:成功返回0,不成功返回1
事件注册函数的第二个参数的动作,有四个宏表示:
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
struct epoll_event结构如下:
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t;
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相`对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里.
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
参数 fd 是epoll_create返回的文件描述符;参数 events 是一个数组,传入传出参数;参数 maxevents 告之内核这个events数组有多大(数组成员的个数),这个 maxevents 的值不能大于创建epoll_create()时的size;参数 timeout 设置超时时间(-1 阻塞,0 立即返回,非阻塞,>0 指定毫秒)。
返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1。返回的有事件发生的描述符都在 events 数组中,数组中实际存放的成员个数是函数的返回值个。
epoll模型的特点
(1)本身没有最大并发连接的限制,仅受系统中进程能打开的最大文件数目限制。
(2)基于事件就绪通知方式:一旦被监听的某个文件描述符就绪,内核会采用类似于callback的回调机制,迅速激活这个文件描述符,这样随着文件描述符数量的增加,也不会影响判定就绪的性能。不会像select/poll中轮询检测每个描述符是否就绪。
(3)当文件描述符就绪,就会被放到一个数组中,这样调用epoll_weit获取就绪文件描述符的时候,只要取数组中的返回的个数个元素即可,不需要全部做轮询检测。
(4)内存拷贝是利用mmap()文件映射内存的方式加速与内核空间的消息传递,减少复制开销。(内核与用户空间共享一块内存)
epoll模型实例
#define MAXLINE 8192 #define SERV_PORT 8000 #define OPEN_MAX 5000 int main(int argc, char *argv[]) { int i, listenfd, connfd, sockfd; int n, num = 0; ssize_t nready, efd, res; char buf[MAXLINE], str[INET_ADDRSTRLEN]; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; struct epoll_event tep, ep[OPEN_MAX]; //tep: epoll_ctl参数 ep[] : epoll_wait参数 listenfd = Socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //端口复用 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(listenfd, 20); efd = epoll_create(OPEN_MAX); //创建epoll模型, efd指向红黑树根节点 if (efd == -1) perr_exit("epoll_create error"); tep.events = EPOLLIN; tep.data.fd = listenfd; //指定lfd的监听时间为"读" res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep); //将lfd及对应的结构体设置到树上,efd可找到该树 if (res == -1) perr_exit("epoll_ctl error"); for (;;) { /*epoll为server阻塞监听事件, ep为struct epoll_event类型数组, OPEN_MAX为数组容量, -1表永久阻塞*/ nready = epoll_wait(efd, ep, OPEN_MAX, -1); if (nready == -1) perr_exit("epoll_wait error"); for (i = 0; i < nready; i++) { if (!(ep[i].events & EPOLLIN)) //如果不是"读"事件, 继续循环 continue; if (ep[i].data.fd == listenfd) { //判断满足事件的fd是不是lfd clilen = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); //接受链接 printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); printf("cfd %d---client %d\n", connfd, ++num); tep.events = EPOLLIN; tep.data.fd = connfd; res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep); if (res == -1) perr_exit("epoll_ctl error"); } else { //不是lfd, sockfd = ep[i].data.fd; n = Read(sockfd, buf, MAXLINE); if (n == 0) { //读到0,说明客户端关闭链接 res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); //将该文件描述符从红黑树摘除 if (res == -1) perr_exit("epoll_ctl error"); Close(sockfd); //关闭与该客户端的链接 printf("client[%d] closed connection\n", sockfd); } else if (n < 0) { //出错 perror("read n < 0 error: "); res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); Close(sockfd); } else { //实际读到了字节数 for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); //转大写,写回给客户端 Write(STDOUT_FILENO, buf, n); Writen(sockfd, buf, n); } } } } Close(listenfd); Close(efd); return 0; }
epoll工作模式
epoll对文件描述符的操作有两种模式:水平触发LT(level trigger)和边缘触发ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
二者的主要差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。
描述符就绪条件
可读条件
(1) “监听socket”:该套接字是一个监听套接字且已完成的连接数不为0。而这样的套接字处于可读状态,是因为套接字收到了对方的connect请求,执行了三次握手的第一步:对方发送SYN请求过来,使该方监听套接字处于可读状态;通常情况下,对这样的套接字执行accept操作不会阻塞;
(2)“已连接socket”:该套接字的接收缓冲区中的数据字节大于等于该套接字的接收缓冲区低水位标记的当前大小。对这样的套接字执行读操作不会阻塞并返回一个大于0的值(也就是返回准备好读入的数据)。可以用SO_RCVLOWAT套接字选项设置该套接字的低水位标记。对于TCP和UDP套接字而言,其缺省值为1,这意味着,默认情况下,只要缓冲区中有数据,那就是可读的。
(3)“已连接socket”:该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF),此时必须且一直会返回0;
(4)“已连接socket”:其上有一个套接字错误待处理。对这样的套接字的读操作将不会阻塞并返回-1(即返回一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
可写条件
(1)“已连接socket/UDP socket”:该套接字发送缓冲区中的可用空间字节数大于等于该套接字的发送缓冲区低水位标记的当前大小(对于TCP的已连接socket或者UDP socket均可)。对这样的套接字的写操作将不阻塞并返回一个大于0的值(也就是返回准备好写入的数据)。可以用SO_SNDLOWAT套接字选项设置该套接字的低水位标记。对于TCP和UDP套接字而言,低水位默认值为2048,发送缓冲区默认大小为8K,这意味着,默认情况下,一个套接字连接成功后,总是可写的;
(2)“已连接socket”:该连接的写半部关闭(主动发送了FIN的TCP连接)。对这样的套接字的写操作将产生SIGPIPE信号,该信号的缺省行为是终止进程;
(3)“已连接socket”:其上有一个套接字错误待处理。对这样的套接字的写操作将不会阻塞并且返回-1(即返回一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ERROR套接字选项调用getsockopt函数来取得并清除;
(4)使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终,即connect已经完成。
异常条件
该套接字存在带外数据或者仍处于带外标记
来源:https://www.cnblogs.com/WindSun/p/11491395.html