摘要:

随着手机游戏、H5游戏以及微信小游戏的普及,越来越多的客户端-服务器端的通讯采用websocket协议。Websocket协议是全双工的、基于数据帧的、建立在tcp之上的长连接协议。Websocket的协议是头是字符串的兼容http的,而握手之后的数据帧则是紧凑的二进制,所以websocket是紧凑和高效的。现在主流的PC浏览器以及手机浏览器对websocket都实现了非常成熟的支持。Websocket协议有着统一的标准的,所有websocket通讯无论实现的语言如何,无论使用的终端如何,最终都是一致的。

Websocket的有点有:

  1. Websocket有公共的标准,有很多公共的库可以使用,比如web端,各个浏览器都已原生的支持websocket,所以拿来即用,非常的方便。比如cocos2dx就继承了websocket。
  2. 比如游戏使用了websocket,那么就可以非常容易的用web调用js发websocket消息,从而模拟客户端的操作。
  3. Websocket相对于http是长连接的,这样就可以实现实时的推送消息。
  4. Websocket既能支持文本格式也可以支持二进制格式,这样无论是js还是c++,都可以适当的选择自己喜欢的数据格式。

Websocket可以说完全治好了大家关于长连接使用什么协议的纠结。再游戏行业,服务器一般都是使用C++专门开发的网络程序,常规的一般都是使用比较传统的二进制协议,现在想用websocket的人越来越多,但是可以用于服务器端的websocket库却很少,要不就是库太重量级依赖了太多不需要的模块要不就是绑定了特定的网络接口实现,github上搜了下还websocket库很少。下面介绍一下我的通用websocket解析库,具有如下特点。

  1. 轻量,只封装websocket的解析,不依赖任何网络接口,拿来即用。
  2. 逻辑清晰,你可以直接看代码,直接能够理解websocket的协议。
  3. One header file only。全部实现就在一个头文件里,集成不能再容易了。
  4. 目前提供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协议详解与c++&c#实现的更多相关文章

  1. WebSocket协议详解及应用

    WebSocket协议详解及应用(七)-WebSocket协议关闭帧 本篇介绍WebSocket协议的关闭帧,包括客户端及服务器如何发送并处理关闭帧.关闭帧错误码及错误处理方法.本篇内容主要翻译自RF ...

  2. WebSocket协议详解

    转自 http://www.cnblogs.com/lizhenghn/p/5155933.html 1. websocket 是什么 websocket 是html5提出的一个协议规范,参考rfc6 ...

  3. websocket协议详解;

    websocket是基于http协议,借用http协议来完成连接阶段的握手: 当连接建立后,浏览器和服务器之间的通信就和http协议没有关系了,b.s之间只用websocket协议来完成基本通信. = ...

  4. WebSocket 协议详解

    一.WebSocket 协议背景 早期,在网站上推送消息给用户,只能通过轮询的方式或 Comet 技术.轮询就是浏览器每隔几秒钟向服务端发送 HTTP 请求,然后服务端返回消息给客户端. 轮询技术一般 ...

  5. HTTP协议详解(转)

    转自:http://blog.csdn.net/gueter/archive/2007/03/08/1524447.aspx Author :Jeffrey 引言 HTTP是一个属于应用层的面向对象的 ...

  6. HTTP协议详解

    Author :Jeffrey 引言 HTTP 是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和 扩展. ...

  7. 动态选路、RIP协议&&OSPF协议详解

    动态选路.RIP协议&&OSPF协议详解 概念 当相邻路由器之间进行通信,以告知对方每个路由器当前所连接的网络,这时就出现了动态选路.路由器之间必须采用选路协议进行通信,这样的选路协议 ...

  8. ASP.NET知识总结(3.HTTP协议详解)

    引言 HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展.目前在WWW中使用的是HTTP/1. ...

  9. 接口测试之HTTP协议详解

    引言 HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展.目前在WWW中使用的是HTTP/1. ...

随机推荐

  1. Sass快速入门学习笔记

    1. 使用变量; sass让人们受益的一个重要特性就是它为css引入了变量.你可以把反复使用的css属性值 定义成变量,然后通过变量名来引用它们,而无需重复书写这一属性值.或者,对于仅使用过一 次的属 ...

  2. 算法训练 2的次幂表示(蓝桥杯C++写法)

    问题描述 任何一个正整数都可以用2进制表示,例如:137的2进制表示为10001001. 将这种2进制表示写成2的次幂的和的形式,令次幂高的排在前面,可得到如下表达式:137=2^7+2^3+2^0 ...

  3. Mac下MySQL无my-default.cnf

    转自https://www.jianshu.com/p/628bcf8bb557 As of MySQL 5.7.18, my-default.ini is no longer included in ...

  4. 读取本地outlook邮件内容

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  5. C++ 模板基础

    我们学习使用C++,肯定都要了解模板这个概念.就我自己的理解,模板其实就是为复用而生,模板就是实现代码复用机制的一种工具,它可以实现类型参数化,即把类型定义为参数:进而实现了真正的代码可重用性.模版可 ...

  6. ajax如何实现、readyState五中状态的含义

    转载:http://www.cnblogs.com/teroy/p/3917439.html 熟悉web开发的程序员想必对Ajax也不会陌生.现在已经有很多js框架封装了ajax实现,例如JQuery ...

  7. spring cloud中利用sidecar整合异构语言(转)

    用spring cloud sidecar的整合异构语言,以前做过没有做笔记,现在再做由于各种坑又浪费了一天,这里记一下 首先是官网:http://cloud.spring.io/spring-clo ...

  8. golang项目中使用条件编译

    golang项目中使用条件编译 C语言中的条件编译 golang中没有类似C语言中条件编译的写法,比如在C代码中可以使用如下语法做一些条件编译,结合宏定义来使用可以实现诸如按需编译release和de ...

  9. SSM-MyBatis-17:Mybatis中一级缓存(主要是一级缓存存在性的证明,增删改对一级缓存会造成什么影响)

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 缓存------------------------------------------> 很熟悉的一个 ...

  10. 关于TCP/IP,必知必会的十个经典问题[转]

    关于TCP/IP,必知必会的十个问题 原创 2018-01-25 Ruheng 技术特工队   本文整理了一些TCP/IP协议簇中需要必知必会的十大问题,既是面试高频问题,又是程序员必备基础素养. 一 ...