当前位置:   article > 正文

WebSocket 协议及服务端实现_此错误通常是由客户端仅使用 websocket 传输但未在服务器上启用 websocket 协议引

此错误通常是由客户端仅使用 websocket 传输但未在服务器上启用 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返回给客户端. 至此握手完成.

下面附上握手相关代码.

  1. #include <openssl/sha.h>
  2. #include <openssl/buffer.h>
  3. #include <openssl/bio.h>
  4. #include <openssl/evp.h>
  5. /**
  6. +------------------------------------------------------------------------------
  7. * @desc : 握手数据解析, 并返回校验数据
  8. +------------------------------------------------------------------------------
  9. * @access : public
  10. * @author : bottle<bottle@fridayws.com>
  11. * @since : 16-05-11
  12. * @param : const char* data 需要校验的数据
  13. * @param : char* request 可发送回客户端的已处理数据, 需要预先分配内存
  14. * @return : 0
  15. +------------------------------------------------------------------------------
  16. **/
  17. int shakeHands(const char* data, char* request)
  18. {
  19. char* key = "Sec-WebSocket-Key: ";
  20. char* begin = NULL;
  21. char* val = NULL;
  22. int needle = 0;
  23. begin = strstr(data, key);
  24. if (!begin)
  25. return -1;
  26. val = (char*)malloc(sizeof(char) * 256); // 这里可以选择使用一个栈变量存
  27. memset(val, 0, 256);
  28. begin += strlen(key);
  29. unsigned int blen = strlen(begin);
  30. int i = 0;
  31. for (i = 0; i < blen; ++i)
  32. {
  33. if (*(begin + i) == '\r' && *(begin + i + 1) == '\n')
  34. break;
  35. *(val + i) = *(begin + i);
  36. }
  37. strcat(val, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
  38. char mt[SHA_DIGEST_LENGTH] = {0};
  39. char accept[256] = {0};
  40. SHA1(val, strlen(val), mt);
  41. memset(accept, 0, 256);
  42. base64_encode(mt, strlen(mt), accept, 256);
  43. memset(request, 0, 1024);
  44. sprintf(request, "HTTP/1.1 101 Switching Protocols\r\n"
  45. "Upgrade: websocket\r\n"
  46. "Connection: Upgrade\r\n"
  47. "Sec-WebSocket-Accept: %*s\r\n"
  48. "Sec-webSocket-Version: 13\r\n"
  49. "Server: Bottle-websocket-server\r\n\r\n"
  50. , strlen(accept), accept);
  51. free(val);
  52. val = NULL;
  53. return 0;
  54. }

注: SHA1和base64的代码, 网上会有很多, 我这边是使用的OpenSSL的库实现. 在最后, 我会给出全部代码. 使用OpenSSL的库需要包含它的一系列头文件. 并在编译时指定-lssl


二. 数据传输阶段.

WebSocket传输数据是通过数据帧的方式传输的. 它的头部结构见下图.


  1. 前两个字节

  1. 数据长度计算方式

从上表中我们可以看到基本的描述. 数据长度字段存储方式是不定的. 这边可以使用更为清楚的方式来说明一下.  长度拓展字段最大可以有8个字节, 一个64bit unsigned int的长度. 选择哪种方式去存放长度. 可以通过第二个字节的后七位来判断.  而长度后面可能会有4个字节的掩码信息(如果前面的mask位为1的话); 下面附上代码.


  1. // Type define
  2. typedef unsigned char BYTE; // 定义一个BYTE类型
  3. typedef unsigned short UINT16; // 定义一个UINT16类型
  4. typedef unsigned long UINT64; // 定义一个UINT64类型
  5. typedef struct _WebSocketMark {
  6. BYTE fin:1;
  7. BYTE rsv1:1;
  8. BYTE rsv2:1;
  9. BYTE rsv3:1;
  10. BYTE opcode:4;
  11. BYTE mask:1;
  12. BYTE payloadlen:7;
  13. } WSMark;
  14. typedef struct _WebSocketHeader {
  15. WSMark mark;
  16. UINT64 reallength;
  17. unsigned char mask[4];
  18. unsigned short headlength;
  19. } WSHeader;

因为前两个字节是非整字节数据, 我这边使用位域结构去表示.

而下面的UINT64 reallength; 表示 数据的实际长度;

unsigned char mask[4]; 存放掩码信息(如果有的话);

unsigned short headlength; 存放整个个头部的长度, 如果后面需要的话, 方便计算.


下面是解析头部的程序代码.

  1. /**
  2. +------------------------------------------------------------------------------
  3. * @desc : 解析接收到的数据包
  4. +------------------------------------------------------------------------------
  5. * @access : public
  6. * @author : bottle<bottle@fridayws.com>
  7. * @since : 16-05-11
  8. * @param : unsigned char* buf 接收到的数据内容
  9. * @param : size_t length 接收的数据长度
  10. * @param : WSHeader* 头部存放结构体
  11. * @return : int 成功返回0
  12. +------------------------------------------------------------------------------
  13. **/
  14. int parsePack(unsigned char* buf, size_t length, WSHeader* header)
  15. {
  16. header->mark.fin = buf[0] >> 7; // 或使用 buf[0] & 0x80
  17. header->mark.rsv1 = buf[0] & 0x40;
  18. header->mark.rsv2 = buf[0] & 0x20;
  19. header->mark.rsv3 = buf[0] & 0x10;
  20. header->mark.opcode = buf[0] & 0xF;
  21. header->mark.mask = buf[1] >> 7;
  22. header->mark.payloadlen = buf[1] & 0x7F;
  23. header->headlength = 2;
  24. header->reallength = header->mark.payloadlen;
  25. if (header->mark.payloadlen == 126) // 如果payload length 值为 0x7E的话
  26. {
  27. UINT16 tmp16 = 0; // 我们使用后面的 2 个字节存放实际数据长度
  28. memcpy(&tmp16, buf + 2, 2);
  29. header->reallength = ntohs(tmp16); // 网络字节序转本地字节序
  30. header->headlength += 2;
  31. }
  32. else if (header->mark.payloadlen == 127) // 如果payload length 值为 0x7F的话
  33. {
  34. UINT64 tmp64 = 0; // 我们使用后续的 8 个字节存放实际数据长度
  35. memcpy(&tmp64, buf + 2, 8);
  36. header->reallength = ntohl(tmp64); // 网络字节序转本地字节序
  37. header->headlength += 8;
  38. }
  39. memset(header->mask, 0, 4);
  40. if (header->mark.mask)
  41. {
  42. memcpy(header->mask, buf + header->headlength, 4);
  43. header->headlength += 4;
  44. }
  45. return 0;
  46. }

备注 1: OpenSSL 库实现 base64_encode

  1. /**
  2. +------------------------------------------------------------------------------
  3. * @desc : 对数据做base64处理
  4. +------------------------------------------------------------------------------
  5. * @access : public
  6. * @author : bottle<bottle@fridayws.com>
  7. * @since : 16-05-11
  8. * @param : char* str 需要做base64的字符串
  9. * @param : int len 数据长度
  10. * @param : char* encode 处理好的数据存放位置
  11. * @param : int 数据的实际长度
  12. * @return : int 长度
  13. +------------------------------------------------------------------------------
  14. **/
  15. int base64_encode(char* str, int len, char* encode, int elen)
  16. {
  17. BIO* bmem, * b64;
  18. BUF_MEM* bptr = NULL;
  19. b64 = BIO_new(BIO_f_base64());
  20. bmem = BIO_new (BIO_s_mem());
  21. b64 = BIO_push(b64, bmem);
  22. BIO_write(b64, str, len);
  23. BIO_flush(b64);
  24. BIO_get_mem_ptr(b64, &bptr);
  25. elen = bptr->length;
  26. memcpy(encode, bptr->data, bptr->length);
  27. if (encode[elen - 1] == '\n' || encode[elen - 1] == '\r')
  28. encode[elen - 1] = '\0';
  29. BIO_free_all(b64);
  30. return elen;
  31. }
  32. /**
  33. +------------------------------------------------------------------------------
  34. * @desc : 反处理, 解析base64
  35. +------------------------------------------------------------------------------
  36. * @access : public
  37. * @author : bottle<bottle@fridayws.com>
  38. * @since : 16-05-11
  39. * @param : char* encode 已编码过的数据
  40. * @param : int elen 编码过的数据长度
  41. * @param : char* decode 存放解码后的数据
  42. * @param : int dlen 存放解码后的数据长度
  43. * @return : void
  44. +------------------------------------------------------------------------------
  45. **/
  46. int base64_decode(char* encode, int elen, char* decode, int dlen)
  47. {
  48. int len = 0;
  49. BIO* b64, * bmem;
  50. b64 = BIO_new(BIO_f_base64());
  51. bmem = BIO_new_mem_buf(encode, elen);
  52. bmem = BIO_push(b64, bmem);
  53. len = BIO_read(bmem, decode, elen);
  54. decode[len] = 0;
  55. BIO_free_all(bmem);
  56. return 0;
  57. }

备注 2: 接收完整数据内容代码

  1. /**
  2. +------------------------------------------------------------------------------
  3. * @desc : 获取消息体
  4. +------------------------------------------------------------------------------
  5. * @access : public
  6. * @author : bottle<bottle@fridayws.com>
  7. * @since : 16-05-17
  8. * @param : const int cfd client fd
  9. * @param : const unsigned char* buf 消息buf
  10. * @param : size_t bufsize 消息长度
  11. * @param : unsigned char* container 获取到的消息体存放地址
  12. * @param : const WSHeader* pHeader 头结构体地址
  13. * @return : int
  14. +------------------------------------------------------------------------------
  15. **/
  16. int getPackPayloadData(const int cfd, const unsigned char* buf, size_t bufsize, unsigned char* container, const WSHeader* pHeader)
  17. {
  18. memset(container, 0, pHeader->reallength + 1);
  19. int readlength = 0;
  20. int recvlength = 0;
  21. int count = 0;
  22. char *_buf = (char*)calloc(bufsize, sizeof(char)); // 动态分配足够大空间
  23. if (pHeader->mark.mask) // 如果有掩码
  24. {
  25. readlength = bufsize - pHeader->headlength;
  26. int x = 0;
  27. memcpy(container, buf + pHeader->headlength, pHeader->reallength > readlength ? readlength : pHeader->reallength);
  28. while(pHeader->reallength > readlength)
  29. {
  30. memset(_buf, 0, bufsize);
  31. count = recv(cfd, _buf, bufsize, MSG_DONTWAIT);
  32. recvlength = (pHeader->reallength - readlength) > bufsize ? bufsize : (pHeader->reallength - readlength);
  33. memcpy(container + readlength, _buf, recvlength);
  34. readlength += recvlength;
  35. }
  36. for (x = 0; x < pHeader->reallength; ++x)
  37. *(container + x) ^= pHeader->mask[x % 4];
  38. }
  39. else
  40. {
  41. readlength = bufsize - pHeader->headlength;
  42. memcpy(container, buf + pHeader->headlength, pHeader->reallength > readlength ? readlength : pHeader->reallength);
  43. while(pHeader->reallength > readlength)
  44. {
  45. memset(_buf, 0, bufsize);
  46. count = recv(cfd, _buf, bufsize, MSG_DONTWAIT);
  47. recvlength = pHeader->reallength - readlength > bufsize ? bufsize : pHeader->reallength - readlength;
  48. memcpy(container + readlength, _buf, recvlength);
  49. readlength += recvlength;
  50. }
  51. }
  52. free(_buf);
  53. _buf = NULL;
  54. return 0;
  55. }

备注 3:  打包需要发送的数据(这边超大数据<数据长度超过0xFFFF>处理可能会有问题, 只是写一个实例没有做测试和处理), 我这个写的不是很好, 后面正式使用会做一些优化.

  1. /**
  2. +------------------------------------------------------------------------------
  3. * @desc : 对发送数据打包
  4. +------------------------------------------------------------------------------
  5. * @author : Bottle<bottle.friday@gmail.com>
  6. * @since : 2016-05-11
  7. * @param : const unsigned char* message 需要发送的消息体
  8. * @param : size_t len 发送数据长度
  9. * @param : BYTE fin 是否是结束消息 (1 bit)
  10. * @param : BYTE opcode 消息类型(4 bit) 共15种类型
  11. * @param : BYTE mask (是否需要做掩码运算 1 bit)
  12. * @param : unsigned char** send 输出参数, 存放处理好的数据包
  13. * @param : size_t* slen 输出参数, 记录数据包的长度
  14. * @return : int 成功返回0
  15. +------------------------------------------------------------------------------
  16. **/
  17. int packData(const unsigned char* message, size_t len, BYTE fin, BYTE opcode, BYTE mask, unsigned char** send, size_t* slen)
  18. {
  19. int headLength = 0;
  20. // 基本一个包可以发送完所有数据
  21. *slen = len;
  22. if (len < 126) // 如果不需要扩展长度位, 两个字节存放 fin(1bit) + rsv[3](1bit) + opcode(4bit); mask(1bit) + payloadLength(7bit);
  23. *slen += 2;
  24. else if (len < 0xFFFF) // 如果数据长度超过126 并且小于两个字节, 我们再用后面的两个字节(16bit) 表示 UINT16
  25. *slen += 4;
  26. else // 如果数据更长的话, 我们使用后面的8个字节(64bit)表示 UINT64
  27. *slen += 8;
  28. // 判断是否有掩码
  29. if (mask & 0x1) // 判断是不是1
  30. *slen += 4; // 4byte 掩码位
  31. // 长度已确定, 现在可以重新分配内存
  32. *send = (unsigned char*)realloc((void*)*send, *slen);
  33. // 做数据设置
  34. memset(*send, 0, *slen);
  35. **send = fin << 7;
  36. **send = **send | (0xF & opcode); //处理opcode
  37. *(*send + 1) = mask << 7;
  38. if (len < 126)
  39. {
  40. *(*send + 1) = *(*send + 1) | len;
  41. //start += 2;
  42. headLength += 2;
  43. }
  44. else if (len < 0xFFFF)
  45. {
  46. *(*send + 1) = *(*send + 1) | 0x7E; // 设置第二个字节后7bit为126
  47. UINT16 tmp = htons((UINT16)len);
  48. //UINT16 tmp = len;
  49. memcpy(*send + 2, &tmp, sizeof(UINT16));
  50. headLength += 4;
  51. }
  52. else
  53. {
  54. *(*send + 1) = *(*send + 1) | 0x7F; // 设置第二个字节后为7bit 127
  55. UINT64 tmp = htonl((UINT64)len);
  56. //UINT64 tmp = len;
  57. memcpy(*send + 2, &tmp, sizeof(UINT64));
  58. headLength += 10;
  59. }
  60. // 处理掩码
  61. if (mask & 0x1)
  62. {
  63. // 因协议规定, 从服务器向客户端发送的数据, 一定不能使用掩码处理. 所以这边省略
  64. headLength += 4;
  65. }
  66. memcpy((*send) + headLength, message, len);
  67. *(*send + (*slen - 1)) = '\0';
  68. return 0;
  69. }

完整程序代码在GitHub上, 地址是:https://github.com/BottleHe/c-demo/blob/master/websocket/websocket.c . 有兴趣的朋友可以看看, 别编译的时候可以直接使用下面的指令, 系统环境是CentOS 6.4.     gcc websocket.c -lssl  






声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号