一.案例描述
Websocket是一种在单个TCP连接上进行全双工通信的协议。它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
websocket在数据交互时有以下优点:
- 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
- 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
- 保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
- 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
- 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
- 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
注:http长连接与websocket的区别
http1.1 出了新头,如果请求头中包含 keep-alive,那么这个 http 请求发送收到返回之后,底层的 tcp 连接不会立马断掉,如果后续有 http 请求还是会利用。但是这个连接保持一来是没有硬性规定时间的,由浏览器和服务端实现来控制。二来这个连接不断是指底层 tcp 连接,不是说一次 http 请求收到返回之后不会断掉,还能再收服务端的返回(如果服务端对这次 http 请求立马返回,那么这次 http 请求就结束了。这种不是应用层面的长连接,其实和模拟 WebSocket 没啥关系。
coment技术是一种 hack 技术,即浏览器发送一个 http 请求,但是服务端不是立马返回,服务端一直不返回直到有浏览器需要的内容了在返回。期间这个 http 请求可以连着维持比较长的时间(在服务端返回之前)。这样模拟一种服务端推送机制。因为浏览器请求的时候等于先把连接建立好,等服务端有消息需要返回时再返回给浏览器。
说一下coment与websocket的区别:
comet 发送 http 请求后服务端如果没有返回则连接是一直连着的,等服务端有东西要“推送”给浏览器时,相当于给之前发送的这个 http 请求回了一个 http 响应。然后这个保持的时间比较长的 http 连接就断了。然后浏览器再次发送一个 http 请求,服务器端再 hold 住不返回,等待有东西需要“推送”给浏览器时,再给这个 http 请求一个响应,然后断开连接。循环往复。一旦浏览器不给服务器发送 http 请求,那么服务器是不能主动给浏览器推送消息的,因为根本没有连着的连接给你推。
WebSocket 则不同,它握手后建立的连接是不会断的(除了意外情况和程序主动掐断)。不需要浏览器在每次收到服务器推送的消息后再发起请求。而且服务器端可以随时给浏览器推送消息,不需要等浏览器发 http 请求,因为 WebSocket 的连接一直在没断。
为什么会有这样的区别呢?
这是协议层面的区别。http 协议规定了 http 连接是一个一来(request)一回(response)的过程。一个请求获得一个响应后必须断掉。而且只有先有请求才会有响应。拿 http1.1 keep-alive 来说,即使底层 tcp 连接没有断,服务端无缘无故给浏览器发一个 http 响应,浏览器是不收的,他找不到收的人啊,因为这个响应没有对应的请求。你看 ajax 必须先发请求才会有一个 onsuccess 回调来响应这个请求。这个 onsuccess 的回调是不会在你 ajax 不发送的情况下被调用到的。
二. 案例分析
1.环境搭建
在实际项目中后端服务器使用c++进行实现,故要使用到libwebsockets-master。
libwebsocket github下载地址:
https://github.com/warmcat/libwebsockets
使用流程:
unzip libwebsockets-master.zip
cd libwebsockets-master/
mkdir bulid
cd build/
cmake ../
make
make install
最终可以在build中的lib目录中得到以下文件
其中libwebsockets.so.16是我们需要的。
2.libwebsocket接口浅析
(1)注册协议回调表
struct TSessionData {
long lContext;
u8 byIsHttp; //是否是Http
};
struct libwebsocket_protocols protocols[] =
{
{
"http", /* 协议名:其与Sec-Websockets-Protocol字段对应 */
ProtobufCB, /* 回调函数:协议对应的回调处理函数 */
DATA_BUFSIZE + LWS_PRE, /* 自定义数据空间大小:每个ws连接均会分配一个自定义数据空间 */
DATA_BUFSIZE, /* 首发缓存 */
NULL, /*固定格式*/
&Tsession /*上下文,自定义结构体*/
},
{
"ws", /* 协议名:其与Sec-Websockets-Protocol字段对应 */
ProtobufCB, /* 回调函数:协议对应的回调处理函数 */
DATA_BUFSIZE + LWS_PRE, /* 自定义数据空间大小:每个ws连接均会分配一个自定义数据空间 */
DATA_BUFSIZE, /* 首发缓存 */
NULL, /*固定格式*/
&Tsession /*上下文,自定义结构体*/
},
{ NULL, NULL, 0, 0 } /* 结束标识 */
};
注:libwebsockets支持http和ws同时使用,监听一个端口,但是protocols中0下标必须为http。
(2)回调函数
回调函数的原型:
ws_service_callback(
strcut lws *wsi,
enum lws_callback_reasons reason,
void *user,
void *in,
size_t len)
序号 | 变量名 | 类型 | 含义 |
1 | context | struct lws* | 全局上下文,负责ws连接的维护和管理 |
2 | reason | enum lws_callback_reasons | 调用回调回调函数的事件 |
3 | pContext | void* | 上下文,用户自定义结构 |
4 | pData | void* | 输入数据 |
5 | len | size_t | 输入数据长度 |
(3)回调事件详解
序号 | 状态值 | 含义 |
1 | LWS_CALLBACK_WSI_CREATE | 正在创建ws连接对象 注:此时回调函数中的参数wsi对象与user对象依然为空指针,因此还不能初始化用户自定义对象 回调函数的参数含义: |
2 | LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION | 此reason可以用来过滤协议 注:在此处返回非0值时,lws库将会关闭该链接;该处返回0时,表示ws连接已经建立成功。此时表1中的wsi对象和user对象已不为空,因此,此时可以对用户自定义对象user进行初始化处理。 |
3 | LWS_CALLBACK_LOCK_POLL | 添加保护ws连接状态的互斥锁 |
4 | LWS_CALLBACK_UNLOCK_POLL | 解除保护ws连接状态的互斥锁 |
5 | LWS_CALLBACK_CLIENT_RECEIVE | 数据出现在客户端连接的服务器上时,可以在通过*in处找到,长度为len字节。 |
6 | LWS_CALLBACK_RECEIVE | 收到一帧完整数据 注:此时表 in表示收到的数据,len表示收到的数据长度。需要注意的是:指针in的回收、释放始终由LWS框架管理,只要出了回调函数,该空间就会被LWS框架回收。因此,开发者若想将接收的数据进行转发,则必须对该数据进行拷贝。
|
7 | LWS_CALLBACK_SERVER_WRITEABLE | 此ws连接为可写状态 注:表示wsi对应的ws连接当前处于可写状态,即:可发送数据至客户端。 |
8 | LWS_CALLBACK_CLOSED | ws连接已经断开 |
9 | LWS_CALLBACK_WSI_DESTROY | 正在销毁ws连接对象 |
10 | LWS_CALLBACK_HTTP | 收到http消息头 |
11 | LWS_CALLBACK_HTTP_BODY | 收到http消息体 |
(4)重要函数说明
序号 | 函数名 | 功能 |
1 | lws_get_peer_write_allowance | 该ws连接允许发送的字节数 |
2 | lws_send_pipe_choked | 判断ws连接是否阻塞 |
3 | lws_write | 将数据发送给对端 备注:函数参数说明 wsi: ws连接对象 buf: 需要发送数据的起始地址。 注意:必须在指针buf前预留长度为LWS_SEND_BUFFER_PRE_PADDING的空间,同时在指针buf+len后预留长度为LWS_SEND_BUFFER_POST_PADDING的空间。 len: 需要发送数据的长度 protocol: 如果该连接是http连接,则该参数的值为LWS_WRITE_HTTP;如果该连接是ws连接,则该参数的值为LWS_WRITE_BINARY,但如果第一次发送的数据长度n < len,则发送后续长度为(len - n)字节的数据时,该参数值改为LWS_WRITE_HTTP。 |
4 | lws_http_transaction_completed | 当前连接为http连接,而非ws连接时,如果当前http请求的应答数据发送完毕,则可使用该函数重置http连接的相关状态,只有收到新的http请求才能激活该http连接。 |
5 | lws_callback_on_writable | 将ws连接加入可写事件监听 |
6 | lws_callback_on_writable_all_protocol | 将某个协议的所有ws连接加入可写事件监听 注:在网络中存在各种情况可能导致服务端并不知道与客户端的连接已经断开:比如客户端掉电。为了应对这种情况的存在。需要每隔一段事件执行该函数,再在协议的回调函数的LWS_CALLBACK_SERVER_WRITEABLE事件中判断一下是否超时,如果超时则返回非零值。 |
三.遇到的问题及分析
1.在调用lws_write时候会发生崩溃
原因分析: 在某一链路断开的时候,如果自己管理链路没有释放该wsi,回复消息的时候依然向该wsi的链路发送消息就会出现崩溃。在libwebsocket库中,可以在回复消息的时候,调用lws_callback_on_writable,然后在回调消息中会收到LWS_CALLBACK _SER VER _WRITEABLE消息,此时调用lws_write可以避免此问题。
2.在同步消息处理中阻塞问题
原因分析:在收到某条消息进行转发处理中,如果一直没有该处理接口出来,就会造成阻塞,导致新的wsi无法连接上,原来的wsi也会出现无法继续通信。解决方法:可以采用异步处理方法,采用消息队列,先存起来然后逐一处理。
3. LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION中的问题
在同一个wsi可能多次由改reason调用回调,且该wsi对象的用户自定义数据的指针会发生变化,导致用户设置的数据丢失,造成严重后果。解决方案:不让lws维护对象,而是我们自己申请和维护数据。
4.ws_server集成http
libwebsocket该库中,支持监听一个端口,同时使用websocket与http协议。存在的问题:在http消息回复时候结束时候,一定要调用该接口lws_http_transaction_completed,否则http第二次请求会造成阻塞,因为http的状态没有刷新。第二点是在回复消息的时候需要带http头,在libwebsocket中有可以使用的接口——lws_add_http_header_by_name。
此外HTTP 协议有一个缺陷:通信只能由客户端发起,做不到服务器主动向客户端推送信息。websocket解决了这个问题。
四.总结
当今客户端已经由C/S架构逐渐过渡到B/S架构,所以websocket显得日益重要,为web的后端奠定了基础,所以在链路处理的细节中决定了服务器的稳定性。