[C/C++后端开发学习] 8 tcp服务器支持websocket协议

背景

尽管HTTP协议在WEB上已广泛应用,但它是一个无状态、单向通信的协议,那么对于一些需要实时刷新页面数据的场合,基于HTTP实现起来就会很尴尬:前端需要不断地发起HTTP请求连接到服务端去获取最新的数据。这造成了一些问题:

  • 大量HTTP请求泛滥造成服务器资源的浪费;
  • 对于前端脚本来说也造成了额外的资源开销;

于是,我们自然而然就会想到,能不能单独建立一个TCP长连接在前后端之间实现数据的双向通信,以便于服务端可以主动地将数据推送给前端。于是 websocket 就诞生了。

websocket

实际上,websocket在连接的建立阶段借用了HTTP协议,这个过程被称为“握手(handshake)”。设计者这么做是非常明智的,其好处在于:可以利用HTTP协议已有的一些组件(代理、过滤器、认证机制),同时可以复用HTTP常使用的80或443端口,等等。基于HTTP建立连接后,websocket就可以自由地通信了。

也是基于此,我们一般将websocket协议的工作过程分为两部分:1)握手和 2)通信。此外还有一个结束过程,但一般可认为其是通信过程的一部分。后面会分开进行解析。

应用场景

  • 网页聊天,即时通信
  • 弹幕
  • 股票数据实时刷新

案例:CSDN上微信扫描二维码登录。扫描之后之所以前端页面能主动实现跳转,就是因为服务器通过websocket协议主动推送数据给前端。

websocket工作流程

握手过程(Opening Handshake)

当客户端向服务端发起握手时,其HTTP报文一般格式如下所示:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

服务端接收到请求后,其响应报文格式一般如下:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

其中,Sec-WebSocket-Protocol指出客户端所支持的基于websocket的其他用户协议,服务端在响应报文中选择其中一种,比如不同的页面需要不同的用户协议。我们重点关注请求报文中的Sec-WebSocket-Key字段和响应报文中的Sec-WebSocket-Accept字段,从字面上来看,这两个字段主要与认证有关。它们有助于确认服务端能够正确地支持websocket协议,同时也确保了客户端发起的这个请求是websocket的握手请求而非普通的HTTP请求。

实际上,Sec-WebSocket-Key是客户端产生的一个随机字串,服务端接收到Sec-WebSocket-Key后,会做两步处理:

  • 1 websocket协议定义了一个Globally Unique Identifier (GUID): 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,服务端将该GUID拼接到Sec-WebSocket-Key内容的后面,如:dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CAC5AB0DC85B11
  • 2 对拼接后的字符串做一次SHA-1 hash运算(160bit, 20 bytes),再对输出的结果做一次base64编码,即可得到我们在响应报文中看到的Sec-WebSocket-Accept的内容s3pPLMBiTxaQ9kYGzzhZRbK+xOo=。将其返回给客户端。

客户端接收到响应报文后,对Sec-WebSocket-Key做一次同样的计算,然后比对与服务端结果是否一致,一致则握手过程完成,服务端证明了自己是一个合格的websocket服务器。注意响应报文首部是HTTP/1.1 101 Switching Protocols

之后,二者就抛开HTTP协议开心地进入了websocket通信环节。

通信协议

websocket通过发送一系列的数据帧来进行通信,帧格式如下图所示:
在这里插入图片描述

  • FIN: 1 bit
    指示当前帧是否为一段完整数据的最后一帧,为0表示后面还有数据,为1表示这是最后一帧;注意FIN是在最高位。

  • RSV1, RSV2, RSV3: 1 bit each
    保留字段,必须为0,除非经过协商。

  • opcode: 4 bits
    操作码,指示payload数据的类型。

    * %x0 denotes a continuation frame
    * %x1 denotes a text frame
    * %x2 denotes a binary frame
    * %x3-7 are reserved for further non-control frames
    * %x8 denotes a connection close
    * %x9 denotes a ping
    * %xA denotes a pong
    * %xB-F are reserved for further control frames

  • MASK: 1 bit
    指示payload数据是否经过掩码处理。如果为1,那么Masking-key就是掩码。协议中规定客户端发送到服务端的数据都要经过掩码处理。这个掩码处理从严格意义上来说,肯定不能起到加密的作用,但我的理解是,别人抓到你的包时,如果其中没有明文,只有二进制的数据,别人可能甚至都不知道这是一个websocket数据帧,从这个角度来想还是有一定意义。

  • Payload len: 7 bits, 7+16 bits, or 7+64 bits
    payload的长度是websocket协议中一个很灵活的设计。如果数据长度小于126字节(0~125),那么Payload len上就直接存储这个长度值,不需要后面的Extended payload length,直接就是Masking-key;如果数据长度大于126字节,则根据需要:1)将Payload len设为126,那么之后的2个字节用于表示数据长度;2)将Payload len设为127,那么之后的8个字节用于表示数据长度。
    一定注意了:websocket采用的网络字节序(大端字节序)!Payload len>=126时,接收到Payload len记得用ntohs进行转换,发送前用htons进行转换后再送入Payload len

  • Masking-key: 0 or 4 bytes
    如果MASK置位1,则这是一个4字节的掩码,否则该数据不存在。掩码数据长度不包含在payload的长度中。

  • Payload Data
    所有的数据。其中文档中还会提到扩展数据,那是双方自行协商的数据部分,可以不理会。

此外,需要说明一下掩码的工作方式:将payload中的数据与掩码数据做异或运算,客户端对数据做异或处理后,服务端使用相同的掩码再做一遍异或处理,就可以恢复数据。这里是利用了异或运算的特性:A异或B得到C,C再异或B可以恢复为A。这里边有4个掩码字节,每一次运算其实是轮流取这4个字节中的一个去与payload数据做异或,看代码会更直观一些:

void umask(char *data,int len,char *mask) 
{
       
	int i;    
	for (i = 0;i < len;i ++)        
		*(data+i) ^= mask[i%4];
}

结束(Closing Handshake)

结束过程比握手要简单得多。两边均可以发起结束过程,只需要发送一个数据帧其opcode为 0x8 就行了,称为结束帧。结束帧也能包含数据,可以用于提示为什么结束。关于结束帧的数据,websocket_rfc6455文档中有这么一点需要指出:

The Close frame MAY contain a body …
… If there is a body, the first two bytes of the body MUST be a 2-byte unsigned integer (in network byte order) representing a status code

… the client SHOULD wait for the server to close the connection but MAY close the connection … if it has not received a TCP Close from the server in a reasonable time period.

大致意思就是说,发送结束帧是,如果要带payload,数据开头的两字节需要是一个错误码(网络字节序形式存储),其具体的错误码定义还需看原文档。其次,客户端发出结束帧后,会等待服务端关闭连接,除非超时客户端才会先关闭连接。

当某一端发出结束帧后,就不应该再发送任何数据。另一端接收到结束帧后,将剩下的数据发完,然后发送一个结束帧的响应。之后,两边就可以关闭连接了。如果两端同时发起了结束帧,则双方都需要响应结束帧后再关闭连接。

既然TCP本身有四次挥手,为什么websocket还要单独搞一出结束帧?websocket_rfc6455文档中是这样解释的:

The closing handshake is intended to complement the TCP closing handshake (FIN/ACK), on the basis that the TCP closing handshake is not always reliable end-to-end, especially in the presence of intercepting proxies and other intermediaries.
_
By sending a Close frame and waiting for a Close frame in response, certain cases are avoided where data may be unnecessarily lost. For instance, on some platforms, if a socket is closed with data in the receive queue, a RST packet is sent, which will then cause recv() to fail for the party that received the RST, even if there was data waiting to be read.

实现

本代码在上一篇博客《tcp服务器的epoll实现以及Reactor模型》的代码基础上增加对websocket协议的支持。

服务端的状态机

初始状态 - 建立好socket,尚未握手
握手状态 - 客户端发起握手的报文并正确响应
通信状态 - 握手过程结束后就会一直处于通信状态,按照既定地协议进行数据收发即可
结束状态 - 客户端发起结束帧,服务端返回结束帧后主动关闭连接

代码实现

结构定义
/* Websocket相关定义 */

enum WS_STATUS  // 状态
{
   
    WS_INIT = 0,
    WS_HANDSHAKE,
    WS_DATATRANSFER,
    WS_END
};

struct ws_opHeader
{
   
    unsigned char opcode : 4,
                  RSV123 : 3,
                  FIN    : 1;

    unsigned char payloadLen : 7,
                  MASK       : 1;
}__attribute__ ((packed));  // __attribute__ ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐

struct ws_dataHeader126
{
   
    unsigned short payloadLen;
}__attribute__ ((packed));

struct ws_dataHeader127
{
   
    unsigned long long payloadLen;
}__attribute__ ((packed));

#define GUID ("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")

/* Websocket相关定义 END */

#define MAX_BUFFER_SIZE 1024
struct sockitem
{
   
	int sockfd;
	int (*callback)(int fd, int events, void *arg);
    int epfd;

    char recvbuffer[MAX_BUFFER_SIZE]; // 接收缓冲
	char sendbuffer[MAX_
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring4GWT GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet.Applet 简单实现!~ 网页表格组件 GWT Advanced Table GWT Advanced Table 是一个基于 GWT 框架的网页表格组件,可实现分页数据显示、数据排序和过滤等功能! Google Tag Library 该标记库和 Google 有关。使用该标记库,利用 Google 为你的网站提供网站查询,并且可以直接在你的网页里面显示搜查的结果。 github-java-api github-java-api 是 Github 网站 API 的 Java 语言版本。 java缓存工具 SimpleCache SimpleCache 是一个简单易用的java缓存工具,用来简化缓存代码的编写,让你摆脱单调乏味的重复工作!1. 完全透明的缓存支持,对业务代码零侵入 2. 支持使用Redis和Memcached作为后端缓存。3. 支持缓存数据分区规则的定义 4. 使用redis作缓存时,支持list类型的高级数据结构,更适合论坛帖子列表这种类型的数据 5. 支持混合使用redis缓存和memcached缓存。可以将列表数据缓存到redis中,其他kv结构数据继续缓存到memcached 6. 支持redis的主从集群,可以做读写分离。缓存读取自redis的slave节点,写入到redis的master节点。 Java对象的SQL接口 JoSQL JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于【自动提示】的需要(如:Google搜索), 而开发的架构无关的公共控件, 以满足该类需求可以通过快速配置来开发。AutoTips基于搜索引擎Apache Lucene实现。AutoTips提供统一UI。 WAP浏览器 j2wap j2wap 是一个基于Java的WAP浏览器,目前处于BETA测试阶段。它支持WAP 1.2规范,除了WTLS 和WBMP。 Java注册表操作类 jared jared是一个用来操作Windows注册表的 Java 类库,你可以用来对注册表信息进行读写。 GIF动画制作工具 GiftedMotion GiftedMotion是一个很小的,免费而且易于使用图像互换格式动画是能够设计一个有趣的动画了一系列的数字图像。使用简便和直截了当,用户只需要加载的图片和调整帧您想要的,如位置,时间显示和处理方法前帧。 Java的PList类库 Blister Blister是一个用于操作苹果二进制PList文件格式的Java开源类库(可用于发送数据给iOS应用程序)。 重复文件检查工具 FindDup.tar FindDup 是一个简单易用的工具,用来检查计算机上重复的文件。 OpenID的Java客户端 JOpenID JOpenID是一个轻量级的OpenID 2.0 Java客户端,仅50KB+(含源代码),允许任何Web网站通过OpenID支持用户直接登录而无需注册,例如Google Account或Yahoo Account。 JActor的文件持久化组件 JFile JFile 是 JActor 的文件持久化组件,以及一个高吞吐量的可靠事务日志组件。 Google地图JSP标签库 利用Google:maps JSP标签库就能够在你的Web站点上实现GoogleMaps的所有功能而且不需要javascript或AJAX编程。它还能够与JSTL相结合生成数据库驱动的动态Maps。 OAuth 实现框架 Agorava Agorava 是一个实现了 OAuth 1.0a 和 OAuth 2.0 的框架,提供了简单的方式通过社交媒体进行身份认证的功能。 Eclipse的JavaScript插件 JSEditor JSEditor 是 Eclipse 下编辑 JavaScript 源码的插件,提供语法高亮以及一些通用的面向对象方法。 Java数据库连接池 BoneCP BoneCP 是一个高性能的开源java数据库连接池实现库。它的设计初衷就是为了提高数据库连接池的性能,根据某些测试数据发现,BoneCP是最快的连接池。BoneCP很小,只有四十几K
Spring4GWT GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet.Applet 简单实现!~ 网页表格组件 GWT Advanced Table GWT Advanced Table 是一个基于 GWT 框架的网页表格组件,可实现分页数据显示、数据排序和过滤等功能! Google Tag Library 该标记库和 Google 有关。使用该标记库,利用 Google 为你的网站提供网站查询,并且可以直接在你的网页里面显示搜查的结果。 github-java-api github-java-api 是 Github 网站 API 的 Java 语言版本。 java缓存工具 SimpleCache SimpleCache 是一个简单易用的java缓存工具,用来简化缓存代码的编写,让你摆脱单调乏味的重复工作!1. 完全透明的缓存支持,对业务代码零侵入 2. 支持使用Redis和Memcached作为后端缓存。3. 支持缓存数据分区规则的定义 4. 使用redis作缓存时,支持list类型的高级数据结构,更适合论坛帖子列表这种类型的数据 5. 支持混合使用redis缓存和memcached缓存。可以将列表数据缓存到redis中,其他kv结构数据继续缓存到memcached 6. 支持redis的主从集群,可以做读写分离。缓存读取自redis的slave节点,写入到redis的master节点。 Java对象的SQL接口 JoSQL JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于【自动提示】的需要(如:Google搜索), 而开发的架构无关的公共控件, 以满足该类需求可以通过快速配置来开发。AutoTips基于搜索引擎Apache Lucene实现。AutoTips提供统一UI。 WAP浏览器 j2wap j2wap 是一个基于Java的WAP浏览器,目前处于BETA测试阶段。它支持WAP 1.2规范,除了WTLS 和WBMP。 Java注册表操作类 jared jared是一个用来操作Windows注册表的 Java 类库,你可以用来对注册表信息进行读写。 GIF动画制作工具 GiftedMotion GiftedMotion是一个很小的,免费而且易于使用图像互换格式动画是能够设计一个有趣的动画了一系列的数字图像。使用简便和直截了当,用户只需要加载的图片和调整帧您想要的,如位置,时间显示和处理方法前帧。 Java的PList类库 Blister Blister是一个用于操作苹果二进制PList文件格式的Java开源类库(可用于发送数据给iOS应用程序)。 重复文件检查工具 FindDup.tar FindDup 是一个简单易用的工具,用来检查计算机上重复的文件。 OpenID的Java客户端 JOpenID JOpenID是一个轻量级的OpenID 2.0 Java客户端,仅50KB+(含源代码),允许任何Web网站通过OpenID支持用户直接登录而无需注册,例如Google Account或Yahoo Account。 JActor的文件持久化组件 JFile JFile 是 JActor 的文件持久化组件,以及一个高吞吐量的可靠事务日志组件。 Google地图JSP标签库 利用Google:maps JSP标签库就能够在你的Web站点上实现GoogleMaps的所有功能而且不需要javascript或AJAX编程。它还能够与JSTL相结合生成数据库驱动的动态Maps。 OAuth 实现框架 Agorava Agorava 是一个实现了 OAuth 1.0a 和 OAuth 2.0 的框架,提供了简单的方式通过社交媒体进行身份认证的功能。 Eclipse的JavaScript插件 JSEditor JSEditor 是 Eclipse 下编辑 JavaScript 源码的插件,提供语法高亮以及一些通用的面向对象方法。 Java数据库连接池 BoneCP BoneCP 是一个高性能的开源java数据库连接池实现库。它的设计初衷就是为了提高数据库连接池的性能,根据某些测试数据发现,BoneCP是最快的连接池。BoneCP很小,只有四十几K
智能发票识别系统 Requirements tomcat服务器 eclipse mysql数据库 redis数据库 Function 自动归类识别机打发票中的发票信息 用户手动绘制用于识别的发票模板和识别区域 查看等待识别发票的任务缓冲队列 可视化发票识别算法的过程,动态展示当前识别的区域和结果 管理员可对系统平台内的用户、用户组进行权限编辑管理 单位负责人可修改使用系统平台的单位信息 个人设置可查看个人信息和权限 Details 前端 Jquery + bootstrap搭建前端框架,处理前端逻辑和展示,负责MVC架构中的View视图层 前后端通过websocket和ajax通信,ajax主要用于按钮等控件的事件处理函数中的请求,websocket用于后端主动向前端推送消息 JSP控制cookies和session,在页面跳转时记录会话用户态,并可通过前端可视化界面对用户权限(用户权限分为继承的用户组权限和个人权限)进行编辑 识别算法的可视化通过websocket实现,算法端将每个区域的识别结果通过后台服务器逐次转送给前端,前端在onmessage回调函数中处理信息并在可视化窗口的canvas画布中显示出来 通过画布的getImageData和putImageData获取图片的像素点,并制造模糊效果,对比突出当前的识别区域 Js + canvas实现用户动态画图的效果,可以在canvas画布中框出自定义的识别区域和填写区域信息 后端 Spring MVC + Spring + JDBC搭建后端框架,Controller负责接收请求,Service负责主要业务逻辑,Dao负责数据库访问 算法端用C++编写,java后端通过多线程+socket+TCP与算法端通讯,利用对象锁完成线程调度 利用redis实现缓冲队列以及模板操作调度队列 利用spring-websocket与前端进行全双工通信 使用shiro作为安全管理框架,通过其内置session实现安全登录,使用shiro注解完成权限管理。 算法端 使用Bag of Words + CNN完成票据分类,根据分类结果查询并获取相应的发票模板。 使用SIFT特征匹配和配准思路完成票据对齐 根据模板中信息区域坐标截取ROI,因为票据可能存在套打情况,故还使用了约束式ROI搜索算法对信息区域进行重定位。 对每个ROI进行去噪、去印章、去直线等预处理操作,并使用形态学处理得到目标文本信息的连通体的最小外接矩形,并将此区域送入OCR模块。 使用搭建好的基于深度学习的卷积神经网络进行文字识别,识别结果组装成协议格式,返回给后台。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值