Netty In Action中文版 - 第十一章:WebSocket

本章介绍

  • WebSocket
  • ChannelHandler,Decoder and Encoder
  • 引导一个Netty基础程序
  • 测试WebSocket
        “real-time-web”实时web现在随处可见,很多的用户希望能从web站点实时获取信息。Netty支持WebSocket实现,并包含了不同的版本,我们可以非常容易的实现WebSocket应用。使用Netty附带的WebSocket,我们不需要关注协议内部实现,只需要使用Netty提供的一些简单的方法就可以实现。本章将通过的例子应用帮助你来使用WebSocket并了解它是如何工作。

11.1 WebSockets some background

        关于WebSocket的一些概念和背景,可以查询网上相关介绍。这里不赘述。

11.2 面临的挑战

        要显示“real-time”支持的WebSocket,应用程序将显示如何使用Netty中的WebSocket实现一个在浏览器中进行聊天的IRC应用程序。你可能知道从Facebook可以发送文本消息到另一个人,在这里,我们将进一步了解其实现。在这个应用程序中,不同的用户可以同时交谈,非常像IRC(Internet Relay Chat,互联网中继聊天)。

上图显示的逻辑很简单:
  1. 一个客户端发送一条消息
  2. 消息被广播到其他已连接的客户端
        它的工作原理就像聊天室一样,在这里例子中,我们将编写服务器,然后使用浏览器作为客户端。带着这样的思路,我们将会很简单的实现它。

11.3 实现

        WebSocket使用HTTP升级机制从一个普通的HTTP连接WebSocket,因为这个应用程序使用WebSocket总是开始于HTTP(s),然后再升级。什么时候升级取决于应用程序本身。直接执行升级作为第一个操作一般是使用特定的url请求。
        在这里,如果url的结尾以/ws结束,我们将只会升级到WebSocket,否则服务器将发送一个网页给客户端。升级后的连接将通过WebSocket传输所有数据。逻辑图如下:

11.3.1 处理http请求

        服务器将作为一种混合式以允许同时处理http和websocket,所以服务器还需要html页面,html用来充当客户端角色,连接服务器并交互消息。因此,如果客户端不发送/ws的uri,我们需要写一个ChannelInboundHandler用来处理FullHttpRequest。看下面代码:
  1. package netty.in.action;  
  2.   
  3. import io.netty.channel.ChannelFuture;  
  4. import io.netty.channel.ChannelFutureListener;  
  5. import io.netty.channel.ChannelHandlerContext;  
  6. import io.netty.channel.DefaultFileRegion;  
  7. import io.netty.channel.SimpleChannelInboundHandler;  
  8. import io.netty.handler.codec.http.DefaultFullHttpResponse;  
  9. import io.netty.handler.codec.http.DefaultHttpResponse;  
  10. import io.netty.handler.codec.http.FullHttpRequest;  
  11. import io.netty.handler.codec.http.FullHttpResponse;  
  12. import io.netty.handler.codec.http.HttpHeaders;  
  13. import io.netty.handler.codec.http.HttpResponse;  
  14. import io.netty.handler.codec.http.HttpResponseStatus;  
  15. import io.netty.handler.codec.http.HttpVersion;  
  16. import io.netty.handler.codec.http.LastHttpContent;  
  17. import io.netty.handler.ssl.SslHandler;  
  18. import io.netty.handler.stream.ChunkedNioFile;  
  19.   
  20. import java.io.RandomAccessFile;  
  21.   
  22. /** 
  23.  * WebSocket,处理http请求 
  24.  *  
  25.  * @author c.k 
  26.  *  
  27.  */  
  28. public class HttpRequestHandler extends  
  29.         SimpleChannelInboundHandler<FullHttpRequest> {  
  30.     //websocket标识  
  31.     private final String wsUri;  
  32.   
  33.     public HttpRequestHandler(String wsUri) {  
  34.         this.wsUri = wsUri;  
  35.     }  
  36.   
  37.     @Override  
  38.     protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg)  
  39.             throws Exception {  
  40.         //如果是websocket请求,请求地址uri等于wsuri  
  41.         if (wsUri.equalsIgnoreCase(msg.getUri())) {  
  42.             //将消息转发到下一个ChannelHandler  
  43.             ctx.fireChannelRead(msg.retain());  
  44.         } else {//如果不是websocket请求  
  45.             if (HttpHeaders.is100ContinueExpected(msg)) {  
  46.                 //如果HTTP请求头部包含Expect: 100-continue,  
  47.                 //则响应请求  
  48.                 FullHttpResponse response = new DefaultFullHttpResponse(  
  49.                         HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);  
  50.                 ctx.writeAndFlush(response);  
  51.             }  
  52.             //获取index.html的内容响应给客户端  
  53.             RandomAccessFile file = new RandomAccessFile(  
  54.                     System.getProperty("user.dir") + "/index.html""r");  
  55.             HttpResponse response = new DefaultHttpResponse(  
  56.                     msg.getProtocolVersion(), HttpResponseStatus.OK);  
  57.             response.headers().set(HttpHeaders.Names.CONTENT_TYPE,  
  58.                     "text/html; charset=UTF-8");  
  59.             boolean keepAlive = HttpHeaders.isKeepAlive(msg);  
  60.             //如果http请求保持活跃,设置http请求头部信息  
  61.             //并响应请求  
  62.             if (keepAlive) {  
  63.                 response.headers().set(HttpHeaders.Names.CONTENT_LENGTH,  
  64.                         file.length());  
  65.                 response.headers().set(HttpHeaders.Names.CONNECTION,  
  66.                         HttpHeaders.Values.KEEP_ALIVE);  
  67.             }  
  68.             ctx.write(response);  
  69.             //如果不是https请求,将index.html内容写入通道  
  70.             if (ctx.pipeline().get(SslHandler.class) == null) {  
  71.                 ctx.write(new DefaultFileRegion(file.getChannel(), 0, file  
  72.                         .length()));  
  73.             } else {  
  74.                 ctx.write(new ChunkedNioFile(file.getChannel()));  
  75.             }  
  76.             //标识响应内容结束并刷新通道  
  77.             ChannelFuture future = ctx  
  78.                     .writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);  
  79.             if (!keepAlive) {  
  80.                 //如果http请求不活跃,关闭http连接  
  81.                 future.addListener(ChannelFutureListener.CLOSE);  
  82.             }  
  83.             file.close();  
  84.         }  
  85.     }  
  86.   
  87.     @Override  
  88.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)  
  89.             throws Exception {  
  90.         cause.printStackTrace();  
  91.         ctx.close();  
  92.     }  
  93. }  

11.3.2 处理WebSocket框架

        WebSocket支持6种不同框架,如下图:

我们的程序只需要使用下面4个框架:
  • CloseWebSocketFrame
  • PingWebSocketFrame
  • PongWebSocketFrame
  • TextWebSocketFrame
        我们只需要显示处理TextWebSocketFrame,其他的会自动由WebSocketServerProtocolHandler处理,看下面代码:
  1. package netty.in.action;  
  2.   
  3. import io.netty.channel.ChannelHandlerContext;  
  4. import io.netty.channel.SimpleChannelInboundHandler;  
  5. import io.netty.channel.group.ChannelGroup;  
  6. import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;  
  7. import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;  
  8.   
  9. /** 
  10.  * WebSocket,处理消息 
  11.  * @author c.k 
  12.  * 
  13.  */  
  14. public class TextWebSocketFrameHandler extends  
  15.         SimpleChannelInboundHandler<TextWebSocketFrame> {  
  16.     private final ChannelGroup group;  
  17.   
  18.     public TextWebSocketFrameHandler(ChannelGroup group) {  
  19.         this.group = group;  
  20.     }  
  21.   
  22.     @Override  
  23.     public void userEventTriggered(ChannelHandlerContext ctx, Object evt)  
  24.             throws Exception {  
  25.         //如果WebSocket握手完成  
  26.         if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {  
  27.             //删除ChannelPipeline中的HttpRequestHandler  
  28.             ctx.pipeline().remove(HttpRequestHandler.class);  
  29.             //写一个消息到ChannelGroup  
  30.             group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel()  
  31.                     + " joined"));  
  32.             //将Channel添加到ChannelGroup  
  33.             group.add(ctx.channel());  
  34.         }else {  
  35.             super.userEventTriggered(ctx, evt);  
  36.         }  
  37.     }  
  38.   
  39.     @Override  
  40.     protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg)  
  41.             throws Exception {  
  42.         //将接收的消息通过ChannelGroup转发到所以已连接的客户端  
  43.         group.writeAndFlush(msg.retain());  
  44.     }  
  45. }  

11.3.3 初始化ChannelPipeline

        看下面代码:
  1. package netty.in.action;  
  2.   
  3. import io.netty.channel.Channel;  
  4. import io.netty.channel.ChannelInitializer;  
  5. import io.netty.channel.ChannelPipeline;  
  6. import io.netty.channel.group.ChannelGroup;  
  7. import io.netty.handler.codec.http.HttpObjectAggregator;  
  8. import io.netty.handler.codec.http.HttpServerCodec;  
  9. import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;  
  10. import io.netty.handler.stream.ChunkedWriteHandler;  
  11.   
  12. /** 
  13.  * WebSocket,初始化ChannelHandler 
  14.  * @author c.k 
  15.  * 
  16.  */  
  17. public class ChatServerInitializer extends ChannelInitializer<Channel> {  
  18.     private final ChannelGroup group;  
  19.       
  20.     public ChatServerInitializer(ChannelGroup group){  
  21.         this.group = group;  
  22.     }  
  23.   
  24.     @Override  
  25.     protected void initChannel(Channel ch) throws Exception {  
  26.         ChannelPipeline pipeline = ch.pipeline();  
  27.         //编解码http请求  
  28.         pipeline.addLast(new HttpServerCodec());  
  29.         //写文件内容  
  30.         pipeline.addLast(new ChunkedWriteHandler());  
  31.         //聚合解码HttpRequest/HttpContent/LastHttpContent到FullHttpRequest  
  32.         //保证接收的Http请求的完整性  
  33.         pipeline.addLast(new HttpObjectAggregator(64 * 1024));  
  34.         //处理FullHttpRequest  
  35.         pipeline.addLast(new HttpRequestHandler("/ws"));  
  36.         //处理其他的WebSocketFrame  
  37.         pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));  
  38.         //处理TextWebSocketFrame  
  39.         pipeline.addLast(new TextWebSocketFrameHandler(group));  
  40.     }  
  41.   
  42. }  
        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。看下面代码:
  1. package netty.in.action;  
  2.   
  3. import io.netty.bootstrap.ServerBootstrap;  
  4. import io.netty.channel.Channel;  
  5. import io.netty.channel.ChannelFuture;  
  6. import io.netty.channel.ChannelInitializer;  
  7. import io.netty.channel.EventLoopGroup;  
  8. import io.netty.channel.group.ChannelGroup;  
  9. import io.netty.channel.group.DefaultChannelGroup;  
  10. import io.netty.channel.nio.NioEventLoopGroup;  
  11. import io.netty.channel.socket.nio.NioServerSocketChannel;  
  12. import io.netty.util.concurrent.ImmediateEventExecutor;  
  13.   
  14. import java.net.InetSocketAddress;  
  15.   
  16. /** 
  17.  * 访问地址:http://localhost:2048 
  18.  *  
  19.  * @author c.k 
  20.  *  
  21.  */  
  22. public class ChatServer {  
  23.   
  24.     private final ChannelGroup group = new DefaultChannelGroup(  
  25.             ImmediateEventExecutor.INSTANCE);  
  26.     private final EventLoopGroup workerGroup = new NioEventLoopGroup();  
  27.     private Channel channel;  
  28.   
  29.     public ChannelFuture start(InetSocketAddress address) {  
  30.         ServerBootstrap b = new ServerBootstrap();  
  31.         b.group(workerGroup).channel(NioServerSocketChannel.class)  
  32.                 .childHandler(createInitializer(group));  
  33.         ChannelFuture f = b.bind(address).syncUninterruptibly();  
  34.         channel = f.channel();  
  35.         return f;  
  36.     }  
  37.   
  38.     public void destroy() {  
  39.         if (channel != null)  
  40.             channel.close();  
  41.         group.close();  
  42.         workerGroup.shutdownGracefully();  
  43.     }  
  44.   
  45.     protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {  
  46.         return new ChatServerInitializer(group);  
  47.     }  
  48.   
  49.     public static void main(String[] args) {  
  50.         final ChatServer server = new ChatServer();  
  51.         ChannelFuture f = server.start(new InetSocketAddress(2048));  
  52.         Runtime.getRuntime().addShutdownHook(new Thread() {  
  53.             @Override  
  54.             public void run() {  
  55.                 server.destroy();  
  56.             }  
  57.         });  
  58.         f.channel().closeFuture().syncUninterruptibly();  
  59.     }  
  60.   
  61. }  


另外,需要将index.html文件放在项目根目录,index.html内容如下:
  1. <html>  
  2. <head>  
  3. <title>Web Socket Test</title>  
  4. </head>  
  5. <body>  
  6. <script type="text/javascript">  
  7. var socket;  
  8. if (!window.WebSocket) {  
  9.   window.WebSocket = window.MozWebSocket;  
  10. }  
  11. if (window.WebSocket) {  
  12.   socket = new WebSocket("ws://localhost:2048/ws");  
  13.   socket.onmessage = function(event) {  
  14.     var ta = document.getElementById('responseText');  
  15.     ta.value = ta.value + '\n' + event.data  
  16.   };  
  17.   socket.onopen = function(event) {  
  18.     var ta = document.getElementById('responseText');  
  19.     ta.value = "Web Socket opened!";  
  20.   };  
  21.   socket.onclose = function(event) {  
  22.     var ta = document.getElementById('responseText');  
  23.     ta.value = ta.value + "Web Socket closed";   
  24.   };  
  25. } else {  
  26.   alert("Your browser does not support Web Socket.");  
  27. }  
  28.   
  29. function send(message) {  
  30.   if (!window.WebSocket) { return; }  
  31.   if (socket.readyState == WebSocket.OPEN) {  
  32.     socket.send(message);  
  33.   } else {  
  34.     alert("The socket is not open.");  
  35.   }  
  36. }  
  37. </script>  
  38.     <form onsubmit="return false;">  
  39.         <input type="text" name="message" value="Hello, World!"><input  
  40.             type="button" value="Send Web Socket Data"  
  41.             onclick="send(this.form.message.value)">  
  42.         <h3>Output</h3>  
  43.         <textarea id="responseText" style="width: 500px; height: 300px;"></textarea>  
  44.     </form>  
  45. </body>  
  46. </html>  
最后在浏览器中输入:http://localhost:2048,多开几个窗口就可以聊天了。

11.5 给WebSocket加密

        上面的应用程序虽然工作的很好,但是在网络上收发消息存在很大的安全隐患,所以有必要对消息进行加密。添加这样一个加密的功能一般比较复杂,需要对代码有较大的改动。但是使用Netty就可以很容易的添加这样的功能,只需要将SslHandler加入到ChannelPipeline中就可以了。实际上还需要添加SslContext,但这不在本例子范围内。
        首先我们创建一个用于添加加密Handler的handler初始化类,看下面代码:
  1. package netty.in.action;  
  2.   
  3. import io.netty.channel.Channel;  
  4. import io.netty.channel.group.ChannelGroup;  
  5. import io.netty.handler.ssl.SslHandler;  
  6.   
  7. import javax.net.ssl.SSLContext;  
  8. import javax.net.ssl.SSLEngine;  
  9.   
  10. public class SecureChatServerIntializer extends ChatServerInitializer {  
  11.     private final SSLContext context;  
  12.   
  13.     public SecureChatServerIntializer(ChannelGroup group,SSLContext context) {  
  14.         super(group);  
  15.         this.context = context;  
  16.     }  
  17.   
  18.     @Override  
  19.     protected void initChannel(Channel ch) throws Exception {  
  20.         super.initChannel(ch);  
  21.         SSLEngine engine = context.createSSLEngine();  
  22.         engine.setUseClientMode(false);  
  23.         ch.pipeline().addFirst(new SslHandler(engine));  
  24.     }  
  25. }  
        最后我们创建一个用于引导配置的类,看下面代码:
  1. package netty.in.action;  
  2.   
  3. import io.netty.channel.Channel;  
  4. import io.netty.channel.ChannelFuture;  
  5. import io.netty.channel.ChannelInitializer;  
  6. import io.netty.channel.group.ChannelGroup;  
  7. import java.net.InetSocketAddress;  
  8. import javax.net.ssl.SSLContext;  
  9.   
  10. /** 
  11.  * 访问地址:https://localhost:4096 
  12.  *  
  13.  * @author c.k 
  14.  *  
  15.  */  
  16. public class SecureChatServer extends ChatServer {  
  17.     private final SSLContext context;  
  18.   
  19.     public SecureChatServer(SSLContext context) {  
  20.         this.context = context;  
  21.     }  
  22.   
  23.     @Override  
  24.     protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {  
  25.         return new SecureChatServerIntializer(group, context);  
  26.     }  
  27.   
  28.     /** 
  29.      * 获取SSLContext需要相关的keystore文件,这里没有 关于HTTPS可以查阅相关资料,这里只介绍在Netty中如何使用 
  30.      *  
  31.      * @return 
  32.      */  
  33.     private static SSLContext getSslContext() {  
  34.         return null;  
  35.     }  
  36.   
  37.     public static void main(String[] args) {  
  38.         SSLContext context = getSslContext();  
  39.         final SecureChatServer server = new SecureChatServer(context);  
  40.         ChannelFuture future = server.start(new InetSocketAddress(4096));  
  41.         Runtime.getRuntime().addShutdownHook(new Thread() {  
  42.             @Override  
  43.             public void run() {  
  44.                 server.destroy();  
  45.             }  
  46.         });  
  47.         future.channel().closeFuture().syncUninterruptibly();  
  48.     }  
  49. }  

11.6 Summary

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值