Netty——WebSocket之事
在說WebSocket以前,我們再來看看HTTP協議,HTTP1.0,HTTP1.1,HTTP2.0每個版本的更新帶來更高效的更豐富的功能:短連線、長連線、快取處理的豐富、斷點續傳、錯誤通知的豐富、多路複用、請求優先順序、header壓縮……更多的我們來看這篇文章:HTTP1.0、HTTP1.1和HTTP2.0的區別 (https://www.cnblogs.com/zhangyfr/p/8662673.html)
HTTP解決了我們很多問題,而且也非常強大,但是它有哪些不足之處呢?我們來看下:1,HTTP協議為半雙工協議,可以在 client和server兩個方向上傳輸,但是不能同時傳輸,意味著同一時刻,只能一個方向上傳送; 2,HTTP訊息冗長而複雜,包含訊息頭、訊息體、換行符,雖然功能強大,但是在某些方面也成了不足。 例如我們需要server向client端推送資料利用HTTP就很難,例如一些監控系統,隨著後臺資料的變化,前邊也跟著相應的變化,隨後後來的輪詢機制等可以解決這個問題,但是這樣採用了長連線,大量消耗了伺服器的寬頻和資源。也就是在client和server期望實時通訊的情況下,Http就有些頭疼了。亦是HTML5定義了WebSocket協議,更好的節省伺服器資源和寬頻,並達到實時通訊的目的。好下邊我們來看WebSocket的相關內容:
WebSocket,瀏覽器和伺服器只需要做一個握手動作(非HTTP三次握手),然後之間就會形成一條快速通道,兩者就可以相互傳送資料了,是基於TCP雙向全雙工進行訊息傳遞,其特點包括:1,單一的TCP連線,採用全雙工模式通訊; 2,對代理、防火牆和路由器透明; 3,無頭部資訊、Cookie和身份驗證; 4,無安全開銷; 5,通過“ping/pong”幀保持鏈路啟用; 6,伺服器可以主動傳遞訊息給客戶端,不再需要客戶端輪詢。
好了,講了WebSocket,我們來看下,利用Netty如何方便快速的開發WebSocket的服務端,下邊通過一個實時傳輸文字的例子來看,服務端程式碼:
public class WebSocketServer { public void run(final int port) { 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)); pipeline.addLast("http-chunked",new ChunkedWriteHandler()); //業務處理 ch.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(); } catch (InterruptedException e) { e.printStackTrace(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) { int port = 8091; if (args != null && args.length > 0) { try { port = Integer.valueOf(args[0]); } catch (NumberFormatException e) { } } new WebSocketServer().run(port); } } public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> { private static final Logger logger = Logger.getLogger(WebSocketServerHandler.class.getName()); private WebSocketServerHandshaker handshaker; /** * 接受到訊息處理 * @param ctx * @param msg * @throws Exception */ @Override protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception { if(msg instanceof FullHttpRequest){ //傳統的http接入 handlerHttpRequest(ctx,(FullHttpRequest)msg); }else if(msg instanceof WebSocketFrame){ //WebSocket接入 handlerWebSocketFrame(ctx,(WebSocketFrame)msg); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } /** * 處理正常請求 * @param ctx * @param req */ private void handlerHttpRequest(ChannelHandlerContext ctx,FullHttpRequest req){ //如果HTTP解碼失敗,返回http異常 if(!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))){ sendHttpResponse(ctx,req,new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.BAD_REQUEST)); return; } //構造握手響應返回,本機測試 WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:8091/websocket",null,false); handshaker = wsFactory.newHandshaker(req); if(handshaker ==null){ WebSocketServerHandshakerFactory .sendUnsupportedVersionResponse(ctx.channel()); }else{ handshaker.handshake(ctx.channel(),req); } } /** * 處理websocket請求 * @param ctx * @param frame */ private void handlerWebSocketFrame(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(); if(logger.isLoggable(Level.FINE)){ logger.fine(String.format("%s received %s",ctx.channel(),request)); } System.out.println("server 接受到的資訊:" + request); ctx.channel().write(new TextWebSocketFrame(request + " ,歡迎使用netty websocket服務,現在時刻:" + new Date().toString())); } /** * 響應客戶端訊息 * @param ctx * @param req * @param res */ 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(); HttpHeaderUtil.setContentLength(res,res.content().readableBytes()); } ChannelFuture f = ctx.channel().writeAndFlush(res); if(!HttpHeaderUtil.isKeepAlive(req) || res.status().code()!=200){ f.addListener(ChannelFutureListener.CLOSE); } } }
前端頁面:
<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:8091/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("sorry,您的瀏覽器不支援websocket協議!");
}
function send(message) {
if(!window.WebSocket){
return;
}
if(socket.readyState = WebSocket.OPEN){
socket.send(message);
socket.send("ljh study netty")
}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>
Netty對WebSocket協議棧的支援還是非常棒的,更多的我們需要了解可以到WebSocket官網 (http://www.websocket.org/index.html)