《Netty权威指南 第2版》学习笔记(7)--- WebSocket协议开发应用

前言

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

HTTP协议的弊端

很多网站为了实现推送技术,所用的技术都是轮询,轮询是在特定的时间间隔,由浏览器对服务器发送HTTP请求,然后由服务器返回最新的数据给客户端的浏览器,这种方式的主要弊端如下。

(1)HTTP协议为半双工协议,半双工协议指数据可以在客户端和服务端两个方向上传输,但是不能同时传输,它意味着在同一个时刻,只有一个方向上的数据传送。

(2)HTTP消息可能包含较长的消息头、消息体、其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

(3)针对服务器推送的黑客攻击,例如长时间轮询。

而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。

在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

WebSocket优点

在WebSocket中,浏览器和服务器只需要完成一次握手的过程,之后,浏览器和服务器之间就形成了一条快速通道,两者就可以直接互相传送数据了,WebSocket基于TCP双向全双工进行消息传递,在同一时刻,既可以发送消息,也可以接收消息,相比HTTP的半双工协议,性能得到很大提升。

具体优点如下:

(1)较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。

(2)更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。

(3)保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。

(4)更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。

(5)可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。

(6)更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

Netty WebSocket开发

我们将通过使用WebSocket协议来实现一个基于浏览器的聊天应用程序,并使多个用户之间可以同时进行相互通信,某一个客户端发送的消息,将被广播到所有其他连接的客户端上。

服务端开发


import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.util.concurrent.ImmediateEventExecutor;

public class ChatServer {
    
    
    /*
    创建DefaultChannelGroup,其将保存所有已经连接的WebSocket Channel
     */
    private static final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);

    public static void main(String[] args) throws InterruptedException {
    
    
        EventLoopGroup group = new NioEventLoopGroup();
        try {
    
    
            ServerBootstrap b = new ServerBootstrap();
            b.group(group)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
    
    
                        protected void initChannel(SocketChannel ch) throws Exception {
    
    
                            ChannelPipeline pipeline = ch.pipeline();
                            /*
                            HttpServerCodec同时包含了(HttpRequestDecoder, HttpResponseEncoder),
                            通过HttpServerCodec将请求和响应编码或者解码为HTTP消息
                             */
                            pipeline.addLast(new HttpServerCodec());
                            //聚合http为一个完整的报文
                            pipeline.addLast(new HttpObjectAggregator(65536));
                            /*
                            指定"/ws"为WebSocket升级请求的路径。
                            并按照 WebSocket 规范的要求,处理 WebSocket 升级握手、
                            PingWebSocketFrame 、 PongWebSocketFrame 和 CloseWebSocketFrame
                             */
                            pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
                            /*
                            处理FullHttpRequest请求。
                            在InitChatRoomHandler中,生成聊天室的HTML页面,并指定使用WS协议通信。
                             */
                            pipeline.addLast(new InitChatRoomHandler());
                            //处理 TextWebSocketFrame 和握手完成事件
                            pipeline.addLast(new TextWebSocketFrameHandler(channelGroup));
                        }
                    });
            ChannelFuture future = b.bind(8088).sync();
            future.channel().closeFuture().sync();
        } finally {
    
    
            group.shutdownGracefully();
        }
    }
}

InitChatRoomHandler


import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;

import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

public class InitChatRoomHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    
    

    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
    
    
        //只接收http://127.0.0.1:8088/请求
        if ("/".equals(req.uri())) {
    
    
            //生成一个HTML页面,并指定使用WebSocket协议访问,最后地址为/ws,服务器将会处理WebSocket升级
            ByteBuf content = MakeIndexPage.getContent("ws://127.0.0.1:8088/ws");
            //构建HTTP请求
            FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
            HttpUtil.setContentLength(res, content.readableBytes());
            sendHttpResponse(ctx, req, res);
        } else {
    
    
            //如果是其他访问,直接返回404
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND));
        }
    }


    private static void sendHttpResponse(ChannelHandlerContext ctx,
                                         FullHttpRequest req,
                                         FullHttpResponse res) {
    
    
        if (res.status().code() != 200) {
    
    
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(),
                    CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
            HttpUtil.setContentLength(res, res.content().readableBytes());
        }
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        //如果非Keep-Alive,或者200,则关闭连接
        if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
    
    
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }
}

TextWebSocketFrameHandler


import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;

import java.util.Locale;

public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    
    

    private final ChannelGroup group;

    public TextWebSocketFrameHandler(ChannelGroup group) {
    
    
        this.group = group;
    }

    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
    
    
        String request = frame.text();
        //把消息写到ChannelGroup中所有已经连接的客户端
        group.writeAndFlush(
                new TextWebSocketFrame(
                        "Client " + ctx.channel() + " say: " + request.toUpperCase(Locale.CHINA)
                ));
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx,
                                   Object evt) throws Exception {
    
    
        //如果是握手事件,则通知所有已经连接上的客户端
        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
    
    
            //从pipeline中移除InitChatRoomHandler,因为将不会收到任何HTTP消息了
            ctx.pipeline().remove(InitChatRoomHandler.class);
            group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel() + " joined"));
            //把新的websocket channel添加到ChannelGroup中,这样它就可以接收到所有的消息了。
            group.add(ctx.channel());
        } else {
    
    
            super.userEventTriggered(ctx, evt);
        }
    }
}

MakeIndexPage


import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;

public final class MakeIndexPage {
    
    

    private static final String NEWLINE = "\r\n";

    public static ByteBuf getContent(String webSocketLocation) {
    
    
        return Unpooled.copiedBuffer(
                "<html><head><title>Web Socket Test</title></head>"
                        + NEWLINE +
                "<body>" + NEWLINE +
                "<script type=\"text/javascript\">" + NEWLINE +
                "var socket;" + NEWLINE +
                "if (!window.WebSocket) {" + NEWLINE +
                "  window.WebSocket = window.MozWebSocket;" + NEWLINE +
                '}' + NEWLINE +
                "if (window.WebSocket) {" + NEWLINE +
                "  socket = new WebSocket(\"" + webSocketLocation + "\");"
                        + NEWLINE +
                "  socket.onmessage = function(event) {" + NEWLINE +
                "    var ta = document.getElementById('responseText');"
                        + NEWLINE +
                "    ta.value = ta.value + '\\n' + event.data" + NEWLINE +
                "  };" + NEWLINE +
                "  socket.onopen = function(event) {" + NEWLINE +
                "    var ta = document.getElementById('responseText');"
                        + NEWLINE +
                "    ta.value = \"Web Socket opened!\";" + NEWLINE +
                "  };" + NEWLINE +
                "  socket.onclose = function(event) {" + NEWLINE +
                "    var ta = document.getElementById('responseText');"
                        + NEWLINE +
                "    ta.value = ta.value + \"Web Socket closed\"; "
                        + NEWLINE +
                "  };" + NEWLINE +
                "} else {" + NEWLINE +
                "  alert(\"Your browser does not support Web Socket.\");"
                        + NEWLINE +
                '}' + NEWLINE +
                NEWLINE +
                "function send(message) {" + NEWLINE +
                "  if (!window.WebSocket) { return; }" + NEWLINE +
                "  if (socket.readyState == WebSocket.OPEN) {" + NEWLINE +
                "    socket.send(message);" + NEWLINE +
                "  } else {" + NEWLINE +
                "    alert(\"The socket is not open.\");" + NEWLINE +
                "  }" + NEWLINE +
                '}' + NEWLINE +
                "</script>" + NEWLINE +
                "<form οnsubmit=\"return false;\">" + NEWLINE +
                "<input type=\"text\" name=\"message\" " +
                        "value=\"Hello, World!\"/>" +
                "<input type=\"button\" value=\"Send Web Socket Data\""
                        + NEWLINE +
                "       οnclick=\"send(this.form.message.value)\" />"
                        + NEWLINE +
                "<h3>服务端返回的消息</h3>" + NEWLINE +
                "<textarea id=\"responseText\" " +
                        "style=\"width:500px;height:300px;\"></textarea>"
                        + NEWLINE +
                "</form>" + NEWLINE +
                "</body>" + NEWLINE +
                "</html>" + NEWLINE, CharsetUtil.UTF_8);
    }

}

运行结果

在这里插入图片描述

ChannelPipeline的状态图

WebSocket协议升级之前的ChannelPipeline的状态如下图:

在这里插入图片描述

当WebSocket协议升级完成之后,WebSocketServerProtocalHandler将会把HttpRequestDecoder替换为WebSocketFrameDecoder,把HttpResponseEncoder替换为WebSocketFrameEncoder,为了性能最大化,还将移除任何不再被WebSocket连接所需要的ChannelHandler。

升级为WebSocket之后ChannelPipeline的状态如下图:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/CSDN_WYL2016/article/details/114521485