Netty WebSocket 协议

1. 前言

上一节,我们主要讲解了 Http 协议,本节我们来讲解 Netty 支持的另外一个 WebSocket 协议,WebSocket 是 HTML5 开始支持的全双工协议。

在真实的开发当中,其实 WebSocket 使用非常的广泛,特别是在目前前端技术越来越完善的情况下,很多应用都是基于前端技术去实现,比如:APP、小程序等主流应用,前端的技术完全可以开发出类似原生技术一样的产品,并且开发效率上比原生更加的快、用户体验更好。
这些应用涉及通信、聊天、推送等业务,则可以使用 WebSocket 来实现,因此,WebSocket 已经是目前非常主流的浏览器和服务端建立长连接的通信技术。

WebSocket 协议架构图如下所示:
图片描述

2. 学习目的

Netty 虽然可以开发客户端和服务端的应用,但是 Netty 和 WebSocket 结合则是另外一个不同的应用方向。

应用方向一: 纯后端的应用,比如:框架、中间件通信等,则完全可以使用 Netty 来开发客户端和服务端,并且双方通过 TCP 协议来进行通信。

应用方向二: 前端(浏览器)和服务端之间通信,并且想实现类似长连接的效果,那么 WebSocket 和 Netty 则是主流,并且这部分的应用场景非常的广泛和运用非常的多。

因此,我们在学习完本章内容之后,我们就可以掌握这类的需求的开发(比如:聊天、消息推送),自己可以尝试去开发一款小程序在线聊天系统。

当真实的开发当中,也许很多人直接使用 Spring 封装的 WebSocket 或者其它第三方的框架(比如:netty-socketio)去实现。本节我们主要讲解原生 Netty 来开发 WebSocket 协议格式的数据请求,毕竟会用和知道知其所以然还是有区别的。

3. WebSocket 的 API

创建 WebSocket 对象,第一个参数 url, 指定连接的 URL。第二个参数 protocol 是可选的,指定了可接受的子协议。

var Socket = new WebSocket(url, [protocol] );

WebSocket 事件

事件 事件处理程序 描述
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发

4. 环境搭建

本节,我们主要开发一个 Demo,主要功能如下:

  1. 前端页面(html)启动的时候,连接 WebSocket 服务端;
  2. 服务端往前端每隔 5s 推送一条消息。

环境搭建步骤:

  1. 创建一个 Maven 项目;
  2. 导入 Netty 坐标。

实例:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.6.Final</version>
</dependency>

5. 代码实现

5.1. Netty 主启动类

public class MyWebSocketServer {
    public static void main(String[] args) throws Exception{
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup);
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();

                    //因为基于http协议,使用http的编码和解码器
                    pipeline.addLast(new HttpServerCodec());

                    //是以块方式写,添加ChunkedWriteHandler处理器
                    pipeline.addLast(new ChunkedWriteHandler());
                    
                    //http数据在传输过程中是分段, HttpObjectAggregator ,就是可以将多个段聚合
                    pipeline.addLast(new HttpObjectAggregator(8192));
                    
                    //将 http协议升级为 ws协议 , 保持长连接
                    pipeline.addLast(new WebSocketServerProtocolHandler("/hello2"));
                    
                    //自定义的handler ,处理业务逻辑
                    pipeline.addLast(new MyWebSocketHandler());
                }
            });

            //启动服务器
            ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();

        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

代码说明:

  1. WebSocket 比 TCP 和 Http 协议都稍微复杂有点,它其实是 TCP 和 Http 协议的结合,首先是连接之前发送的是 Http 协议请求,但是新增 Http 所没有的附加头信息 Upgrade: WebSocket 表明是一个申请协议升级的 Http 请求。其次,真正建立连接之后,其实底层是 TCP 协议长连接;
  2. HttpServerCodec 将请求和应答消息解码为 HTTP 消息;
  3. ChunkedWriteHandler 向客户端发送 HTML5 文件;
  4. http 数据在传输过程中是分段,当浏览器发送大量数据时,就会发出多次 http 请求, HttpObjectAggregator 可以将 HTTP 消息的多个部分合成一条完整的 HTTP 消息;
  5. WebSocketServerProtocolHandler 定义了 WebSocket 的对外暴露地址。

5.2 WebSocket 业务类

public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{
    private SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //1.获取Channel通道
        final Channel channel=ctx.channel();
        
        //2.创建一个定时线程池
        ScheduledExecutorService ses=Executors.newScheduledThreadPool(1);
        //3.一秒钟之后只需,并且每隔5秒往浏览器发送数据
        ses.scheduleWithFixedDelay(new Runnable() {
            public void run() {
                String sendTime=format.format(new Date());
                channel.writeAndFlush(new TextWebSocketFrame("推送时间=" + sendTime));
            }
        },1,5, TimeUnit.SECONDS);
    }

    //接受浏览器消息
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {

        System.out.println("收到消息 " + msg.text());
    }

    //当web客户端连接后,触发方法
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
		
    }

    //当web客户端断开后,触发方法
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

    }
}

代码说明:

其实 WebSocket 对于的 Handler 跟我们普通业务的 Handler 没有什么区别,这里主要使用定时线程池定时往浏览器推送消息,这个是传统的 Http+Ajax 请求无法实现的逆向推送效果。

5.3. 前端代码

function WebSocketTest() {
    if ("WebSocket" in window) {
        var ws = new WebSocket("ws://localhost:7000/hello2");
        
        //发送数据
        ws.onopen = function() {
            ws.send("发送数据");
        };

        //接受数据
        ws.onmessage = function(evt) {
            var received_msg = evt.data;
            console.log(">>>"+received_msg)
        };

        //监听连接关闭
        ws.onclose = function() {
            alert("连接已关闭...");
        };
    } else {
        alert("您的浏览器不支持 WebSocket!");
    }
}

5.4 测试效果

浏览器控制台每隔 5s 钟就能收到服务端推送过来的消息,如下图所示:

图片描述

6. 小结

  1. 我们需要了解 WebSocket 的应用方向,主要是前端和服务端建立长连接通信;
  2. 掌握 WebSocket 几个事件监听方法,open ()、message ()、error ()、close ();
  3. 掌握 Netty 编写的服务端写法,主要是几个类 HttpServerCodec、ChunkedWriteHandler、HttpObjectAggregator、WebSocketServerProtocolHandler,Handler 业务跟普通业务没啥区别。