之前在Windows环境下用多线程模型实现了一个聊天室
但是多线程模型下存在着不少缺点:
而Select模型具有着
因为服务端的在listen之前以及listen的内容几乎一样故省略,感兴趣的可以去看
windows环境下C/C++的socket相关网络编程详解
select模型及其工作流程重要的内容个人认为就这三个
fd_set select FD_ISSET
建立fd_set
集合保存需要监控的套接字,并用FD_ZERO宏来初始化我们需要的fd_set。
调用select()
监听套接字,它会返回就绪套接字的数量,如果一个套接字没有数据需要接收,select函数会把该套接字从可读性检查队列中删除掉
然后使用FD_ISSET()
函数检查每个套接字是否在相应的集合中,从而确定该套接字是否就绪,并执行该套接字对应的内容,比如一个分配给select第一个参数的套接字句柄在select返回后仍然在select第一个参数的fd_set里,那么说明当前数据已经来了, 马上可以读取成功而不会被阻塞。
在使用Select函数前,首先我们需要一个fd_set结构体,用作select函数的第二三四个参数。结构体原型如下所示:
#ifndef FD_SETSIZE
#define FD_SETSIZE 64
#endif /* FD_SETSIZE */
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
//u_int fd_count 结构体成员的个数
//SOCKET fd_array[FD_SETSIZE]; socket类型的数组,默认最多有64个客户端连接
注意FD_SETSIZE这个宏,这个宏的意思是select模型最多处理多少个链接的数量。
同时我们需要使用两个宏设置服务端的sock绑定
fd_set reads;
// 清空或者初始化reads
FD_ZERO(&reads);
// 设置sockServer到reads
FD_SET(sockServer, &reads);
之后调用select函数
select原型如下:
int WSAAPI select(
[in] int nfds,
[in, out] fd_set *readfds,
[in, out] fd_set *writefds,
[in, out] fd_set *exceptfds,
[in] const timeval *timeout
);
select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,const timeval *timeout);
// select(0, 0, 0, 0, 0);
int nfds, windows 下默认0(win下没用) linux下最大的文件描述符+1
fd_set *readfds, // 检测可读
fd_set *writefds,// 检测可写
fd_set *exceptfds,// 检测异常 一般不用
const struct timeval *timeout // 超时时间
select通过轮询来检测各个集合中的描述符(fd)的状态,如果描述符的状态发生改变,则会在该集合中设置相应的标记位;如果指定描述符的状态没有发生改变,则将该描述符从对应集合中移除。很明显,select的调用复杂度是线性的,即O(n)。
select的限制:
(1)前面FD_SET里有提到FD_SETSIZE宏,这个宏是操作系统定义的。在windows下面通常是64,也就是说select最多只能管理64个描述符。如果大于64的个描述,select将会产生不可预知的行为。那在没有poll或epoll的情况下,怎样使用select来处理连接数大于64的情况呢?答案是使用多线程技术,每个线程单独使用一个select进行检测。这样的话,你的系统能够处理的并发连接数等于线程数*64。早期的apache就是这种技术来支撑海量连接的。
(2)需要修改传入的参数数组
(3)不能指定某个有数据的socket
(4)线程不安全
接着使用FD_ISSET用于监听
FD_ISSET(fd, set)宏接受两个参数:
因为这是在之前的多线程聊天室服务端基础上更改,所以有部分没介绍的,可以参考之前的文章,或者文章之后的完整代码
while (1)
{
fd_set reads;
// 清空或者初始化reads
FD_ZERO(&reads);
// 设置sockServer到reads
FD_SET(sockServer, &reads);
for (auto v : g_clients)
{
FD_SET(v->clientSock, &reads);
}
int sRet = select(0, &reads, 0, 0, 0);
// 表示select超时或者出错
if (sRet <= 0) continue;
if (FD_ISSET(sockServer, &reads)) {
printf("服务端响应\n");
SOCKADDR_IN clientAddr = {};
int nAddrLen = sizeof(SOCKADDR_IN);
SOCKET sockClient = accept(sockServer, (sockaddr*)&clientAddr, &nAddrLen);
if (INVALID_SOCKET == sockClient) {
printf("接收客户端连接失败\n");
return -1;
}
std::cout << "开始处理客户端: " << std::endl;
UserInfo* user = new UserInfo{ false, "undefined", sockClient };
g_clients.push_back(user);
}
for (auto v : g_clients) {
if (FD_ISSET(v->clientSock, &reads))
{
SelectClientConnection(v->clientSock, v);
}
}
}
前置知识准备的差不多后,我们直接写吧~
小提示建立一个数组来存放所有建立联系的套结字描述符,循环检测这些套接字是否有相应从而达到检测聊天端信息的作用
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <ws2tcpip.h>
#include <winsock2.h>
#include <windows.h>
#include <iostream>
#include <thread>
#include <vector>
#pragma comment(lib, "ws2_32.lib")
class UserInfo
{
public:
bool isLogin = false; //是否登陆
std::string _userName; //客户端的用户名
SOCKET clientSock; //客户端的socket
UserInfo(bool isLogin, std::string _userName, SOCKET clientSock) :isLogin(isLogin), _userName(_userName), clientSock(clientSock)
{
}
};
std::vector<UserInfo*> g_clients; //用于服务端存储用户登陆信息
// 判定用户是否处在登陆状态函数
// INPUIT: const std::string& userName 用户姓名
// RETURN: bool true:在线 false:不在线
bool isUserLoggedIn(const std::string& _userName)
{
for (const auto& user : g_clients)
{
if (user->_userName == _userName && user->isLogin)
return true;
}
return false;
}
// 广播信息函数
// INPUIT: SOCKET selfSock 客户端的Sock描述符, const char* msg 广播信息
// 广播信息给除了特定客户端以外的所有客户端信息
void SendMsg(SOCKET selfSock, const char* msg)
{
int msglen = strlen(msg);
for (int i = 0; i < g_clients.size(); i++)
{
if (g_clients[i]->clientSock == selfSock)continue;
send(g_clients[i]->clientSock, msg, msglen, 0);
}
}
// 分割字符串函数
// INPUIT: const std::string& s 待分割字符串, char delimiter 分割符号
// RETURN: std::vector<std::string> 存储分割的字符串的数组
std::vector<std::string> splitString(const std::string& s, char delimiter)
{
std::vector<std::string> result;
std::string path;
for (size_t i = 0; i < s.size(); i++)
{
if (s[i] != delimiter)
{
path.push_back(s[i]);
}
else if (!path.empty()) // 确保path非空时才push_back
{
result.push_back(path);
path.clear();
}
}
if (!path.empty()) // 处理字符串以分隔符结尾的情况
{
result.push_back(path);
}
return result;
}
// 客户端的Select处理函数
// INPUIT: SOCKET clientSocket 客户端对应的SOCKET, UserInfo* currentUser 客户端对应的用户
void SelectClientConnection(SOCKET clientSocket, UserInfo* currentUser)
{
char szData[1024] = {};
int ret = recv(clientSocket, szData, sizeof(szData), 0);
if (ret > 0)
{
std::cout << "收到数据: [" << szData << "]" << std::endl;
std::vector<std::string> splits = splitString(szData, '|');
if (splits[0] == "Login")
{
// 验证用户是否已登录
if (isUserLoggedIn(splits[1]))
{
char loginFailedMsg[64];
snprintf(loginFailedMsg, sizeof(loginFailedMsg), "Error|%s|LoginFailed", splits[1].c_str());
send(clientSocket, loginFailedMsg, sizeof(loginFailedMsg), 0);
return;
}
// 用户未登录,创建并登录
currentUser->isLogin = true;
currentUser->_userName = splits[1];
}
else if (splits[0] == "其它命令")
{
}
else
{
std::string chatMsg;
chatMsg.append(currentUser->_userName);
chatMsg.append(":");
chatMsg.append(szData);
//chatMsg = currentUser->_userName + ":" + szData;
SendMsg(INVALID_SOCKET, chatMsg.c_str());
}
}
else if (ret <= 0)
{
std::cout << "客户端断开链接" << std::endl;
if (currentUser->_userName != "undefined")
{
for (auto it = g_clients.begin(); it != g_clients.end(); ++it)
{
// 找到并移除对应的UserInfo对象
if (*it == currentUser)
{
std::cout << "User:" << currentUser->_userName << "is erase!" << std::endl;
delete currentUser;
g_clients.erase(it);
break;
}
}
}
closesocket(currentUser->clientSock);
}
}
int main()
{
// 0. 初始化网络环境
WSADATA wsaData = {};
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET sockServer = socket(AF_INET, SOCK_STREAM, 0);
if (sockServer == INVALID_SOCKET)
{
std::cerr << "创建服务端句柄失败" << std::endl;
WSACleanup();
return -1;
}
printf("1. 创建服务端成功\n");
SOCKADDR_IN addr = { 0 };
addr.sin_family = AF_INET;
addr.sin_port = htons(9870);
addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
if (bind(sockServer, (SOCKADDR*)&addr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
{
std::cerr << "绑定端口号失败" << std::endl;
closesocket(sockServer);
WSACleanup();
return -1;
}
if (listen(sockServer, SOMAXCONN) == SOCKET_ERROR)
{
std::cerr << "监听端口号失败" << std::endl;
closesocket(sockServer);
WSACleanup();
return -1;
}
std::cout << "服务器正在监听..." << std::endl;
while (1)
{
fd_set reads;
// 清空或者初始化reads
FD_ZERO(&reads);
// 设置sockServer到reads
FD_SET(sockServer, &reads);
for (auto v : g_clients)
{
FD_SET(v->clientSock, &reads);
}
int sRet = select(0, &reads, 0, 0, 0);
// 表示select超时或者出错
if (sRet <= 0) continue;
if (FD_ISSET(sockServer, &reads)) {
printf("服务端响应\n");
SOCKADDR_IN clientAddr = {};
int nAddrLen = sizeof(SOCKADDR_IN);
SOCKET sockClient = accept(sockServer, (sockaddr*)&clientAddr, &nAddrLen);
if (INVALID_SOCKET == sockClient) {
printf("接收客户端连接失败\n");
return -1;
}
std::cout << "开始处理客户端: " << std::endl;
UserInfo* user = new UserInfo{ false, "undefined", sockClient };
g_clients.push_back(user);
}
for (auto v : g_clients) {
if (FD_ISSET(v->clientSock, &reads))
{
SelectClientConnection(v->clientSock, v);
}
}
}
closesocket(sockServer);
WSACleanup();
return 0;
}
代码演示:
我们采用sokit工具作为客户端
也可以自己实现一个简单的客户端,结合本篇文章和参考我之前的文章有过简单的客户端实现
windows环境下C/C++的socket相关网络编程详解
在处理大量并发连接的场景下,select模型的服务端与多线程模型的服务端相比性能有了一定的提高,然而,在每个连接处理逻辑较为复杂,且计算密集型任务较多的情况下,多线程模型可能表现得更好,并且,当select函数投递一组socket给操作系统时,操作系统将有信号的socket装进fe_set中并返回,这一过程是阻塞的,尤其是在大量连接的情况下,因为它需要轮询所有的套接字,会导致性能的下降,为了提高执行效率,可以使用事件投递模型,一个以Select为核心的事件投递模型,其实就是WSAEventSelect模型,在后续,我会从底层简单的实现一个WSAEventSelect模型
另外感兴趣的也可以自己去实现一个简单的Select模型客户端
免责声明:
以上内容均属参考得知,未曾阅读过专业书籍,纯属兴趣使然,若有谬误欢迎指出
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有