一、websocket协议原理(一)websocket 协议的官方文档 : https://tools./html/draft-ietf-hybi-thewebsocketprotocol-13#section-5 Linux 下c语言 实现 websocket 包含客户端 和服务器测试代码 : http://blog.csdn.net/sguniver_22/article/details/74273839 c语言实现websocket服务器: http://blog.csdn.net/lell3538/article/details/60470558 细说webosokcet-php篇 https://www.cnblogs.com/hustskyking/p/websocket-with-php.html (二)需要学习哪些东西? 1. 如何建立连接 2. 如何交换数据 3. 数据帧格式 4. 如何维持连接 websocket连接建立过程: websocket 复用了HTTP的握手通道。具体指的是,客户端HTTP请求与websocket 服务端协商升级协议。 1. client -> server 发送Sec-WebSocket-Key 2. server-> client 加密返回Sec-WebScoket-Accept 3 client -> server 本地校验 1. 客户端发起协议升级请求。 采用标准的HTTP报文格式,只支持 GET GET / HTTP/1.1 Host: localhost:8080 Origin: http://127.0.0.1:3000 Connection: Upgrade Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw== 2. 服务端相应协议升级 HTTP/1.1 101 Switching Protocols Connection:Upgrade Upgrade: websocket Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU= 注意每个header都以\r\n结尾,并且最后一行加上一个额外的空行\r\n Sec-WebSocket-Accept的计算 伪代码如下: >toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) ) (三) 数据帧格式
WebSocket客户端,服务端通信的最小单位是帧(frame),由一个或多个帧组成一条完整的消息(message) 1. 发送端: 将消息切割成多个帧,并发送给服务端; 2. 接收端: 接受消息帧,并将关联的帧重新组装成完整的消息; 数据帧格式详解: 第一个字节 FIN:1位,用于描述消息是否结束,如果为1则该消息为消息尾部,如果为零则还有后续数据包; uint8_t fin = (uint8_t)msg[pos] >> 7; RSV1,RSV2,RSV3,各1位,用于扩展定义的,如果没有扩展约定的情况则必须为0 OPCODE:4位,用于表示消息接收类型,如果接收到未知的opcode,接收端必须关闭连接。 uint8_t opcode = msg[pos] & 0x0f; 0x0表示附加数据帧 0x1表示文本数据帧 0x2表示二进制数据帧 0x3-7暂时无定义,为以后的非控制帧保留 0x8表示连接关闭 0x9表示ping 0xA表示pong 0xB-F暂时无定义,为以后的控制帧保留 第二个字节 MASK:1位,用于标识PayloadData是否经过掩码处理,客户端发出的数据帧需要进行掩码处理,所以此位是1。数据需要解码 uint8_t mask = (uint8_t)msg[pos] >> 7; Mask: 1个比特。 不需要对数据进行掩码操作。 从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时, 如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。 掩码算法: 首先,假设: original-octet-i:为原始数据的第i字节。 transformed-octet-i:为转换后的数据的第i字节。 j:为i mod 4的结果。 masking-key-octet-j:为mask key第j字节。 算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。 j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j PayloadData的长度:7位,7+16位,7+64位 如果其值在0-125,则是payload的真实长度。 如果值是126,则后面2个字节形成的16位无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。 如果值是127,则后面8个字节形成的64位无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。 长度表示遵循一个原则,用最少的字节表示长度(我理解是尽量减少不必要的传输)。举例说,payload真实长度是124,在0-125之间,必须用前7位表示;不允许长度1是126或127,然后长度2是124,这样违反原则。 uint64_t payload_length_ = msg[pos] & 0x7f;
pos++;
if(payload_length_ == 126){
uint16_t length = 0;
memcpy(&length, msg + pos, 2);
pos += 2;
payload_length_ = ntohs(length);
}
else if(payload_length_ == 127){
uint32_t length = 0;
memcpy(&length, msg + pos, 4);
pos += 4;
payload_length_ = ntohl(length);
}
后面的字节就是消息体,获取消息体内如如下: char payload_[2048]; memset(payload_, 0, sizeof(payload_));
if(mask_ != 1){
memcpy(payload_, msg + pos, payload_length_);
}
else {
for(uint i = 0; i < payload_length_; i++){
int j = i % 4;
payload_[i] = msg[pos + i] ^ masking_key_[j];
}
}
pos += payload_length_; (四)、数据传递 一旦WebSocket 客户端、服务端连接后,后续的操作都是基于数据帧的传递。 1、数据分片 第一条消息 FIN=1, 表示是当前消息的最后一个数据帧。服务端收到当前数据帧后,可以处理消息。opcode=0x1,表示客户端发送的是文本类型。 第二条消息 FIN=0,opcode=0x1,表示发送的是文本类型,且消息还没发送完成,还有后续的数据帧。 FIN=0,opcode=0x0,表示消息还没发送完成,还有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。 FIN=1,opcode=0x0,表示消息已经发送完成,没有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。服务端可以将关联的数据帧组装成完整的消息。 Client: FIN=1, opcode=0x1, msg="hello" Server: (process complete message immediately) Hi. Client: FIN=0, opcode=0x1, msg="and a" Server: (listening, new message containing text started) Client: FIN=0, opcode=0x0, msg="happy new" Server: (listening, payload concatenated to previous message) Client: FIN=1, opcode=0x0, msg="year!" Server: (process complete message) Happy new year to you too! (五)、 连接保持+心跳 WebSocket为了保持客户端,服务端的实时双向通信,需要确保客户端和服务端之间的TCP通道长期连接不断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费连接资源。 但是不排除有些场景,客户端和服务端虽然长时间没有数据往来,但仍需保持连接。这个时候可以采用心跳 实现。 发送方-> 接收方 :ping 接收方-> 发送方 : pong (六)、Sec-WebSocket-Key/Accept的作用? 2. 确保服务端理解websocket连接,因为握手阶段采用的是http协议,因此ws连接可能是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key来确保服务端认识ws协议。(并非百分百保险,比如总是存在那么些无聊的http服务器,光处理Sec-WebSocket-Key,但并没有实现ws协议。。。) 5. Sec-WebSocket-Key主要目的并不是保证数据的安全,因为Sec-WebSocket-Key, Sec-WebSocket-Accept的转换计算公式是公开的,而且很简单,主要作用是防止一些常见的以外情况(非故意的) (七),数据掩码的作用 安全。但并不是为了防止数据泄密,而是为了防止早期版本协议存在的代理缓存污染攻击(proxy cache poisoning attacks) 等问题。 (不甚理解) 下面给出一段 c++实现 websocket 服务端 ,作为研究包解析,数据解码,链接建立过程等,不能作为生产环境使用! 二、c++实现部分源码下面是主要代码。 #ifndef __WebSocketProtocol_H__ #define __WebSocketProtocol_H__
static CWebSocketProtocol * getInstance();
int getResponseHttp(string &request, string &response); int wsDecodeFrame(string inFrame, string &outMessage); //解码帧 int wsEncodeFrame(string inMessage, string &outFrame, enum WS_FrameType frameType); //编码帧打包
static CWebSocketProtocol * m_inst;
#include "WebSocketProtocol.h"
const char * MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; CWebSocketProtocol::CGrabo CWebSocketProtocol::m_grabo; CWebSocketProtocol * CWebSocketProtocol::m_inst = 0;
CWebSocketProtocol::CWebSocketProtocol()
CWebSocketProtocol::~CWebSocketProtocol()
CWebSocketProtocol * CWebSocketProtocol::getInstance() m_inst = new CWebSocketProtocol;
int CWebSocketProtocol::getResponseHttp(string &request, string &response) int ret = WS_STATUS_UNCONNECT; std::istringstream stream(request.c_str()); std::getline(stream, reqType); if (reqType.substr(0, 4) != "GET ")
std::string::size_type pos = 0; std::string websocketKey; while (std::getline(stream, header) && header != "\r") header.erase(header.end() - 1); pos = header.find(": ", 0); if (pos != std::string::npos) std::string key = header.substr(0, pos); std::string value = header.substr(pos + 2); if (key == "Sec-WebSocket-Key")
if (ret != WS_STATUS_CONNECT)
response = "HTTP/1.1 101 Switching Protocols\r\n"; response += "Connection: upgrade\r\n"; response += "Sec-WebSocket-Accept: ";
std::string serverKey = websocketKey + MAGIC_KEY;
unsigned int message_digest[5]; sha << serverKey.c_str();
sha.Result(message_digest); for (int i = 0; i < 5; i++) { message_digest[i] = htonl(message_digest[i]); serverKey = base64_encode(reinterpret_cast<const unsigned char*>(message_digest), 20); response += "Upgrade: websocket\r\n\r\n";
int CWebSocketProtocol::wsDecodeFrame(string inFrame, string &outMessage) int ret = WS_OPENING_FRAME; const char *frameData = inFrame.c_str(); const int frameLength = inFrame.size();
if ((frameData[0] & 0x70) != 0x0)
// fin位: 为1表示已接收完整报文, 为0表示继续监听后续报文 ret = (frameData[0] & 0x80); if ((frameData[0] & 0x80) != 0x80)
if ((frameData[1] & 0x80) != 0x80)
uint16_t payloadLength = 0; uint8_t payloadFieldExtraBytes = 0; uint8_t opcode = static_cast<uint8_t>(frameData[0] & 0x0f); if (opcode == 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) 146 else if (opcode == WS_BINARY_FRAME || opcode == WS_PING_FRAME || opcode == WS_PONG_FRAME) 148 // 二进制/ping/pong帧暂不处理 150 else if (opcode == WS_CLOSING_FRAME) 152 ret = WS_CLOSING_FRAME; 156 ret = WS_ERROR_FRAME; 160 if ((ret != WS_ERROR_FRAME) && (payloadLength > 0)) 162 // header: 2字节, masking key: 4字节 163 const char *maskingKey = &frameData[2 + payloadFieldExtraBytes]; 164 char *payloadData = new char[payloadLength + 1]; 165 memset(payloadData, 0, payloadLength + 1); 166 memcpy(payloadData, &frameData[2 + payloadFieldExtraBytes + 4], payloadLength); 167 for (int i = 0; i < payloadLength; i++) 169 payloadData[i] = payloadData[i] ^ maskingKey[i % 4]; 172 outMessage = payloadData; 173 delete[] payloadData; 179 int CWebSocketProtocol::wsEncodeFrame(string inMessage, string &outFrame, enum WS_FrameType frameType) int ret = WS_EMPTY_FRAME; const uint32_t messageLength = inMessage.size(); if (messageLength > 32767) std::cout << "暂不支持这么长的数据" << std::endl;
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) frameHeader[1] = static_cast<uint8_t>(messageLength); 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.c_str(), messageLength);
|