webSocket 是 HTML5 開始提供的一種瀏覽器於服務器間進行全雙工通訊的技術.java
在 WebSocket API 中, 瀏覽器和服務器只須要作一個握手的動做, 而後, 瀏覽器和服務器之間就造成了一條快速通道, 二者就能夠直接相互傳送數據了. WebSocket 基於 TCP 雙向全雙工進行消息傳遞, 在同一時刻, 既能夠發送消息, 也能夠接收消息, 相比 HTTP 的半雙工協議, 性能獲得很大提高.web
WebSocket 的特色:瀏覽器
ping/pong
幀保持鏈路激活;創建 webSocket 鏈接時, 須要經過客戶端或瀏覽器發出握手請求, 相似下面的 http 報文.安全
這個請求和一般的 HTTP 請求不一樣, 包含了一些附加頭信息, 其中附加頭信息 Upgrade:WebSocket
代表這是一個申請協議升級的 HTTP 請求.服務器
服務器解析這些附加的頭信息, 而後生成應答信息返回給客戶端, 客戶端和服務端的 WebSocket 鏈接就創建起來了, 雙方能夠經過這個鏈接通道自由的傳遞信息, 而且這個鏈接會持續存在直到客戶端或服務端的某一方主動關閉鏈接.websocket
服務端返回給客戶端的應答消息, 相似以下報文socket
請求消息中的 Sec-WebSocket-Key
是隨機的, 服務端會用這些數據來構造出一個 SHA-1 的信息摘要, 把 Sec-WebSocket-Key
加上一個魔幻字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
. 使用 SHA-1 加密, 而後進行 BASE-64 編碼, 將結果作爲 Sec-WebSocket-Accept
頭的值, 返回給客戶端.ide
握手成功以後, 服務端和客戶端就能夠經過 messages
的方式進行通信, 一個消息由一個或多個幀組成.oop
幀都有本身對應的類型, 屬於同一個消息的多個幀具備相同類型的數據. 從廣義上講, 數據類型能夠是文本數據(UTF-8文字)、二進制數據和控制幀(協議級信令, 例如信號).性能
WebSocket 鏈接生命週期以下:
public class TimeServer { public void bind(int port) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .handler(new LoggingHandler(LogLevel.DEBUG)) .childHandler(new ChildChannelHandler()); // 綁定端口, 同步等待成功 ChannelFuture f = b.bind(port).sync(); // 等待服務端監聽端口關閉 f.channel().closeFuture().sync(); } finally { System.out.println("shutdownGracefully"); bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast("http-codec", new HttpServerCodec()); ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536)); ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler()); ch.pipeline().addLast("handler", new WebSOcketServerHandler()); } } private class WebSOcketServerHandler extends SimpleChannelInboundHandler<Object> { private WebSocketServerHandshaker handshaker; @Override protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception { // 傳統的 HTTP 接入 if (msg instanceof FullHttpRequest) { System.out.println("傳統的 HTTP 接入"); handleHttpRequest(ctx, (FullHttpRequest) msg); } // WebSocket 接入 else if (msg instanceof WebSocketFrame) { System.out.println("WebSocket 接入"); handleWebSocketFrame(ctx, (WebSocketFrame) msg); } } private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { // 若是 HTTP 解碼失敗, 返回HTTP異常 if (!req.getDecoderResult().isSuccess() || (!"websocket".equalsIgnoreCase(req.headers().get("Upgrade")))) { sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST)); return; } // 構造握手響應返回, 本機測試 WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:8080/websocket", null, false); handshaker = wsFactory.newHandshaker(req); if (handshaker == null) { WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel()); } else { handshaker.handshake(ctx.channel(), req); } } private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { // 判斷是不是關閉鏈路的指令 if (frame instanceof CloseWebSocketFrame) { handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); return; } // 判斷是不是 ping 信息 if (frame instanceof PingWebSocketFrame) { ctx.channel().write(new PongWebSocketFrame(frame.content().retain())); return; } // 本例程僅支持文本消息, 不支持二進制消息 if (!(frame instanceof TextWebSocketFrame)) { throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass().getName())); } // 返回應答信息 String request = ((TextWebSocketFrame) frame).text(); ctx.channel().write(new TextWebSocketFrame(request + " , 歡迎使用 Netty WebSocket 服務, 如今時刻: " + new java.util.Date().toString())); } private void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { if (res.getStatus().code() != 200) { ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8); res.content().writeBytes(buf); buf.release(); setContentLength(res, res.content().readableBytes()); } // 若是是非 Keep-Alive, 關閉鏈接 ChannelFuture f = ctx.channel().writeAndFlush(res); if (!isKeepAlive(req) || res.getStatus().code() != 200) { f.addListener(ChannelFutureListener.CLOSE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } } }
HttpServerCodec
: 將請求和應答消息編碼或解碼爲 HTTP 消息.HttpObjectAggregator
: 它的目的是將 HTTP 消息的多個部分組合成一條完整的 HTTP 消息. Netty 能夠聚合 HTTP 消息, 使用 FullHttpResponse
和 FullHttpRequest
到 ChannelPipeline
中的下一個 ChannelHandler
, 這就消除了斷裂消息, 保證了消息的完整.ChunkedWriteHandler
: 來向客戶端發送 HTML5 文件, 主要用於支持瀏覽器和服務端進行 WebSocket 通訊.
第一次握手請求消息由 HTTP 協議承載, 因此它是一個 HTTP 消息, 執行 handleHttpRequest
方法來處理 WebSocket 握手請求. 經過判斷請求消息判斷是否包含 Upgrade
字段或它的值不是 websocket, 則返回 HTTP 400 響應.
握手請求校驗經過以後, 開始構造握手工廠, 建立握手處理類 WebSocketServerHandshaker
, 經過它構造握手響應消息返回給客戶端.
添加 WebSocket Encoder 和 WebSocket Decoder 以後, 服務端就能夠自動對 WebSocket 消息進行編解碼了, 後面的 handler 能夠直接對 WebSocket 對象進行操做.
handleWebSocketFrame
對消息進行判斷, 首先判斷是不是控制幀, 若是是就關閉鏈路. 若是是維持鏈路的 Ping
消息, 則構造 Pong
消息返回. 因爲本例程的 WebSocket 通訊雙方使用的都是文本消息, 因此對請求新消息的類型進行判斷, 而不是文本的拋出異常.
最後, 從 TextWebSocketFrame
中獲取請求消息字符串, 對它處理後經過構造新的 TextWebSocketFrame
消息返回給客戶端, 因爲握手應答時, 動態增長了 TextWebSocketFrame
的編碼類, 因此能夠直接發送 TextWebSocketFrame
對象.