c++SOCKET升级为select模型(待完善)

本文介绍了如何在C++中使用select系统调用实现服务端与客户端的通信。服务端通过select监听多个客户端连接,处理登录、登出等命令,当有新客户端加入时,会通知所有在线客户端。客户端则采用select模型,非阻塞地接收服务器数据,并在接收到新用户加入通知时作出响应。此外,客户端的命令处理移到了单独的线程中,实现了多线程处理输入输出。
摘要由CSDN通过智能技术生成

上节接服务端与客户端1.2——发送结构化消息_贪睡的蜗牛的博客-CSDN博客

 

FD_SET 

参考

【一文搞懂】FD_SET的使用_欧恩意的博客-CSDN博客_fd_set 

Linux编程之select - Madcola - 博客园 

一个long类型的数组(我们可以认为这是一个很大的字节数组),提供给select()机制使用的一种数据结构。主要功能是建立联系。其中每一个数组元素都能与任意一个打开的句柄(socket句柄、文件、命名管道、设备句柄等)建立联系。

设置这种联系是“可以理解为给打开的句柄添加了一种标识 ( or  or 异常 )的标识”

提供了四个宏定义接口来操作fd_set,具体如下

// 这里的fd 实际使用都是以 句柄 传入
FD_ZERO(fd_set *fdset);              // 将set清零使集合中不含任何fd
FD_SET(int fd, fd_set *fdset);       // 将fd加入set集合
FD_CLR(int fd, fd_set *fdset);       // 将fd从set集合中清除
FD_ISSET(int fd, fd_set *fdset);     // 检测fd是否在set集合中,不在则返回0

_In_表示输入参数

__out的输出参数 

—Inout 输入和输出参数

__in_opt:可选的输入参数 

上面这些参数其实可以去掉

select函数

select(
    _In_ int nfds,		//在windows下面没有意义  在Unix衍生的操作系统中使用  
    _Inout_opt_ fd_set FAR * readfds, 		 //_Inout_  表示这个参数传入时是有意义的 传出时也是有意义的   指向检查可读性的套接字集合的可选的指针。表示客户端有读的socket客户端需求等待处理
    _Inout_opt_ fd_set FAR * writefds,		//指向检查可写性的套接字集合的可选的指针。表示客户端有写的socket客户端需求等待处理  
    _Inout_opt_ fd_set FAR * exceptfds,	//指向检查错误的套接字集合的可选的指针。表示客户端有异常的Socket客户端的需求等待处理
    _In_opt_ const struct timeval FAR * timeout 	//函数需要等待的最长时间,需要以TIMEVAL结构体格式提供此参数,对于阻塞操作,此参数为null。 
    );

以readfds为例,比方说传入了一个1,2,3,select处理后发现1和2是可读的,那么它就只返回1和2,在writefds中,传入了5,6,7,select发现只有5是可写的,那么它只会返回5,在exceptfds中,传入了8,9,10,发现只有10是异常的,那么它就会返回10,上面都是通过修改指针指向的内容返回的,而函数总返回的是int类型的,这里返回的是总返回的个数,那这里会返回4 

timeout,用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。如果设置的是0,那么就是非阻塞监听,select直接处理运行然后返回,如果设置的是NULL,那么就是阻塞监听,一直等到有可读,可写,有异常的才返回,如果设置的是大于0的数,那么就会在监听设置的值后运行返回。

timeval结构体定义如下:

struct timeval
{      
    long tv_sec;   /*秒 */
    long tv_usec;  /*微秒 */   
};

使用的时候

struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;

err = select(1,&rd,NULL,NULL,&tv);

其他几个需要注意的地方

  • 在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返回,这样整个程序被锁死在这里,而select是事前将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回,可达到在同一个线程内同时处理多个IO请求的目的
  • 可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。
  • 当声明了一个文件描述符集后,必须用FD_ZERO将所有位置零
  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd(也就是需要有两个array保存现有的socket),一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
  • 数据结构array需要将监听socket加入到里面,当select返回后,通过监听这个可用来判断是否是有新的请求连接建立
  • select能处理的异常情况只有一种:socket上接收到带外数据。带外数据也称为加速数据在下面处理时带外数据设计需要比普通数据有更高的优先级。
  • socket缓冲区在每个套接字中单独存在;socket缓冲区在创建套接字时自动生成;即使关闭套接字也会继续传送发送缓冲区中遗留的数据;关闭套接字将丢失接收缓冲区中的数据。所以在select返回后,用recv进行对缓冲区的处理。

 

service修改后的程序

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<Windows.h>
#include<WinSock2.h>
#include<iostream>
#include<string>
#include<vector>
using namespace std;
#pragma comment(lib,"ws2_32.lib") //windows socket2 32的lib库


enum CMD  //消息枚举
{
	CMD_LOGIN,
	CMD_LOGIN_RESULT,
	CMD_LOGINOUT,
	CMD_LOGOUT_RESULT,
	CMD_ERROR
};
//消息头
struct DataHeader
{
	short dataLength;    //数据长度 32767字节
	short cmd;
};

struct Login : public DataHeader
{
	Login()
	{
		dataLength = sizeof(Login);
		cmd = CMD_LOGIN;
	}
	char userName[32];
	char Password[32];
};
struct Logout :public DataHeader
{
	Logout()
	{
		dataLength = sizeof(Logout);
		cmd = CMD_LOGINOUT;
	}
	char userName[32];
};
struct LoginResult :public DataHeader
{
	LoginResult()
	{
		dataLength = sizeof(LoginResult);
		cmd = CMD_LOGIN_RESULT;
	}
	int result;
};
struct LogoutResult :public DataHeader
{
	LogoutResult()
	{
		dataLength = sizeof(LogoutResult);
		cmd = CMD_LOGOUT_RESULT;
	}
	int result;
};

int processor(SOCKET _cSocket)
{
	//使用一个缓冲区接收数据 暂定最大收发1024个字节		后续会改进大文件的传输
	char* szRecv = new char[1024];

	//5 首先接收数据包头
	int nlen = recv(_cSocket, szRecv, sizeof(DataHeader), 0); //接受客户端的数据 第一个参数应该是客户端的socket对象

	if (nlen <= 0)
	{
		//客户端退出
		cout << "客户端已退出,任务结束" << endl;
		return -1;
	}
	//用一个指针指向这个头部
	DataHeader* header = (DataHeader*)szRecv;
	switch (header->cmd)
	{
	case CMD_LOGIN:
	{
		Login* _login;
		//读取Header->dataLength的数据长度  将数据继续读入saRecv这个块里面
		recv(_cSocket, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
		_login = (Login*)szRecv;
		cout << "收到命令:CMD_LOGIN" << " 数据长度 = " << header->dataLength << " UserName = " << _login->userName << " Password = " << _login->Password << endl;
		//忽略了判断用户名密码是否正确的过程
		LoginResult _loginres;
		_loginres.result = 200;
		send(_cSocket, (char*)&_loginres, sizeof(LoginResult), 0);
	}break;
	case CMD_LOGINOUT:
	{
		Logout* _logout;
		recv(_cSocket, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
		_logout = (Logout*)szRecv;
		cout << "收到命令:CMD_LOGOUT" << " 数据长度 = " << header->dataLength << " UserName = " << _logout->userName << endl;
		LogoutResult _logoutres;
		_logoutres.result = 200;
		send(_cSocket, (char*)&_logoutres, sizeof(LogoutResult), 0);
	}break;
	default:
	{
		header->cmd = CMD_ERROR;
		header->dataLength = 0;
		send(_cSocket, (char*)&header, sizeof(DataHeader), 0);
	}
	break;
	}
}


std::vector<SOCKET>g_clinets;

int main()
{
	//启动 windows socket 2.x 环境
	WORD versionCode = MAKEWORD(2, 2);	//创建一个版本号 
	WSADATA data;
	WSAStartup(versionCode, &data);  //启动Socket网络API的函数
									 ///


	//(1) 用Socket API建立简易的TCP服务端

	//	1. 建立一个Socket
	SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // ipv4 面向字节流的 tcp协议

	//	2. 绑定接受客户端连接的端口 bind

	sockaddr_in _sin = {};
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567); //端口号 host to net  sockaddr_in中port是USHORT类型
								 //网络中port是 unsigend short类型 因此需要Htons进行转换
	//_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  //服务器绑定的IP地址  127.0.0.1是本地地址
	_sin.sin_addr.S_un.S_addr = INADDR_ANY; //不限定访问该服务端的IP
	if (bind(_sock, (sockaddr*)&_sin, sizeof(_sin)) == SOCKET_ERROR)  //sockaddr 不利于编码  
	{
		cout << "ERROR: 绑定用于接受客户端连接的网络端口失败..." << endl;
	}
	else
	{
		cout << "SUCCESS: 绑定端口成功..." << endl;
	}
	//	3. 监听网络端口 listen

	if (listen(_sock, 5) == SOCKET_ERROR)//第二个参数 backbag 最大允许连接数量
	{
		cout << "ERROR: 监听用于接受客户端连接的网络端口失败..." << endl;
	}
	else
	{
		cout << "SUCCESS: 监听端口成功..." << endl;
	}
	
	while (true)
	{
		fd_set fdRead;
		fd_set fdWrite;
		fd_set fdExpect;

		FD_ZERO(&fdRead);		//清空fd集合的数据
		FD_ZERO(&fdWrite);
		FD_ZERO(&fdExpect);
		//这个宏的功能是 将服务端的_sock 放到fdRead这个集合中 
		//当socket在listen状态,如果已经接收一个连接请求,这个socket会被标记为可读,例如一个accept会确保不会阻塞的完成
		//对于其他的socket,可读性意味着队列中的数据适合读,当调用recv后不会阻塞。
		FD_SET(_sock, &fdRead);  //将服务端的socket放入可读列表,确保accept不阻塞
		FD_SET(_sock, &fdWrite);
		FD_SET(_sock, &fdExpect);

		for (size_t n = 0; n < g_clinets.size(); n++)
		{
			FD_SET(g_clinets[n], &fdRead);		//所有连入的客户端放入可读列表 保证recv不阻塞
		}

		//nfds第一个参数 是一个整数值 是指fd_set集合中所有socket值的范围 不是数量 
		int ret = select(_sock + 1, &fdRead, &fdWrite, &fdExpect, NULL);
		if (ret < 0)
		{
			cout << "select任务结束" << endl;
			break;
		}
		if (FD_ISSET(_sock, &fdRead))	//判断_sock是否在fdRead中
		{
			FD_CLR(_sock, &fdRead);
			//	4. 等待接受客户端连接 accept
			sockaddr_in _clientAddr = {};
			int cliendAddrLen = sizeof(_clientAddr);
			SOCKET _clientSock = INVALID_SOCKET; // 初始化无效的socket 用来存储接入的客户端

			_clientSock = accept(_sock, (sockaddr*)&_clientAddr, &cliendAddrLen);//当客户端接入时 会得到连入客户端的socket地址和长度
			if (INVALID_SOCKET == _clientSock) //接受到无效接入
			{
				cout << "ERROR: 接受到无效客户端SOCKET..." << endl;
			}
			else
			{
				cout << "新Client加入:" << "socket = " << _clientSock << " IP = " << inet_ntoa(_clientAddr.sin_addr) << endl;  //inet_ntoa 将ip地址转换成可读的字符串
			}
			g_clinets.push_back(_clientSock);
		}

		for (size_t n = 0; n < fdRead.fd_count; n++)
		{
			if (processor(fdRead.fd_array[n]) == -1)//processor函数是处理命令的逻辑 recv接到的数据并做出相应的判断和输出日志
			{
				auto it = find(g_clinets.begin(), g_clinets.end(), fdRead.fd_array[n]);
				if (it != g_clinets.end())
					g_clinets.erase(it);
			}
		}
	}



	//	6. 关闭socket
	closesocket(_sock);
	// 清除Windows socket环境
	WSACleanup();
	return 0;
}

结果

1.3版本 将客户端升级为select模型

客户端升级了select模型,然后如果有服务端发来的数据,进入process函数中处理

服务端也做出修改,当有新的服务端进入后,服务端通知所有的客户端,有新的客户端加入

 客户端和服务端都增加一个新的客户端加入的结构体

客户端做出修改:

 

服务端做出修改

 

 

 用户端代码

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<Windows.h>
#include<WinSock2.h>
#include<iostream>
#include<string>
using namespace std;
#pragma comment(lib,"ws2_32.lib") //windows socket2 32的lib库


enum CMD
{
	CMD_LOGIN,
	CMD_LOGIN_RESULT,
	CMD_LOGINOUT,
	CMD_LOGOUT_RESULT,
	CMD_ERROR,
	CMD_NEWUSERJOIN,
};




//消息头
struct DataHeader
{
	short dataLength;    //数据长度 32767字节
	short cmd;
};

struct Login : public DataHeader
{
	Login()
	{
		dataLength = sizeof(Login);
		cmd = CMD_LOGIN;
	}
	char userName[32];
	char Password[32];
};
struct Logout :public DataHeader
{
	Logout()
	{
		dataLength = sizeof(Logout);
		cmd = CMD_LOGINOUT;
	}
	char userName[32];
};
struct LoginResult :public DataHeader
{
	LoginResult()
	{
		dataLength = sizeof(LoginResult);
		cmd = CMD_LOGIN_RESULT;
	}
	int result;
};
struct LogoutResult :public DataHeader
{
	LogoutResult()
	{
		dataLength = sizeof(LogoutResult);
		cmd = CMD_LOGOUT_RESULT;
	}
	int sockId;
	int result;
};

struct NewUserJoin :public DataHeader
{
	NewUserJoin()
	{
		dataLength = sizeof(LogoutResult);
		cmd = CMD_NEWUSERJOIN;
		result = 0;
	}
	int sockId;
	int result;
};

int processor(SOCKET _sock)
{
	char* szRecv = new char[1024];
	//5 首先接收数据包头
	int nlen = recv(_sock, szRecv, sizeof(DataHeader), 0); //接受客户端的数据 第一个参数应该是客户端的socket对象
	if (nlen <= 0)
	{
		//客户端退出
		cout << "客户端:Socket = " << _sock << " 与服务器断开连接,任务结束" << endl;
		return -1;
	}
	DataHeader* header = (DataHeader*)szRecv;
	switch (header->cmd)
	{
	case CMD_NEWUSERJOIN:
	{
		NewUserJoin _userJoin;
		recv(_sock, (char*)&_userJoin + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
		cout << "收到服务器消息: CMD_NEWUSERJOIN:" << _userJoin.sockId << endl;
	}break;
	case CMD_LOGIN_RESULT:
	{
		LoginResult _lgRes;
		recv(_sock, (char*)&_lgRes + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
		cout << "收到服务器消息: CMD_LOGIN_RESULT:" << _lgRes.result << endl;
	}break;
	case CMD_LOGOUT_RESULT:
	{
		LogoutResult _lgRes;
		recv(_sock, (char*)&_lgRes + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
		cout << "收到服务器消息: CMD_LOGIN_RESULT:" << _lgRes.result << endl;
	}break;
	default:
	{
		header->cmd = CMD_ERROR;
		header->dataLength = 0;
		send(_sock, (char*)&header, sizeof(DataHeader), 0);
	}
	break;
	}
	return 0;
}


int main()
{
	//启动 windows socket 2.x 环境
	WORD versionCode = MAKEWORD(2, 2);	//创建一个版本号 
	WSADATA data;
	WSAStartup(versionCode, &data);  //启动Socket网络API的函数
									 ///


	//(1) 用Socket API建立简易的TCP客户端

	//	1. 建立一个Socket  下面第三个参数不需要指定
	SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0); // ipv4 面向字节流的 tcp协议
	if (INVALID_SOCKET == _sock)
	{
		cout << "错误,建立socket失败"<<endl;
	}
	else
	{
		cout << "成功建立客户端socket"<<endl;
	}
	//	2. 连接服务器 connect
	sockaddr_in _sin = {};
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567); //端口号 host to net  sockaddr_in中port是USHORT类型
								 //网络中port是 unsigend short类型 因此需要Htons进行转换
	//_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  //服务器绑定的IP地址  127.0.0.1是本地地址
	_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); 

	int ret = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));
	if (SOCKET_ERROR == ret)
	{
		cout << "错误,connect失败" << endl;
	}
	else
	{
		cout << "成功,connect 成功" << endl;
	}

	//3 接收服务器数据 resv
	char recvBuf[256] = {};

	while (true)
	{
		fd_set fdReads;
		FD_ZERO(&fdReads);
		FD_SET(_sock, &fdReads);  
		timeval t = { 1,0 };
			int ret = select(_sock + 1, &fdReads, NULL, NULL, &t);
		if (ret < 0)
		{
			cout << "select任务结束" << endl;
			break;
		}
		if (FD_ISSET(_sock, &fdReads))	//如果_sock在fdRead里面,表明有需求等待处理
		{
			FD_CLR(_sock, &fdReads);
			if (processor(_sock) == -1)
			{
				cout << "Select任务已结束2" << endl;
				break;
			}
		}
		cout << "客户端非阻塞的执行其它业务" << endl;
	}




	//	6. 关闭socket
	closesocket(_sock);
	// 清除Windows socket环境
	WSACleanup();
	getchar();//防止一闪而过
	return 0;
}

服务端 

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<Windows.h>
#include<WinSock2.h>
#include<iostream>
#include<string>
#include<vector>
using namespace std;
#pragma comment(lib,"ws2_32.lib") //windows socket2 32的lib库


enum CMD  //消息枚举
{
	
	CMD_LOGIN,
	CMD_LOGIN_RESULT,
	CMD_LOGINOUT,
	CMD_LOGOUT_RESULT,
	CMD_ERROR,
	CMD_NEWUSERJOIN,
};
//消息头
struct DataHeader
{
	short dataLength;    //数据长度 32767字节
	short cmd;
};


struct Login : public DataHeader
{
	Login()
	{
		dataLength = sizeof(Login);
		cmd = CMD_LOGIN;
	}
	char userName[32];
	char Password[32];
};
struct Logout :public DataHeader
{
	Logout()
	{
		dataLength = sizeof(Logout);
		cmd = CMD_LOGINOUT;
	}
	char userName[32];
};
struct LoginResult :public DataHeader
{
	LoginResult()
	{
		dataLength = sizeof(LoginResult);
		cmd = CMD_LOGIN_RESULT;
	}
	int result;
};
struct LogoutResult :public DataHeader
{
	LogoutResult()
	{
		dataLength = sizeof(LogoutResult);
		cmd = CMD_LOGOUT_RESULT;
	}
	int result;
};
struct NewUserJoin :public DataHeader
{
	NewUserJoin()
	{
		dataLength = sizeof(LogoutResult);
		cmd = CMD_NEWUSERJOIN;
		result = 0;
	}
	int sockId;
	int result;
};

int processor(SOCKET _cSocket)
{
	//使用一个缓冲区接收数据 暂定最大收发1024个字节		后续会改进大文件的传输
	char* szRecv = new char[1024];

	//5 首先接收数据包头
	int nlen = recv(_cSocket, szRecv, sizeof(DataHeader), 0); //接受客户端的数据 第一个参数应该是客户端的socket对象

	if (nlen <= 0)
	{
		//客户端退出
		cout << "客户端已退出,任务结束" << endl;
		return -1;
	}
	//用一个指针指向这个头部
	DataHeader* header = (DataHeader*)szRecv;
	switch (header->cmd)
	{
	case CMD_LOGIN:
	{
		Login* _login;
		//读取Header->dataLength的数据长度  将数据继续读入saRecv这个块里面
		recv(_cSocket, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
		_login = (Login*)szRecv;
		cout << "收到命令:CMD_LOGIN" << " 数据长度 = " << header->dataLength << " UserName = " << _login->userName << " Password = " << _login->Password << endl;
		//忽略了判断用户名密码是否正确的过程
		LoginResult _loginres;
		_loginres.result = 200;
		send(_cSocket, (char*)&_loginres, sizeof(LoginResult), 0);
	}break;
	case CMD_LOGINOUT:
	{
		Logout* _logout;
		recv(_cSocket, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
		_logout = (Logout*)szRecv;
		cout << "收到命令:CMD_LOGOUT" << " 数据长度 = " << header->dataLength << " UserName = " << _logout->userName << endl;
		LogoutResult _logoutres;
		_logoutres.result = 200;
		send(_cSocket, (char*)&_logoutres, sizeof(LogoutResult), 0);
	}break;
	default:
	{
		header->cmd = CMD_ERROR;
		header->dataLength = 0;
		send(_cSocket, (char*)&header, sizeof(DataHeader), 0);
	}
	break;
	}
}


std::vector<SOCKET>g_clinets;

int main()
{
	//启动 windows socket 2.x 环境
	WORD versionCode = MAKEWORD(2, 2);	//创建一个版本号 
	WSADATA data;
	WSAStartup(versionCode, &data);  //启动Socket网络API的函数
									 ///


	//(1) 用Socket API建立简易的TCP服务端

	//	1. 建立一个Socket
	SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // ipv4 面向字节流的 tcp协议

	//	2. 绑定接受客户端连接的端口 bind

	sockaddr_in _sin = {};
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567); //端口号 host to net  sockaddr_in中port是USHORT类型
								 //网络中port是 unsigend short类型 因此需要Htons进行转换
	//_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  //服务器绑定的IP地址  127.0.0.1是本地地址
	_sin.sin_addr.S_un.S_addr = INADDR_ANY; //不限定访问该服务端的IP
	if (bind(_sock, (sockaddr*)&_sin, sizeof(_sin)) == SOCKET_ERROR)  //sockaddr 不利于编码 
	{
		cout << "ERROR: 绑定用于接受客户端连接的网络端口失败..." << endl;
	}
	else
	{
		cout << "SUCCESS: 绑定端口成功..." << endl;
	}
	//	3. 监听网络端口 listen

	if (listen(_sock, 5) == SOCKET_ERROR)//第二个参数 backbag 最大允许连接数量
	{
		cout << "ERROR: 监听用于接受客户端连接的网络端口失败..." << endl;
	}
	else
	{
		cout << "SUCCESS: 监听端口成功..." << endl;
	}
	
	while (true)
	{
		fd_set fdRead;
		fd_set fdWrite;
		fd_set fdExpect;

		FD_ZERO(&fdRead);		//清空fd集合的数据
		FD_ZERO(&fdWrite);
		FD_ZERO(&fdExpect);
		//这个宏的功能是 将服务端的_sock 放到fdRead这个集合中 
		//当socket在listen状态,如果已经接收一个连接请求,这个socket会被标记为可读,例如一个accept会确保不会阻塞的完成
		//对于其他的socket,可读性意味着队列中的数据适合读,当调用recv后不会阻塞。
		FD_SET(_sock, &fdRead);  //将服务端的socket放入可读列表,确保accept不阻塞
		FD_SET(_sock, &fdWrite);
		FD_SET(_sock, &fdExpect);

		for (size_t n = 0; n < g_clinets.size(); n++)
		{
			FD_SET(g_clinets[n], &fdRead);		//所有连入的客户端放入可读列表 保证recv不阻塞
		}

		//nfds第一个参数 是一个整数值 是指fd_set集合中所有socket值的范围 不是数量    。。
		int ret = select(_sock + 1, &fdRead, &fdWrite, &fdExpect, NULL);
		if (ret < 0)
		{
			cout << "select任务结束" << endl;
			break;
		}
		if (FD_ISSET(_sock, &fdRead))	//判断_sock是否在fdRead中
		{
			FD_CLR(_sock, &fdRead);
			//	4. 等待接受客户端连接 accept
			sockaddr_in _clientAddr = {};
			int cliendAddrLen = sizeof(_clientAddr);
			SOCKET _clientSock = INVALID_SOCKET; // 初始化无效的socket 用来存储接入的客户端

			_clientSock = accept(_sock, (sockaddr*)&_clientAddr, &cliendAddrLen);//当客户端接入时 会得到连入客户端的socket地址和长度
			if (INVALID_SOCKET == _clientSock) //接受到无效接入
			{
				cout << "ERROR: 接受到无效客户端SOCKET..." << endl;
			}
			else
			{
				cout << "新Client加入:" << "socket = " << _clientSock << " IP = " << inet_ntoa(_clientAddr.sin_addr) << endl;  //inet_ntoa 将ip地址转换成可读的字符串
			}
			for (int n = g_clinets.size() - 1; n >= 0; n--)
			{
				NewUserJoin userJoin;
				userJoin.cmd = CMD_NEWUSERJOIN;
				userJoin.sockId = _clientSock;
				send(g_clinets[n], (const char*)&userJoin, userJoin.dataLength, 0);
			}


			g_clinets.push_back(_clientSock);


		}

		for (size_t n = 0; n < fdRead.fd_count; n++)
		{
			if (processor(fdRead.fd_array[n]) == -1)//processor函数是处理命令的逻辑 recv接到的数据并做出相应的判断和输出日志
			{
				auto it = find(g_clinets.begin(), g_clinets.end(), fdRead.fd_array[n]);
				if (it != g_clinets.end())
					g_clinets.erase(it);
			}
		}
	}



	//	6. 关闭socket
	closesocket(_sock);
	// 清除Windows socket环境
	WSACleanup();
	return 0;
}

 

1.32版本 将客户端引入多线程处理输入输出

客户端 添加

加入头

 

编写子线程需要执行的程序 

 

 

用户端修改后的代码

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<Windows.h>
#include<WinSock2.h>
#include<iostream>
#include<string>
#include <thread>
using namespace std;
#pragma comment(lib,"ws2_32.lib") //windows socket2 32的lib库


enum CMD
{
	CMD_LOGIN,
	CMD_LOGIN_RESULT,
	CMD_LOGINOUT,
	CMD_LOGOUT_RESULT,
	CMD_ERROR,
	CMD_NEWUSERJOIN,
};




//消息头
struct DataHeader
{
	short dataLength;    //数据长度 32767字节
	short cmd;
};

struct Login : public DataHeader
{
	Login()
	{
		dataLength = sizeof(Login);
		cmd = CMD_LOGIN;
	}
	char userName[32];
	char Password[32];
};
struct Logout :public DataHeader
{
	Logout()
	{
		dataLength = sizeof(Logout);
		cmd = CMD_LOGINOUT;
	}
	char userName[32];
};
struct LoginResult :public DataHeader
{
	LoginResult()
	{
		dataLength = sizeof(LoginResult);
		cmd = CMD_LOGIN_RESULT;
	}
	int result;
};
struct LogoutResult :public DataHeader
{
	LogoutResult()
	{
		dataLength = sizeof(LogoutResult);
		cmd = CMD_LOGOUT_RESULT;
	}
	int sockId;
	int result;
};

struct NewUserJoin :public DataHeader
{
	NewUserJoin()
	{
		dataLength = sizeof(LogoutResult);
		cmd = CMD_NEWUSERJOIN;
		result = 0;
	}
	int sockId;
	int result;
};

int processor(SOCKET _sock)
{
	char* szRecv = new char[1024];
	//5 首先接收数据包头
	int nlen = recv(_sock, szRecv, sizeof(DataHeader), 0); //接受客户端的数据 第一个参数应该是客户端的socket对象
	if (nlen <= 0)
	{
		//客户端退出
		cout << "客户端:Socket = " << _sock << " 与服务器断开连接,任务结束" << endl;
		return -1;
	}
	DataHeader* header = (DataHeader*)szRecv;
	switch (header->cmd)
	{
	case CMD_NEWUSERJOIN:
	{
		NewUserJoin _userJoin;
		recv(_sock, (char*)&_userJoin + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
		cout << "收到服务器消息: CMD_NEWUSERJOIN:" << _userJoin.sockId << endl;
	}break;
	case CMD_LOGIN_RESULT:
	{
		LoginResult _lgRes;
		recv(_sock, (char*)&_lgRes + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
		cout << "收到服务器消息: CMD_LOGIN_RESULT:" << _lgRes.result << endl;
	}break;
	case CMD_LOGOUT_RESULT:
	{
		LogoutResult _lgRes;
		recv(_sock, (char*)&_lgRes + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
		cout << "收到服务器消息: CMD_LOGIN_RESULT:" << _lgRes.result << endl;
	}break;
	default:
	{
		header->cmd = CMD_ERROR;
		header->dataLength = 0;
		send(_sock, (char*)&header, sizeof(DataHeader), 0);
	}
	break;
	}
	return 0;
}

bool g_bRun = true;

void cmdThread(SOCKET _sock)
{
	while (true)
	{
		// 3 输入请求命令
		char cmdBuf[128] = {};
		cout << "输入命令: ";
		cin >> cmdBuf;
		// 4 处理请求
		if (strcmp(cmdBuf, "exit") == 0)
		{
			cout << "退出cmdThread线程" << endl;
			g_bRun = false;
			return;
		}
		else if (0 == strcmp(cmdBuf, "login"))
		{
			Login _login;
			strcpy_s(_login.userName, "Evila");
			strcpy_s(_login.Password, "Evila_Password");
			// 5 向服务器发送请求命令
			send(_sock, (const char*)&_login, _login.dataLength, 0);
		}
		else if (0 == strcmp(cmdBuf, "logout"))
		{
			Logout _logout;
			strcpy_s(_logout.userName, "Evila");
			//5 向服务器发送请求命令
			send(_sock, (const char*)&_logout, _logout.dataLength, 0);
		}
		else
		{
			cout << "不受支持的命令" << endl;
		}
	}
	return;
}


int main()
{
	//启动 windows socket 2.x 环境
	WORD versionCode = MAKEWORD(2, 2);	//创建一个版本号 
	WSADATA data;
	WSAStartup(versionCode, &data);  //启动Socket网络API的函数
									 ///


	//(1) 用Socket API建立简易的TCP客户端

	//	1. 建立一个Socket  下面第三个参数不需要指定
	SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0); // ipv4 面向字节流的 tcp协议
	if (INVALID_SOCKET == _sock)
	{
		cout << "错误,建立socket失败"<<endl;
	}
	else
	{
		cout << "成功建立客户端socket"<<endl;
	}
	//	2. 连接服务器 connect
	sockaddr_in _sin = {};
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567); //端口号 host to net  sockaddr_in中port是USHORT类型
								 //网络中port是 unsigend short类型 因此需要Htons进行转换
	//_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  //服务器绑定的IP地址  127.0.0.1是本地地址
	_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); 

	int ret = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));
	if (SOCKET_ERROR == ret)
	{
		cout << "错误,connect失败" << endl;
	}
	else
	{
		cout << "成功,connect 成功" << endl;
	}

	//3 接收服务器数据 resv
	char recvBuf[256] = {};

	//启动线程
	std::thread t1(cmdThread, _sock);
	t1.detach();


	while (g_bRun)
	{
		fd_set fdReads;
		FD_ZERO(&fdReads);
		FD_SET(_sock, &fdReads);  
		timeval t = { 1,0 };
			int ret = select(_sock + 1, &fdReads, NULL, NULL, &t);
		if (ret < 0)
		{
			cout << "select任务结束" << endl;
			break;
		}
		if (FD_ISSET(_sock, &fdReads))	//如果_sock在fdRead里面,表明有需求等待处理
		{
			FD_CLR(_sock, &fdReads);
			if (processor(_sock) == -1)
			{
				cout << "Select任务已结束2" << endl;
				break;
			}
		}
	}




	//	6. 关闭socket
	closesocket(_sock);
	// 清除Windows socket环境
	WSACleanup();
	getchar();//防止一闪而过
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贪睡的蜗牛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值