查看原文
其他

写了一个基于select的并发服务器

The following article is from 涛歌依旧 Author 点击关注👉👉

点击关注公众号,一周多次包邮送书

来源:经授权转自 涛歌依旧(ID:ai_taogeyijiu_2021)

作者:涛哥


有很多读者对网络编程很感兴趣,觉得挺有意思,希望我多写一些网络编程方面的实战例子。没问题,这就来了。

对于后端开发同学而言,肯定了解多线程并发服务器,那么多路复用的并发服务又怎么玩呢?看完这篇就清楚了。

今天,我们来实战一下基于select模型的并发服务器,并给出实际实验效果,希望大家能有一些新的认识和收获。

如果你还不清楚,那我来手绘一下,你就明白啦:8个男生追求一个女生,这个女生同时处理8个男生的通信请求

涛哥手绘


服务端实现

大家对select函数的作用应该很熟悉了,接下来,我们看看服务端程序,在关键的地方,基本都有详细的注释:

#include <stdio.h>#include <winsock2.h> #pragma comment(lib, "ws2_32.lib") int totalSockets = 0; // socket的总数SOCKET socketArray[100]; // socket组成的数组,假设最多有100个socket吧 // 日志打印void log(const char *pStr){ FILE *fp = fopen("log.txt", "a"); fprintf(fp, "log:%s\n", pStr); fclose(fp);} // 创建socketvoid addToSocketArr(SOCKET s) {  socketArray[totalSockets] = s;         totalSockets++; } // 启动服务器int main(){ log("into main"); // 网络初始化 WSADATA wsaData;  WSAStartup(MAKEWORD(1, 1), &wsaData); // 创建socket  SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, 0);  addToSocketArr(listenSocket);     // 服务地信息 SOCKADDR_IN srvAddr;  srvAddr.sin_family = AF_INET;     srvAddr.sin_addr.s_addr = htonl(INADDR_ANY);     srvAddr.sin_port = htons(8888);   // 绑定  bind(listenSocket, (SOCKADDR*)&srvAddr, sizeof(srvAddr));  // 监听  listen(listenSocket, 10);  // 设置socket为非阻塞模式  unsigned long nonBlock = 1;     ioctlsocket(listenSocket, FIONBIO, &nonBlock);  while(1)   { // 读集, 要记得清零初始化 FD_SET readSet;    FD_ZERO(&readSet);        // 将每个socket都塞入读集,便于让内核来监测这些socket int i = 0; for(i = 0; i < totalSockets; i++) { FD_SET(socketArray[i], &readSet); } // 应用程序通知内核来监测读集中的socket, 最后的NULL表示超时时间无限长 int total = select(0, &readSet, NULL, NULL, NULL);     // 我们不考虑select失败,那么程序到这里,说明读集中必有socket处于"就绪状态" for(i = 0; i < totalSockets; i++) { char szTmp[20] = {0}; sprintf(szTmp, "%d", i); log(szTmp); if(socketArray[i] == listenSocket) // 对监听的socket进行判断 { log("socketArray[i] == listenSocket");         if(FD_ISSET(listenSocket, &readSet)) // 如果该socket在可读集中,则表明有客户端来连接        { log("listenSocket, socketArray[i] == listenSocket"); // 接收来自于客户端的connect请求 SOCKADDR_IN addrClient; int len = sizeof(SOCKADDR); SOCKET acceptSocket = 0; acceptSocket = accept(listenSocket,(SOCKADDR*)&addrClient, &len); // 设置为非阻塞模式 nonBlock = 1; ioctlsocket(acceptSocket, FIONBIO, &nonBlock); // 添加到socket数组中 addToSocketArr(acceptSocket); } continue; }        // 注意:上面的listenSocket是不负责通信的,下面的一些socket都是负责通信的socket  // 如果通信socket处于读就绪状态 if (FD_ISSET(socketArray[i], &readSet))       {        log("to receive");  char szRecvBuf[1024] = {0}; recv(socketArray[i], szRecvBuf, sizeof(szRecvBuf) - 1, 0); printf("socketArray[i] is %d, %s\n", socketArray[i], szRecvBuf); } }    } // 省略了关闭socket等后续操作 return 0;}

编译并运行程序,开启服务端,等待客户端的连接。


客户端实现

接下来,我们看客户端的程序,很常规,也很简洁:

#include <winsock2.h>#include <stdio.h>#pragma comment(lib, "ws2_32.lib") int main(){ WORD wVersionRequested; WSADATA wsaData; wVersionRequested = MAKEWORD(1, 1); WSAStartup( wVersionRequested, &wsaData ); SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(8888); connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));   while(1) { char szSendBuf[100] = {0}; scanf("%s", szSendBuf); send(sockClient, szSendBuf, strlen(szSendBuf) + 1, 0); } closesocket(sockClient); WSACleanup(); return 0;}

实战的结果

我们编译上述的客户端程序,然后连续运行8次客户端,也就产生了8个客户端进程。请注意,保持这8个客户端进程都运行,不要关闭他们。
然后,在8个客户端进程中依次输入1, 2, 3, 4, 5, 6, 7, 8后,分别按Enter发送,然后在第一个客户端进程中再次输入111,并按Enter发送。
接下来,我们一起看看服务端的结果:
socketArray[i] is 136, 1
socketArray[i] is 152, 2
socketArray[i] is 164, 3
socketArray[i] is 176, 4
socketArray[i] is 188, 5
socketArray[i] is 200, 6
socketArray[i] is 212, 7
socketArray[i] is 224, 8
socketArray[i] is 136, 111
可以看到,服务端可以并发地与8个客户端进行通信,而且还不会混淆它们,这就是基于select模型的并发服务器。
另外,从log.txt的日志中可以看出,尽管有8个客户端,但服务端的监听socket是唯一的,这也是很好理解的事情。



建议有兴趣的同学实际玩一下这个例子,加深对多路复用并发服务器的理解,提高实战能力。

网络编程就是这样,定好思路,多编程,多抓包,多调试,爽爽哒!大家如果有疑问请留言。

·················END·················

推荐阅读

•   本地与远程设备之间如何有效地进行文件同步?•   老板说我最近飘了,都敢用 MySQL 实现分布式锁了•   阿里终面:业务主表读写缓慢如何优化?•   函数调用时栈是如何变化的?•   9张图,32个案例带你轻松玩转Java stream•   JVM 运行时数据区域,书中没有说清楚的方法区、永久代、元空间•   一个由“ YYYY-MM-dd ”引发的惨案 !


👇更多内容请点击👇

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存