介绍

什么是IO多路复用?
select只是IO复用的一种方式,其他的还有:poll,epoll等。linux
举一个简单地网络服务器的例子,如果你的服务器需要和多个客户端保持连接,处理客户端的请求,属于多进程的并发问题,如果创建很多个进程来处理这些IO流,会导致CPU占有率很高。所以人们提出了I/O多路复用模型:一个线程,通过记录I/O流的状态来同时管理多个I/O。
Select函数:允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
I/O复用模型也称为选择模型或select模型,它可以使Windows Sockets应用程序同时对多个套接字进行管理。select()可以同时监听多个socket状态。
调用select()函数可以获取一组指定套接字的状态,这样可以保证及时捕获到最先满足条件的I/O事件,进而对select()函数中观测的多个不同套接字上的不同网络事件进行及时处理。
select()函数可以决定一组套接字的状态,通常用于操作处于就绪状态的套接字。在select()函数中使用fd_set结构来管理多个套接字。
select()函数允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为“准备好”的状态。所谓的”准备好“状态是指:文件描述符不再是阻塞状态,可以用于某类IO操作了,包括可读,可写,发生异常三种。

我们使用select来监视文件描述符时,要向内核传递的信息包括:
1、我们要监视的文件描述符个数
2、每个文件描述符,我们可以监视它的一种或多种状态,包括:可读,可写,发生异常三种。
3、要等待的时间,监视是一个过程,我们希望内核监视多长时间,然后返回给我们监视结果呢?
4、监视结果包括:准备好了的文件描述符个数,对于读,写,异常,分别是哪儿个文件描述符准备好了。

函数参数说明

  1. select(); //函数定义如下
  2. int select(
  3. int nfds;
  4. fd_set *readfds;
  5. fd_set *writefds;
  6. fd_set *expectedfds;
  7. const struct timeval *timeout;
  8. )

函数参数

  • int nfds: 指定待测试的描述符个数,它应该被设置为待测试的最大数目+1。fd_set通常支持的最大描述符是1023+1。windows默认64不过可以在winsock2.h中修改
  • fd_set
    • readfds:指向一个套接字集合,用来检查其可读性;
    • writefds:指向一个套接字集合,用来检查其可写性;
    • exceptfds:指向一个套接字集合,用来检查错误;
  • timeout:指定此函数等待的最长时间,如果是阻塞模式的操作,则将此参数设置为NULL,表明最长时间为无限大。

函数返回

readfds中含有待检查可读性的套接字,在满足以下条件时select()函数返回:
● 当套接字处于监听状态时,有连接请求到来;
● 当套接字不处于监听状态时,套接字输入缓冲区有数据可读;
● 连接已经关闭、重置或终止。
writefds中含有待检查可写性的套接字,在满足以下条件时select()函数返回:
● 已经调用非阻塞的connect()函数,并且连接成功;
● 有数据可以发送。
exceptfds中含有待检查带外数据以及异常错误条件的套接字,在满足以下条件时select()函数返回:
● 已经调用非阻塞的connect()函数,但连接失败;
● 有带外数据可以读取。

举个栗子,我们可以调用Select,告诉内核仅仅在下列情况发生时才返回:

  • 集合{1,4,5}中任何描述符准备好读
  • 集合{2,7}中任何描述符准备好写
  • 集合{1,4}中任何描述符由异常条件待处理
  • 已经经历了10秒

    fd_set操作

  • FD_CLR(s,*set):从集合set中删除套接字s;

  • FD_ISSET(s,*set):若套接字s为集合中的一员,返回值非零,否则为零;
  • FD_SET(s,*set):将套接字s添加到集合set中;
  • FD_ZERO(*set):将set集合初始化为空集NULL。

struct timeval *timeout

  1. struct timeval {
  2. long tv_sec; /* seconds */
  3. long tv_usec; /* microseconds */
  4. };
  5. 把该参数设置为NULL,阻塞,仅在有一个描述符准备好IO时才返回。
  6. tv_sec != 0 || tv_usec != 0 ,超时返回,或在有一个描述符准备好IO时返回。
  7. tv_sec == 0 || tv_usec == 0 ,立即返回,这称为轮训。

函数基本流程

  1. FD_ZERO()初始化套接字集合
  2. FD_SET() 添加套接字到集合中
  3. 调用select()函数,此后程序处于等待状态,等待集合中套接字上的I/O活动,每个I/O活动都会设置相应集合中的套接字句柄,当需要返回时,它将返回集合中设置的套接字句柄总数,并更新相关集合:删除不存在的等待I/O操作的套接字句柄,留下需要处理的套接字。
  4. 调用FD_ISSET()检查特定的套接字是否还在集合中,如果仍在集合中,则可以对其进行与集合相对应的I/O处理。

    I/O复用模型的编程框架

    套接字创建后,在I/O复用模型下,当发生网络I/O时,应用程序的执行过程是:向select()函数注册等待I/O操作的套接字,循环执行select()系统调用,阻塞等待,直到网络事件发生或超时返回,对返回的结果进行判断,针对不同的等待套接字进行对应的网络处理。
    image.png

    代码实现

    ```c

    include

    include

    include

    include

    include

    include

pragma comment(lib,”ws2_32.lib”)

define DEFAULT_BUFLEN 512

define DEFAULT_PORT 27015

int __cdecl main(int argc,TCHAR* argv[]) {

  1. WSADATA wsaData;
  2. int iResult;
  3. SOCKET ServerSocket = INVALID_SOCKET;
  4. SOCKET AcceptSocket = INVALID_SOCKET;
  5. char recvbuf[DEFAULT_BUFLEN];
  6. int recvBufLen = DEFAULT_BUFLEN;
  7. sockaddr_in addrClient;
  8. int addrClinetLen = sizeof(sockaddr_in);
  9. //初始化socket
  10. iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
  11. if (iResult!=0)
  12. {
  13. printf_s("wsatartup faild with error code %d\n", iResult);
  14. return 1;
  15. }
  16. //创建监听socket
  17. ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
  18. if (ServerSocket==INVALID_SOCKET)
  19. {
  20. printf_s("SOCKET FAILED WITH ERROR CODE %d",WSAGetLastError());
  21. WSACleanup();
  22. return 1;
  23. }
  24. //绑定地址和端口
  25. SOCKADDR_IN addrServ;
  26. addrServ.sin_family = AF_INET;
  27. addrServ.sin_port = htons(DEFAULT_PORT); //监听端口为DEFAULT_PORT
  28. addrServ.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
  29. iResult = bind(ServerSocket,(const struct sockaddr*)&addrServ,sizeof(SOCKADDR_IN));
  30. if (iResult==SOCKET_ERROR)
  31. {
  32. printf_s("bind falied with error code %d\n",WSAGetLastError());
  33. closesocket(ServerSocket);
  34. WSACleanup();
  35. return 1;
  36. }
  37. //设置socket为非阻塞模式
  38. int iMode = 1;
  39. iResult = ioctlsocket(ServerSocket, FIONBIO, (u_long*)&iMode);
  40. if (iResult==SOCKET_ERROR)
  41. {
  42. printf_s("ioctlsocket failed with error code %d\n",iResult);
  43. closesocket(ServerSocket);
  44. WSACleanup();
  45. return 1;
  46. }
  47. iResult = listen(ServerSocket,SOMAXCONN);
  48. if (iResult==SOCKET_ERROR)
  49. {
  50. printf_s("listen failed with error code %d",iResult);
  51. closesocket(ServerSocket);
  52. WSACleanup();
  53. return -1;
  54. }
  55. printf_s("TCP server starting");
  56. fd_set fdRead, fdSocket;
  57. FD_ZERO(&fdSocket);
  58. FD_SET(ServerSocket,&fdSocket);
  59. while (true)
  60. {
  61. fdRead = fdSocket;
  62. iResult = select(0, &fdRead, NULL, NULL, NULL);
  63. if (iResult>0)
  64. {
  65. for (int i = 0; i < (int)fdSocket.fd_count; i++)
  66. {
  67. if (FD_ISSET(fdSocket.fd_array[i],&fdRead))
  68. {
  69. if (fdSocket.fd_array[i]==ServerSocket)
  70. {
  71. if (fdSocket.fd_count<FD_SETSIZE)
  72. {
  73. //判断当前的连接个数是否超过select()函数能够监管的最大个数FD_SETSIZE。如果没有超限,则将新返回的连接套接字增加到fdRead集合中,在下一次select()调用时,新连接上的套接字的网络事件也被select()函数监管。
  74. AcceptSocket = accept(ServerSocket, (sockaddr FAR*) & addrClient,&addrClinetLen);
  75. if (AcceptSocket == INVALID_SOCKET)
  76. {
  77. printf_s("accept failed\n");
  78. closesocket(ServerSocket);
  79. WSACleanup();
  80. return 1;
  81. }
  82. FD_SET(AcceptSocket,&fdSocket);
  83. WCHAR wchar[32];
  84. InetNtopW(AF_INET,&addrClient,(PWSTR)&wchar,sizeof(wchar));
  85. wprintf(L"%s\n", wchar);
  86. }
  87. else
  88. {
  89. printf_s("conn flow \n");
  90. continue;
  91. }
  92. }
  93. else
  94. { //有数据到达
  95. memset(recvbuf, 0, recvBufLen);
  96. iResult = recv(fdSocket.fd_array[i],recvbuf,recvBufLen,0);
  97. if (iResult>0)
  98. {
  99. //成功接收数据,处理数据
  100. //前满足网络I/O条件的套接字上接收网络数据,打印接收到的数据长度,并判断接收返回值。
  101. printf_s("\nbyte recved %d\n",iResult);
  102. }
  103. else if (iResult == 0)
  104. {
  105. //情况2关闭连接
  106. printf_s("conn closeing....\n");
  107. closesocket(fdSocket.fd_array[i]);
  108. FD_CLR(fdSocket.fd_array[i],&fdSocket);
  109. }
  110. else
  111. {
  112. //接收失败
  113. printf_s("recv failed with error %d",GetLastError());
  114. closesocket(fdSocket.fd_array[i]);
  115. FD_CLR(fdSocket.fd_array[i], &fdSocket);
  116. }
  117. }
  118. }
  119. //其他等待套接字,需要依次判断
  120. //......
  121. }
  122. }
  123. else
  124. {
  125. printf_s("select faild with error %d\n",GetLastError());
  126. break;
  127. }
  128. }
  129. closesocket(ServerSocket);
  130. WSACleanup();
  131. return 0;

} ```

io复用模型下,套接字仍然以阻塞模式进行(select处),但是可以用单线程监听多个socket,达到并发执行的效果,以上代码用select()函数对一个监听套接字和多个连接套接字上等待的网络事件进行同时监控,并在任何满足I/O条件的套接字返回时,根据套接字的类型进行分别处理。//

评价

比起阻塞io模型优点:

  • 使用I/O复用模型的好处在于select()函数可以等待多个套接字准备好,即使程序在单个线程中仍然能够及时处理多个套接字的I/O事件,达到跟多线程操作类似的效果,这避免了阻塞模式下的线程膨胀的问题。

缺点:

  • 尽管select()函数可以管理多个套接字,但其数量仍然是有限的,默认64个,最大1024
  • 与阻塞I/O模型相比,I/O复用模型似乎没有明显的优越性,在使用了系统调用select()后,要求两次系统调用而不是一次,还稍显劣势。
  • 用集合的方式管理套接字比较烦琐,而且每次在使用套接字进行网络操作前都需要调用select()函数判断套接字的状态,这也给CPU带来了额外的系统调用开销。

优点:

  • select()函数还有其他方面的应用。在工业控制以及实时性要求较高的环境下,高精度的定时器对提高工作效率有很重要的意义,除了应用于网络中多个I/O事件的捕获之外,select()函数的超时处理能力还可以应用于对精度要求比较高的定时应用中。
  • select()函数对超时时间的精度可以帮助设计者提高定时器的设置精度,通过设置超时时间参数可以将其改造成一个精度可达百万分之一秒(微秒级)的定时器