select模型是对简单C/S模型的优化,他解决了accept函数阻塞等待连接的问题。并且允许应用程序同时监视多个套接字,从而实现简单的并发请求。通过调用select函数确认一个或多个套接字当前的状态,并根据当前状态进行相应操作。在select模型模型中,select函数是最关键的。
select模型工作原理
select模型维护了一个Socket数组,通过遍历该数组检查当前是否存在就绪socket,并将所有就绪的socket返回。我们遍历该数组提供相应服务。工作原理大致如下:
- 将服务端Socket添加至socket数组中。
- 调用select()函数遍历socket数组。
- 返回就绪socket数组,对该数组集中处理。
- 如果是服务端Socket,调用accept()函数接收连接请求,并将该客户端数组添加至socket数组。
- 如果是客户端Socket,调用send()、recv()函数进行通信,当客户端下线时,从socket数组中移除该Socket。
- 重复执行2-6步。
select模型使用步骤
- 启动Socket服务
- 创建套接字
- 为套接字绑定端口信息
- 监听套接字
- 使用select模型监听套接字集合,处理数据
- 关闭套接字和网络服务
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集合有大小限制,后续的拓展性差,无法适用于高并发场景。
- 无法处理大量数据,如果有大量数据需要处理,会导致阻塞其他套接字的读写。