Netty实战(一) —— 编写WebSocket服务器

6,587 阅读5分钟

Netty在Java程序之间通信很方便,直接用Netty的客户端和服务器端就可以了。但是如果客户端是浏览器怎么办(Java程序一般不会开发Web应用),这时候的客户端是个JavaScript程序,进行相互通信就要用到WebScoket协议啦。

WebSocket协议

概念

熟悉Java网络编程的应该都会了解Socket编程,它是用于Java程序之间通信的技术(Socket编程也叫套接字编程,它是在TCP/IP协议中传输层和应用层之间的一个抽象层,提供接口方便应用层的调用以实现网络之间的通信。Socket编程又分为OIO和NIO,Netty是在NIO的基础上封装的一个通信框架)。

那么WebSocket就很好理解了,前面加了个Web说明这个协议是用于Web应用中的通信(类似Socket也是双向通信)。那Web不是有了HTTP嘛,还要WebSocket干嘛。HTTP有一个缺陷就是,通信只能由客户端发起,而服务器不能主动向客户端发起通信。这就导致客户端要想获取服务器的变化或通知,只能通过轮询或保持HTTP长连接,这样做是非常浪费资源的。WebSocket就是为了解决这个问题而诞生的。

维基百科:

WebSocket是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

特点

它的特点主要有以下几点。

  1. 建立在 TCP 协议之上,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向通信
  2. 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  3. 数据格式比较轻量,性能开销小,通信高效。可以发送文本,也可以发送二进制数据。
  4. 没有同源限制,客户端可以与任意服务器通信。
  5. 协议标识符是ws(如果使用SSL/TLS加密,则为wss),服务器网址就是 URL。

基于Netty编写WebSocket服务器

因为WebSocket协议是用于Web应用通信的,所以它天生支持用Node来编写服务器。所以就有了很多现成的基于node的websocket服务器。而Java编写Websocket服务器一般都是在Socket编程上再封装一层,这里当然推荐使用Netty来搭建啦。毕竟Netty是高性能的通信框架,它也是在NIO的基础上封装的,操作更加的简单。

服务器主程序

主程序很简单,就和普通的Netty服务器端一样。对Webscoket的封装主要是处理器中。

public class WsServer {

    public static void main(String[] args) {
        // 一个主线程组(用于监听新连接并初始化通道),一个分发线程组(用于IO事件的处理)
        EventLoopGroup mainGroup = new NioEventLoopGroup(1);
        EventLoopGroup subGroup = new NioEventLoopGroup();
        ServerBootstrap sb = new ServerBootstrap();
        try {
            sb.group(mainGroup, subGroup)
                    .channel(NioServerSocketChannel.class)
                    // 这里是一个自定义的通道初始化器,用来添加编解码器和处理器
                    .childHandler(new WsChannelInitializer());
            // 绑定88端口,Websocket服务器的端口就是这个
            ChannelFuture future = sb.bind(88).sync();
            // 一直阻塞直到服务器关闭
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            mainGroup.shutdownGracefully();
            subGroup.shutdownGracefully();
        }
    }
}

通道初始化器

由于Websocket协议是基于HTTP协议的(握手阶段使用HTTP协议),所以需要添加HTTP的编解码器以及消息处理器。还要添加Webscoket的处理器,用于处理Webscoket的握手以及数据传输,最后添加一个自定义的处理器,用于处理IO事件与客户端进行交互。

public class WsChannelInitializer extends ChannelInitializer {

    @Override
    protected void initChannel(Channel ch) {
        ChannelPipeline pipeline = ch.pipeline();
        // websocket是基于http协议的,所以需要使用http编解码器
        pipeline.addLast(new HttpServerCodec())
                // 对写大数据流的支持
                .addLast(new ChunkedWriteHandler())
                // 对http消息的聚合,聚合成FullHttpRequest或FullHttpResponse
                // 在Netty的编程中,几乎都会使用到这个handler
                .addLast(new HttpObjectAggregator(1024 * 64));
        // 以上三个处理器是对http协议的支持

        // websocket 服务器处理的协议,并用于指定客户端连接的路由(这里指定的是 /ws)
        // 这里的URL就是 ws://ip:port/ws
        // 该处理器为运行websocket服务器承担了所有繁重的工作
        // 它会负责websocket的握手以及处理控制帧
        // websocket的数据传输都是以frames进行的
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
        // 自定义的处理器
        pipeline.addLast(new WsServerHandler());
    }
}

自定义处理器

这个处理器要记录和管理所有的客户端通道,并接收客户端发来的消息,进行相应的IO事件的处理(可以在初始化连接的时候绑定客户端用户唯一标识和该客户端对应的通道,然后就可以进行定向推送消息了)。

// TextWebSocketFrame: 在Netty中,专门用于websocket处理文本消息的对象,frame是消息的载体
public class WsServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    /**
     * 用于记录和管理所有客户端的channel
     */
    private ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        // 获取客户端传输来的文本消息
        String text = msg.text();
        // 这个是自定义的日志工具类,可见其它文章
        LogUtil.info("收到的文本消息:[{}]", text);
        // 在这里可以判断消息类型(比如初始化连接、消息在客户端间传输等)
        // 然后可以将客户端Channel与对应的唯一标识用Map关联起来,就可以做定向推送,而不是广播

        // 写回客户端,这里是广播
        clients.writeAndFlush(new TextWebSocketFrame("服务器收到消息: " + text));
    }

    /**
     * 当客户端连接服务端(打开连接)后
     * 获取客户端的channel,并放到ChannelGroup中进行管理
     * @param ctx ChannelHandlerContext
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        // 不能在这里做关联,因为这里不能接受客户端的消息,是没法绑定的
        clients.add(ctx.channel());
    }

    /**
     * 当触发当前方法时,ChannelGroup会自动移除对应客户端的channel
     * @param ctx ChannelHandlerContext
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        LogUtil.info("客户端断开连接,channel的长ID:[{}]", ctx.channel().id().asLongText());
        LogUtil.info("客户端断开连接,channel的短ID:[{}]", ctx.channel().id().asShortText());
    }
}