三-select模型

发布时间 2023-12-05 11:21:10作者: 西兰花战士

select模型是对简单C/S模型的优化,他解决了accept函数阻塞等待连接的问题。并且允许应用程序同时监视多个套接字,从而实现简单的并发请求。通过调用select函数确认一个或多个套接字当前的状态,并根据当前状态进行相应操作。在select模型模型中,select函数是最关键的。

select模型工作原理

select模型维护了一个Socket数组,通过遍历该数组检查当前是否存在就绪socket,并将所有就绪的socket返回。我们遍历该数组提供相应服务。工作原理大致如下:

  1. 将服务端Socket添加至socket数组中。
  2. 调用select()函数遍历socket数组。
  3. 返回就绪socket数组,对该数组集中处理。
  4. 如果是服务端Socket,调用accept()函数接收连接请求,并将该客户端数组添加至socket数组。
  5. 如果是客户端Socket,调用send()、recv()函数进行通信,当客户端下线时,从socket数组中移除该Socket。
  6. 重复执行2-6步。

select模型使用步骤

  1. 启动Socket服务
  2. 创建套接字
  3. 为套接字绑定端口信息
  4. 监听套接字
  5. 使用select模型监听套接字集合,处理数据
  6. 关闭套接字和网络服务

select()函数

该函数定义如下:

int select(int nfds, fd_set readfds, fd_set writefds, 
                fd_set exceptfds, const struct timeval timeout);

参数

  • nfds:忽略,传入0。为了与Berkeley 套接字兼容。
  • readfds:可读取套接字的集合,调用函数时传入要监视的套接字,函数返回时保存可读套接字。
  • writefds:可写套接字的集合。只要建立连接,则任何时候都可写入,可以传NULL。
  • exceptfds:异常套接字集合。
  • timeout:指定超时时间。

如果是服务端socket套接字,可读表示当前有客户端进行连接。如果是客户端套接字,可读表示当前有数据发送至服务端。select()函数是一个阻塞函数,如果指定了超时时间且没有socket就绪,select()函数返回。

返回值

  • 0:超时
  • >0:当前就绪的套接字数量,就绪的套接字保存在readfds中。
  • -1:出现错误,可以通过WSAGetLasterror()函数获取错误码。

fd_set结构体

fd_set结构体定义如下:

typedef struct fd_set {
        u_int   fd_count;               //fd_array数组中当前元素个数
        SOCKET  fd_array[FD_SETSIZE];   //socket数组
} fd_set;

在给select函数传递参数时,我们将需要监听的socket封装进fd_set结构体。select函数会顺序遍历数组,当发现有socket就绪,select函数返回,就绪的socket集合通常保存在readfds中。

下面列出了几个关于fd_set结构体的操作宏:

示例

  1 #define _WINSOCK_DEPRECATED_NO_WARNINGS
  2 
  3 #include <iostream>
  4 #include <WinSock2.h>
  5 #pragma comment(lib, "ws2_32.lib")
  6 using namespace std;
  7 const int nMajorVersion = 2;
  8 const int nMinorVersion = 2;
  9 
 10 int main()
 11 {
 12     DWORD dwVersion = MAKEWORD(nMajorVersion, nMinorVersion);
 13     WSADATA wsaData;
 14     int nStartRet = WSAStartup(dwVersion, &wsaData);
 15     if (nStartRet != 0)
 16     {
 17         cout << "WSAStartup failed with error :" << nStartRet << endl;
 18         return 1;
 19     }
 20 
 21     if (LOBYTE(wsaData.wVersion) != nMajorVersion || HIBYTE(wsaData.wVersion) != nMinorVersion)
 22     {
 23         cout << "version failed" << endl;
 24         WSACleanup();
 25         return 1;
 26     }
 27 
 28     SOCKET sockServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 29     if (sockServer == INVALID_SOCKET)
 30     {
 31         cout << "socket create failed with code: "<< WSAGetLastError() << endl;
 32         WSACleanup();
 33         return 1;
 34     }
 35 
 36     sockaddr_in addInfo;
 37     addInfo.sin_family = AF_INET;
 38     addInfo.sin_port = htons(12345);
 39     addInfo.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
 40     int nBindRet = bind(sockServer, reinterpret_cast<sockaddr*>(&addInfo), sizeof(addInfo));
 41     if (SOCKET_ERROR == nBindRet)
 42     {
 43         cout << "bind failed with code: " << WSAGetLastError() << endl;
 44         closesocket(sockServer);
 45         WSACleanup();
 46         return 1;
 47     }
 48 
 49     int nListenRet = listen(sockServer, SOMAXCONN);
 50     if (SOCKET_ERROR == nBindRet)
 51     {
 52         cout << "bind failed with code: " << WSAGetLastError() << endl;
 53         closesocket(sockServer);
 54         WSACleanup();
 55         return 1;
 56     }
 57 
 58     fd_set allSockets;
 59     FD_ZERO(&allSockets);
 60     FD_SET(sockServer, &allSockets);
 61     struct timeval tv = { 3,0 };
 62 
 63     while (1)
 64     {
 65         fd_set tmpAllSockets = allSockets;
 66         int nSelectRet = select(0, &tmpAllSockets, NULL, NULL, &tv);
 67         if(nSelectRet == 0) continue;        //超时
 68         else if (nSelectRet == -1)            //出错
 69         {
 70             cout << "select failed with code: " << WSAGetLastError() << endl;
 71             break;
 72         }
 73         else if (nSelectRet > 0)
 74         {
 75             for (int i = 0; i < tmpAllSockets.fd_count; i++)
 76             {
 77                 SOCKET socketID = tmpAllSockets.fd_array[i];
 78 
 79                 if (socketID == sockServer)
 80                 {
 81                     SOCKET socketClient = accept(socketID, NULL, NULL);
 82                     if (socketClient == INVALID_SOCKET)
 83                         continue;
 84 
 85                     FD_SET(socketClient, &allSockets);
 86                 }
 87                 else
 88                 {
 89                     char buf[1024] = { 0 };
 90                     int nRecvRet = recv(socketID, buf, 1024, 0);
 91                     if (nRecvRet == 0)        //客户端断开连接
 92                     {
 93                         FD_CLR(socketID, &allSockets);
 94                         closesocket(socketID);
 95                         continue;
 96                     }
 97                     else if (nRecvRet > 0)
 98                     {
 99                         cout << socketID << " : " << buf << endl;
100                         send(socketID, "我收到了你发送的数据!", sizeof("我收到了你发送的数据!"), 0);
101                     }
102                     else if (nRecvRet == SOCKET_ERROR)
103                     {
104                         int nError = WSAGetLastError();
105                         if (nError == 10054)
106                         {
107                             FD_CLR(socketID, &allSockets);
108                             closesocket(socketID);
109                             continue;
110                         }
111                         else
112                             cout << "recv error." << endl;
113                     }
114                 }
115             }
116         }
117     }
118 
119     for (int i = 0; i < allSockets.fd_count; i++)
120     {
121         closesocket(allSockets.fd_array[i]);
122     }
123     FD_ZERO(&allSockets);
124     WSACleanup();
125     return 0;
126 }

总结

select模型有以下优点:

  • select允许同时监视多个套接字的读写状态,使得在单个线程中可以处理多个套接字的I/O操作,提高了系统的效率。
  • select模型使用简单,适合初学者设计简单的网络通信。

select模型有以下缺点:

  • select()函数本身会造成阻塞。
  • 由于需要遍历整个被监视的套接字集合,在大规模并发场景下,select模型性能低下。
  • 受限于FD_SET集合有大小限制,后续的拓展性差,无法适用于高并发场景。
  • 无法处理大量数据,如果有大量数据需要处理,会导致阻塞其他套接字的读写。