前言
本文是websocket客户端、服务器开发总结文档,记录从资料收集、代码编写到程序测试等需要注意的事项,帮助同样需要开发websocket的同学能快速完成开发任务。
一、websocket资料
1.什么是websocket
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。 WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。 而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。 在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
2.websocket优缺点
1、较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。 2、更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。 3、保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。 4、更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。 5、可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。 6、更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
3.WebSocket 原理
websocket原理可以看这位大神的笔记:https://www.zhihu.com/question/20215561
4.WebSocket 源码下载
本文websocket开发使用的开源库是libwebsocket,其官网可以查看库的相关资料。在Getting started-》Browse git可以下载源码,当前使用的版本是v4.2。当然,你也可以从GitHub上下载源码。
二、客户端
1.开发
在下载的库中,有客户端的许多demo,路径:libwebsockets-main\minimal-examples\ws-client。客户端的目的是根据URL连接服务器,接受服务器推送来的数据以及把客户端的数据发送到服务器。
客户端的关键代码解析: 1、connect_client填写的user可以在callback_client中的user获取,是一个上下文的用户数据,在处理业务的时候需要用到; 2、连接成功后,会触发LWS_CALLBACK_CLIENT_ESTABLISHED事件; 3、当服务器向客户端推送数据时,触发LWS_CALLBACK_CLIENT_RECEIVE,此时可以处理收到的数据,in是数据,len是数据长度; 4、当需要给服务器发数据的时候,需要手动触发LWS_CALLBACK_CLIENT_WRITEABLE事件,触发方法:调用lws_callback_on_writable((struct lws*)(client_wsi))方法,数据可以通过user保存,在callback_client中取出后发送。 5、客户端需要收到数据,必须一直调用lws_service(context, 0)函数。只有调用这个函数,回调函数才会接收来自服务器的函数。我们可以启动一个定时器或线程,一直执行这个函数即可。 关键源码如下:
static int callback_client(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
char* out_payload = NULL;
int out_payload_len = 0;
int write_len = 0;
char* sen_data = NULL;
switch (reason) {
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
log_warn("CLIENT_CONNECTION_ERROR: %s\n", in ? (char *)in : "(null)");
break;
case LWS_CALLBACK_CLIENT_ESTABLISHED:
log_info("%s: established\n", __func__);
break;
case LWS_CALLBACK_CLIENT_RECEIVE:
log_info("RX: %s\n", (const char *)in);
break;
case LWS_CALLBACK_CLIENT_CLOSED:
break;
case LWS_CALLBACK_CLIENT_WRITEABLE:
write_len = lws_write(wsi, (unsigned char*)sen_data, send_data_len, LWS_WRITE_TEXT);
break;
default:
break;
}
return 0;
}
static int connect_client(void* user,struct lws_context *context, const char* protocol_name, void* client_wsi) {
int ret = 0;
struct lws_client_connect_info i;
memset(&i, 0, sizeof(i));
i.context = context;
i.port = 1881;
i.address = localhost;
i.path = '/';
i.host = i.address;
i.origin = i.address;
i.protocol = protocol_name;
i.pwsi = (struct lws**)&(client_wsi);
i.userdata = user;
if (!lws_client_connect_via_info(&i)) {
ret = -1;
}
url_destroy(server_url);
return ret;
}
static int websocket_client_init(void* user) {
struct lws_context *context;
const struct lws_protocols protocols[] = {
{ "lws-minimal-client", callback_client, 0, 0, 0, node, 0 },
LWS_PROTOCOL_LIST_TERM
};
struct lws_context_creation_info info;
int ret = 0;
memset(&info, 0, sizeof info);
log_info("LWS minimal ws client\n");
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
info.port = CONTEXT_PORT_NO_LISTEN;
info.protocols = protocols;
info.timeout_secs = 10;
info.connect_timeout_secs = 30;
info.gid = -1;
info.uid = -1;
info.fd_limit_per_thread = 1 + 1 + 1;
context = lws_create_context(&info);
if (!context) {
log_info("lws init failed\n");
return -1;
}
return connect_client(user, context, protocols[0].name);
}
2.测试
测试客户端,我们需要一个服务器,我们可以利用node-red搭建一个简易的websocket服务器,用来测试我们的客户端是否工作正常。关于node-red搭建客户端和服务器做测试,可以参照这个大神的教程。
三、服务器
1.开发
在下载的库中,有服务器端的许多demo,路径:libwebsockets-main\minimal-examples\ws-server。服务器端的目的是创建websocket服务器,接受客户端推送来的数据以及把数据发送到客户端。
服务器的关键代码解析: 1、websocket_listener_init填写的user可以在callback_server中获取,是一个上下文的用户数据,在处理业务的时候需要用到,获取方法:lws_context_user(lws_get_context(wsi)),我们可以用这个方式获取上下文传递的user数据。这和客户端获取user数据是不同的,有点绕,本人也是研究了好一会才弄明白的。demo的user参数使用起来更迷糊,不看文档,根本不会用。 2、有客户端连接本服务器成功后,会触发LWS_CALLBACK_CLIENT_ESTABLISHED事件,我们可以在这里记录连接进来的客户端; 3、当服务器向客户端推送数据时,需要触发LWS_CALLBACK_CLIENT_WRITEABLE事件,触发方法:调用lws_callback_on_writable((struct lws*)(client_wsi))方法。需要发送的数据,需要事先保存在websocket_listener_init时的user对象内,在LWS_CALLBACK_CLIENT_WRITEABLE分支取出数据,发送给指定客户端。 4、服务器端需要收到数据,必须一直调用lws_service(context, 0)函数。只有调用这个函数,回调函数才会接收来自客户端的函数。我们可以启动一个定时器或线程,一直执行这个函数即可。 关键源码如下:
static int callback_server(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
int write_len = 0;
char* sen_data = NULL;
struct lws *client_wsi = NULL;
struct session_data* client_session_data = NULL;
switch (reason) {
case LWS_CALLBACK_ESTABLISHED:
log_info("%s: established\n", __func__);
break;
case LWS_CALLBACK_CLOSED:
log_info("%s: closed\n", __func__);
break;
case LWS_CALLBACK_SERVER_WRITEABLE:
write_len = lws_write(wsi, (unsigned char*)sen_data, send_data_len, LWS_WRITE_TEXT);
break;
case LWS_CALLBACK_RECEIVE:
break;
case LWS_CALLBACK_WSI_DESTROY:
break;
default:
break;
}
return 0;
}
static void* websocket_listener_init(void* user) {
struct lws_context *context;
const struct lws_protocols protocols[] = {
{ "ws", callback_server, 0, 0, 0, node, 0 },
LWS_PROTOCOL_LIST_TERM
};
struct lws_context_creation_info info;
memset(&info, 0, sizeof info);
log_info("LWS ws server\n");
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | LWS_SERVER_OPTION_VALIDATE_UTF8;
info.port = 1881;
info.protocols = protocols;
info.timeout_secs = 10;
info.connect_timeout_secs = 30;
info.gid = -1;
info.uid = -1;
info.user = user;
context = lws_create_context(&info);
if (!context) {
log_info("lws init failed\n");
return RET_FAIL;
}
return context;
}
2.测试
测试服务器端,我们需要一个客户端,我们可以利用node-red搭建一个简易的websocket客户端,用来测试我们的服务器是否工作正常。关于node-red搭建客户端和服务器做测试,可以参照这个大神的教程。
总结
websocket是HTTP协议的升级,是一个全新的协议,使用libwebsockets库开发客户端和服务器是非常方便的,代码也比较相似,关键是需要理清工作流程,把自己的业务代码添加进这个框架内即可。
|