WebSocket 协议及服务端实现

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传输数据是通过数据帧的方式传输的. 它的头部结构见下图.


  1. 前两个字节

  1. 数据长度计算方式

从上表中我们可以看到基本的描述. 数据长度字段存储方式是不定的. 这边可以使用更为清楚的方式来说明一下.  长度拓展字段最大可以有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  






  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值