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協議的具體使用,可完成不一樣客戶端之間的通訊,也謝謝各位園友的觀看~