C++实现WebSocket简单服务器

参考链接:
链接1
链接2
链接3

WebSocket简介

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

WebSocket 与 HTTP/2 一样,其实都是为了解决 HTTP/1.1 的一些缺陷而诞生的,而 WebSocket 针对的就是「请求-应答」这种"半双工"的模式的通信缺陷。

「请求-应答」是"半双工"的通信模式,数据的传输必须经过一次请求应答,这个完整的通信过程,通信的同一时刻数据只能在一个方向上传递。它最大的问题在于,HTTP 是一种被动的通信模式,服务端必须等待客户端请求才可以返回数据,无法主动向客户端发送数据。

这也导致在 WebSocket 出现之前,一些对实时性有要求的服务,通常是基于轮询(Polling)这种简单的模式来实现。轮询就是由客户端定时发起请求,如果服务端有需要传递的数据,可以借助这个请求去响应数据。轮询的缺点也非常明显,大量空闲的时间,其实是在反复发送无效的请求,这显然是一种资源的损耗。

创建WebSocket服务器的一般步骤

  1. 创建一个服务器监听
  2. 开始接收数据,此时开始接收的数据主要是客户端发出的协议握手报文,报文内容大概如下:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

其中

  • | Sec-WebSocket-Key |, 必传, 由客户端随机生成的 16 字节值, 然后做 base64 编码, 客户端需要保证该值是足够随机, 不可被预测的 (换句话说, 客户端应使用熵足够大的随机数发生器), 在 WebSocket 协议中, 该头部字段必传, 若客户端发起握手时缺失该字段, 则无法完成握手
  • | Sec-WebSocket-Version |, 必传, 指示 WebSocket 协议的版本, RFC 6455 的协议版本为 13, 在 RFC 6455 的 Draft 阶段已经有针对相应的 WebSocket 实现, 它们当时使用更低的版本号, 若客户端同时支持多个 WebSocket 协议版本, 可以在该字段中以逗号分隔传递支持的版本列表 (按期望使用的程序降序排列), 服务端可从中选取一个支持的协议版本
  • | Sec-WebSocket-Protocol |, 可选, 客户端发起握手的时候可以在头部设置该字段, 该字段的值是一系列客户端希望在于服务端交互时使用的子协议 (subprotocol), 多个子协议之间用逗号分隔, 按客户端期望的顺序降序排列, 服务端可以根据客户端提供的子协议列表选择一个或多个子协议
  • | Sec-WebSocket-Extensions |, 可选, 客户端在 WebSocket 握手阶段可以在头部设置该字段指示自己希望使用的 WebSocket 协议拓展
  1. 根据接收到的数据(就是上面的报文),可以先判断一下是不是握手协议(这里可以直接判断一下recv从缓冲区读到的数据是否包含“GET”),对这个报文进行解析,获取其中的Sec-WebSocket-Key(也就是这里的dGhlIHNhbXBsZSBub25jZQ==)。然后服务端要对这个key(包含24个字符)进行解析,解析完成后会得到一个密码,如果服务器发给客户端的报文密码一致的话,此时就完成了协议握手,然后现在就已经成功创建了一个基于webosket协议的连接了。
    当握手成功后传入的报文信息如下:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13

  • | Sec-WebSocket-Accept |, 必传, 客户端发起握手时通过 | Sec-WebSocket-Key | 字段传递了一个将随机生成的 16 字节做 base64 编码后的字符串, 服务端若接收握手, 则应将该值与 WebSocket 魔数 (Magic Number) “258EAFA5-E914-47DA- 95CA-C5AB0DC85B11” 进行字符串连接, 将得到的字符串做 SHA-1 哈希, 将得到的哈希值再做 base64 编码, 最终的值便是该字段的值, 举例来说, 假设客户端传递的 Sec-WebSocket-Key 为 “dGhlIHNhbXBsZSBub25jZQ==”, 服务端应首先将该字符串与 WebSocket 魔数进行字符串拼接, 得到 “dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA- C5AB0DC85B11”, 然后对该字符串做 SHA-1 哈希运算得到哈希值 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea, 然后对该哈希值做 base64 编码, 最终得到 Sec-WebSocket-Accept 的值为 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=, 当客户端收到服务端的握手响应后, 会做同样的运算来校验该值是否符合预期, 以便于判断服务端是否真的支持 WebSocket 协议, 设置这个环节的目的就是为了最终校验服务端对 WebSocket 协议的支持性, 因为单纯使用 Upgrade 机制, 对于一些没有正确实现 HTTP Upgrade 机制的 Web Server, 可能也会返回预期的 Upgrade, 但实际上它并不支持 WebSocket, 而引入 WebSocket 魔数并进行这一系列操作后可以很大程度上确定服务端确实支持 WebSocket 协议
  1. 最后,如果握手完成,服务端和客户端之间就可以传输数据了。此时传入的数据就是用WebSocket协议封装好的数据。

WebSocket协议解析

下面就来讲下WebSocket协议,协议格式如下:
在这里插入图片描述

第一个字节:
在这里插入图片描述
FIN:1位,用于描述消息是否结束,如果为1则该消息为消息尾部,如果为零则还有后续数据包;
RSV1,RSV2,RSV3:各1位,用于扩展定义的,如果没有扩展约定的情况则必须为0
OPCODE:4位,用于表示消息接收类型,如果接收到未知的opcode,接收端必须关闭连接。长连接探活包就是这里标识的。

OPCODE定义的范围:
  0x0表示附加数据帧
  0x1表示文本数据帧
  0x2表示二进制数据帧
  0x3-7暂时无定义,为以后的非控制帧保留
  0x8表示连接关闭
  0x9表示ping
  0xA表示pong
  0xB-F暂时无定义,为以后的控制帧保留

第二个字节和以后的字节:
第二个字节:
在这里插入图片描述
其他字节:
在这里插入图片描述
MASK:1位,用于标识PayloadData是否经过掩码处理,客户端发出的数据帧需要进行掩码处理,所以此位是1,数据需要解码。当是服务端发给客户端的代码时,此位为0,数据不需解码。

Payload length === x,

  • 如果 x值在0-125,则是payload的真实长度,即数据的真实长度为x。那么当MASK位为0时,从第3个字节开始全是数据信息。当MASK位为1时,从第7个字节开始全是数据信息(Maskinf-key占4个字节)。

  • 如果 x值是126,则后面2个字节形成的16位无符号整型数的值是payload的真实长度。即当MASK位为0时,第2个字节数为0x7e(0111 1110(126)),第3到4字节为实际的数据长度,第5个字节后就为数据信息了。当MASK位为1时,第2个字节数为0xfe(第一个比特为1,后7位为111 1110(126))。
    在这里插入图片描述

  • 如果 x值是127,则后面8个字节形成的64位无符号整型数的值是payload的真实长度。
    在这里插入图片描述
    此外,如果payload length占用了多个字节的话,payload length的二进制表达采用网络序(big endian,重要的位在前)。

数据解码

此处主要处理接收客户端传入的信息,数据已经经过掩码处理,代码如下:

int Websocket_Codetool::wsDecodeFrame(const char* frameData, int len, char* outMessage)
{
	int ret = WS_OPENING_FRAME;

	const int frameLength = len;
	if (frameLength < 2)
	{
		ret = WS_ERROR_FRAME;
	}

	// 检查扩展位并忽略
	if ((frameData[0] & 0x70) != 0x0)
	{
		ret = WS_ERROR_FRAME;
	}

	// fin位: 为1表示已接收完整报文, 为0表示继续监听后续报文
	ret = (frameData[0] & 0x80);

	if ((frameData[0] & 0x80) != 0x80)
	{
		ret = WS_ERROR_FRAME;
	}

	// mask位, 为1表示数据被加密
	if ((frameData[1] & 0x80) != 0x80)
	{
		ret = WS_ERROR_FRAME;
	}

	// 操作码
	uint16_t payloadLength = 0;
	uint8_t payloadFieldExtraBytes = 0;
	uint8_t opcode = static_cast<uint8_t>(frameData[0] & 0x0f);

	//std::cout << "mask:" << ((frameData[1] & 0x80) != 0x80) << std::endl;
	//std::cout << "frameLength: " << frameLength << std::endl;
	//std::cout << "payloadLength: " << payloadLength << std::endl;
	//std::cout << "payloadFieldExtraBytes: " << payloadFieldExtraBytes << std::endl;
	//std::cout << "opcode: " << opcode << std::endl;


	if (opcode == WS_TEXT_FRAME)
	{
		// 处理utf-8编码的文本帧
		ret = WS_TEXT_FRAME;
		payloadLength = static_cast<uint16_t>(frameData[1] & 0x7f);
		if (payloadLength == 0x7e)
		{
			uint16_t payloadLength16b = 0;
			payloadFieldExtraBytes = 2;
			memcpy(&payloadLength16b, &frameData[2], payloadFieldExtraBytes);
			payloadLength = ntohs(payloadLength16b);
		}
		else if (payloadLength == 0x7f)
		{
			// 数据过长,暂不支持
			ret = WS_ERROR_FRAME;
		}
	}
	else if (opcode == WS_BINARY_FRAME || opcode == WS_PING_FRAME || opcode == WS_PONG_FRAME)
	{
		// 二进制/ping/pong帧暂不处理
	}
	else if (opcode == WS_CLOSING_FRAME)
	{
		ret = WS_CLOSING_FRAME;
	}
	else
	{
		ret = WS_ERROR_FRAME;
	}


	// 数据解码
	if ((ret != WS_ERROR_FRAME) && (payloadLength > 0))
	{
		// header: 2字节, masking key: 4字节
		const char *maskingKey = &frameData[2 + payloadFieldExtraBytes];
		char *payloadData = new char[payloadLength + 1];
		memset(payloadData, 0, payloadLength + 1);
		memcpy(payloadData, &frameData[2 + payloadFieldExtraBytes + 4], payloadLength);
		for (int i = 0; i < payloadLength; i++)
		{
			payloadData[i] = payloadData[i] ^ maskingKey[i % 4];
		}

		//outMessage = payloadData;
		int totLen = payloadLength;
		memcpy(outMessage, payloadData, totLen);
		outMessage[totLen] = 0x00;
		delete[] payloadData;
	}
	return ret;
}

数据编码

主要是对服务器传给客户端的数据加入协议头,不涉及掩码处理。

int Websocket_Codetool::wsEncodeFrame(const char * inMessage, int messageLen, char* outFrame, enum WS_FrameType frameType)
{
	int ret = WS_EMPTY_FRAME;
	const uint32_t messageLength = messageLen;

	if (messageLength > 32767)
	{
		// 暂不支持这么长的数据
		return WS_ERROR_FRAME;
	}

	uint8_t payloadFieldExtraBytes = (messageLength <= 0x7d) ? 0 : 2;
	// header: 2字节, mask位设置为0(不加密), 则后面的masking key无须填写, 省略4字节
	uint8_t frameHeaderSize = 2 + payloadFieldExtraBytes;
	uint8_t *frameHeader = new uint8_t[frameHeaderSize];
	memset(frameHeader, 0, frameHeaderSize);
	// fin位为1, 扩展位为0, 操作位为frameType
	frameHeader[0] = static_cast<uint8_t>(0x80 | frameType);

	// 填充数据长度
	if (messageLength <= 0x7d)
	{
		//头1个字节+ 1个字节长度 + 0x0000
		frameHeader[1] = static_cast<uint8_t>(messageLength);
	}
	else
	{
		//头1个字节+ 0x7e +2个字节长度
		frameHeader[1] = 0x7e;
		uint16_t len = htons(messageLength);
		memcpy(&frameHeader[2], &len, payloadFieldExtraBytes);
	}

	// 填充数据
	uint32_t frameSize = frameHeaderSize + messageLength;
	char *frame = new char[frameSize + 1];
	memcpy(frame, frameHeader, frameHeaderSize);
	memcpy(frame + frameHeaderSize, inMessage, messageLength);
	frame[frameSize] = '\0';

	//outFrame = frame;
	memcpy(outFrame, frame, frameSize);
	outFrame[frameSize] = 0x00;

	delete[] frame;
	delete[] frameHeader;
	return ret;
}
  • 28
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值