之前写的网络模型,都是客户端和服务端1对1阻塞的网络程序
每次服务端想要接收新的客户端连接的时候都必须要有一个客户端向server发送连接,才能返回执行下一步
想要recv的时候也必须要客户端send真正发送之后才能执行下一步
因为发送的速度比较快所以感觉不出来,
像这样的程序就必须等到谁执行才能执行下一步,这样就不能执行其他业务,被动的去等待数据,返回,处理业务
如果需要多个客户端连接处理业务就需要使用到select模型
-
select
select 支持标准c/c++ 以及已linux系统为版本的系统里面是都可以使用的
像iocp都是在特性环境下才能使用用select已经可以用来完成像网络游戏还有其他的网络视频处理通过server/client都可以完成
在后期想处理更高的吞吐量使用到多线程的方式来提高他的并发数量用内存池去提高吞吐量并发数量可以做到另外一个级别,
只要掌握select就可以用来实现中小型的网络模型 -
win下的 select API
select(
// _In_ 作用是被传入用的
_In_ int nfds, // 在win下这个值是没有意义的 在linux/mac os系统下是用来最大描述符的 这个描述符指的就是socket
// 上面的集合最大的单位就是这个fd_set,用来操作 nfds 最大的值的socket +1
// _Inout_ 传入传出,传入一个readfds需要查询的套接字集合,哪些可读
_Inout_opt_ fd_set FAR * readfds, // readfds 去查询可读的socket
_Inout_opt_ fd_set FAR * writefds, // writefds 查询可写的socket
_Inout_opt_ fd_set FAR * exceptfds, // exceptfds 异常socket
_In_opt_ const struct timeval FAR * timeout // 如果什么都不传select是阻塞的,传入一个时间就会在指定的时间内没有东西就会直接返回 非阻塞
);
- 改进
受到影响的accept,recv,send
代码修改:
- server
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <windows.h>
#include <WinSock2.h>
#include <vector>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
enum CMD
{
CMD_LOGIN,
CMD_LOGIN_RESULT,
CMD_LOGOUT,
CMD_LOGOUT_RESULT,
CMD_ERROR
};
struct DataHeader
{
short dataLength;
short cmd;
};
struct Login : public DataHeader
{
Login()
{
dataLength = sizeof(Login);
cmd = CMD_LOGIN;
}
char UserName[32];
char PassWord[32];
};
struct LoginResult : public DataHeader
{
LoginResult()
{
dataLength = sizeof(Login);
cmd = CMD_LOGIN_RESULT;
result = 0; // 0表示正常
}
int result;
};
struct Logout : public DataHeader
{
Logout()
{
dataLength = sizeof(Logout);
cmd = CMD_LOGOUT;
}
char UserName[32];
};
struct LogoutResult : public DataHeader
{
LogoutResult()
{
dataLength = sizeof(LogoutResult);
cmd = CMD_LOGOUT_RESULT;
result = 0;
}
int result;
};
// 定义一个vector来保存连接的客户端
vector<SOCKET> g_clients;
int processor(SOCKET _client_sock)
{
char szRecv[1024] = {};
int ret = recv(_client_sock, szRecv, sizeof(DataHeader), 0);
DataHeader* header = (DataHeader*)szRecv;
if (ret <= 0)
{
cout << "client exit...." << endl;
return -1;
}
switch (header->cmd)
{
case CMD_LOGIN:
{
recv(_client_sock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
Login* login = (Login*)szRecv;
printf("收到客户端<Socket=%d>请求:CMD_LOGIN, 数据长度 : %d\n", _client_sock, login->dataLength);
// LoginResult ret = { CMD_LOGIN }; // 就不能这样初始化了 因为是一个类了
LoginResult ret;
send(_client_sock, (char*)&ret, sizeof(LoginResult), 0);
}
break;
case CMD_LOGOUT:
{
recv(_client_sock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
Logout* logout = (Logout*)szRecv;
printf("收到客户端<Socket=%d>请求:CMD_LOGIN, 数据长度 : %d\n", _client_sock, logout->dataLength);
LogoutResult ret;
send(_client_sock, (char*)&ret, sizeof(ret), 0);
}
break;
default:
DataHeader header = { 0, CMD_ERROR };
send(_client_sock, (char*)&header, sizeof(DataHeader), 0);
break;
}
}
int main()
{
WORD ver = MAKEWORD(2, 2);
WSADATA dat;
WSAStartup(ver, &dat);
SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;
_sin.sin_port = htons(8888);
_sin.sin_addr.S_un.S_addr = INADDR_ANY;
if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin)))
{
cout << "绑定端口失败" << endl;
exit(1);
}
cout << "绑定网络端口成功...." << endl;
if (SOCKET_ERROR == listen(_sock, 5))
{
cout << "监听端口失败" << endl;
exit(1);
}
cout << "监听网络端口成功...." << endl;
while (true)
{
// 伯克利 socket 描述符
// 第一个参数在win下是不要的,在linux/mac os系统下是用来最大描述符的 这个描述符指的就是socket
fd_set fdRead;
fd_set fdWrite;
fd_set fdExp;
// 清空
FD_ZERO(&fdRead);
FD_ZERO(&fdWrite);
FD_ZERO(&fdExp);
// 将描述符加入集合
FD_SET(_sock, &fdRead);
FD_SET(_sock, &fdWrite);
FD_SET(_sock, &fdExp);
// //将socket加入到集合中 多个客户端循环处理
for (int n = (int)g_clients.size() - 1; n >= 0; n--)
{
FD_SET(g_clients[n], &fdRead);
}
// nfds 是一个整数值,是指fd_set集合中所有描述符(socket)的范围,而不是数量
// 即是所有文件描述符最大值+1,在win中这个参数无所谓可以写0
/*
select 现在是阻塞的情况
如果是做一个纯应答型的网络服务程序那么这样就可以足够的使用
如果除了相应客户端的消息以外,还要主动的去给客户端推送一些消息这样就有些不方便
*/
int ret = select(_sock + 1, &fdRead, &fdWrite, &fdExp, NULL);
if (ret < 0)
{
cout << "select error" << endl;
break;
}
// 判断是否在集合中
if (FD_ISSET(_sock, &fdRead))
{
FD_CLR(_sock, &fdRead);
sockaddr_in _client_addr = {};
int n_addr_len = sizeof(_client_addr);
SOCKET _client_sock = INVALID_SOCKET;
_client_sock = accept(_sock, (sockaddr*)&_client_addr, &n_addr_len);
if (INVALID_SOCKET == _client_sock)
{
cout << "accept失败" << endl;
exit(1);
}
cout << "accept 成功.... ip = " << inet_ntoa(_client_addr.sin_addr) << endl;
// 使用vector存起连接进来的客户端
g_clients.push_back(_client_sock);
}
/*
因为我们本是服务器创建的socket已经处理过了
所以只需要用来接收数据就可以了accept
*/
// 循环处理数组里面的客户端
for (size_t n = 0; n < fdRead.fd_count; n++)
{
// 用一个函数来处理接收、发送的数据
if (-1 == processor(fdRead.fd_array[n]))
{
auto iter = find(g_clients.begin(), g_clients.end(), fdRead.fd_array[n]);
if (iter != g_clients.end())
{
g_clients.erase(iter);
}
}
}
}
// 清理
for (size_t n = g_clients.size() - 1; n >= 0; --n)
{
closesocket(g_clients[n]);
}
// 6 关闭套接字
closesocket(_sock);
// 清理win socket环境
WSACleanup();
getchar();
return 0;
}
- client 代码不变
-------------------------------- the end ----------------------------------