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 结构的指针,用于设置等待的超时时间。如果 timeoutNULLselect 函数将会一直阻塞,直到有文件描述符准备就绪。

       否则,它会等待一段时间,如果超时则返回。

  函数返回值: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 }
View Code

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 }
View Code

 

 

posted @ 2023-08-18 01:47  C++杀我  阅读(194)  评论(0编辑  收藏  举报