【Netty】WebSocket

1、前言javascript

  前面學習了codec和ChannelHandler之間的關係,接着學習WebSocket。html

2、WebSocketjava

  2.1. WebSocket介紹
bootstrap

  WebSocket協議容許客戶端和服務器隨時傳輸消息,要求他們異步處理接收的消息,而幾乎全部的瀏覽器都支持WebSocket協議,Netty支持WebSocket協議的全部實現,能夠在應用中直接使用。瀏覽器

  2.2. WebSocket應用示例
服務器

  下面示例展現瞭如何使用WebSocket協議實現基於瀏覽器的實時聊天應用,示例邏輯圖以下圖所示。app

  

  處理邏輯以下dom

    · 客戶端發送消息。異步

    · 消息轉發至其餘全部客戶端。socket

  本示例中只實現服務端部分,客戶端網頁爲index.html。

  2.3 添加WebSocket支持

  升級握手機制可用於從標準HTTP或HTTPS協議切換到WebSocket,使用WebSocket的應用程序以HTTP/S開頭,當請求指定URL時將會啓動該協議。本應用有以下慣例:若是URL請求以/ ws結尾,咱們將使用升級的WebSocket協議,不然,將使用HTTP/S協議,鏈接升級後,全部數據將使用WebSocket傳輸。下圖展現服務端的邏輯。

  

  1. 處理HTTP請求

  首先咱們實現處理HTTP請求的組件,該組件將爲訪問聊天室的頁面提供服務,並顯示鏈接的客戶端發送的消息。下面是HttpRequestHandler代碼,其繼承SimpleChannelInboundHandler。 

public class HttpRequestHandler
    extends SimpleChannelInboundHandler<FullHttpRequest> {
    private final String wsUri;
    private static final File INDEX;
    static {
        URL location = HttpRequestHandler.class
            .getProtectionDomain()
            .getCodeSource().getLocation();
        try {
            String path = location.toURI() + "index.html";
            path = !path.contains("file:") ? path : path.substring(5);
            INDEX = new File(path);
        } catch (URISyntaxException e) {
            throw new IllegalStateException(
                "Unable to locate index.html", e);
        }
    }
    public HttpRequestHandler(String wsUri) {
        this.wsUri = wsUri;
    }
    @Override
    public void channelRead0(ChannelHandlerContext ctx,
        FullHttpRequest request) throws Exception {
        if (wsUri.equalsIgnoreCase(request.getUri())) {
            ctx.fireChannelRead(request.retain());
        } else {
            if (HttpHeaders.is100ContinueExpected(request)) {
                send100Continue(ctx);
            }
            RandomAccessFile file = new RandomAccessFile(INDEX, "r");
            HttpResponse response = new DefaultHttpResponse(
            request.getProtocolVersion(), HttpResponseStatus.OK);
            response.headers().set(
                HttpHeaders.Names.CONTENT_TYPE,
                "text/plain; charset=UTF-8");
            boolean keepAlive = HttpHeaders.isKeepAlive(request);
            if (keepAlive) {
                response.headers().set(
                    HttpHeaders.Names.CONTENT_LENGTH, file.length());
                response.headers().set( HttpHeaders.Names.CONNECTION,
                    HttpHeaders.Values.KEEP_ALIVE);
            }
            ctx.write(response);
            if (ctx.pipeline().get(SslHandler.class) == null) {
                ctx.write(new DefaultFileRegion(
                file.getChannel(), 0, file.length()));
            } else {
                ctx.write(new ChunkedNioFile(file.getChannel()));
            }
            ChannelFuture future = ctx.writeAndFlush(
                LastHttpContent.EMPTY_LAST_CONTENT);
            if (!keepAlive) {
                future.addListener(ChannelFutureListener.CLOSE);
            }
        }
    }
    private static void send100Continue(ChannelHandlerContext ctx) {
        FullHttpResponse response = new DefaultFullHttpResponse(
            HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
        ctx.writeAndFlush(response);
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
        throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

  上述代碼用於處理純HTTP請求,對於WebSocket而言,數據使用幀進行傳輸,完整的數據包含多幀。

  2. 處理WebSocket幀

  WebSocket定義了六種幀,以下圖所示。

  

  對於聊天應用而言,其包含以下幀:CloseWebSocketFrame、PingWebSocketFrame、PongWebSocketFrame、TextWebSocketFrame。

  下面代碼展現了用於處理TextWebSocketFrames的ChannelHandler。  

public class TextWebSocketFrameHandler
    extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    private final ChannelGroup group;
    public TextWebSocketFrameHandler(ChannelGroup group) {
        this.group = group;
    }
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx,
        Object evt) throws Exception {
        if (evt == WebSocketServerProtocolHandler
            .ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
            ctx.pipeline().remove(HttpRequestHandler.class);
            group.writeAndFlush(new TextWebSocketFrame(
                "Client " + ctx.channel() + " joined"));
            group.add(ctx.channel());
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
    @Override
    public void channelRead0(ChannelHandlerContext ctx,
        TextWebSocketFrame msg) throws Exception {
        group.writeAndFlush(msg.retain());
    }
}

  3. 初始化ChannelPipeline

  爲在ChannelPipeline中添加ChannelHandler,須要繼承ChannelInitializer而且實現initChannel方法,下面是ChatServerInitializer的代碼。 

public class ChatServerInitializer extends ChannelInitializer<Channel> {
    private final ChannelGroup group;
    public ChatServerInitializer(ChannelGroup group) {
        this.group = group;
    }
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new HttpServerCodec());
        pipeline.addLast(new ChunkedWriteHandler());
        pipeline.addLast(new HttpObjectAggregator(64 * 1024));
        pipeline.addLast(new HttpRequestHandler("/ws"));
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
        pipeline.addLast(new TextWebSocketFrameHandler(group));
    }
}

  對於使用HTTP協議(升級前)和WebSocket協議(升級後)的管道中的處理器分別以下圖所示。

  

  

  4. Bootstrapping

  ChatServer類用於啓動服務器而且安裝ChatServerInitializer,其代碼以下。  

public class ChatServer {
    private final ChannelGroup channelGroup =
        new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
    private final EventLoopGroup group = new NioEventLoopGroup();
    private Channel channel;
    public ChannelFuture start(InetSocketAddress address) {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(group)
            .channel(NioServerSocketChannel.class)
            .childHandler(createInitializer(channelGroup));
        ChannelFuture future = bootstrap.bind(address);
        future.syncUninterruptibly();
        channel = future.channel();
        return future;
    }
    protected ChannelInitializer<Channel> createInitializer(
        ChannelGroup group) {
        return new ChatServerInitializer(group);
    }
    public void destroy() {
        if (channel != null) {
            channel.close();
        }
        channelGroup.close();
        group.shutdownGracefully();
    }
    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("Please give port as argument");
            System.exit(1);
        }
        int port = Integer.parseInt(args[0]);
        final ChatServer endpoint = new ChatServer();
        ChannelFuture future = endpoint.start(
            new InetSocketAddress(port));
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                endpoint.destroy();
            }
        });
        future.channel().closeFuture().syncUninterruptibly();
    }
}

  上述代碼就完成了服務端的全部代碼,接着進行測試。

  2.4 加密應用

  上述代碼中可正常進行通訊,可是並未加密,首先須要添加SecureChatServerInitializer,其代碼以下。  

public class SecureChatServerInitializer extends ChatServerInitializer {
    private final SslContext context;
    public SecureChatServerInitializer(ChannelGroup group,
        SslContext context) {
        super(group);
        this.context = context;
    }    
    @Override
    protected void initChannel(Channel ch) throws Exception {
        super.initChannel(ch);
        SSLEngine engine = context.newEngine(ch.alloc());
        ch.pipeline().addFirst(new SslHandler(engine));
    }
}

  而後添加SecureChatServerInitializer,代碼以下。  

public class SecureChatServer extends ChatServer {
    private final SslContext context;
    public SecureChatServer(SslContext context) {
        this.context = context;
    }
    @Override
    protected ChannelInitializer<Channel> createInitializer(
        ChannelGroup group) {
        return new SecureChatServerInitializer(group, context);
    }
    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("Please give port as argument");
            System.exit(1);
        }
        int port = Integer.parseInt(args[0]);
        SelfSignedCertificate cert = new SelfSignedCertificate();
        SslContext context = SslContext.newServerContext(
        cert.certificate(), cert.privateKey());
        final SecureChatServer endpoint = new SecureChatServer(context);
        ChannelFuture future = endpoint.start(new InetSocketAddress(port));
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                endpoint.destroy();
            }
        });
        future.channel().closeFuture().syncUninterruptibly();
    }
}

  2.5 測試應用

  在編譯的classes文件夾中加入index.html(客戶端),其中index.html的源碼以下  

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Chat</title>
</head>
<body>
    <script type="text/javascript">
        var socket;
        if (!window.WebSocket) {
            window.WebSocket = window.MozWebSocket;
        }
        if (window.WebSocket) {
            socket = new WebSocket("ws://localhost:8080/ws");
            socket.onmessage = function(event) {
                var ta = document.getElementById('responseText');
                ta.value = ta.value + '\n' + event.data
            };
            socket.onopen = function(event) {
                var ta = document.getElementById('responseText');
                ta.value = "connected!";
            };
            socket.onclose = function(event) {
                var ta = document.getElementById('responseText');
                ta.value = ta.value + "connection is shutdown";
            };
        } else {
            alert("your broswer do not support WebSocket!");
        }

        function send(message) {
            if (!window.WebSocket) {
                return;
            }
            if (socket.readyState == WebSocket.OPEN) {
                socket.send(message);
            } else {
                alert("connection is not start.");
            }
        }
    </script>
    <form onsubmit="return false;">
        <h3>WebSocket ChatRoom:</h3>
        <textarea id="responseText" style="width: 500px; height: 300px;"></textarea>
        <br> 
        <input type="text" name="message"  style="width: 300px" value="">
        <input type="button" value="Send Message" onclick="send(this.form.message.value)">
        <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="Clear message">
    </form>
    <br> 
    <br> 
</body>
</html>

  而後啓動ChatServer(非加密方式),最後在瀏覽器中訪問localhost:8080(可打開多個窗口,多個客戶端),其運行效果以下。  

  

  

  能夠看到兩個客戶端之間能夠正常進行通訊,互相發送消息。

3、總結

  本篇博文經過一個示例講解了WebSocket協議的具體使用,可完成不一樣客戶端之間的通訊,也謝謝各位園友的觀看~

相關文章
相關標籤/搜索