前言
对于原始的socket网络编程模型,由于accept与recv函数均会阻塞,使用多线程过于麻烦,于是知道了select的网络模型。但是看了不少讲述select模型的文章,总是云里雾里的,一点也不明白,于是专门看了一个视频,再把代码打出来,自己调试,终于理解了。
select模型介绍
select 模型主要有 select() 函数和 fd_set 结构体,以及 FD_SET, FD_ZERO, FD_ISSET 和 FD_CLR 这几个宏。
fd_set 顾名思义就是集合,实际上是 select() 函数待监控的 SOCKET 集合,在调用 select() 之前,应该把需要监控的 SOCKET 全部加入到 fd_set 集合中,否则无法处理该 SOCKET 上的消息(连接,断开,收到数据等)。
调用 select() 后, select() 会把有消息的 SOCKET 记录到 fd_set 集合中,没有消息的 SOCKET 会被清除标记,因此在重新调用 select() 之前应该再次把要监控的 SOCKET 重新加入到 fd_set 集合中。
代码
主要是理解调用select()之后该函数做了什么,调用之前要传入什么。
客户端用原来先的代码就可以进行测试了。处理函数 processor() 只是封装了大小写转换的 recv() 和 send() ,没什么好说的。
主体流程介绍:
- 主体中把 select() 放入 while 循环中以便可以处理多个客户端消息。
- 先将服务器的 SOCKET 加入到待监控的 fs_set 集合中,再将已经连接的客户端 SOCKET 加入 fd_set 集合中。
- 调用 select() 对这些 SOCKET 进行监听。
- 有新的客户端连接时,fdRead 集合中会保留服务器的 SOCKET ,于是在调用 FD_ISSET 宏时会检查到该服务器的 SOCKET,返回计数1,其余没有消息的 SOCKT会被计数减去。在取出该新建的连接时要调用 FD_CLR 将计数器减一。
- 当已连接 SOCKET 发送过来数据时由于上一步已经去除了新建的连接,于是可以循环 fd_set 的计数器,对 fd_set 集合中保留的 SOCKET 进行 recv() 和 send() 操作(此时 recv() 和 send() 实际上依旧是阻塞的)。当客户端断开时,清理断开的SOCKET。
程序主体(处理函数比较独立,就放下面了):
#include <stdio.h>
#include <winsock.h>
#include <vector>
#pragma comment(lib,"ws2_32.lib")
std::vector<SOCKET>vecClient;
int processor(SOCKET sockClient);
#define DEFAULT_PORT 12345
#define BUFFER_SIZE 512
int main() {
WSADATA wsa;
sockaddr_in addrServer = {
}, addrClient = {
};
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(DEFAULT_PORT);
addrServer.sin_addr.S_un.S_addr = INADDR_ANY;
int nAddrLen = sizeof(sockaddr_in);
fd_set fdRead, fdWrite, fdExp;
WSAStartup(MAKEWORD(2, 2), &wsa);
SOCKET sockServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
bind(sockServer, (sockaddr*)&addrServer, sizeof(addrServer));
listen(sockServer, 5);
while (true) {
// 1.首先初始化待监听的socket列表
// 2.调用select进行连接监听
// 3.检查服务器socket是否在可读的集合中,如果在,那么就调用accept取出新的连接
// 4.除去新建连接后,剩下为需要处理的socket,对剩余集合进行recv及send操作
// 5.断开连接后将客户端连接去除
FD_ZERO(&fdRead); FD_ZERO(&fdWrite); FD_ZERO(&fdExp);
FD_SET(sockServer, &fdRead); FD_SET(sockServer, &fdWrite); FD_SET(sockServer, &fdExp);
for (int i = 0; i < vecClient.size(); i++) {
FD_SET(vecClient[i], &fdRead);
}
int ret = select(sockServer + 1, &fdRead, &fdWrite, &fdExp, NULL);
if (FD_ISSET(sockServer, &fdRead)) {
// 服务器有需要处理的连接
FD_CLR(sockServer, &fdRead); // 取出连接后计数减一
SOCKET sockClient = accept(sockServer, (sockaddr*)&addrClient, &nAddrLen);
printf("accept sock %d form %s:%d\n", sockClient, inet_ntoa(addrClient.sin_addr), addrClient.sin_port);
vecClient.push_back(sockClient); // 保存新的连接
}
for (size_t n = 0; n < fdRead.fd_count; n++) {
if (-1 == processor(fdRead.fd_array[n])) {
auto iter = find(vecClient.begin(), vecClient.end(), fdRead.fd_array[n]);
if (iter != vecClient.end()) {
closesocket(*iter);
vecClient.erase(iter);
}
}
}
}
for (size_t n = vecClient.size() - 1; n >= 0; n--) {
closesocket(vecClient[n]);
}
closesocket(sockServer);
WSACleanup();
do {
printf("Press Ctrl + C to exit!\n"); } while (getchar());
return 0;
}
处理函数:
int processor(SOCKET sockClient) {
char szRecvBuff[BUFFER_SIZE] = {
};
char szSendBuff[BUFFER_SIZE] = {
};
int iRtn = recv(sockClient, szRecvBuff, BUFFER_SIZE, 0);
if (iRtn > 0) {
printf("recv socket %d %d bytes :%s\n", sockClient, iRtn, szRecvBuff);
}
else{
printf("disconnect with sock %d!\n", sockClient);
return -1;
}
// 大小写转换
int i = 0;
for (i = 0; szRecvBuff[i - 1] != '\0'; i++) {
if (szRecvBuff[i] >= 'a' && szRecvBuff[i] <= 'z') {
szSendBuff[i] = szRecvBuff[i] + ('A' - 'a');
}
else if (szRecvBuff[i] >= 'A' && szRecvBuff[i] <= 'Z') {
szSendBuff[i] = szRecvBuff[i] - ('A' - 'a');
}
else {
szSendBuff[i] = szRecvBuff[i];
}
}
iRtn = send(sockClient, szSendBuff, i, 0); // 最后一个参数默认为0(为5时导致数据发送不完整)
if (iRtn > 0) {
printf("send to socket %d %d bytes!\n", sockClient, iRtn);
}
else {
printf("send to socket %d error!\n", sockClient);
return -1;
}
// int iSleep = 30 * 1000;
// printf("sleep %d s\n", iSleep / 1000);
// Sleep(iSleep); // 当前连接阻塞后依旧会影响其他连接的消息收发
printf("\n");
}
参考资料:
【C++教程】C++百万并发网络通信引擎架构与实现(Socket、全栈、跨平台) https://www.bilibili.com/video/BV1LT4y1A7Pn