系列连载 | 带你玩转Netty 之 WebSocket

2,804 阅读5分钟
原文链接: mp.weixin.qq.com

作者:大招

本文为原创文章,转载请注明作者及出

背景

为了支持 CCtalk 在 Web 端观看视频直播,以及实时聊天等功能,传统的短连接无法实现实时推送的目的,需要建立长连接,而 Web 端用户建立连接的成本很低,长连接的资源消耗较大,这里就需要框架对大并发有足够的支持。满足大并发,又支持长连接, Netty + Websocket 是一个不错的解决方案,所以接下来会通过三篇文章,详细介绍一下这块内容。

  1. 原理篇

  2. 应用篇

  3. WebSocket

摘要

前面两篇文章分别给大家介绍了原理篇,应用篇,这回给大家介绍 WebSocket在 Netty 中的应用。

什么是WebSocket

传统的 HTTP 协议只能客户端发起通信,而不能做到服务端主动通知。这里可能有人说可以采用 long polling,也就是客户端不断的向服务端请求,获取新数据,虽然能解决问题,但效率低下,浪费资源,只能说是笨办法。所以WebSocket就出现了。

WebSocket 协议是2008年诞生,2011年成为国际标准,所有浏览器都支持。他最大的特点就是服务端和客户端全双工通信,即客户端能主动给服务端发消息,服务端也能给客户端主动发消息。

该协议默认端口80或者443 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL,例如:

ws://example.com:8080/path

不管是 HTTP 和 Websocket 都是建立在 TC P协议之上, 如下图:

所不一样的就是与服务端交互流程上有所区别,如下图:

从图中可以看到,传统的 HTTP 只有请求和响应,而 WebSocket 分为这几个阶段。

  1. 握手阶段(handshake)

  • 客户端发起握手请求,如下报文:

    Upgrade , Connection 这两个说明目前发起的是WebSocket 协议。

    Sec-WebSocket-Key 是一个 Base64 encode 的值,这个是浏览器随机生成的,用于验证服务端是不是支持websocket协议

    Sec_WebSocket-Protocol 这个是根据业务不同,用户自己定义的,具体体现在 URL 上

  • 服务端响应握手请求,如下报文:

    Sec-WebSocket-Accept 这串东西是经过服务器确认,并且加密过后的 Sec-WebSocket-Key

    可以看出握手阶段采用的是 HTTP 协议,能通过 HTTP 的各种代理服务器,与 HTTP 协议有良好的兼容性。

  1. 长连接阶段

    通过握手从 HTTP 协议升级到了 Websocket 协议,

    这个时你可以向服务端发送任何消息,服务也可以向你主动推送消息。数据格式可以根据业务需要与服务端商议,灵活度高,通信效率高效。

  2. 连接关闭客户端与服务端其中一方发起关闭即为关闭。

WebSocket应用

我们主要使用 Netty 框架来实现服务端,采用 JS 来实现客户端。

服务端实现

主要使用了 Netty-socketio 这个开源框架,基于 Netty 框架里面封装了WebSocket 的协议解析,响应,如果你是 maven 工程,可以在 pom.xml 中添加 dependecy。

<dependency> <groupId>com.corundumstudio.socketio</groupId> <artifactId>netty-socketio</artifactId> <version>1.7.12</version></dependency>

应用起来比较简单,如下代码:

public static void main( String[] args ) throws InterruptedException {  final SocketIOServer server = new SocketIOServer(loadSocketIOConfig(doc)); // (1) // 启动login final SocketIONamespace ns = server.addNamespace("/echo"); // (2) EchoConnectListener listener = new EchoConnectListener(); // (3) EchoEventListener evtlistener = new EchoEventListener(); // (4) ns.addConnectListener(listener); ns.addDisconnectListener(listener); ns.addEventListener(WebEventMessage.EVT_TAG, WebEventMessage.class, evtlistener);  // (5) server.start(); logger.info("============>WSServer started ... ok");}private static Configuration loadSocketIOConfig(Document doc) { Configuration config = new Configuration(); config.setHostname(getNodeStr(doc, "host")); config.setPort(getNodeInt(doc, "port")); config.setBossThreads(getNodeInt(doc, "bossThreads")); config.setWorkerThreads(getNodeInt(doc, "workerThreads")); //config.setUseLinuxNativeEpoll(PlatformDependent.isWindows() ? false : true); return config;}
public class EchoEventListener implements DataListener<WebEventMessage> {private static final Logger logger = LoggerFactory.getLogger(EchoEventListener.class); public EchoEventListener() { }@Override public void onData(SocketIOClient client, WebEventMessage data, AckRequest ackSender) throws Exception { System.out.println("message:" + data.getMessage()); WebEventMessage hello = new WebEventMessage(); JSONObject object = new JSONObject(data.getMessage()); if (UserTokenMgr.getInstance().verify(object.getInt("userid"), object.getString("token"))) { hello.setMessage("login success"); } else { hello.setMessage("login failed"); } client.getNamespace().getBroadcastOperations().sendEvent(WebEventMessage.EVT_TAG, hello); // (6) }}

注:示例代码中的阿拉伯数字与文字说明中的阿拉伯数字是一一对应解释。

  1. 加载配置,实例化 SocketIOServer, 配置主要是ip,端口,处理线程和工作线程数量。

  2. 加入一个命名为echo的Namespace,这个具体访问是在url体现的,如下:

    ws://example.com:8080/echo

    只要是这个url的访问,都会在这个namespace处理。

  3. 创建连接监听器

  4. 创建业务消息处理器

  5. 将连接监听器,和业务消息处理器添加到 namespace, 在添加业务消息处理器时,需要指定消息协议对象,比如 WebEventMessage。

  6. 服务端接收数据,并返回给客户端。

客户端实现

客户端这里采用的是 socket.io, 采用 JS 实现,与前端对接。

var socket = io.connect('http://127.0.0.1:8080/echo');socket.on('connect', function() { output('<span class="connect-msg">Client has connected to the server!</span>');}); // (1)socket.on('message', function(data) { output('<span class="username-msg"> echo' + ':</span> ' + data.message);}); // (2)socket.on('disconnect', function() { output('<span class="disconnect-msg">The client has disconnected!</span>');}); // (3)

发送数据:

var message = JSON.stringify(jsonmsg);var cmd = 1;var jsonObject = {'@class': 'WebEventMessage', cmd:cmd, message: message}; socket.emit('message', jsonObject, function(arg1, arg2) {}); // (4)

注:示例代码中的阿拉伯数字与文字说明中的阿拉伯数字是一一对应解释。

  1. connect连接建立后回调函数。

  2. message用于指定收到服务器数据后的回调函数。

  3. disconnect断开连接后回调函数。

  4. emit向服务器发送消息。

总结

基于 Netty 的 Websocket,在提供良好性能的同时大大增强直播,聊天等产品在前端页面的体验。目前 CCtalk 的 Web 端直播,以及正在开发的另一款产品都使用了这一技术, 大家可以打开 CCtalk (http://www.cctalk.com/)网站 随便找一节直播课看看。这三篇文章主要还是抛砖迎玉,提供一个解决方案,如果有更好的,或者有其他问题,欢迎随时交流。

参考链接

  1. Sokect.io(https://socket.io/)

  2. netty-socketio(https://github.com/mrniko/netty-socketio)

  3. WebSocket 教程(http://www.ruanyifeng.com/blog/2017/05/websocket.html)

推荐系统那些事儿

一个关于 nolock 的故事

交易系统 - 领域驱动设计浅析

基于 Electron 的爬虫框架 Nightmare

翻译 | Android O 中的 seccomp 过滤器