项目(百万并发网络通信架构)3---将服务端、客户端升级为select模型

一、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;
}

四、演示效果

  • 开启了一个服务端,和三个客户端

五、附加

  • 在本篇文章中,我们使用代码为服务端发送一条数据,而没有手动从命令行进行输入
  • 在下一篇文章中,我们为客户端设计一个输入线程,专门用来输入发送数据,参阅下一篇文章:https://blog.csdn.net/qq_41453285/article/details/105312935
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是一个基于Mmonibuca v4的代码示例,用于将GB28181视频流转换为WebSocket-FLV格式并发送到客户端: ```go package main import ( "fmt" "github.com/Monibuca/engine/v4" "github.com/Monibuca/plugin-gb28181" "github.com/Monibuca/plugin-pusher" "github.com/Monibuca/utils/v4" "github.com/gorilla/websocket" "net/http" "time" ) func main() { // 创建一个Monibuca实例 engine := engine.Default() if err := engine.ConfigFromFile("./config.toml"); err != nil { panic(err) } // 注册GB28181插件,用于接收GB28181视频流 engine.InstallPlugin(&gb28181.GB28181{}) // 注册Pusher插件,用于将WebSocket-FLV格式的视频流发送到客户端 engine.InstallPlugin(&pusher.Pusher{}) // 创建WebSocket服务器 upgrader := websocket.Upgrader{ // 允许所有的来源连接 CheckOrigin: func(r *http.Request) bool { return true }, } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { fmt.Println("Upgrade:", err) return } // 创建一个Pusher实例 pusher := pusher.NewPusher(conn, "flv") // 将Pusher实例注册到Monibuca实例中 engine.AddProc(pusher) // 监听GB28181视频流的变化,并将转换后的WebSocket-FLV格式的视频流发送到客户端 go func() { // 从GB28181插件中获取视频流 stream := <-engine.Plugins["GB28181"].(gb28181.GB28181).Streams fmt.Println("stream:", stream) // 将视频流转换为WebSocket-FLV格式的视频流 wsflv := utils.NewWebSocketFLV(stream) for { // 发送WebSocket-FLV格式的视频流到客户端 pusher.WritePacket(wsflv.ReadPacket()) time.Sleep(time.Millisecond * 40) } }() }) // 启动Monibuca实例 if err := engine.Start(); err != nil { panic(err) } // 启动WebSocket服务器 if err := http.ListenAndServe(":8080", nil); err != nil { panic(err) } } ``` 在此示例中,我们使用了Mmonibuca v4来创建一个WebSocket服务器,并使用GB28181插件来接收GB28181视频流。我们还使用了Pusher插件来将WebSocket-FLV格式的视频流发送到客户端。 在客户端连接到WebSocket服务器时,我们创建了一个Pusher实例,并将其注册到Monibuca实例中。然后,我们监听GB28181视频流的变化,并将其转换为WebSocket-FLV格式的视频流。最后,我们将WebSocket-FLV格式的视频流发送到客户端。 请注意,此代码示例仅供参考。您需要根据您的具体情况进行调整和修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

董哥的黑板报

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

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

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

打赏作者

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

抵扣说明:

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

余额充值