websocket客户端和服务器开发总结


前言

本文是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)");
		//config_websocket_client->client_wsi = 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; /* we do not run any server */
	info.protocols = protocols;
  info.timeout_secs = 10;
	info.connect_timeout_secs = 30;
  info.gid = -1;
  info.uid = -1;
  /*
	 * since we know this lws context is only ever going to be used with
	 * one client wsis / fds / sockets at a time, let lws know it doesn't
	 * have to use the default allocations for fd tables up to ulimit -n.
	 * It will just allocate for 1 internal and 1 (+ 1 http2 nwsi) that we
	 * will use.
	 */
  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库开发客户端和服务器是非常方便的,代码也比较相似,关键是需要理清工作流程,把自己的业务代码添加进这个框架内即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值