WebSocket协议详解与c++&c#实现
摘要:
随着手机游戏、H5游戏以及微信小游戏的普及,越来越多的客户端-服务器端的通讯采用websocket协议。Websocket协议是全双工的、基于数据帧的、建立在tcp之上的长连接协议。Websocket的协议是头是字符串的兼容http的,而握手之后的数据帧则是紧凑的二进制,所以websocket是紧凑和高效的。现在主流的PC浏览器以及手机浏览器对websocket都实现了非常成熟的支持。Websocket协议有着统一的标准的,所有websocket通讯无论实现的语言如何,无论使用的终端如何,最终都是一致的。
Websocket的有点有:
- Websocket有公共的标准,有很多公共的库可以使用,比如web端,各个浏览器都已原生的支持websocket,所以拿来即用,非常的方便。比如cocos2dx就继承了websocket。
- 比如游戏使用了websocket,那么就可以非常容易的用web调用js发websocket消息,从而模拟客户端的操作。
- Websocket相对于http是长连接的,这样就可以实现实时的推送消息。
- Websocket既能支持文本格式也可以支持二进制格式,这样无论是js还是c++,都可以适当的选择自己喜欢的数据格式。
Websocket可以说完全治好了大家关于长连接使用什么协议的纠结。再游戏行业,服务器一般都是使用C++专门开发的网络程序,常规的一般都是使用比较传统的二进制协议,现在想用websocket的人越来越多,但是可以用于服务器端的websocket库却很少,要不就是库太重量级依赖了太多不需要的模块要不就是绑定了特定的网络接口实现,github上搜了下还websocket库很少。下面介绍一下我的通用websocket解析库,具有如下特点。
- 轻量,只封装websocket的解析,不依赖任何网络接口,拿来即用。
- 逻辑清晰,你可以直接看代码,直接能够理解websocket的协议。
- One header file only。全部实现就在一个头文件里,集成不能再容易了。
- 目前提供C++和c#的实现。别的语言我就没空写了,刚兴趣的可以照猫画虎来一个。
Websocket消息头:
模拟发送websocket非常的容易,我们写一个很简单的html+js就可以实现,当然你可以直接使用我的这个模拟客户端: https://fanchy.github.io/client.html。比如我们输入ip为127.0.0.1端口44000,将会受到这样的文本协议。
GET /chat HTTP/1.1
Host: 127.0.0.1:44000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36
Upgrade: websocket
Origin: https://fanchy.github.io
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Sec-WebSocket-Key: 8SIMf+o8pqn1RCe/ivxtPg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
关键参数有:
- Get /chat 这个是客户端指定的目录,我们做游戏服务器的,基本上根据目录区分服务器,只根据端口区分服务器,所以这个参数实际上可以忽略。
- Upgrade: websocket 这个必须有,这个是兼容http的需要,有这个字段说明这个不是普通的http是一个websocket的连接。
- Sec-WebSocket-Version版本号,可以忽略。
- Sec-WebSocket-Key这个是用作握手的key,具体使用见下文。
Websocket协议的验证
我们游戏服务器可能使用多种协议,比如同时兼容二进制协议和websocket协议。因为有websocket一定是GET开头的,所以我们可以通过验证第一个消息是不是带GET字符串从而判断对方连接是websocket连接还是普通连接。示例代码:
if (statusWebSocketConnection == -1)
{
return false;
}
cacheRecvData.append(buff, len);
if (dictParams.empty() == true)
{
std::string& strRecvData = cacheRecvData;
if (strRecvData.size() >= 3)
{
if (strRecvData.find("GET") == std::string::npos)
{
statusWebSocketConnection = -1;
return false;
}
}
else if (strRecvData.size() >= 2)
{
if (strRecvData.find("GE") == std::string::npos)
{
statusWebSocketConnection = -1;
return false;
}
}
else
{
if (strRecvData.find("G") == std::string::npos)
{
statusWebSocketConnection = -1;
return false;
}
}
statusWebSocketConnection = 1;
if (strRecvData.find("\r\n\r\n") == std::string::npos)//!header data not end
{
return true;
}
if (strRecvData.find("Upgrade: websocket") == std::string::npos)
{
statusWebSocketConnection = -1;
return false;
}
std::vector<std::string> strLines;
strSplit(strRecvData, strLines, "\r\n");
for (size_t i = 0; i < strLines.size(); ++i)
{
const std::string& line = strLines[i];
std::vector<std::string> strParams;
strSplit(line, strParams, ": ");
if (strParams.size() == 2)
{
dictParams[strParams[0]] = strParams[1];
}
else if (strParams.size() == 1 && strParams[0].find("GET") != std::string::npos)
{
dictParams["PATH"] = strParams[0];
}
}
Websocket的握手
Websocket因为要兼容http,所以会发一个常规的http的协议头,然后进行一次握手从而建立安全连接。Websocket握手的时候也就是建立连接后第一个消息会包含Sec-WebSocket-Key这个字段,服务器接收到这个字段后追加一个固定的guid值"258EAFA5-E914-47DA-95CA-C5AB0DC85B11",然后做sha1加密并转base64变成可见字符返回给客户端。
if (dictParams.find("Sec-WebSocket-Key") != dictParams.end())
{
const std::string& Sec_WebSocket_Key = dictParams["Sec-WebSocket-Key"];
std::string strGUID = Sec_WebSocket_Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
std::string dataHashed = sha1Encode(strGUID);
std::string strHashBase64 = base64Encode(dataHashed.c_str(), dataHashed.length(), false);
char buff[512] = {0};
snprintf(buff, sizeof(buff), "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n", strHashBase64.c_str());
addSendPkg(buff);
}
组装成websocket协议头如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: mzjDI+C9Ekz6tc/f5gWv38L5Hu0=
客户端收到服务器的这个应答消息后,握手完成,连接建立完成,开始数据传输。
数据帧
与tcp的流式数据不同,与http相似,websocket使用帧的方式传输数据,这样解包实际上是方便的,根据长度解析消息包这个最清晰了。
ABNF如下图所示:
- FIN:1 bit,如果不是分片,这个就是1,如果是分片,并且不是最后一个片,那么就是0
- RSV1, RSV2, RSV3: 每个1 bit,简单说用不到
- Opcode: 4 bits, 0,1, 2都代表数据,8代表关闭连接,0X9为ping,0XA为pong其他用不到。
- Mask: 1 bit 这个客户端必须是1.
- Payload length: 7 bits, 7+16 bits, 或者 7+64 bits,,如果是小于126就用一个字节表示数据长度,如果等于126,表示后续2字节表示长度,如果是127后续8字节表示长度。
- Masking-key: 0 or 4 bytes 客户端发送的必须有掩码
- Payload data不出意外剩下的就是数据了。
int nFIN = ((cacheRecvData[0] & 0x80) == 0x80)? 1: 0;
int nOpcode = cacheRecvData[0] & 0x0F;
//int nMask = ((cacheRecvData[1] & 0x80) == 0x80) ? 1 : 0; //!this must be 1
int nPayload_length = cacheRecvData[1] & 0x7F;
int nPlayLoadLenByteNum = 1;
if (nPayload_length == 126)
{
nPlayLoadLenByteNum = 3;
}
int nMaskingKeyByteNum = 4;
std::string aMasking_key;
aMasking_key.assign(cacheRecvData.c_str() + 1 + nPlayLoadLenByteNum, nMaskingKeyByteNum);
std::string aPayload_data;
aPayload_data.assign(cacheRecvData.c_str() + 1 + nPlayLoadLenByteNum + nMaskingKeyByteNum, nPayload_length);
int nLeftSize = cacheRecvData.size() - (1 + nPlayLoadLenByteNum + nMaskingKeyByteNum + nPayload_length);
if (nLeftSize > 0)
{
std::string leftBytes;
leftBytes.assign(cacheRecvData.c_str() + 1 + nPlayLoadLenByteNum + nMaskingKeyByteNum + nPayload_length, nLeftSize);
cacheRecvData = leftBytes;
}
for (int i = 0; i < nPayload_length; i++)
{
aPayload_data[i] = (char)(aPayload_data[i] ^ aMasking_key[i % nMaskingKeyByteNum]);
}
if (8 == nOpcode)
{
addSendPkg(buildPkg("", nOpcode));// close
bIsClose = true;
}
else if (2 == nOpcode || 1 == nOpcode || 0 == nOpcode || 9 == nOpcode)
{
if (9 == nOpcode)//!ping
{
addSendPkg(buildPkg("", 0xA));// pong
}
if (nFIN == 1)
{
if (dataFragmentation.size() == 0)
{
addRecvPkg(aPayload_data);
}
else
{
dataFragmentation += aPayload_data;
addRecvPkg(dataFragmentation);
dataFragmentation.clear();
}
}
else
{
dataFragmentation += aPayload_data;
}
}
Ping/pong/close
收到ping就发pong,有可能ping的时候也带着数据,所以要处理下。但是貌似Chrome很长时间不会自动发ping。
服务器收到close消息可以回一个消息应答一下,也可以直接关闭连接。
集成到网络层
在自己的socket里加一个WSProtocol对象,在收到消息的地方一般是HandleRecv函数里加一段WSProtocol判断和处理的代码就可以了,示例如下:
if (m_oWSProtocol.handleRecv(buff, len))
{
const vector<string>& waitToSend = m_oWSProtocol.getSendPkg();
for (size_t i = 0; i < waitToSend.size(); ++i)
{
sp_->sendRaw(waitToSend[i]);
}
m_oWSProtocol.clearSendPkg();
const vector<string>& recvPkg = m_oWSProtocol.getRecvPkg();
for (size_t i = 0; i < recvPkg.size(); ++i)
{
const string& eachRecvPkg = recvPkg[i];
uint16_t nCmd = 0;
m_message.getHead().cmd = nCmd;
m_message.appendToBody(eachRecvPkg.c_str(), eachRecvPkg.size());
m_message.getHead().size = eachRecvPkg.size();
this->post_msg(sp_);
m_message.clear();
}
m_oWSProtocol.clearRecvPkg();
if (m_oWSProtocol.isClose())
{
sp_->close();
}
return 0;
}
总结:
- 本文实现的websocket为纯协议解析,不依赖网络层,这样想用老的网络层支持websocket就非常容易啦。
- 本实现就一个头文件,依赖OpenSSL(sha1加密)
- Github地址 https://github.com/fanchy/h2engine/tree/master/fflib/net/wsprotocol.h
- 同时提供一个c#的版本https://github.com/fanchy/h2engine/blob/master/workercs/fflib/wsprotocol.cs
- 归属于项目:h2engine 地址:https://github.com/fanchy/h2engine,感兴趣的可以star。
- 这是我的github主页https://github.com/fanchy,有些有意思的分享。
WebSocket协议详解与c++&c#实现的更多相关文章
- WebSocket协议详解及应用
WebSocket协议详解及应用(七)-WebSocket协议关闭帧 本篇介绍WebSocket协议的关闭帧,包括客户端及服务器如何发送并处理关闭帧.关闭帧错误码及错误处理方法.本篇内容主要翻译自RF ...
- WebSocket协议详解
转自 http://www.cnblogs.com/lizhenghn/p/5155933.html 1. websocket 是什么 websocket 是html5提出的一个协议规范,参考rfc6 ...
- websocket协议详解;
websocket是基于http协议,借用http协议来完成连接阶段的握手: 当连接建立后,浏览器和服务器之间的通信就和http协议没有关系了,b.s之间只用websocket协议来完成基本通信. = ...
- WebSocket 协议详解
一.WebSocket 协议背景 早期,在网站上推送消息给用户,只能通过轮询的方式或 Comet 技术.轮询就是浏览器每隔几秒钟向服务端发送 HTTP 请求,然后服务端返回消息给客户端. 轮询技术一般 ...
- HTTP协议详解(转)
转自:http://blog.csdn.net/gueter/archive/2007/03/08/1524447.aspx Author :Jeffrey 引言 HTTP是一个属于应用层的面向对象的 ...
- HTTP协议详解
Author :Jeffrey 引言 HTTP 是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和 扩展. ...
- 动态选路、RIP协议&&OSPF协议详解
动态选路.RIP协议&&OSPF协议详解 概念 当相邻路由器之间进行通信,以告知对方每个路由器当前所连接的网络,这时就出现了动态选路.路由器之间必须采用选路协议进行通信,这样的选路协议 ...
- ASP.NET知识总结(3.HTTP协议详解)
引言 HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展.目前在WWW中使用的是HTTP/1. ...
- 接口测试之HTTP协议详解
引言 HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展.目前在WWW中使用的是HTTP/1. ...
随机推荐
- python笔记:#001#python简介
认识 Python 人生苦短,我用 Python -- Life is short, you need Python 目标 Python 的起源 为什么要用 Python? Python 的特点 Py ...
- Golang 交叉编译 window/linux 文件
gox - 一款简单的交叉编译工具 下载地址:https://github.com/mitchellh/gox 使用 go get 命令安装: go get github.com/mitchellh/ ...
- 单片机开发——03工欲善其事必先利其器(AD软件安装破解)
在单片机开发中,有了Keil的程序编程,Protues的模拟仿真,那么问题来了,怎么去进行电路图设计以及硬件调试呢?此刻就必须引入本文的Altium Designer (下文简称AD)安装与破解.在硬 ...
- Spring Boot 发送邮件
需求 最近因为业务的变更,需要对老用户进行发送邮件处理.目前市面上也有很多代发邮件的接口,可以接入.由于量不是特别大,放弃了这个途径.改用我们自己通过 smtp 发送邮件来处理. 技术选择 Java ...
- vue国际化高逼格多语言
## 1.NPM 项目安装 ``` cnpm i vue-i18n ``` ## 2.使用方法 ``` /* 国际化使用规则 */ import Vue from 'vue' import VueI1 ...
- day12 EL 表达式和国际化开发
day12 EL 表达式和国际化开发 1. EL(Expression Language) 表达式简介 1.1 执行运算 1.2 获取web开发常用对象(el 中定义了11个隐式对象) 1.3 使用 ...
- linux设备驱动之字符设备驱动模型(1)
一:字符设备驱动 在linux下面,应用层看到的一切皆为文件(名字)所有的设备都是文件,都可以调用open,read,write来操作,而在内核中每个中每个设备有唯一的对应一个设备号: APP ( ...
- float之脱离文档流
所谓的文档流:指的是元素在排版过程中,元素自动从左到右,从上到下的顺序排列. 脱离文档流:也就是将元素从普通的布局排版中拿走,其他盒子在定位的时候,会当做脱离文档流的元素不存在而进行定位 只有绝对定位 ...
- Mysql 30条军规
一.基础规范 (1)必须使用InnoDB存储引擎 解读:支持事务.行级锁.并发性能更好.CPU及内存缓存页优化使得资源利用率更高 (2)必须使用UTF8字符集 解读:万国码,无需转码,无乱码风险,节省 ...
- Linux kernel的中断子系统之(九):tasklet
返回目录:<ARM-Linux中断系统>. 总结: 二介绍了tasklet存在的意义. 三介绍了通过tasklet_struct来抽想一个tasklet,每个CPU维护一个tasklet链 ...