赞
踩
WebSocket 笔记
协议理解及服务器端实现
Bottle, May 24 2016
bottle@fridayws.com
前言: HTML 从1993年的HTML第一版发展到现在的 2014年的 HTML 5; 从最早为科学家们共享信息的文档, 到现在的包罗万象, 日常生活中的无处不在(信息, 游戏, 商城等等). 现在传统的Web协议(HTTP, FTP等) 已经不够满足需要. 而HTML 5的来临开启了一个全新的时代.
在 HTML5 来临的同时, 它带来了一个强大的功能, 使我们的 B/S 应用和传统的 C/S应用 更近了一步, 它就是 WebSocket. 早期浏览器中通过 HTTP协议 仅能实现单向的通信, Comet 可以一定程度上模拟双向通信,但效率较低,并需要服务器有较好的支持; Flash 中的 Socket 和 XMLSocket 可以实现真正的双向通信, 通过 Flex Ajax Bridge, 可以在 Javascript 中使用这两项功能, 但是它需要flash的支持, 另外在性能上也会有更多的开销. WebSocket 的出现, 必然会替代上面两项技术, 得到广泛的使用. 面对这种状况, HTML5 定义了WebSocket协议, 能更好的节省服务器资源和带宽并达到实时通信.
目前网络止介绍WebSocket协议的文章有很多, 本文描述在Linux下, 用C去实现WebSocket的一个例子, 为了这个文档的完整性, 我就直接把WebSocket的协议规范拷贝过来了.
注: 我这里主要是介绍WebSocket 13版本. 早期版本因为在浏览器中基本没有在用了. 所以就不说了. 有兴趣的朋友可以在Google上考一下古. - -
WebSocket 13版本中, 通讯的实现主要是有两个阶段. 下面简单介绍一下.
一. 握手阶段:
握手的一个目的是为了兼容基于HTTP的服务器端程序, 这样一个端口可以同时处理HTTP客户端和WebSocket客户端,因此WebSocket客户端握手是一个HTTP Upgrade请求; 第二个目的是在客户端和服务器之前互相验证对方的有效性.
在WebSocket 13版本中, 握手过程可以见下图:
客户端发送一个HTTP Upgrade的请求到服务器端, 服务端接收请求后主要是成针对客户端发送的Sec-WebSocket-Key, 生成Sec-WebSocket-Accept. 生成方式比较简单就是将Sec-WebSocket-Key拼接上一个UUID (258EAFA5-E914-47DA-95CA-C5AB0DC85B11),然后对拼接后的字符串做sha1哈希, 然后将hash的结果使用base64编码成明文放入Sec-WebSocket-Accept返回给客户端. 至此握手完成.
下面附上握手相关代码.
- #include <openssl/sha.h>
- #include <openssl/buffer.h>
- #include <openssl/bio.h>
- #include <openssl/evp.h>
-
- /**
- +------------------------------------------------------------------------------
- * @desc : 握手数据解析, 并返回校验数据
- +------------------------------------------------------------------------------
- * @access : public
- * @author : bottle<bottle@fridayws.com>
- * @since : 16-05-11
- * @param : const char* data 需要校验的数据
- * @param : char* request 可发送回客户端的已处理数据, 需要预先分配内存
- * @return : 0
- +------------------------------------------------------------------------------
- **/
- int shakeHands(const char* data, char* request)
- {
- char* key = "Sec-WebSocket-Key: ";
- char* begin = NULL;
- char* val = NULL;
- int needle = 0;
- begin = strstr(data, key);
- if (!begin)
- return -1;
- val = (char*)malloc(sizeof(char) * 256); // 这里可以选择使用一个栈变量存
- memset(val, 0, 256);
- begin += strlen(key);
- unsigned int blen = strlen(begin);
- int i = 0;
- for (i = 0; i < blen; ++i)
- {
- if (*(begin + i) == '\r' && *(begin + i + 1) == '\n')
- break;
- *(val + i) = *(begin + i);
- }
- strcat(val, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
- char mt[SHA_DIGEST_LENGTH] = {0};
- char accept[256] = {0};
- SHA1(val, strlen(val), mt);
- memset(accept, 0, 256);
- base64_encode(mt, strlen(mt), accept, 256);
- memset(request, 0, 1024);
- sprintf(request, "HTTP/1.1 101 Switching Protocols\r\n"
- "Upgrade: websocket\r\n"
- "Connection: Upgrade\r\n"
- "Sec-WebSocket-Accept: %*s\r\n"
- "Sec-webSocket-Version: 13\r\n"
- "Server: Bottle-websocket-server\r\n\r\n"
- , strlen(accept), accept);
- free(val);
- val = NULL;
- return 0;
- }
注: SHA1和base64的代码, 网上会有很多, 我这边是使用的OpenSSL的库实现. 在最后, 我会给出全部代码. 使用OpenSSL的库需要包含它的一系列头文件. 并在编译时指定-lssl
二. 数据传输阶段.
WebSocket传输数据是通过数据帧的方式传输的. 它的头部结构见下图.
从上表中我们可以看到基本的描述. 数据长度字段存储方式是不定的. 这边可以使用更为清楚的方式来说明一下. 长度拓展字段最大可以有8个字节, 一个64bit unsigned int的长度. 选择哪种方式去存放长度. 可以通过第二个字节的后七位来判断. 而长度后面可能会有4个字节的掩码信息(如果前面的mask位为1的话); 下面附上代码.
- // Type define
- typedef unsigned char BYTE; // 定义一个BYTE类型
- typedef unsigned short UINT16; // 定义一个UINT16类型
- typedef unsigned long UINT64; // 定义一个UINT64类型
-
-
- typedef struct _WebSocketMark {
- BYTE fin:1;
- BYTE rsv1:1;
- BYTE rsv2:1;
- BYTE rsv3:1;
- BYTE opcode:4;
- BYTE mask:1;
- BYTE payloadlen:7;
- } WSMark;
-
- typedef struct _WebSocketHeader {
- WSMark mark;
- UINT64 reallength;
- unsigned char mask[4];
- unsigned short headlength;
- } WSHeader;
因为前两个字节是非整字节数据, 我这边使用位域结构去表示.
而下面的UINT64 reallength; 表示 数据的实际长度;
unsigned char mask[4]; 存放掩码信息(如果有的话);
unsigned short headlength; 存放整个个头部的长度, 如果后面需要的话, 方便计算.
下面是解析头部的程序代码.
- /**
- +------------------------------------------------------------------------------
- * @desc : 解析接收到的数据包
- +------------------------------------------------------------------------------
- * @access : public
- * @author : bottle<bottle@fridayws.com>
- * @since : 16-05-11
- * @param : unsigned char* buf 接收到的数据内容
- * @param : size_t length 接收的数据长度
- * @param : WSHeader* 头部存放结构体
- * @return : int 成功返回0
- +------------------------------------------------------------------------------
- **/
- int parsePack(unsigned char* buf, size_t length, WSHeader* header)
- {
- header->mark.fin = buf[0] >> 7; // 或使用 buf[0] & 0x80
- header->mark.rsv1 = buf[0] & 0x40;
- header->mark.rsv2 = buf[0] & 0x20;
- header->mark.rsv3 = buf[0] & 0x10;
- header->mark.opcode = buf[0] & 0xF;
- header->mark.mask = buf[1] >> 7;
- header->mark.payloadlen = buf[1] & 0x7F;
- header->headlength = 2;
- header->reallength = header->mark.payloadlen;
- if (header->mark.payloadlen == 126) // 如果payload length 值为 0x7E的话
- {
- UINT16 tmp16 = 0; // 我们使用后面的 2 个字节存放实际数据长度
- memcpy(&tmp16, buf + 2, 2);
- header->reallength = ntohs(tmp16); // 网络字节序转本地字节序
- header->headlength += 2;
- }
- else if (header->mark.payloadlen == 127) // 如果payload length 值为 0x7F的话
- {
- UINT64 tmp64 = 0; // 我们使用后续的 8 个字节存放实际数据长度
- memcpy(&tmp64, buf + 2, 8);
- header->reallength = ntohl(tmp64); // 网络字节序转本地字节序
- header->headlength += 8;
- }
- memset(header->mask, 0, 4);
- if (header->mark.mask)
- {
- memcpy(header->mask, buf + header->headlength, 4);
- header->headlength += 4;
- }
- return 0;
- }
备注 1: OpenSSL 库实现 base64_encode
- /**
- +------------------------------------------------------------------------------
- * @desc : 对数据做base64处理
- +------------------------------------------------------------------------------
- * @access : public
- * @author : bottle<bottle@fridayws.com>
- * @since : 16-05-11
- * @param : char* str 需要做base64的字符串
- * @param : int len 数据长度
- * @param : char* encode 处理好的数据存放位置
- * @param : int 数据的实际长度
- * @return : int 长度
- +------------------------------------------------------------------------------
- **/
- int base64_encode(char* str, int len, char* encode, int elen)
- {
- BIO* bmem, * b64;
- BUF_MEM* bptr = NULL;
- b64 = BIO_new(BIO_f_base64());
- bmem = BIO_new (BIO_s_mem());
- b64 = BIO_push(b64, bmem);
- BIO_write(b64, str, len);
- BIO_flush(b64);
- BIO_get_mem_ptr(b64, &bptr);
-
- elen = bptr->length;
- memcpy(encode, bptr->data, bptr->length);
- if (encode[elen - 1] == '\n' || encode[elen - 1] == '\r')
- encode[elen - 1] = '\0';
- BIO_free_all(b64);
- return elen;
- }
-
- /**
- +------------------------------------------------------------------------------
- * @desc : 反处理, 解析base64
- +------------------------------------------------------------------------------
- * @access : public
- * @author : bottle<bottle@fridayws.com>
- * @since : 16-05-11
- * @param : char* encode 已编码过的数据
- * @param : int elen 编码过的数据长度
- * @param : char* decode 存放解码后的数据
- * @param : int dlen 存放解码后的数据长度
- * @return : void
- +------------------------------------------------------------------------------
- **/
- int base64_decode(char* encode, int elen, char* decode, int dlen)
- {
- int len = 0;
- BIO* b64, * bmem;
- b64 = BIO_new(BIO_f_base64());
- bmem = BIO_new_mem_buf(encode, elen);
- bmem = BIO_push(b64, bmem);
- len = BIO_read(bmem, decode, elen);
- decode[len] = 0;
- BIO_free_all(bmem);
- return 0;
- }
备注 2: 接收完整数据内容代码
- /**
- +------------------------------------------------------------------------------
- * @desc : 获取消息体
- +------------------------------------------------------------------------------
- * @access : public
- * @author : bottle<bottle@fridayws.com>
- * @since : 16-05-17
- * @param : const int cfd client fd
- * @param : const unsigned char* buf 消息buf
- * @param : size_t bufsize 消息长度
- * @param : unsigned char* container 获取到的消息体存放地址
- * @param : const WSHeader* pHeader 头结构体地址
- * @return : int
- +------------------------------------------------------------------------------
- **/
- int getPackPayloadData(const int cfd, const unsigned char* buf, size_t bufsize, unsigned char* container, const WSHeader* pHeader)
- {
- memset(container, 0, pHeader->reallength + 1);
- int readlength = 0;
- int recvlength = 0;
- int count = 0;
- char *_buf = (char*)calloc(bufsize, sizeof(char)); // 动态分配足够大空间
- if (pHeader->mark.mask) // 如果有掩码
- {
- readlength = bufsize - pHeader->headlength;
- int x = 0;
- memcpy(container, buf + pHeader->headlength, pHeader->reallength > readlength ? readlength : pHeader->reallength);
- while(pHeader->reallength > readlength)
- {
- memset(_buf, 0, bufsize);
- count = recv(cfd, _buf, bufsize, MSG_DONTWAIT);
- recvlength = (pHeader->reallength - readlength) > bufsize ? bufsize : (pHeader->reallength - readlength);
- memcpy(container + readlength, _buf, recvlength);
- readlength += recvlength;
- }
- for (x = 0; x < pHeader->reallength; ++x)
- *(container + x) ^= pHeader->mask[x % 4];
- }
- else
- {
- readlength = bufsize - pHeader->headlength;
- memcpy(container, buf + pHeader->headlength, pHeader->reallength > readlength ? readlength : pHeader->reallength);
- while(pHeader->reallength > readlength)
- {
- memset(_buf, 0, bufsize);
- count = recv(cfd, _buf, bufsize, MSG_DONTWAIT);
- recvlength = pHeader->reallength - readlength > bufsize ? bufsize : pHeader->reallength - readlength;
- memcpy(container + readlength, _buf, recvlength);
- readlength += recvlength;
- }
- }
- free(_buf);
- _buf = NULL;
- return 0;
- }
备注 3: 打包需要发送的数据(这边超大数据<数据长度超过0xFFFF>处理可能会有问题, 只是写一个实例没有做测试和处理), 我这个写的不是很好, 后面正式使用会做一些优化.
- /**
- +------------------------------------------------------------------------------
- * @desc : 对发送数据打包
- +------------------------------------------------------------------------------
- * @author : Bottle<bottle.friday@gmail.com>
- * @since : 2016-05-11
- * @param : const unsigned char* message 需要发送的消息体
- * @param : size_t len 发送数据长度
- * @param : BYTE fin 是否是结束消息 (1 bit)
- * @param : BYTE opcode 消息类型(4 bit) 共15种类型
- * @param : BYTE mask (是否需要做掩码运算 1 bit)
- * @param : unsigned char** send 输出参数, 存放处理好的数据包
- * @param : size_t* slen 输出参数, 记录数据包的长度
- * @return : int 成功返回0
- +------------------------------------------------------------------------------
- **/
- int packData(const unsigned char* message, size_t len, BYTE fin, BYTE opcode, BYTE mask, unsigned char** send, size_t* slen)
- {
- int headLength = 0;
- // 基本一个包可以发送完所有数据
- *slen = len;
- if (len < 126) // 如果不需要扩展长度位, 两个字节存放 fin(1bit) + rsv[3](1bit) + opcode(4bit); mask(1bit) + payloadLength(7bit);
- *slen += 2;
- else if (len < 0xFFFF) // 如果数据长度超过126 并且小于两个字节, 我们再用后面的两个字节(16bit) 表示 UINT16
- *slen += 4;
- else // 如果数据更长的话, 我们使用后面的8个字节(64bit)表示 UINT64
- *slen += 8;
- // 判断是否有掩码
- if (mask & 0x1) // 判断是不是1
- *slen += 4; // 4byte 掩码位
- // 长度已确定, 现在可以重新分配内存
- *send = (unsigned char*)realloc((void*)*send, *slen);
- // 做数据设置
- memset(*send, 0, *slen);
- **send = fin << 7;
- **send = **send | (0xF & opcode); //处理opcode
- *(*send + 1) = mask << 7;
- if (len < 126)
- {
- *(*send + 1) = *(*send + 1) | len;
- //start += 2;
- headLength += 2;
- }
- else if (len < 0xFFFF)
- {
- *(*send + 1) = *(*send + 1) | 0x7E; // 设置第二个字节后7bit为126
- UINT16 tmp = htons((UINT16)len);
- //UINT16 tmp = len;
- memcpy(*send + 2, &tmp, sizeof(UINT16));
- headLength += 4;
- }
- else
- {
- *(*send + 1) = *(*send + 1) | 0x7F; // 设置第二个字节后为7bit 127
- UINT64 tmp = htonl((UINT64)len);
- //UINT64 tmp = len;
- memcpy(*send + 2, &tmp, sizeof(UINT64));
- headLength += 10;
- }
- // 处理掩码
- if (mask & 0x1)
- {
- // 因协议规定, 从服务器向客户端发送的数据, 一定不能使用掩码处理. 所以这边省略
- headLength += 4;
- }
- memcpy((*send) + headLength, message, len);
- *(*send + (*slen - 1)) = '\0';
- return 0;
- }
完整程序代码在GitHub上, 地址是:https://github.com/BottleHe/c-demo/blob/master/websocket/websocket.c . 有兴趣的朋友可以看看, 别编译的时候可以直接使用下面的指令, 系统环境是CentOS 6.4. gcc websocket.c -lssl
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。