將HTTP協議的主要弊端總結以下:javascript
如今不少網站的消息推送都是使用輪詢,即客戶端每隔1S或者其餘時間給服務器發送請求,而後服務器返回最新的數據給客戶端。HTTP協議中的Header很是冗長,所以會佔用不少的帶寬和服務器資源。html
比較新的技術是Comet,使用了AJAX。雖然能夠雙向通訊,可是依然須要發送請求,並且在Comet中,廣泛採用了長鏈接,也會大量消耗服務器的帶寬和資源。html5
爲了解決這個問題,HTML5定義的WebSocket協議。java
在WebSocket API中,瀏覽器和服務器只須要一個握手的動做,而後,瀏覽器和服務器之間就造成了一條快速通道,二者就能夠直接互相傳送數據了。web
WebSocket基於TCP雙向全雙工協議,即在同一時刻,便可以發送消息,也能夠接收消息,相比於HTTP協議,是一個性能上的提高。bootstrap
特色:瀏覽器
擁有以上特色的WebSocket就是爲了取代輪詢和Comet技術,使得客戶端瀏覽器具有像C/S架構下桌面系統同樣的實時能力。安全
瀏覽器經過js創建一個WebSocket的請求,鏈接創建後,客戶端和服務器端能夠經過TCP直接交換數據。服務器
由於WebSocket本質上是一個TCP鏈接,穩定,因此在Comet和輪詢比擁有性能優點,如圖所示:websocket
client端發送握手請求,請求消息如圖所示:
服務端的應答請求如圖所示:
client消息中的"Sec-WebSocket-Key"是隨機的,服務器端會用這些數據來構造一個"SHA-1"的信息摘要,把"Sec-WebSocket-Key"加上一個魔幻字符串。使用"SHA-1"加密,而後進行BASE64編碼,將結果做爲"Sec-Webscoket-Accept"頭的值。
官方demo: http://netty.io/4.1/xref/io/netty/example/http/websocketx/server/package-summary.html
功能介紹:
import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; 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.stream.ChunkedWriteHandler; /** * @author lilinfeng * @version 1.0 * @date 2014年2月14日 */ public class WebSocketServer { public void run(int port) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("http-codec", new HttpServerCodec()); pipeline.addLast("aggregator", new HttpObjectAggregator(65536)); ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler()); pipeline.addLast("handler", new WebSocketServerHandler()); } }); Channel ch = b.bind(port).sync().channel(); System.out.println("Web socket server started at port " + port + '.'); System.out .println("Open your browser and navigate to http://localhost:" + port + '/'); ch.closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if (args.length > 0) { try { port = Integer.parseInt(args[0]); } catch (NumberFormatException e) { e.printStackTrace(); } } new WebSocketServer().run(port); } }
HttpServerCodec:將請求和應答消息解碼爲HTTP消息
HttpObjectAggregator:將HTTP消息的多個部分合成一條完整的HTTP消息
ChunkedWriteHandler:向客戶端發送HTML5文件
看上去和HTTP協議的很是相似,下面從Handler中來尋找答案:
1 import io.netty.buffer.ByteBuf; 2 import io.netty.buffer.Unpooled; 3 import io.netty.channel.ChannelFuture; 4 import io.netty.channel.ChannelFutureListener; 5 import io.netty.channel.ChannelHandlerContext; 6 import io.netty.channel.SimpleChannelInboundHandler; 7 import io.netty.handler.codec.http.DefaultFullHttpResponse; 8 import io.netty.handler.codec.http.FullHttpRequest; 9 import io.netty.handler.codec.http.FullHttpResponse; 10 import io.netty.handler.codec.http.HttpUtil; 11 import io.netty.handler.codec.http.websocketx.*; 12 import io.netty.util.CharsetUtil; 13 14 import java.util.logging.Level; 15 import java.util.logging.Logger; 16 17 import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; 18 import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; 19 20 /** 21 * @author lilinfeng 22 * @version 1.0 23 * @date 2014年2月14日 24 */ 25 public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> { 26 private static final Logger logger = Logger 27 .getLogger(WebSocketServerHandler.class.getName()); 28 29 private WebSocketServerHandshaker handshaker; 30 31 32 @Override 33 public void channelRead0(ChannelHandlerContext ctx, Object msg) 34 throws Exception { 35 // 傳統的HTTP接入 36 if (msg instanceof FullHttpRequest) { 37 handleHttpRequest(ctx, (FullHttpRequest) msg); 38 } 39 // WebSocket接入 40 else if (msg instanceof WebSocketFrame) { 41 handleWebSocketFrame(ctx, (WebSocketFrame) msg); 42 } 43 } 44 45 @Override 46 public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 47 ctx.flush(); 48 } 49 50 private void handleHttpRequest(ChannelHandlerContext ctx, 51 FullHttpRequest req) throws Exception { 52 53 // 若是HTTP解碼失敗,返回HHTP異常 54 if (!req.decoderResult().isSuccess() 55 || (!"websocket".equals(req.headers().get("Upgrade")))) { 56 sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, 57 BAD_REQUEST)); 58 return; 59 } 60 61 // 構造握手響應返回,本機測試 62 WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( 63 "ws://localhost:8080/websocket", null, false); 64 handshaker = wsFactory.newHandshaker(req); 65 if (handshaker == null) { 66 WebSocketServerHandshakerFactory 67 .sendUnsupportedVersionResponse(ctx.channel()); 68 } else { 69 handshaker.handshake(ctx.channel(), req); 70 } 71 } 72 73 private void handleWebSocketFrame(ChannelHandlerContext ctx, 74 WebSocketFrame frame) { 75 76 // 判斷是不是關閉鏈路的指令 77 if (frame instanceof CloseWebSocketFrame) { 78 handshaker.close(ctx.channel(), 79 (CloseWebSocketFrame) frame.retain()); 80 return; 81 } 82 // 判斷是不是Ping消息 83 if (frame instanceof PingWebSocketFrame) { 84 ctx.channel().write( 85 new PongWebSocketFrame(frame.content().retain())); 86 return; 87 } 88 // 本例程僅支持文本消息,不支持二進制消息 89 if (!(frame instanceof TextWebSocketFrame)) { 90 throw new UnsupportedOperationException(String.format( 91 "%s frame types not supported", frame.getClass().getName())); 92 } 93 94 // 返回應答消息 95 String request = ((TextWebSocketFrame) frame).text(); 96 if (logger.isLoggable(Level.FINE)) { 97 logger.fine(String.format("%s received %s", ctx.channel(), request)); 98 } 99 ctx.channel().write( 100 new TextWebSocketFrame(request 101 + " , 歡迎使用Netty WebSocket服務,如今時刻:" 102 + new java.util.Date().toString())); 103 } 104 105 private static void sendHttpResponse(ChannelHandlerContext ctx, 106 FullHttpRequest req, FullHttpResponse res) { 107 // 返回應答給客戶端 108 if (res.getStatus().code() != 200) { 109 ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(), 110 CharsetUtil.UTF_8); 111 res.content().writeBytes(buf); 112 buf.release(); 113 HttpUtil.setContentLength(res, res.content().readableBytes()); 114 } 115 116 // 若是是非Keep-Alive,關閉鏈接 117 ChannelFuture f = ctx.channel().writeAndFlush(res); 118 if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) { 119 f.addListener(ChannelFutureListener.CLOSE); 120 } 121 } 122 123 @Override 124 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) 125 throws Exception { 126 cause.printStackTrace(); 127 ctx.close(); 128 } 129 }
(1) 第一次握手由HTTP協議承載,因此是一個HTTP消息,根據消息頭中是否包含"Upgrade"字段來判斷是不是websocket。
(2) 經過校驗後,構造WebSocketServerHandshaker,經過它構造握手響應信息返回給客戶端,同時將WebSocket相關的編碼和解碼類動態添加到ChannelPipeline中。
下面分析鏈路創建以後的操做:
(1) 客戶端經過文本框提交請求給服務端,Handler收到以後已經解碼以後的WebSocketFrame消息。
(2) 若是是關閉按鏈路的指令就關閉鏈路
(3) 若是是維持鏈路的ping消息就返回Pong消息。
(4) 不然就返回應答消息
html5中的JS代碼:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> Netty WebSocket 時間服務器 </head> <br> <body> <br> <script type="text/javascript"> var socket; if (!window.WebSocket) { window.WebSocket = window.MozWebSocket; } if (window.WebSocket) { socket = new WebSocket("ws://localhost:8080/websocket"); socket.onmessage = function (event) { var ta = document.getElementById('responseText'); ta.value = ""; ta.value = event.data }; socket.onopen = function (event) { var ta = document.getElementById('responseText'); ta.value = "打開WebSocket服務正常,瀏覽器支持WebSocket!"; }; socket.onclose = function (event) { var ta = document.getElementById('responseText'); ta.value = ""; ta.value = "WebSocket 關閉!"; }; } else { alert("抱歉,您的瀏覽器不支持WebSocket協議!"); } function send(message) { if (!window.WebSocket) { return; } if (socket.readyState == WebSocket.OPEN) { socket.send(message); } else { alert("WebSocket鏈接沒有創建成功!"); } } </script> <form onsubmit="return false;"> <input type="text" name="message" value="Netty最佳實踐"/> <br><br> <input type="button" value="發送WebSocket請求消息" onclick="send(this.form.message.value)"/> <hr color="blue"/> <h3>服務端返回的應答消息</h3> <textarea id="responseText" style="width:500px;height:300px;"></textarea> </form> </body> </html>
演示效果大體以下: