分享

Netty4实战第十一章:WebSockets

 柠檬冰啡咖 2017-12-28

本章主要内容

  • WebSocket
  • ChannelHandler,Decoder和Encoder
  • 启动基于Netty的应用
  • 测试WebSocket
  现在很多地方都可以看到“real-time-web”这个术语。大部分用户在访问网站时希望能实时获取信息。

  Netty提供了很多WebSocket相关的支持,包括不同的版本。在你的应用中可以无脑使用这些技术。因为你不需要详细了解那些底层协议相关的知识,只需要使用它提供的简单的API即可。

  本章我们将学习开发一个关于WebSocket的例子,来理解如何在实际项目中使用它。你也可以将例子中的一些代码集成到自己的应用中,复用这些代码。

一、挑战

  为了说明WebSocket的实时性,这个例子我们将会用WebSocket实现一个WEB版本的即时通讯应用,类似QQ那样。这个应用很类似QQ中的群聊天功能,一个用户发消息,其他用户都能收到。


  从上图可以看出,一个用户发了消息,然后服务端转发给其他用户。不过我们主要实现服务器端部分,因为客户端我们会使用浏览器。现在就开始实现这个应用。

二、实现

  WebSocket使用HTTP升级机制将HTTP连接转成WebSocket连接。因此WebSocket应用都是先从HTTP开始,然后升级到WebSocket。升级发生的时间是应用指定的。有的是上来就升级,有的是访问了指定的URL之后才升级。

  这个例子中,当URL以/ws结尾时才开始升级到WebSocket,否则就返回一个网页给客户端。一旦升级之后就通过WebSocket传输数据。


  主要的逻辑都通过ChannelHandler来实现,方便以后复用。下一小节就详细介绍这些ChannelHandler。

2.1、处理HTTP请求

  上一小节说过,服务端将同时处理HTTP请求和WebSocket请求,因为服务端也要返回HTML页面,比如聊天室页面,用于客户端展示。

  因此我们需要编写一个ChannelInboundHandler,处理客户端的HTTP请求,消息实体使用FullHttpRequest。

  1. package com.nan.netty.chatroom;  
  2.   
  3. import io.netty.buffer.Unpooled;  
  4. import io.netty.channel.*;  
  5. import io.netty.handler.codec.http.*;  
  6. import io.netty.handler.ssl.SslHandler;  
  7. import io.netty.handler.stream.ChunkedNioFile;  
  8.   
  9. import java.io.RandomAccessFile;  
  10.   
  11. public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {  
  12.   
  13.     private final String wsUri;  
  14.   
  15.     public HttpRequestHandler(String wsUri) {  
  16.         this.wsUri = wsUri;  
  17.     }  
  18.   
  19.     @Override  
  20.     public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {  
  21.         if (wsUri.equalsIgnoreCase(request.uri())) {  
  22.             //如果是WebSocket请求,则保留数据并传递到下一个ChannelHandler  
  23.             ctx.fireChannelRead(request.retain());  
  24.         } else {  
  25.             if (HttpUtil.is100ContinueExpected(request)) {  
  26.                 //收到100-continue,则返回给客户端100  
  27.                 send100Continue(ctx);  
  28.             }  
  29.             boolean keepAlive;  
  30.             ChannelFuture future;  
  31.             try (RandomAccessFile file = new RandomAccessFile(this.getClass().getResource("/").getPath() + "../resources/index.html""r")) {  
  32.                 HttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);  
  33.                 response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");  
  34.                 response.headers().set(HttpHeaderNames.CONTENT_LENGTH, file.length());  
  35.                 keepAlive = HttpUtil.isKeepAlive(request);  
  36.                 if (keepAlive) {  
  37.                     //如果需要keep-alive,则添加相应头信息  
  38.                     response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);  
  39.                 }  
  40.                 ctx.write(response);  
  41.                 if (ctx.pipeline().get(SslHandler.class) == null) {  
  42.                     //不用加密使用零内存复制发送文件  
  43.                     future = ctx.writeAndFlush(new DefaultFileRegion(file.getChannel(), 0, file.length()));  
  44.                 } else {  
  45.                     future = ctx.writeAndFlush(new ChunkedNioFile(file.getChannel()));  
  46.                 }  
  47.             }  
  48.             //如果不是keep-alive,则关闭Channel  
  49.             if (!keepAlive) {  
  50.                 future.addListener(ChannelFutureListener.CLOSE);  
  51.             }  
  52.         }  
  53.   
  54.     }  
  55.   
  56.     private static void send100Continue(ChannelHandlerContext ctx) {  
  57.         FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);  
  58.         ctx.writeAndFlush(response);  
  59.     }  
  60.   
  61.     @Override  
  62.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  
  63.         cause.printStackTrace();  
  64.         ctx.close();  
  65.     }  
  66. }  

  上面实现的HttpRequestHandler坐了以下的事情:

  • 首先检查请求路径是否是WebSocket,如果是WebSocket则使用retain()方法和fireChannelRead(msg)方法将数据传递下去。
  • 如果客户端发送的100,则返回100。
  • 如果不是WebSocket请求,则返回一个HttpResponse,返回的内容就是一个HTML文件的内容。
  当然,上边这个ChannelHandler只处理了普通HTTP的请求,我们还需要处理WebSocket的请求,然后将收到的数据转发,下一小节就来学习这个知识点。

2.2、处理WebSocket请求

  Netty提供了6种不同的WebSocket类型,如下表所示。
名称 描述
BinaryWebSocketFrame
二进制数据类型
TextWebSocketFrame
文本数据类型
ContinuationWebSocketFrame
属于前一个二进制类型或文本类型
CloseWebSocketFrame
关闭请求
PingWebSocketFrame
请求发送了PongWebSocketFrame
PongWebSocketFrame
响应发送了PingWebSocketFrame

  而我们的应用,一般只需要处理下面四种类型:

  • CloseWebSocketFrame
  • PingWebSocketFrame
  • PongWebSocketFrame
  • TextWebSocketFrame

  幸运的是,其实只需要处理TextWebSocketFrame,其他的可以使用Netty提供的WebSocketServerProtocolHandler处理,Netty帮助开发者简化开发工作,下面的代码就是处理TextWebSocketFrame的ChannelHandler。

  1. package com.nan.netty.chatroom;  
  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. public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {  
  10.   
  11.     private final ChannelGroup group;  
  12.   
  13.     public TextWebSocketFrameHandler(ChannelGroup group) {  
  14.         this.group = group;  
  15.     }  
  16.   
  17.     @Override  
  18.     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {  
  19.         //当WebSocket连接成功  
  20.         if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {  
  21.             //不需要再处理HTTP请求  
  22.             ctx.pipeline().remove(HttpRequestHandler.class);  
  23.             //告诉已经连接的客户端有新的客户端连接进来了  
  24.             group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel() + " joined"));  
  25.             //将新的客户端添加到ChannelGroup中  
  26.             group.add(ctx.channel());  
  27.         } else {  
  28.             super.userEventTriggered(ctx, evt);  
  29.         }  
  30.     }  
  31.   
  32.     @Override  
  33.     public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {  
  34.         //消息转发给每一个客户端  
  35.         group.writeAndFlush(msg.retain());  
  36.     }  
  37. }  
  主要的业务逻辑已经实现完了,现在只需要再实现一个ChannelInitializer,用来初始化每个Channel。

2.3、初始化ChannelPipeline

  最后一步就是需要初始化ChannelPipeline,添加所有需要的ChannelHandler。前面的章节讲过,只需要继承ChannelInitializer,然后重写initChannel(…)方法即可,如下面的代码。

  1. package com.nan.netty.chatroom;  
  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. public class ChatServerInitializer extends ChannelInitializer<Channel> {  
  13.   
  14.     private final ChannelGroup group;  
  15.   
  16.     public ChatServerInitializer(ChannelGroup group) {  
  17.         this.group = group;  
  18.     }  
  19.   
  20.     @Override  
  21.     protected void initChannel(Channel ch) throws Exception {  
  22.         ChannelPipeline pipeline = ch.pipeline();  
  23.           
  24.         pipeline.addLast(new HttpServerCodec());  
  25.         pipeline.addLast(new ChunkedWriteHandler());  
  26.         pipeline.addLast(new HttpObjectAggregator(64 * 1024));  
  27.         pipeline.addLast(new HttpRequestHandler("/ws"));  
  28.         pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));  
  29.         pipeline.addLast(new TextWebSocketFrameHandler(group));  
  30.     }  
  31. }  

  大部分情况下initChannel(...)方法都是用来设置新注册Channel的ChannelPipeline,主要代码就是将应用中需要的ChannelHandler添加到ChannelPipeline中。

  我们来看看上边的代码中我们添加的那些ChannelHandler的作用。

名称

描述

HttpServerCodec

解码HTTP请求,编码HTTP响应

ChunkedWriteHandler

用来写文件内容

HttpObjectAggregator

解码HttpRequest/HttpContent /LastHttpContent聚合成FullHttpRequest,
使用它你接收的使用FullHttpRequest

HttpRequestHandler

这个我们自定义的实现,普通HTTP请求返回HTML,WebSocket传递给

下一个ChannelHandler

WebSocketServerProtocolHandler

处理WebSocket请求的Ping/Pong/Close事件

TextWebSocketFrameHandler

我们自定义实现的处理WebSocket文本消息,转发给其他客户端

  WebSocketServerProtcolHandler比较特殊一些,所以这里多做一些介绍。WebSocketServerProtocol只处理WebSocket的Ping/Pong/Close请求,所以可以通过它帮忙将应用升级到WebSocket协议。
  通过执行握手连接,一旦成功后就可以修改
ChannelPipeline,添加需要的编解码器,把不需要的编解码器移除。我们刚初始化后的ChannelPipeline如下图


  一旦WebSocket握手完成,则就会发生变化。WebSocketServerProtocolHandler会将HttpRequestDecoder替换成WebSocketFrameDecoder,将HttpResponseEncoder替换成WebSocketFrameEncoder。除此之外它还会移除其他不需要的ChannelHandler,例如我们本例中HttpObjectAggregator,然后我们在实现代码中移除了HttpRequestHandler,升级到WebSocket后的ChannelPipeline如下图。


  上面ChannelPipeline的更新是Netty背后帮我们完成的,这种方式还是很灵活的,并且可以将不同的任务分配到不同的ChannelHandler中。

三、编写服务端启动类和客户端

  主要的业务逻辑都已经完成,现在只需要按照Netty提供的固定套路,设置服务端启动器然后启动服务端即可。

  1. package com.nan.netty.chatroom;  
  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. public class ChatServer {  
  17.   
  18.     //使用DefaultChannelGroup保存所有WebSocket客户端  
  19.     private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);  
  20.       
  21.     private final EventLoopGroup group = new NioEventLoopGroup();  
  22.   
  23.     private Channel channel;  
  24.   
  25.     /** 
  26.      * 启动服务端 
  27.      */  
  28.     public ChannelFuture start(InetSocketAddress address) {  
  29.         //服务端启动器  
  30.         ServerBootstrap bootstrap = new ServerBootstrap();  
  31.         bootstrap.group(group)  
  32.                 .channel(NioServerSocketChannel.class)  
  33.                 .childHandler(createInitializer(channelGroup));  
  34.         ChannelFuture future = bootstrap.bind(address);  
  35.         future.syncUninterruptibly();  
  36.         channel = future.channel();  
  37.         return future;  
  38.     }  
  39.   
  40.     /** 
  41.      * 创建Channel初始化器 
  42.      */  
  43.     protected ChannelInitializer<Channel> createInitializer(ChannelGroup channelGroup) {  
  44.         return new ChatServerInitializer(channelGroup);  
  45.     }  
  46.   
  47.     /** 
  48.      * 释放资源 
  49.      */  
  50.     public void destroy() {  
  51.         if (channel != null) {  
  52.             channel.close();  
  53.         }  
  54.         channelGroup.close();  
  55.         group.shutdownGracefully();  
  56.     }  
  57.   
  58.     /** 
  59.      * 主方法 
  60.      */  
  61.     public static void main(String[] args) {  
  62.         //服务端监听端口  
  63.         int port = 9999;  
  64.         final ChatServer endpoint = new ChatServer();  
  65.         ChannelFuture future = endpoint.start(new InetSocketAddress(port));  
  66.         Runtime.getRuntime().addShutdownHook(new Thread(() -> endpoint.destroy()));  
  67.         future.channel().closeFuture().syncUninterruptibly();  
  68.     }  
  69. }  

  服务端的代码已经完成,现在我们实现一个客户端。本例中我们使用浏览器作为客户端,所以需要编写一个HTML页面,并使用JS的WebSocket API与服务端连接并通信。

  1. <!DOCTYPE html>  
  2. <html>  
  3. <head>  
  4.     <meta charset="UTF-8">  
  5.     <title>聊天室</title>  
  6. </head>  
  7. <body style="display: flex; justify-content: center; align-items: center">  
  8. <form onsubmit="return false;">  
  9.     <h3>WebSocket聊天室:</h3>  
  10.     <div>  
  11.         <textarea id="responseText" style="width: 500px; height: 300px;"></textarea>  
  12.     </div>  
  13.     <div>  
  14.         <input type="text" name="message"  style="width: 430px" value="我是Netty">  
  15.         <input type="button" value="发送消息" onclick="send(this.form.message.value)">  
  16.     </div>  
  17. </form>  
  18. <script type="text/javascript">  
  19.     var socket;  
  20.     if (!window.WebSocket) {  
  21.         window.WebSocket = window.MozWebSocket;  
  22.     }  
  23.     if (window.WebSocket) {  
  24.         socket = new WebSocket("ws://localhost:9999/ws");  
  25.         socket.onmessage = function(event) {  
  26.             console.log(event);  
  27.             var ta = document.getElementById('responseText');  
  28.             ta.value = ta.value + '\n' + event.data  
  29.         };  
  30.         socket.onopen = function(event) {  
  31.             var ta = document.getElementById('responseText');  
  32.             ta.value = "连接开启!";  
  33.         };  
  34.         socket.onclose = function(event) {  
  35.             var ta = document.getElementById('responseText');  
  36.             ta.value = ta.value + "连接被关闭";  
  37.         };  
  38.     } else {  
  39.         alert("你的浏览器不支持 WebSocket!");  
  40.     }  
  41.   
  42.     function send(message) {  
  43.         if (!window.WebSocket) {  
  44.             return;  
  45.         }  
  46.         if (socket.readyState == WebSocket.OPEN) {  
  47.             socket.send(message);  
  48.         } else {  
  49.             alert("连接没有开启.");  
  50.         }  
  51.     }  
  52. </script>  
  53. </body>  
  54. </html>  
  这个HTML文件的存放位置要和HttpRequestHandler代码中的路径一致,不然会发生找不到文件异常,比如本例中,我使用的是Gradle工具,所以将这个文件放到了Resources目录下。

  现在就可以尝试一下我们的聊天室了。首先启动服务端,然后在浏览器中输入http://localhost:9999/,最好使用Chrome浏览器,然后就能看到如下页面。

  然后再打开一个浏览器标签,同样访问地址http://localhost:9999/,可以同样看到上图画面,并且第一个打开的浏览器页面会编程如下所示。


  说明另一个客户端已经成功连接,并且通知了已经存在的客户端,当然,发消息功能也是没问题。

四、加密

  上面我们已经实现了聊天室的主要功能,但是可能有人会提出,消息内容很私密,需要我们需要在传输过程中加密。

  一般来说,添加加密功能不能一件容易的工作,往往需要大改项目。但是因为我们使用的是Netty,只需要将SslHandler添加到ChannelPipeline即可。虽然说还需要改一点点代码,但也是很简单的修改,不过如果还需要配置SslContext,可能就会麻烦一些。

  总体来说,使用Netty添加加密功能还是很简单的,主要是ChannelPipeline的存在,这里再次感谢它。

  因为我们需要将SslHandler添加到ChannelPipeline中,所以需要改造一下ChatServerInitializer,这里我们继承ChatServerInitializer再开发一个有加密功能的初始化类。

  1. package com.nan.netty.chatroom;  
  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 SecureChatServerInitializer extends ChatServerInitializer {  
  11.   
  12.     private final SSLContext context;  
  13.   
  14.     public SecureChatServerInitializer(ChannelGroup group, SSLContext context) {  
  15.         super(group);  
  16.         this.context = context;  
  17.     }  
  18.   
  19.     @Override  
  20.     protected void initChannel(Channel ch) throws Exception {  
  21.         //父类原样初始化  
  22.         super.initChannel(ch);  
  23.         SSLEngine engine = context.createSSLEngine();  
  24.         engine.setUseClientMode(false);  
  25.         //加密解密的SslHandler需要放到首位  
  26.         ch.pipeline().addFirst(new SslHandler(engine));  
  27.     }  
  28. }  

  安全的ChannelInitializer逻辑很简单,就是将SslHandler添加到ChannelPipeline第一个位置。下一步呢,我们就是需要在启动服务端时候使用这个新的SecureChatServerInitializer,并且在构造的时候将SSLContext传入,但是SSLContext是需要SSL证书的,这里为了方便就不向专业结构申请了,直接使用JDK自带的keytool工具生成自签名证书,当然这个证书是不被浏览器认可的,所以不能用在生产环境中。

  我这边使用Windows系统,Linux或其他系统基本操作一致。打开CMD窗口,执行下面命令。

  1. keytool -genkey -keysize 2048 -validity 365 -keyalg RSA -keypass netty123 -storepass netty123 -keystore netty.jks  

  keytool就是JDK自带的工具,大家应该把JAVA安装目录下面的bin目录加到环境变量中了吧,参数意义如下。

  • -keysize 2048 密钥长度2048位
  • -validity 365 证书有效期365天
  • -keyalg RSA 使用RSA非对称加密算法
  • -keypass netty123 密钥的访问密码
  • -storepass netty123 密钥库的访问密码
  • -keystore netty.jks 指定生成的密钥库文件
  然后需要填写一些信息,最后确认一步输入“y”即可,如下图。
  有的情况下会出现无权限访问文件的问题,就要看看你的用户有没有目录权限了,Windows系统的同学可以打开具有管理员权限的CMD窗口解决这种问题。
  然后将生成的证书复制到指定目录,我这里还是放到项目的Resources目录下,然后编写安全版本的启动类。
  1. package com.nan.netty.chatroom;  
  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.   
  8. import javax.net.ssl.KeyManagerFactory;  
  9. import javax.net.ssl.SSLContext;  
  10. import java.io.FileInputStream;  
  11. import java.io.InputStream;  
  12. import java.net.InetSocketAddress;  
  13. import java.security.KeyStore;  
  14.   
  15. public class SecureChatServer extends ChatServer {  
  16.   
  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.         //Channel初始化使用安全版本的  
  26.         return new SecureChatServerInitializer(group, context);  
  27.   
  28.     }  
  29.   
  30.     public static void main(String[] args) {  
  31.         int port = 9999;  
  32.         SSLContext sslContext = null;  
  33.         //读取SSL证书文件,配置SSLContext  
  34.         try (InputStream ksInputStream = new FileInputStream(SecureChatServer.class.getResource("/").getPath() + "../resources/netty.jks")) {  
  35.             KeyStore ks = KeyStore.getInstance("JKS");  
  36.             ks.load(ksInputStream, "netty123".toCharArray());  
  37.             KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());  
  38.             kmf.init(ks, "netty123".toCharArray());  
  39.             sslContext = SSLContext.getInstance("TLS");  
  40.             sslContext.init(kmf.getKeyManagers(), nullnull);  
  41.         } catch (Exception e) {  
  42.             e.printStackTrace();  
  43.         }  
  44.         final SecureChatServer endpoint = new SecureChatServer(sslContext);  
  45.         ChannelFuture future = endpoint.start(new InetSocketAddress(port));  
  46.         Runtime.getRuntime().addShutdownHook(new Thread(() -> endpoint.destroy()));  
  47.         future.channel().closeFuture().syncUninterruptibly();  
  48.     }  
  49. }  
  现在还有最后一步,将之前的HTML文件里面的WebSocket连接修改为wss://localhost:9999/ws,因为我们使用了Ssl连接,那自然WebSocket协议也要修改成Ssl连接的。
  1. <!DOCTYPE html>  
  2. <html>  
  3. <head>  
  4.     <meta charset="UTF-8">  
  5.     <title>聊天室</title>  
  6. </head>  
  7. <body style="display: flex; justify-content: center; align-items: center">  
  8. <form onsubmit="return false;">  
  9.     <h3>WebSocket聊天室:</h3>  
  10.     <div>  
  11.         <textarea id="responseText" style="width: 500px; height: 300px;"></textarea>  
  12.     </div>  
  13.     <div>  
  14.         <input type="text" name="message"  style="width: 430px" value="我是Netty">  
  15.         <input type="button" value="发送消息" onclick="send(this.form.message.value)">  
  16.     </div>  
  17. </form>  
  18. <script type="text/javascript">  
  19.     var socket;  
  20.     if (!window.WebSocket) {  
  21.         window.WebSocket = window.MozWebSocket;  
  22.     }  
  23.     if (window.WebSocket) {  
  24.         socket = new WebSocket("wss://localhost:9999/ws");  
  25.         socket.onmessage = function(event) {  
  26.             console.log(event);  
  27.             var ta = document.getElementById('responseText');  
  28.             ta.value = ta.value + '\n' + event.data  
  29.         };  
  30.         socket.onopen = function(event) {  
  31.             var ta = document.getElementById('responseText');  
  32.             ta.value = "连接开启!";  
  33.         };  
  34.         socket.onclose = function(event) {  
  35.             var ta = document.getElementById('responseText');  
  36.             ta.value = ta.value + "连接被关闭";  
  37.         };  
  38.     } else {  
  39.         alert("你的浏览器不支持 WebSocket!");  
  40.     }  
  41.   
  42.     function send(message) {  
  43.         if (!window.WebSocket) {  
  44.             return;  
  45.         }  
  46.         if (socket.readyState == WebSocket.OPEN) {  
  47.             socket.send(message);  
  48.         } else {  
  49.             alert("连接没有开启.");  
  50.         }  
  51.     }  
  52. </script>  
  53. </body>  
  54. </html>  
  好了,代码都写完了,现在我们启动安全版的服务端,在浏览器输入https://localhost:9999/,一般浏览器都会提示这是不安全的连接,因为我们的证书是自签名的嘛。剩下的聊天功能和前面的一样,这里就不详说了。

五、总结

  这一章我们主要学习了如何使用Netty提供的WebSocket开发一个聊天室功能。学习了如何处理HTTP请求,已经如何转到WebSocket请求,并知道如何转发数据。也应该明白了为什么WebSocket相比较HTTP比较重要,当然也大概能知道WebSocket的使用场景。
  下一章中我们将学习Web 2.0其他的东西,这些东西可以增加你的应用更有魅力。下一章主要内容就是SPDY,看看它的优点,已经适合什么样的业务场景。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多