第十一章:WebSocket

本章介紹javascript

WebSockethtml

ChannelHandler,Decoder and Encoderjava

引導一個Netty基礎程序web

測試WebSocketbootstrap

使用Netty附帶的WebSocket,咱們不須要關注協議內部實現,只須要使用Netty提供的一些簡單的方法就能夠實現瀏覽器

11.1 WebSockets some background安全

        關於WebSocket的一些概念和背景,能夠查詢網上相關介紹。這裏不贅述。服務器

11.2 面臨的挑戰websocket

要顯示「real-time」支持的WebSocket,應用程序將顯示如何使用Netty中的WebSocket實現一個在瀏覽器中進行聊天的IRC應用程序。網絡

在這個應用程序中,不一樣的用戶能夠同時交談,很是像IRC(Internet Relay Chat,互聯網中繼聊天)。

上圖顯示的邏輯很簡單:

一個客戶端發送一條消息

消息被廣播到其餘已鏈接的客戶端

它的工做原理就像聊天室同樣,在這裏例子中,咱們將編寫服務器,而後使用瀏覽器做爲客戶端。帶着這樣的思路,咱們將會很簡單的實現它。

11.3 實現

WebSocket使用HTTP升級機制從一個普通的HTTP鏈接WebSocket,由於這個應用程序使用WebSocket老是開始於HTTP(s),而後再升級。

在這裏,若是url的結尾以/ws結束,咱們將只會升級到WebSocket,不然服務器將發送一個網頁給客戶端。升級後的鏈接將經過WebSocket傳輸全部數據。邏輯圖以下:

11.3.1 處理http請求

服務器將做爲一種混合式以容許同時處理http和websocket,因此服務器還須要html頁面,html用來充當客戶端角色,鏈接服務器並交互消息。所以,若是客戶端不發送/ws的uri,咱們須要寫一個ChannelInboundHandler用來處理FullHttpRequest。看下面代碼:

package netty.in.action;  
import io.netty.channel.ChannelFuture;  
import io.netty.channel.ChannelFutureListener;  
import io.netty.channel.ChannelHandlerContext;  
import io.netty.channel.DefaultFileRegion;  
import io.netty.channel.SimpleChannelInboundHandler;  
import io.netty.handler.codec.http.DefaultFullHttpResponse;  
import io.netty.handler.codec.http.DefaultHttpResponse;  
import io.netty.handler.codec.http.FullHttpRequest;  
import io.netty.handler.codec.http.FullHttpResponse;  
import io.netty.handler.codec.http.HttpHeaders;  
import io.netty.handler.codec.http.HttpResponse;  
import io.netty.handler.codec.http.HttpResponseStatus;  
import io.netty.handler.codec.http.HttpVersion;  
import io.netty.handler.codec.http.LastHttpContent;  
import io.netty.handler.ssl.SslHandler;  
import io.netty.handler.stream.ChunkedNioFile;  
import java.io.RandomAccessFile;  
/** 
 * WebSocket,處理http請求 
 */  
public class HttpRequestHandler extends  
        SimpleChannelInboundHandler<FullHttpRequest> {  
    //websocket標識  
    private final String wsUri;  
  
    public HttpRequestHandler(String wsUri) {  
        this.wsUri = wsUri;  
    }  
    @Override  
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg)  
            throws Exception {  
        //若是是websocket請求,請求地址uri等於wsuri  
        if (wsUri.equalsIgnoreCase(msg.getUri())) {  
            //將消息轉發到下一個ChannelHandler  
            ctx.fireChannelRead(msg.retain());  
        } else {//若是不是websocket請求  
            if (HttpHeaders.is100ContinueExpected(msg)) {  
                //若是HTTP請求頭部包含Expect: 100-continue,  
                //則響應請求  
                FullHttpResponse response = new DefaultFullHttpResponse(  
                        HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);  
                ctx.writeAndFlush(response);  
            }  
            //獲取index.html的內容響應給客戶端  
            RandomAccessFile file = new RandomAccessFile(  
                    System.getProperty("user.dir") + "/index.html", "r");  
            HttpResponse response = new DefaultHttpResponse(  
                    msg.getProtocolVersion(), HttpResponseStatus.OK);  
            response.headers().set(HttpHeaders.Names.CONTENT_TYPE,  
                    "text/html; charset=UTF-8");  
            boolean keepAlive = HttpHeaders.isKeepAlive(msg);  
            //若是http請求保持活躍,設置http請求頭部信息  
            //並響應請求  
            if (keepAlive) {  
                response.headers().set(HttpHeaders.Names.CONTENT_LENGTH,  
                        file.length());  
                response.headers().set(HttpHeaders.Names.CONNECTION,  
                        HttpHeaders.Values.KEEP_ALIVE);  
            }  
            ctx.write(response);  
            //若是不是https請求,將index.html內容寫入通道  
            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) {  
                //若是http請求不活躍,關閉http鏈接  
                future.addListener(ChannelFutureListener.CLOSE);  
            }  
            file.close();  
        }  
    }  
    @Override  
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)  
            throws Exception {  
        cause.printStackTrace();  
        ctx.close();  
    }  
}

11.3.2 處理WebSocket框架

WebSocket支持6種不一樣框架,以下圖:

咱們的程序只須要使用下面4個框架:

CloseWebSocketFrame

PingWebSocketFrame

PongWebSocketFrame

TextWebSocketFrame

咱們只須要顯示處理TextWebSocketFrame,其餘的會自動由WebSocketServerProtocolHandler處理,看下面代碼:

package netty.in.action;  
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;  
/** 
 * WebSocket,處理消息 
 */  
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 {  
        //若是WebSocket握手完成  
        if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {  
            //刪除ChannelPipeline中的HttpRequestHandler  
            ctx.pipeline().remove(HttpRequestHandler.class);  
            //寫一個消息到ChannelGroup  
            group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel()  
                    + " joined"));  
            //將Channel添加到ChannelGroup  
            group.add(ctx.channel());  
        }else {  
            super.userEventTriggered(ctx, evt);  
        }  
    }  
  
    @Override  
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg)  
            throws Exception {  
        //將接收的消息經過ChannelGroup轉發到因此已鏈接的客戶端  
        group.writeAndFlush(msg.retain());  
    }  
}

11.3.3 初始化ChannelPipeline

 看下面代碼:

package netty.in.action;  
import io.netty.channel.Channel;  
import io.netty.channel.ChannelInitializer;  
import io.netty.channel.ChannelPipeline;  
import io.netty.channel.group.ChannelGroup;  
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.handler.stream.ChunkedWriteHandler;  
/** 
 * WebSocket,初始化ChannelHandler 
 */  
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();  
        //編解碼http請求  
        pipeline.addLast(new HttpServerCodec());  
        //寫文件內容  
        pipeline.addLast(new ChunkedWriteHandler());  
        //聚合解碼HttpRequest/HttpContent/LastHttpContent到FullHttpRequest  
        //保證接收的Http請求的完整性  
        pipeline.addLast(new HttpObjectAggregator(64 * 1024));  
        //處理FullHttpRequest  
        pipeline.addLast(new HttpRequestHandler("/ws"));  
        //處理其餘的WebSocketFrame  
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));  
        //處理TextWebSocketFrame  
        pipeline.addLast(new TextWebSocketFrameHandler(group));  
    }  
}

WebSocketServerProtcolHandler不只處理Ping/Pong/CloseWebSocketFrame,還和它本身握手並幫助升級WebSocket。這是執行完成握手和成功修改ChannelPipeline,而且添加須要的編碼器/解碼器和刪除不須要的ChannelHandler。

 看下圖:

ChannelPipeline經過ChannelInitializer的initChannel(...)方法完成初始化,完成握手後就會更改事情。一旦這樣作了,WebSocketServerProtocolHandler將取代HttpRequestDecoder、WebSocketFrameDecoder13和HttpResponseEncoder、WebSocketFrameEncoder13。另外也要刪除全部不須要的ChannelHandler已得到最佳性能。這些都是HttpObjectAggregator和HttpRequestHandler。下圖顯示ChannelPipeline握手完成:

咱們甚至沒注意到它,由於它是在底層執行的。以很是靈活的方式動態更新ChannelPipeline讓單獨的任務在不一樣的ChannelHandler中實現。

11.4 結合在一塊兒使用

一如既往,咱們要將它們結合在一塊兒使用。使用Bootstrap引導服務器和設置正確的ChannelInitializer。看下面代碼:

package netty.in.action;  
  
import io.netty.bootstrap.ServerBootstrap;  
import io.netty.channel.Channel;  
import io.netty.channel.ChannelFuture;  
import io.netty.channel.ChannelInitializer;  
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.nio.NioServerSocketChannel;  
import io.netty.util.concurrent.ImmediateEventExecutor;  
  
import java.net.InetSocketAddress;  
/** 
 * 訪問地址:http://localhost:2048 
 */  
public class ChatServer {  
  
    private final ChannelGroup group = new DefaultChannelGroup(  
            ImmediateEventExecutor.INSTANCE);  
    private final EventLoopGroup workerGroup = new NioEventLoopGroup();  
    private Channel channel;  
  
    public ChannelFuture start(InetSocketAddress address) {  
        ServerBootstrap b = new ServerBootstrap();  
        b.group(workerGroup).channel(NioServerSocketChannel.class)  
                .childHandler(createInitializer(group));  
        ChannelFuture f = b.bind(address).syncUninterruptibly();  
        channel = f.channel();  
        return f;  
    }  
  
    public void destroy() {  
        if (channel != null)  
            channel.close();  
        group.close();  
        workerGroup.shutdownGracefully();  
    }  
  
    protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {  
        return new ChatServerInitializer(group);  
    }  
  
    public static void main(String[] args) {  
        final ChatServer server = new ChatServer();  
        ChannelFuture f = server.start(new InetSocketAddress(2048));  
        Runtime.getRuntime().addShutdownHook(new Thread() {  
            @Override  
            public void run() {  
                server.destroy();  
            }  
        });  
        f.channel().closeFuture().syncUninterruptibly();  
    }  
}

另外,須要將index.html文件放在項目根目錄,index.html內容以下:

<html>  
<head>  
<title>Web Socket Test</title>  
</head>  
<body>  
<script type="text/javascript">  
var socket;  
if (!window.WebSocket) {  
  window.WebSocket = window.MozWebSocket;  
}  
if (window.WebSocket) {  
  socket = new WebSocket("ws://localhost:2048/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 = "Web Socket opened!";  
  };  
  socket.onclose = function(event) {  
    var ta = document.getElementById('responseText');  
    ta.value = ta.value + "Web Socket closed";   
  };  
} else {  
  alert("Your browser does not support Web Socket.");  
}  
  
function send(message) {  
  if (!window.WebSocket) { return; }  
  if (socket.readyState == WebSocket.OPEN) {  
    socket.send(message);  
  } else {  
    alert("The socket is not open.");  
  }  
}  
</script>  
    <form onsubmit="return false;">  
        <input type="text" name="message" value="Hello, World!"><input  
            type="button" value="Send Web Socket Data"  
            onclick="send(this.form.message.value)">  
        <h3>Output</h3>  
        <textarea id="responseText" style="width: 500px; height: 300px;"></textarea>  
    </form>  
</body>  
</html>

最後在瀏覽器中輸入:http://localhost:2048,多開幾個窗口就能夠聊天了。

11.5 給WebSocket加密

上面的應用程序雖然工做的很好,可是在網絡上收發消息存在很大的安全隱患,因此有必要對消息進行加密。添加這樣一個加密的功能通常比較複雜,須要對代碼有較大的改動。可是使用Netty就能夠很容易的添加這樣的功能,只須要將SslHandler加入到ChannelPipeline中就能夠了。實際上還須要添加SslContext,但這不在本例子範圍內。

首先咱們建立一個用於添加加密Handler的handler初始化類,看下面代碼:

package netty.in.action;  
import io.netty.channel.Channel;  
import io.netty.channel.group.ChannelGroup;  
import io.netty.handler.ssl.SslHandler;  
import javax.net.ssl.SSLContext;  
import javax.net.ssl.SSLEngine;  
public class SecureChatServerIntializer extends ChatServerInitializer {  
    private final SSLContext context;  
    public SecureChatServerIntializer(ChannelGroup group,SSLContext context) {  
        super(group);  
        this.context = context;  
    }  
  
    @Override  
    protected void initChannel(Channel ch) throws Exception {  
        super.initChannel(ch);  
        SSLEngine engine = context.createSSLEngine();  
        engine.setUseClientMode(false);  
        ch.pipeline().addFirst(new SslHandler(engine));  
    }  
}

最後咱們建立一個用於引導配置的類,看下面代碼:

package netty.in.action;  
import io.netty.channel.Channel;  
import io.netty.channel.ChannelFuture;  
import io.netty.channel.ChannelInitializer;  
import io.netty.channel.group.ChannelGroup;  
import java.net.InetSocketAddress;  
import javax.net.ssl.SSLContext;  
/** 
 * 訪問地址:https://localhost:4096 
 */  
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 SecureChatServerIntializer(group, context);  
    }  
  
    /** 
     * 獲取SSLContext須要相關的keystore文件,這裏沒有 關於HTTPS能夠查閱相關資料,這裏只介紹在Netty中如何使用 
     *  
     * @return 
     */  
    private static SSLContext getSslContext() {  
        return null;  
    }  
  
    public static void main(String[] args) {  
        SSLContext context = getSslContext();  
        final SecureChatServer server = new SecureChatServer(context);  
        ChannelFuture future = server.start(new InetSocketAddress(4096));  
        Runtime.getRuntime().addShutdownHook(new Thread() {  
            @Override  
            public void run() {  
                server.destroy();  
            }  
        });  
        future.channel().closeFuture().syncUninterruptibly();  
    }  
}
相關文章
相關標籤/搜索