一、select函数
Unix下的select
Windows下的select
- Windows下的select函数与Unix下的select函数语法相同,但是有些语言稍有不同
- Unix下的select函数其参数1为操作的最大描述符的值加1。但是Windows下的select函数的第一个参数可以填最大的文件描述符加1,也可以默认填0(其参数1只是为了兼容而已)
- 另外,Windows下的fd_set数据类型定义如下,其两个成员变量可以进行调用(但是Unix下的fd_set不提供),含义如下:
- fd_count:当前fd_set集合中存放的描述符的数量
- fd_array:加入到fd_set集合中的描述符都存放在这个数组中
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
二、将服务端改为select模型
- 根据前一篇文章中的服务端代码,我们为服务端加入select模型
- 并且有一些新的特点和功能,有:
- 新增了一种新的消息类型“CMD_NEW_USER_JOIN”,当有新客户端加入之后,服务端将“CMD_NEW_USER_JOIN”类型的数据包发送给其他所有客户端,通知有新客户加入
- 使用select接收新的客户端,并且设置一个单独的processor()函数用于与客户端进行数据交互
- select为非阻塞的
- 新接收的新的客户端的套接字都存放在一个全局的vector数组中(名为g_clients)
服务端代码如下
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()
#include <windows.h>
#include <WinSock2.h>
#include <iostream>
#include <string.h>
#include <stdio.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_NEW_USER_JOIN, //新的客户端加入
CMD_ERROR //错误
};
//数据报文的头部
struct DataHeader
{
short cmd; //命令的类型
short dataLength; //数据的长度
};
//登录消息体
struct Login :public DataHeader
{
Login() {
cmd = CMD_LOGIN;
dataLength = sizeof(Login); //消息长度=消息头(父类)+消息体(子类)
}
char userName[32]; //账号
char PassWord[32]; //密码
};
//登录结果
struct LoginResult :public DataHeader
{
LoginResult():result(0) {
cmd = CMD_LOGIN_RESULT;
dataLength = sizeof(LoginResult);
}
int result; //登录的结果,0代表正常
};
//退出消息体
struct Logout :public DataHeader
{
Logout() {
cmd = CMD_LOGOUT;
dataLength = sizeof(Logout);
}
char userName[32]; //账号
};
//退出结果
struct LogoutResult :public DataHeader
{
LogoutResult() :result(0) {
cmd = CMD_LOGOUT_RESULT;
dataLength = sizeof(LogoutResult);
}
int result; //退出的结果,0代表正常
};
//新的客户端加入,服务端给其他所有客户端发送此报文
struct NewUserJoin :public DataHeader
{
NewUserJoin(int _cSocket=0) :sock(_cSocket) {
cmd = CMD_NEW_USER_JOIN;
dataLength = sizeof(LogoutResult);
}
int sock; //新客户端的socket
};
//存放客户端的套接字
std::vector<SOCKET> g_clients;
//参数:客户端的套接字
//功能:服务端调用,函数内与客户端进行数据的交互
int processor(SOCKET _cSock);
int main()
{
WORD ver = MAKEWORD(2, 2);
WSADATA dat;
WSAStartup(ver, &dat);
//建立socket
SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == _sock){
std::cout << "ERROR:建立socket失败!" << std::endl;
}
else {
std::cout << "建立socket成功!" << std::endl;
}
//初始化服务端地址
struct sockaddr_in _sin = {};
_sin.sin_addr.S_un.S_addr = inet_addr("192.168.0.104");
//_sin.sin_addr.S_un.S_addr = INADDR_ANY;
_sin.sin_family = AF_INET;
_sin.sin_port = htons(4567);
//绑定服务端地址
if (SOCKET_ERROR == bind(_sock, (struct sockaddr*)&_sin, sizeof(_sin))){
std::cout << "ERROR:绑定地址失败!" << std::endl;
}
else {
std::cout << "绑定地址成功!" << std::endl;
}
//监听网络端口
if (SOCKET_ERROR == listen(_sock, 5)) {
std::cout << "ERROR:监听网络端口失败!" << std::endl;
}
else {
std::cout << "监听网络端口成功!" << std::endl;
}
//循环处理客户端的数据
while (true)
{
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);
//每次select之前,将所有客户端加入到读集中(此处为了演示,只介绍客户端读的情况)
for (int n = (int)g_clients.size() - 1; n >= 0; --n)
{
FD_SET(g_clients[n], &fdRead);
}
struct timeval t = { 3,0 };
int ret = select(_sock + 1, &fdRead, &fdWrite, &fdExp, &t);
if (ret < 0)
{
std::cout << "select出错!" << std::endl;
break;
}
if (FD_ISSET(_sock, &fdRead))//如果一个客户端连接进来,那么服务端的socket就会变为可读的,此时我们使用accept来接收这个客户端
{
FD_CLR(_sock, &fdRead);
//用来保存客户端地址
struct sockaddr_in _clientAddr = {};
int nAddrLen = sizeof(_clientAddr);
SOCKET _cSock = INVALID_SOCKET;
//接收客户端连接
_cSock = accept(_sock, (struct sockaddr*)&_clientAddr, &nAddrLen);
if (INVALID_SOCKET == _cSock) {
std::cout << "ERROR:接收到无效客户端!" << std::endl;
}
else {
//通知其他已存在的所有客户端,有新的客户端加入
NewUserJoin newUserInfo(static_cast<int>(_cSock));
for (int n = 0; n < g_clients.size(); ++n)
{
send(g_clients[n], (const char*)&newUserInfo, sizeof(newUserInfo), 0);
}
g_clients.push_back(_cSock); //将客户端的套接字存入vector内
std::cout << "接受到新的客户端连接,IP=" << inet_ntoa(_clientAddr.sin_addr)
<< ",Socket=" << static_cast<int>(_cSock) << std::endl;
}
}
//遍历fdRead集合中所有就绪的客户端套接字,然后调用processor()函数进行数据的交互
for (std::size_t n = 0; n < fdRead.fd_count; ++n)
{
//如果函数返回-1,说明recv出错或客户端退出
if (-1 == processor(fdRead.fd_array[n]))
{
//那么就查找到这个客户端套接字从vector中移除
auto iter = std::find(g_clients.cbegin(), g_clients.cend(), fdRead.fd_array[n]);
if (iter != g_clients.cend())
{
g_clients.erase(iter);
}
}
}
std::cout << "空闲时间,处理其他业务..." << std::endl;
}//end while
//将所有的客户端套接字关闭
for (int n = (int)g_clients.size() - 1; n >= 0; --n)
{
closesocket(g_clients[n]);
}
//关闭服务端套接字
closesocket(_sock);
WSACleanup();
std::cout << "服务端停止工作!" << std::endl;
getchar(); //防止程序一闪而过
return 0;
}
int processor(SOCKET _cSock)
{
//设置接收缓冲区,并接收命令
char szRecv[1024];
int _nLen = recv(_cSock, szRecv, sizeof(DataHeader), 0);
if (_nLen < 0) {
std::cout << "recv函数出错!" << std::endl;
return -1;
}
else if (_nLen == 0) {
std::cout << "客户端<Socket=" << _cSock << ">:已退出!" << std::endl;
return -1;
}
//在此处还应该判断少包黏包的问题,但是现在处于单机处理状态,后面介绍到复杂的消息通信时再介绍
//获取消息中头部中的信息
DataHeader* header = (DataHeader*)szRecv;
switch (header->cmd)
{
case CMD_LOGIN: //如果是登录
{
//接收消息体
recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
Login *login = (Login*)szRecv;
std::cout << "客户端<Socket=" << _cSock << ">:CMD_LOGIN,用户名:" << login->userName << ",密码:" << login->PassWord << std::endl;
//此处可以判断用户账户和密码是否正确等等(省略)
//返回登录的结果给客户端
LoginResult ret;
send(_cSock, (const char*)&ret, sizeof(ret), 0);
}
break;
case CMD_LOGOUT: //如果是退出
{
//接收消息体
recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
Logout *logout = (Logout*)szRecv;
std::cout << "客户端<Socket=" << _cSock << ">:CMD_LOGOUT,用户名:" << logout->userName << std::endl;
//返回退出的结果给客户端
LogoutResult ret;
send(_cSock, (const char*)&ret, sizeof(ret), 0);
}
break;
default: //如果有错误
{
DataHeader header = { CMD_ERROR,0 };
send(_cSock, (const char*)&header, sizeof(header), 0);
}
break;
}
return 0;
}
三、将客户端改为select模型
- 根据前一篇文章中的客户端代码,我们为客户端加入select模型
- 并且有一些新的特点和功能,有:
- 在select之前连接上服务端
- select延迟1秒超时
- select第一次超时之后,用代码书写一个数据包,然后发送给服务端,之后服务端的套接字变为可读,一直的发送数据与读取数据
- 代码中取消了用命令行输入数据发送给服务端。封装了一个函数processor(),在其中接收解析服务端回送的数据
客户端代码如下
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <WinSock2.h>
#include <iostream>
#include <string.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
enum CMD
{
CMD_LOGIN,
CMD_LOGIN_RESULT,
CMD_LOGOUT,
CMD_LOGOUT_RESULT,
CMD_NEW_USER_JOIN,
CMD_ERROR
};
struct DataHeader
{
short cmd;
short dataLength;
};
struct Login :public DataHeader
{
Login() {
cmd = CMD_LOGIN;
dataLength = sizeof(Login);
}
char userName[32];
char PassWord[32];
};
struct LoginResult :public DataHeader
{
LoginResult() :result(0) {
cmd = CMD_LOGIN_RESULT;
dataLength = sizeof(LoginResult);
}
int result;
};
struct Logout :public DataHeader
{
Logout() {
cmd = CMD_LOGOUT;
dataLength = sizeof(Logout);
}
char userName[32];
};
struct LogoutResult :public DataHeader
{
LogoutResult():result(0){
cmd = CMD_LOGOUT_RESULT;
dataLength = sizeof(LogoutResult);
}
int result;
};
struct NewUserJoin :public DataHeader
{
NewUserJoin(int _cSocket = 0) :sock(_cSocket) {
cmd = CMD_NEW_USER_JOIN;
dataLength = sizeof(LogoutResult);
}
int sock;
};
int processor(SOCKET _cSock);
int main()
{
WORD ver = MAKEWORD(2, 2);
WSADATA dat;
WSAStartup(ver, &dat);
//建立socket
SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == _sock) {
std::cout << "ERROR:建立socket失败!" << std::endl;
}
else {
std::cout << "建立socket成功!" << std::endl;
}
//声明要连接的服务端地址
struct sockaddr_in _sin = {};
_sin.sin_addr.S_un.S_addr = inet_addr("192.168.0.104");
_sin.sin_family = AF_INET;
_sin.sin_port = htons(4567);
//连接服务端
int ret = connect(_sock, (struct sockaddr*)&_sin, sizeof(_sin));
if (SOCKET_ERROR == ret) {
std::cout << "ERROR:连接服务端失败!" << std::endl;
}
else {
std::cout << "连接服务端成功!" << std::endl;
}
while (true)
{
fd_set fdRead;
FD_ZERO(&fdRead);
FD_SET(_sock, &fdRead);
struct timeval t = { 1,0 };
int ret = select(_sock + 1, &fdRead, NULL, NULL, &t);
if (ret < 0)
{
std::cout << "select出错!" << std::endl;
break;
}
if (FD_ISSET(_sock, &fdRead)) //如果服务端有数据发送过来,接收显示数据
{
FD_CLR(_sock, &fdRead);
if (-1 == processor(_sock))
{
std::cout << "数据接收失败,或服务端已断开!" << std::endl;
break;
}
}
//此处模拟用代码输入一条数据给服务端
Login login;
strcpy(login.userName, "dongshao");
strcpy(login.PassWord, "123456");
send(_sock, (const char*)&login, sizeof(login), 0);
//Sleep(1000); 可以让发送与接受速度延迟1秒
std::cout << "空闲时间,处理其他业务..." << std::endl;
}
//关闭服务端套接字
closesocket(_sock);
WSACleanup();
std::cout << "客户端停止工作!" << std::endl;
getchar(); //防止程序一闪而过
return 0;
}
int processor(SOCKET _cSock)
{
//设置接收缓冲区,并接收命令
char szRecv[1024];
int _nLen = recv(_cSock, szRecv, sizeof(DataHeader), 0);
if (_nLen < 0) {
std::cout << "recv函数出错!" << std::endl;
return -1;
}
else if (_nLen == 0) {
std::cout << "服务端已关闭!" << std::endl;
return -1;
}
//在此处还应该判断少包黏包的问题,但是现在处于单机处理状态,后面介绍到复杂的消息通信时再介绍
//获取消息中头部中的信息
DataHeader* header = (DataHeader*)szRecv;
switch (header->cmd)
{
case CMD_LOGIN_RESULT: //如果返回的是登录的结果
{
recv(_cSock, (char*)szRecv + sizeof(DataHeader), header->dataLength + sizeof(DataHeader), 0);
LoginResult* loginResult = (LoginResult*)szRecv;
std::cout << "收到服务端数据:CMD_LOGIN_RESULT,数据长度:" << loginResult->dataLength << ",结果为:" << loginResult->result << std::endl;
}
break;
case CMD_LOGOUT_RESULT: //如果是退出的结果
{
recv(_cSock, (char*)szRecv + sizeof(DataHeader), header->dataLength + sizeof(DataHeader), 0);
LogoutResult* logoutResult = (LogoutResult*)szRecv;
std::cout << "收到服务端数据:CMD_LOGOUT_RESULT,数据长度:" << logoutResult->dataLength << ",结果为:" << logoutResult->result << std::endl;
}
break;
case CMD_NEW_USER_JOIN: //有新用户加入
{
recv(_cSock, (char*)szRecv + sizeof(DataHeader), header->dataLength + sizeof(DataHeader), 0);
NewUserJoin* newUserJoin = (NewUserJoin*)szRecv;
std::cout << "收到服务端数据:CMD_NEW_USER_JOIN,数据长度:" << newUserJoin->dataLength << ",新用户Socket为:" << newUserJoin->sock << std::endl;
}
break;
}
return 0;
}
四、演示效果
五、附加