select网络模型
Select 网络模型
1.为什么需要用select网络模型?
为了解决基本C/S模型中,accept()的阻塞的问题。
2. 它和基本的C/S模型有什么区别?
- 基本C/S模型中,每次处理一个独立的客户端都要单独的线程,这样会导致客户连接数很大时,线程数也会很多。
- 并且accept()会阻塞一直等待socket来链接,这期间服务端是干不了其他事情的。
- 使用select的话,只需要两个线程,一个主线程用于接受Accept客户端的连接,一个子线程用于监听客户端是否有信号状态,如果有则进行相应的消息处理。
- select模型解决了实现多个客户端链接,与多个客户端分别通信
- 但是select模型实际上只解决了accept()阻塞的问题,并没有解决recv()、send()阻塞的问题
select模型逻辑
1.将所有的socket(服务器端+客户端)装进数组中,然后告诉select应用进程
2.通过调用select(),将数组传给内核(这里也就产生了一次fd_set从用户空间到内核空间的复制)
3.内核收到fd_set后对fd_set进行遍历,然后一个个去扫描对应fd是否满足可读写事件。
4.当内核发现有响应的socket后,内核会把它们又返回给应用进程(这里又会把fd_set从内核空间复制用户空间),然后装入相应的fd_set数组中。并且内核把其他那些没有响应事件的fd句柄清除。
5.对装有响应的socket数组集中处理,向对应的fd发起数据读取或者写入数据操作。
比如说服务器端,收到了write类型的socket,那么会执行accept()、recv();收到了read类型的socket,会执行send()。
又比如客户端,收到了write类型的socket,客户端知道有人想要写数据过来了,那么它就接受recv();收到了read类型的socket,执行send();
原文链接:https://blog.csdn.net/aa804738534/article/details/117073745
Select原型分析
1 int select( 2 int nfds, /*填0*/ 3 fd_set *readfds, /*检查是否有可读的socket*/ 4 fd_set *writefds, /*检查是否有可写的socket*/ 5 fd_set *exceptfds, /*检查socket上的异常错误*/ 6 const timeval *timeout /*最大等待时间*/ 7 );
参数5:这是一个指向 timeval
结构的指针,用于设置等待的超时时间。如果 timeout
为 NULL
,select
函数将会一直阻塞,直到有文件描述符准备就绪。
否则,它会等待一段时间,如果超时则返回。
函数返回值:select()函数调用成功返回发生响应事件的所有 socket 数量的综合,超过时间限制就返回 0,继续下一次等待。
套接字集合
fd_set 结构是 socket 集合,它可以把多个套接字连在一起,select 函数可以测试这个集合中哪些套接字有事件发生。
1 typedef struct fd_set { 2 u_int fd_count; /* how many are SET? */ 3 SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */ 4 } fd_set;
四个操作fd_set的宏操作:
注意:
select 提供一种可以用一个进程监控多个网络连接的方式,但也还遗留了一些问题,这些问题也是后来select面对高并发环境的性能瓶颈。
1、每调用一次select 就需要3个事件类型的fd_set需从用户空间拷贝到内核空间去,返回时select也会把保留了活跃事件的fd_set返回(从内核拷贝到用户空间)。当fd_set数据大的时候,这个过程消耗是很大的。
2、select需要逐个遍历fd_set集合 ,然后去检查对应fd的可读写状态,如果fd_set 数据量多,那么遍历fd_set 就是一个比较耗时的过程。
3、fd_set是个集合类型的数据结构有长度限制,32位系统长度1024,62位系统长度2048,这个就限制了select最多能同时监控1024个连接。
代码:
server.cpp
1 #define WIN32_LEAN_AND_MEAN 2 #define _WINSOCK_DEPRECATED_NO_WARNINGS 3 4 #include<windows.h> 5 #include<WinSock2.h> 6 #include<stdio.h> 7 #include <vector> 8 9 #pragma comment(lib,"ws2_32.lib") 10 11 enum CMD 12 { 13 CMD_LOGIN, 14 CMD_LOGIN_RESULT, 15 CMD_LOGOUT, 16 CMD_LOGOUT_RESULT, 17 CMD_NEW_USER_JOIN, 18 CMD_ERROR 19 }; 20 21 struct DataHeader 22 { 23 short dataLength; 24 short cmd; 25 }; 26 27 //DataPackage 28 struct Login : public DataHeader 29 { 30 Login() 31 { 32 dataLength = sizeof(Login); 33 cmd = CMD_LOGIN; 34 } 35 char userName[32]; 36 char PassWord[32]; 37 }; 38 39 struct LoginResult : public DataHeader 40 { 41 LoginResult() 42 { 43 dataLength = sizeof(LoginResult); 44 cmd = CMD_LOGIN_RESULT; 45 result = 0; 46 } 47 int result; 48 }; 49 50 struct Logout : public DataHeader 51 { 52 Logout() 53 { 54 dataLength = sizeof(Logout); 55 cmd = CMD_LOGOUT; 56 } 57 char userName[32]; 58 }; 59 60 struct LogoutResult : public DataHeader 61 { 62 LogoutResult() 63 { 64 dataLength = sizeof(LogoutResult); 65 cmd = CMD_LOGOUT_RESULT; 66 result = 0; 67 } 68 int result; 69 }; 70 71 struct NewUserJoin : public DataHeader 72 { 73 NewUserJoin() 74 { 75 dataLength = sizeof(NewUserJoin); 76 cmd = CMD_NEW_USER_JOIN; 77 scok = 0; 78 } 79 int scok; 80 }; 81 82 std::vector<SOCKET> g_clients; 83 84 int processor(SOCKET _cSock) 85 { 86 //缓冲区 87 char szRecv[4096] = {}; 88 // 5 接收客户端数据 89 int nLen = recv(_cSock, szRecv, sizeof(DataHeader), 0); 90 DataHeader* header = (DataHeader*)szRecv; 91 if (nLen <= 0) 92 { 93 printf("客户端<Socket=%d>已退出,任务结束。\n", _cSock); 94 return -1; 95 } 96 switch (header->cmd) 97 { 98 case CMD_LOGIN: 99 { 100 recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0); //除去数据包头部分的长度 101 Login* login = (Login*)szRecv; 102 printf("收到客户端<Socket=%d>请求:CMD_LOGIN,数据长度:%d,userName=%s PassWord=%s\n", _cSock, login->dataLength, login->userName, login->PassWord); 103 //忽略判断用户密码是否正确的过程 104 LoginResult ret; 105 send(_cSock, (char*)&ret, sizeof(LoginResult), 0); 106 return 1; 107 } 108 break; 109 case CMD_LOGOUT: 110 { 111 recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0); 112 Logout* logout = (Logout*)szRecv; 113 printf("收到客户端<Socket=%d>请求:CMD_LOGOUT,数据长度:%d,userName=%s \n", _cSock, logout->dataLength, logout->userName); 114 //忽略判断用户密码是否正确的过程 115 LogoutResult ret; 116 send(_cSock, (char*)&ret, sizeof(ret), 0); 117 return 1; 118 } 119 break; 120 default: 121 { 122 DataHeader header = { 0,CMD_ERROR }; 123 send(_cSock, (char*)&header, sizeof(header), 0); 124 return 1; 125 } 126 break; 127 } 128 } 129 130 int main() 131 { 132 //启动Windows socket 2.x环境 133 WORD ver = MAKEWORD(2, 2); 134 WSADATA dat; 135 WSAStartup(ver, &dat); 136 //------------ 137 138 //-- 用Socket API建立简易TCP服务端 139 // 1 建立一个socket 套接字 140 SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 141 // 2 bind 绑定用于接受客户端连接的网络端口 142 sockaddr_in _sin = {}; 143 _sin.sin_family = AF_INET; 144 _sin.sin_port = htons(4567);//host to net unsigned short 145 _sin.sin_addr.S_un.S_addr = INADDR_ANY;//inet_addr("127.0.0.1"); 146 if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin))) 147 { 148 printf("错误,绑定网络端口失败...\n"); 149 } 150 else { 151 printf("绑定网络端口成功...\n"); 152 } 153 // 3 listen 监听网络端口 154 if (SOCKET_ERROR == listen(_sock, 5)) 155 { 156 printf("错误,监听网络端口失败...\n"); 157 } 158 else { 159 printf("监听网络端口成功...\n"); 160 } 161 162 while (true) 163 { 164 //伯克利套接字 BSD socket 165 fd_set fdRead;//描述符(socket) 集合 166 fd_set fdWrite; 167 fd_set fdExp; 168 //清理集合 169 FD_ZERO(&fdRead); 170 FD_ZERO(&fdWrite); 171 FD_ZERO(&fdExp); 172 //将描述符(socket)加入集合 换句话说 -- 指定应该被监视的套接字 173 FD_SET(_sock, &fdRead); 174 FD_SET(_sock, &fdWrite); 175 FD_SET(_sock, &fdExp); 176 177 for (int n = (int)g_clients.size() - 1; n >= 0; n--) 178 { 179 FD_SET(g_clients[n], &fdRead); 180 } 181 ///nfds 是一个整数值 是指fd_set集合中所有描述符(socket)的范围,而不是数量 182 ///既是所有文件描述符最大值+1 在Windows中这个参数可以写0 183 timeval t = { 1,0 }; 184 int ret = select(_sock + 1, &fdRead, &fdWrite, &fdExp, &t); 185 if (ret < 0) 186 { 187 printf("select任务结束。\n"); 188 break; 189 } 190 //判断描述符(socket)是否在集合中 191 if (FD_ISSET(_sock, &fdRead)) 192 { 193 FD_CLR(_sock, &fdRead); 194 // 4 accept 等待接受客户端连接 195 sockaddr_in clientAddr = {}; 196 int nAddrLen = sizeof(sockaddr_in); 197 SOCKET _cSock = INVALID_SOCKET; 198 _cSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen); 199 if (INVALID_SOCKET == _cSock) 200 { 201 printf("错误,接受到无效客户端SOCKET...\n"); 202 } 203 else 204 { 205 //然后通知已有的客户端有新客户端加入,并将新客户端套接字添加到客户端列表中,以便在后续循环中对其进行监视。 206 //这个想法还挺好的,通知现有的客户端,有新人加入。 207 for (int n = (int)g_clients.size() - 1; n >= 0; n--) 208 { 209 NewUserJoin userJoin; 210 send(g_clients[n], (const char*)&userJoin, sizeof(NewUserJoin), 0); 211 } 212 g_clients.push_back(_cSock); 213 printf("新客户端加入:socket = %d,IP = %s \n", (int)_cSock, inet_ntoa(clientAddr.sin_addr)); 214 } 215 } 216 //for (size_t n = 0; n < fdRead.fd_count; n++) 这是老师给出的代码,其实也没问题,但没必要遍历所有的fdRead 只需要遍历g_client就行了 217 //{ 218 // if (-1 == processor(fdRead.fd_array[n])) 219 // { 220 // auto iter = find(g_clients.begin(), g_clients.end(), fdRead.fd_array[n]); 221 // if (iter != g_clients.end()) 222 // { 223 // g_clients.erase(iter); 224 // } 225 // } 226 //} 227 for (size_t n = 0; n < g_clients.size(); n++) 228 { 229 if (-1 == processor(g_clients[n])) 230 { 231 closesocket(g_clients[n]); 232 g_clients.erase(g_clients.begin() + n); 233 n--; //将 n 的值减少一个,使循环变量回到移除套接字之前的位置,以便正确处理下一个套接字。这样确保不会跳过任何要处理的套接字。 234 } 235 } 236 237 printf("空闲时间处理其它业务..\n"); 238 } 239 240 //for (size_t n = g_clients.size() - 1; n >= 0; n--) 241 //{ 242 // closesocket(g_clients[n]); 243 //} 244 245 // 8 关闭套节字closesocket 246 closesocket(_sock); 247 //------------ 248 //清除Windows socket环境 249 WSACleanup(); 250 printf("已退出。\n"); 251 getchar(); 252 return 0; 253 }
client.cpp
1 #define WIN32_LEAN_AND_MEAN 2 #define _WINSOCK_DEPRECATED_NO_WARNINGS 3 #define _CRT_SECURE_NO_WARNINGS 4 #include<windows.h> 5 #include<WinSock2.h> 6 #include<stdio.h> 7 8 #pragma comment(lib,"ws2_32.lib") 9 10 enum CMD 11 { 12 CMD_LOGIN, 13 CMD_LOGIN_RESULT, 14 CMD_LOGOUT, 15 CMD_LOGOUT_RESULT, 16 CMD_NEW_USER_JOIN, 17 CMD_ERROR 18 }; 19 20 struct DataHeader 21 { 22 short dataLength; 23 short cmd; 24 }; 25 26 //DataPackage 27 struct Login : public DataHeader 28 { 29 Login() 30 { 31 dataLength = sizeof(Login); 32 cmd = CMD_LOGIN; 33 } 34 char userName[32]; 35 char PassWord[32]; 36 }; 37 38 struct LoginResult : public DataHeader 39 { 40 LoginResult() 41 { 42 dataLength = sizeof(LoginResult); 43 cmd = CMD_LOGIN_RESULT; 44 result = 0; 45 } 46 int result; 47 }; 48 49 struct Logout : public DataHeader 50 { 51 Logout() 52 { 53 dataLength = sizeof(Logout); 54 cmd = CMD_LOGOUT; 55 } 56 char userName[32]; 57 }; 58 59 struct LogoutResult : public DataHeader 60 { 61 LogoutResult() 62 { 63 dataLength = sizeof(LogoutResult); 64 cmd = CMD_LOGOUT_RESULT; 65 result = 0; 66 } 67 int result; 68 }; 69 70 struct NewUserJoin : public DataHeader 71 { 72 NewUserJoin() 73 { 74 dataLength = sizeof(NewUserJoin); 75 cmd = CMD_NEW_USER_JOIN; 76 scok = 0; 77 } 78 int scok; 79 }; 80 81 int processor(SOCKET _cSock) 82 { 83 //缓冲区 84 char szRecv[4096] = {}; 85 // 5 接收客户端数据 86 int nLen = recv(_cSock, szRecv, sizeof(DataHeader), 0); 87 DataHeader* header = (DataHeader*)szRecv; 88 if (nLen <= 0) 89 { 90 printf("与服务器断开连接,任务结束。\n"); 91 return -1; 92 } 93 switch (header->cmd) 94 { 95 case CMD_LOGIN_RESULT: 96 { 97 recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0); 98 LoginResult* login = (LoginResult*)szRecv; 99 printf("收到服务端<Socket=%d>消息:CMD_LOGIN_RESULT,数据长度:%d\n", _cSock,login->dataLength); 100 return 1; 101 } 102 break; 103 case CMD_LOGOUT_RESULT: 104 { 105 recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0); 106 LogoutResult* logout = (LogoutResult*)szRecv; 107 printf("收到服务端<Socket=%d>消息:CMD_LOGOUT_RESULT,数据长度:%d\n",_cSock , logout->dataLength); 108 return 1; 109 } 110 break; 111 case CMD_NEW_USER_JOIN: 112 { 113 recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0); 114 NewUserJoin* userJoin = (NewUserJoin*)szRecv; 115 printf("收到服务端<Socket=%d>消息:CMD_NEW_USER_JOIN,数据长度:%d\n", _cSock, userJoin->dataLength); 116 return 1; 117 } 118 break; 119 } 120 } 121 122 int main() 123 { 124 //启动Windows socket 2.x环境 125 WORD ver = MAKEWORD(2, 2); 126 WSADATA dat; 127 WSAStartup(ver, &dat); 128 //------------ 129 //-- 用Socket API建立简易TCP客户端 130 // 1 建立一个socket 131 SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0); 132 if (INVALID_SOCKET == _sock) 133 { 134 printf("错误,建立Socket失败...\n"); 135 } 136 else { 137 printf("建立Socket成功...\n"); 138 } 139 // 2 连接服务器 connect 140 sockaddr_in _sin = {}; 141 _sin.sin_family = AF_INET; 142 _sin.sin_port = htons(4567); 143 _sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); 144 int ret = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in)); 145 if (SOCKET_ERROR == ret) 146 { 147 printf("错误,连接服务器失败...\n"); 148 } 149 else { 150 printf("连接服务器成功...\n"); 151 } 152 153 154 while (true) 155 { 156 fd_set fdReads; 157 FD_ZERO(&fdReads); 158 FD_SET(_sock, &fdReads); 159 timeval t = { 1,0 }; 160 int ret = select(_sock, &fdReads, 0, 0, &t); 161 if (ret < 0) 162 { 163 printf("select任务结束1\n"); 164 break; 165 } 166 if (FD_ISSET(_sock, &fdReads)) 167 { 168 FD_CLR(_sock, &fdReads); 169 170 if (-1 == processor(_sock)) 171 { 172 printf("select任务结束2\n"); 173 break; 174 } 175 } 176 177 printf("空闲时间处理其它业务..\n"); 178 Login login; 179 strcpy(login.userName, "lyd"); 180 strcpy(login.PassWord, "lyd"); 181 send(_sock, (const char*)&login, sizeof(Login), 0); 182 //Sleep(1000); 183 } 184 // 7 关闭套节字closesocket 185 closesocket(_sock); 186 //清除Windows socket环境 187 WSACleanup(); 188 printf("已退出。\n"); 189 getchar(); 190 return 0; 191 }