首页 > 代码库 > Netty笔记:使用WebSocket协议开发聊天系统

Netty笔记:使用WebSocket协议开发聊天系统

  转载请注明出处:http://blog.csdn.net/a906998248/article/details/52839425

前言,之前一直围绕着Http协议来开发项目,最近由于参与一个类似竞拍项目的开发,有这样一个场景,多个客户端竞拍一个商品,当一个客户端加价后,其它关注这个商品的客户端需要立即知道该商品的最新价格。
       这里有个问题,Http协议是基于请求/响应的,客户端发送请求,然后服务端响应返回,客户端是主动方,服务端被动的接收客户端的请求来响应,无法解决上述场景中服务端主动将最新的数据推送给客户端的需求。
       当然,有人会提出ajax轮询的方案,就是客户端不断的请求(假如1秒1次)最新竞拍价格。显然这种模式具有很明显的缺点,即浏览器需要不断地向服务器发出请求,但是Http request的Header是非常冗长的,里面包含的可用数据比例可能非常低,这会占用很多的带宽和服务器资源。

       还有一种比较新颖的方案,long poll(长轮询)。利用长轮询,客户端可以打开指向服务端的Http连接,而服务器会一直保持连接打开,直到服务端数据更新再发送响应。虽然这种方式比ajax轮询有进步,但都存在一个共同问题:由于Http协议的开销,导致它们不适合用于低延迟应用。

一.WebSocket协议简介

       WebSocket 是 Html5 开始提供的一种浏览器与服务器间进行全双工通信的网络技术。(全双工:同一时刻,数据可以在客户端和服务端两个方向上传输)

       在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后浏览器和服务器之间就形成了一条快速通道,两者就可以直接互相传送数据了


二.相比传统Http协议的优点及作用
  1.Http协议的弊端:
    a.Http协议为半双工协议。(半双工:同一时刻,数据只能在客户端和服务端一个方向上传输)
    b.Http协议冗长且繁琐
    c.易收到攻击,如长轮询
    d.非持久化协议
  2.WebSocket的特性:
    a.单一的 TCP 连接,采用全双工模式通信
    b.对代理、防火墙和路由器透明
    c.无头部信息、Cookie 和身份验证
    d.无安全开销
    e.通过 ping/pong 帧保持链路激活
    f.持久化协议,连接建立后,服务器可以主动传递消息给客户端,不再需要客户端轮询


三.聊天实例
       前面提到过,WebSocket通信需要建立WebSocket连接,客户端首先要向服务端发起一个 Http 请求,这个请求和通常的 Http 请求不同,包含了一些附加头信息,其中附加信息"Upgrade:WebSocket"表明这是一个基于 Http 的 WebSocket 握手请求。如下:

[html] view plain copy
 
 print?技术分享技术分享
  1. GET /chat HTTP/1.1  
  2. Host: server.example.com  
  3. Upgrade: websocket  
  4. Connection: Upgrade  
  5. Sec-WebSocket-Key: sdewgzgfewfsgergzgewrfaf==  
  6. Sec-WebSocket-Protocol: chat, superchat  
  7. Sec-WebSocket-Version: 13  
  8. Origin: http://example.com  

       其中,Sec-WebSocket-Key是随机的,服务端会使用它加密后作为Sec-WebSocket-Accept的值返回;Sec-WebSocket-Protocol是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议;Sec-WebSocket-Version是告诉服务器所使用的Websocket Draft(协议版本)
  
       不出意外,服务端会返回下列信息表示握手成功,连接已经建立:

[html] view plain copy
 
 print?技术分享技术分享
  1. HTTP/1.1 101 Switching Protocols  
  2. Upgrade: websocket  
  3. Connection: Upgrade  
  4. Sec-WebSocket-Accept: sdgdfshgretghsdfgergtbd=  
  5. Sec-WebSocket-Protocol: chat  


       到这里 WebSocket 连接已经成功建立,服务端和客户端可以正常通信了,此时服务端和客户端都是对等端点,都可以主动发送请求到另一端。

       下面是前端和后端的实现过程,后端我采用了 Netty 的 API,因为最近在学 Netty,所以就采用了 Netty 中的 NIO 来构建 WebSocket 后端,我看了下网上也有用 Tomcat API 来实现,看起来也很简单,朋友们可以试试。前端使用HTML5 来构建,可以参考WebSocket接口文档,非常方便简单。

 

Lanucher用来启动WebSocket服务端

 

[java] view plain copy
 
 print?技术分享技术分享
  1. import com.company.server.WebSocketServer;  
  2.   
  3. public class Lanucher {  
  4.   
  5.     public static void main(String[] args) throws Exception {  
  6.         // 启动WebSocket  
  7.         new WebSocketServer().run(WebSocketServer.WEBSOCKET_PORT);  
  8.     }  
  9.       
  10. }  


使用 Netty 构建的 WebSocket 服务

 

 

[java] view plain copy
 
 print?技术分享技术分享
  1. import org.apache.log4j.Logger;  
  2.   
  3. import io.netty.bootstrap.ServerBootstrap;  
  4. import io.netty.channel.Channel;  
  5. import io.netty.channel.ChannelInitializer;  
  6. import io.netty.channel.ChannelPipeline;  
  7. import io.netty.channel.EventLoopGroup;  
  8. import io.netty.channel.nio.NioEventLoopGroup;  
  9. import io.netty.channel.socket.nio.NioServerSocketChannel;  
  10. import io.netty.handler.codec.http.HttpObjectAggregator;  
  11. import io.netty.handler.codec.http.HttpServerCodec;  
  12. import io.netty.handler.stream.ChunkedWriteHandler;  
  13.   
  14. /** 
  15.  * WebSocket服务 
  16.  * 
  17.  */  
  18. public class WebSocketServer {  
  19.     private static final Logger LOG = Logger.getLogger(WebSocketServer.class);  
  20.       
  21.     // websocket端口  
  22.     public static final int WEBSOCKET_PORT = 9090;  
  23.   
  24.     public void run(int port) throws Exception {  
  25.         EventLoopGroup bossGroup = new NioEventLoopGroup();  
  26.         EventLoopGroup workerGroup = new NioEventLoopGroup();  
  27.         try {  
  28.             ServerBootstrap b = new ServerBootstrap();  
  29.             b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<Channel>() {  
  30.   
  31.                 @Override  
  32.                 protected void initChannel(Channel channel) throws Exception {  
  33.                     ChannelPipeline pipeline = channel.pipeline();  
  34.                     pipeline.addLast("http-codec", new HttpServerCodec()); // Http消息编码解码  
  35.                     pipeline.addLast("aggregator", new HttpObjectAggregator(65536)); // Http消息组装  
  36.                     pipeline.addLast("http-chunked", new ChunkedWriteHandler()); // WebSocket通信支持  
  37.                     pipeline.addLast("handler", new BananaWebSocketServerHandler()); // WebSocket服务端Handler  
  38.                 }  
  39.             });  
  40.               
  41.             Channel channel = b.bind(port).sync().channel();  
  42.             LOG.info("WebSocket 已经启动,端口:" + port + ".");  
  43.             channel.closeFuture().sync();  
  44.         } finally {  
  45.             bossGroup.shutdownGracefully();  
  46.             workerGroup.shutdownGracefully();  
  47.         }  
  48.     }  
  49.       
  50. }  


WebSocket 服务端处理类,注意第一次握手是 Http 协议

 

 

[java] view plain copy
 
 print?技术分享技术分享
  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.ChannelPromise;  
  7. import io.netty.channel.SimpleChannelInboundHandler;  
  8. import io.netty.handler.codec.http.DefaultFullHttpResponse;  
  9. import io.netty.handler.codec.http.FullHttpRequest;  
  10. import io.netty.handler.codec.http.FullHttpResponse;  
  11. import io.netty.handler.codec.http.HttpHeaders;  
  12. import io.netty.handler.codec.http.HttpResponseStatus;  
  13. import io.netty.handler.codec.http.HttpVersion;  
  14. import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;  
  15. import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;  
  16. import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;  
  17. import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;  
  18. import io.netty.handler.codec.http.websocketx.WebSocketFrame;  
  19. import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;  
  20. import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;  
  21. import io.netty.util.CharsetUtil;  
  22.   
  23. import org.apache.log4j.Logger;  
  24.   
  25. import com.company.serviceimpl.BananaService;  
  26. import com.company.util.CODE;  
  27. import com.company.util.Request;  
  28. import com.company.util.Response;  
  29. import com.google.common.base.Strings;  
  30. import com.google.gson.JsonSyntaxException;  
  31.   
  32.   
  33. /** 
  34.  * WebSocket服务端Handler 
  35.  * 
  36.  */  
  37. public class BananaWebSocketServerHandler extends SimpleChannelInboundHandler<Object> {  
  38.     private static final Logger LOG = Logger.getLogger(BananaWebSocketServerHandler.class.getName());  
  39.       
  40.     private WebSocketServerHandshaker handshaker;  
  41.     private ChannelHandlerContext ctx;  
  42.     private String sessionId;  
  43.   
  44.     @Override  
  45.     public void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {  
  46.         if (msg instanceof FullHttpRequest) { // 传统的HTTP接入  
  47.             handleHttpRequest(ctx, (FullHttpRequest) msg);  
  48.         } else if (msg instanceof WebSocketFrame) { // WebSocket接入  
  49.             handleWebSocketFrame(ctx, (WebSocketFrame) msg);  
  50.         }  
  51.     }  
  52.   
  53.     @Override  
  54.     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {  
  55.         ctx.flush();  
  56.     }  
  57.       
  58.     @Override  
  59.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  
  60.         LOG.error("WebSocket异常", cause);  
  61.         ctx.close();  
  62.         LOG.info(sessionId + "  注销");  
  63.         BananaService.logout(sessionId); // 注销  
  64.         BananaService.notifyDownline(sessionId); // 通知有人下线  
  65.     }  
  66.   
  67.     @Override  
  68.     public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {  
  69.         LOG.info("WebSocket关闭");  
  70.         super.close(ctx, promise);  
  71.         LOG.info(sessionId + " 注销");  
  72.         BananaService.logout(sessionId); // 注销  
  73.         BananaService.notifyDownline(sessionId); // 通知有人下线  
  74.     }  
  75.   
  76.     /** 
  77.      * 处理Http请求,完成WebSocket握手<br/> 
  78.      * 注意:WebSocket连接第一次请求使用的是Http 
  79.      * @param ctx 
  80.      * @param request 
  81.      * @throws Exception 
  82.      */  
  83.     private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {  
  84.         // 如果HTTP解码失败,返回HHTP异常  
  85.         if (!request.getDecoderResult().isSuccess() || (!"websocket".equals(request.headers().get("Upgrade")))) {  
  86.             sendHttpResponse(ctx, request, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));  
  87.             return;  
  88.         }  
  89.   
  90.         // 正常WebSocket的Http连接请求,构造握手响应返回  
  91.         WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://" + request.headers().get(HttpHeaders.Names.HOST), null, false);  
  92.         handshaker = wsFactory.newHandshaker(request);  
  93.         if (handshaker == null) { // 无法处理的websocket版本  
  94.             WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());  
  95.         } else { // 向客户端发送websocket握手,完成握手  
  96.             handshaker.handshake(ctx.channel(), request);  
  97.             // 记录管道处理上下文,便于服务器推送数据到客户端  
  98.             this.ctx = ctx;  
  99.         }  
  100.     }  
  101.   
  102.     /** 
  103.      * 处理Socket请求 
  104.      * @param ctx 
  105.      * @param frame 
  106.      * @throws Exception  
  107.      */  
  108.     private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {  
  109.         // 判断是否是关闭链路的指令  
  110.         if (frame instanceof CloseWebSocketFrame) {  
  111.             handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());  
  112.             return;  
  113.         }  
  114.         // 判断是否是Ping消息  
  115.         if (frame instanceof PingWebSocketFrame) {  
  116.             ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));  
  117.             return;  
  118.         }  
  119.         // 当前只支持文本消息,不支持二进制消息  
  120.         if (!(frame instanceof TextWebSocketFrame)) {  
  121.             throw new UnsupportedOperationException("当前只支持文本消息,不支持二进制消息");  
  122.         }  
  123.           
  124.         // 处理来自客户端的WebSocket请求  
  125.         try {  
  126.             Request request = Request.create(((TextWebSocketFrame)frame).text());  
  127.             Response response = new Response();  
  128.             response.setServiceId(request.getServiceId());  
  129.             if (CODE.online.code.intValue() == request.getServiceId()) { // 客户端注册  
  130.                 String requestId = request.getRequestId();  
  131.                 if (Strings.isNullOrEmpty(requestId)) {  
  132.                     response.setIsSucc(false).setMessage("requestId不能为空");  
  133.                     return;  
  134.                 } else if (Strings.isNullOrEmpty(request.getName())) {  
  135.                     response.setIsSucc(false).setMessage("name不能为空");  
  136.                     return;  
  137.                 } else if (BananaService.bananaWatchMap.containsKey(requestId)) {  
  138.                     response.setIsSucc(false).setMessage("您已经注册了,不能重复注册");  
  139.                     return;  
  140.                 }  
  141.                 if (!BananaService.register(requestId, new BananaService(ctx, request.getName()))) {  
  142.                     response.setIsSucc(false).setMessage("注册失败");  
  143.                 } else {  
  144.                     response.setIsSucc(true).setMessage("注册成功");  
  145.                       
  146.                     BananaService.bananaWatchMap.forEach((reqId, callBack) -> {  
  147.                         response.getHadOnline().put(reqId, ((BananaService)callBack).getName()); // 将已经上线的人员返回  
  148.                           
  149.                         if (!reqId.equals(requestId)) {  
  150.                             Request serviceRequest = new Request();  
  151.                             serviceRequest.setServiceId(CODE.online.code);  
  152.                             serviceRequest.setRequestId(requestId);  
  153.                             serviceRequest.setName(request.getName());  
  154.                             try {  
  155.                                 callBack.send(serviceRequest); // 通知有人上线  
  156.                             } catch (Exception e) {  
  157.                                 LOG.warn("回调发送消息给客户端异常", e);  
  158.                             }  
  159.                         }  
  160.                     });  
  161.                 }  
  162.                 sendWebSocket(response.toJson());  
  163.                 this.sessionId = requestId; // 记录会话id,当页面刷新或浏览器关闭时,注销掉此链路  
  164.             } else if (CODE.send_message.code.intValue() == request.getServiceId()) { // 客户端发送消息到聊天群  
  165.                 String requestId = request.getRequestId();  
  166.                 if (Strings.isNullOrEmpty(requestId)) {  
  167.                     response.setIsSucc(false).setMessage("requestId不能为空");  
  168.                 } else if (Strings.isNullOrEmpty(request.getName())) {  
  169.                     response.setIsSucc(false).setMessage("name不能为空");  
  170.                 } else if (Strings.isNullOrEmpty(request.getMessage())) {  
  171.                     response.setIsSucc(false).setMessage("message不能为空");  
  172.                 } else {  
  173.                     response.setIsSucc(true).setMessage("发送消息成功");  
  174.                       
  175.                     BananaService.bananaWatchMap.forEach((reqId, callBack) -> { // 将消息发送到所有机器  
  176.                         Request serviceRequest = new Request();  
  177.                         serviceRequest.setServiceId(CODE.receive_message.code);  
  178.                         serviceRequest.setRequestId(requestId);  
  179.                         serviceRequest.setName(request.getName());  
  180.                         serviceRequest.setMessage(request.getMessage());  
  181.                         try {  
  182.                             callBack.send(serviceRequest);  
  183.                         } catch (Exception e) {  
  184.                             LOG.warn("回调发送消息给客户端异常", e);  
  185.                         }  
  186.                     });  
  187.                 }  
  188.                 sendWebSocket(response.toJson());  
  189.             } else if (CODE.downline.code.intValue() == request.getServiceId()) { // 客户端下线  
  190.                 String requestId = request.getRequestId();  
  191.                 if (Strings.isNullOrEmpty(requestId)) {  
  192.                     sendWebSocket(response.setIsSucc(false).setMessage("requestId不能为空").toJson());  
  193.                 } else {  
  194.                     BananaService.logout(requestId);  
  195.                     response.setIsSucc(true).setMessage("下线成功");  
  196.                       
  197.                     BananaService.notifyDownline(requestId); // 通知有人下线  
  198.                       
  199.                     sendWebSocket(response.toJson());  
  200.                 }  
  201.                   
  202.             } else {  
  203.                 sendWebSocket(response.setIsSucc(false).setMessage("未知请求").toJson());  
  204.             }  
  205.         } catch (JsonSyntaxException e1) {  
  206.             LOG.warn("Json解析异常", e1);  
  207.         } catch (Exception e2) {  
  208.             LOG.error("处理Socket请求异常", e2);  
  209.         }  
  210.     }  
  211.   
  212.     /** 
  213.      * Http返回 
  214.      * @param ctx 
  215.      * @param request 
  216.      * @param response 
  217.      */  
  218.     private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) {  
  219.         // 返回应答给客户端  
  220.         if (response.getStatus().code() != 200) {  
  221.             ByteBuf buf = Unpooled.copiedBuffer(response.getStatus().toString(), CharsetUtil.UTF_8);  
  222.             response.content().writeBytes(buf);  
  223.             buf.release();  
  224.             HttpHeaders.setContentLength(response, response.content().readableBytes());  
  225.         }  
  226.   
  227.         // 如果是非Keep-Alive,关闭连接  
  228.         ChannelFuture f = ctx.channel().writeAndFlush(response);  
  229.         if (!HttpHeaders.isKeepAlive(request) || response.getStatus().code() != 200) {  
  230.             f.addListener(ChannelFutureListener.CLOSE);  
  231.         }  
  232.     }  
  233.       
  234.     /** 
  235.      * WebSocket返回 
  236.      * @param ctx 
  237.      * @param req 
  238.      * @param res 
  239.      */  
  240.     public void sendWebSocket(String msg) throws Exception {  
  241.         if (this.handshaker == null || this.ctx == null || this.ctx.isRemoved()) {  
  242.             throw new Exception("尚未握手成功,无法向客户端发送WebSocket消息");  
  243.         }  
  244.         this.ctx.channel().write(new TextWebSocketFrame(msg));  
  245.         this.ctx.flush();  
  246.     }  
  247.   
  248. }  

 

 

聊天服务接口和实现类

 

[java] view plain copy
 
 print?技术分享技术分享
  1. import com.company.util.Request;  
  2.   
  3. public interface BananaCallBack {  
  4.       
  5.     // 服务端发送消息给客户端  
  6.     void send(Request request) throws Exception;  
  7.       
  8. }  

 

[java] view plain copy
 
 print?技术分享技术分享
  1. import io.netty.channel.ChannelHandlerContext;  
  2. import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;  
  3.   
  4. import java.util.Map;  
  5. import java.util.concurrent.ConcurrentHashMap;  
  6.   
  7. import org.apache.log4j.Logger;  
  8.   
  9. import com.company.service.BananaCallBack;  
  10. import com.company.util.CODE;  
  11. import com.company.util.Request;  
  12. import com.google.common.base.Strings;  
  13.   
  14. public class BananaService implements BananaCallBack {  
  15.     private static final Logger LOG = Logger.getLogger(BananaService.class);  
  16.       
  17.     public static final Map<String, BananaCallBack> bananaWatchMap = new ConcurrentHashMap<String, BananaCallBack>(); // <requestId, callBack>  
  18.       
  19.     private ChannelHandlerContext ctx;  
  20.     private String name;  
  21.       
  22.     public BananaService(ChannelHandlerContext ctx, String name) {  
  23.         this.ctx = ctx;  
  24.         this.name = name;  
  25.     }  
  26.   
  27.     public static boolean register(String requestId, BananaCallBack callBack) {  
  28.         if (Strings.isNullOrEmpty(requestId) || bananaWatchMap.containsKey(requestId)) {  
  29.             return false;  
  30.         }  
  31.         bananaWatchMap.put(requestId, callBack);  
  32.         return true;  
  33.     }  
  34.       
  35.     public static boolean logout(String requestId) {  
  36.         if (Strings.isNullOrEmpty(requestId) || !bananaWatchMap.containsKey(requestId)) {  
  37.             return false;  
  38.         }  
  39.         bananaWatchMap.remove(requestId);  
  40.         return true;  
  41.     }  
  42.       
  43.     @Override  
  44.     public void send(Request request) throws Exception {  
  45.         if (this.ctx == null || this.ctx.isRemoved()) {  
  46.             throw new Exception("尚未握手成功,无法向客户端发送WebSocket消息");  
  47.         }  
  48.         this.ctx.channel().write(new TextWebSocketFrame(request.toJson()));  
  49.         this.ctx.flush();  
  50.     }  
  51.       
  52.       
  53.     /** 
  54.      * 通知所有机器有机器下线 
  55.      * @param requestId 
  56.      */  
  57.     public static void notifyDownline(String requestId) {  
  58.         BananaService.bananaWatchMap.forEach((reqId, callBack) -> { // 通知有人下线  
  59.             Request serviceRequest = new Request();  
  60.             serviceRequest.setServiceId(CODE.downline.code);  
  61.             serviceRequest.setRequestId(requestId);  
  62.             try {  
  63.                 callBack.send(serviceRequest);  
  64.             } catch (Exception e) {  
  65.                 LOG.warn("回调发送消息给客户端异常", e);  
  66.             }  
  67.         });  
  68.     }  
  69.       
  70.     public String getName() {  
  71.         return name;  
  72.     }  
  73.   
  74. }  


前端html5聊天页面及js

 

 

[html] view plain copy
 
 print?技术分享技术分享
  1. <!DOCTYPE html>  
  2. <html>  
  3. <head>  
  4. <meta charset="UTF-8">  
  5. <title>Netty WebSocket 聊天实例</title>  
  6. </head>  
  7. <script src=http://www.mamicode.com/"jquery.min.js" type="text/javascript"></script>  
  8. <script src=http://www.mamicode.com/"map.js" type="text/javascript"></script>  
  9. <script type="text/javascript">  
  10. $(document).ready(function() {  
  11.     var uuid = guid(); // uuid在一个会话唯一  
  12.     var nameOnline = ‘‘; // 上线姓名  
  13.     var onlineName = new Map(); // 已上线人员, <requestId, name>  
  14.       
  15.     $("#name").attr("disabled","disabled");  
  16.     $("#onlineBtn").attr("disabled","disabled");  
  17.     $("#downlineBtn").attr("disabled","disabled");  
  18.       
  19.     $("#banana").hide();  
  20.   
  21.     // 初始化websocket  
  22.     var socket;  
  23.     if (!window.WebSocket) {  
  24.         window.WebSocket = window.MozWebSocket;  
  25.     }  
  26.     if (window.WebSocket) {  
  27.         socket = new WebSocket("ws://localhost:9090/");  
  28.         socket.onmessage = function(event) {  
  29.             console.log("收到服务器消息:" + event.data);  
  30.             if (event.data.indexOf("isSucc") != -1) {// 这里需要判断是客户端请求服务端返回后的消息(response)  
  31.                 var response = JSON.parse(event.data);  
  32.                 if (response != undefined && response != null) {  
  33.                     if (response.serviceId == 1001) { // 上线  
  34.                         if (response.isSucc) {  
  35.                             // 上线成功,初始化已上线人员  
  36.                             onlineName.clear();  
  37.                             $("#showOnlineNames").empty();  
  38.                             for (var reqId in response.hadOnline) {  
  39.                                 onlineName.put(reqId, response.hadOnline[reqId]);  
  40.                             }  
  41.                             initOnline();  
  42.                               
  43.                             $("#name").attr("disabled","disabled");  
  44.                             $("#onlineBtn").attr("disabled","disabled");  
  45.                             $("#downlineBtn").removeAttr("disabled");  
  46.                             $("#banana").show();  
  47.                         } else {  
  48.                             alert("上线失败");  
  49.                         }  
  50.                     } else if (response.serviceId == 1004) {  
  51.                         if (response.isSucc) {  
  52.                             onlineName.clear();  
  53.                             $("#showBanana").empty();  
  54.                             $("#showOnlineNames").empty();  
  55.                             $("#name").removeAttr("disabled");  
  56.                             $("#onlineBtn").removeAttr("disabled");  
  57.                             $("#downlineBtn").attr("disabled","disabled");  
  58.                             $("#banana").hide();  
  59.                         } else {  
  60.                             alert("下线失败");  
  61.                         }  
  62.                     }  
  63.                 }  
  64.             } else {// 还是服务端向客户端的请求(request)  
  65.                 var request = JSON.parse(event.data);  
  66.                 if (request != undefined && request != null) {  
  67.                     if (request.serviceId == 1001 || request.serviceId == 1004) { // 有人上线/下线  
  68.                         if (request.serviceId == 1001) {  
  69.                             onlineName.put(request.requestId, request.name);  
  70.                         }  
  71.                         if (request.serviceId == 1004) {  
  72.                             onlineName.removeByKey(request.requestId);  
  73.                         }  
  74.                           
  75.                         initOnline();  
  76.                     } else if (request.serviceId == 1003) { // 有人发消息  
  77.                         appendBanana(request.name, request.message);  
  78.                     }  
  79.                 }  
  80.             }  
  81.         };  
  82.         socket.onopen = function(event) {  
  83.             $("#name").removeAttr("disabled");  
  84.             $("#onlineBtn").removeAttr("disabled");  
  85.             console.log("已连接服务器");  
  86.         };  
  87.         socket.onclose = function(event) { // WebSocket 关闭  
  88.             console.log("WebSocket已经关闭!");  
  89.         };  
  90.         socket.onerror = function(event) {  
  91.             console.log("WebSocket异常!");  
  92.         };  
  93.     } else {  
  94.         alert("抱歉,您的浏览器不支持WebSocket协议!");  
  95.     }  
  96.       
  97.     // WebSocket发送请求  
  98.     function send(message) {  
  99.         if (!window.WebSocket) { return; }  
  100.         if (socket.readyState == WebSocket.OPEN) {  
  101.             socket.send(message);  
  102.         } else {  
  103.             console.log("WebSocket连接没有建立成功!");  
  104.             alert("您还未连接上服务器,请刷新页面重试");  
  105.         }  
  106.     }  
  107.       
  108.     // 刷新上线人员  
  109.     function initOnline() {  
  110.         $("#showOnlineNames").empty();  
  111.         for (var i=0;i<onlineName.size();i++) {  
  112.             $("#showOnlineNames").append(‘<tr><td>‘ + (i+1) + ‘</td>‘ +  
  113.             ‘<td>‘ + onlineName.element(i).value + ‘</td>‘ +  
  114.             ‘</tr>‘);  
  115.         }  
  116.     }  
  117.     // 追加聊天信息  
  118.     function appendBanana(name, message) {  
  119.         $("#showBanana").append(‘<tr><td>‘ + name + ‘: ‘ + message + ‘</td></tr>‘);  
  120.     }  
  121.       
  122.     $("#onlineBtn").bind("click", function() {  
  123.         var name = $("#name").val();  
  124.         if (name == null || name == ‘‘) {  
  125.             alert("请输入您的尊姓大名");  
  126.             return;  
  127.         }  
  128.   
  129.         nameOnline = name;  
  130.         // 上线  
  131.         send(JSON.stringify({"requestId":uuid, "serviceId":1001, "name":name}));  
  132.     });  
  133.       
  134.     $("#downlineBtn").bind("click", function() {  
  135.         // 下线  
  136.         send(JSON.stringify({"requestId":uuid, "serviceId":1004}));  
  137.     });  
  138.       
  139.     $("#sendBtn").bind("click", function() {  
  140.         var message = $("#messageInput").val();  
  141.         if (message == null || message == ‘‘) {  
  142.             alert("请输入您的聊天信息");  
  143.             return;  
  144.         }  
  145.           
  146.         // 发送聊天消息  
  147.         send(JSON.stringify({"requestId":uuid, "serviceId":1002, "name":nameOnline, "message":message}));  
  148.         $("#messageInput").val("");  
  149.     });  
  150.       
  151. });  
  152.   
  153. function guid() {  
  154.     function S4() {  
  155.        return (((1+Math.random())*0x10000)|0).toString(16).substring(1);  
  156.     }  
  157.     return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());  
  158. }  
  159. </script>  
  160. <body>  
  161.   <h1>Netty WebSocket 聊天实例</h1>  
  162.   <input type="text" id="name" value=http://www.mamicode.com/"佚名" placeholder="姓名" />  
  163.   <input type="button" id="onlineBtn" value=http://www.mamicode.com/"上线" />  
  164.   <input type="button" id="downlineBtn" value=http://www.mamicode.com/"下线" />  
  165.   <hr/>  
  166.   <table id="banana" border="1" >  
  167.     <tr>  
  168.       <td width="600" align="center">聊天</td>  
  169.       <td width="100" align="center">上线人员</td>  
  170.     </tr>  
  171.     <tr height="200" valign="top">  
  172.       <td>  
  173.         <table id="showBanana" border="0" width="600">  
  174.             <!--  
  175.             <tr>  
  176.               <td>张三: 大家好</td>  
  177.             </tr>  
  178.             <tr>  
  179.               <td>李四: 欢迎加入群聊</td>  
  180.             </tr>  
  181.             -->  
  182.         </table>  
  183.       </td>  
  184.       <td>  
  185.         <table id="showOnlineNames" border="0">  
  186.             <!--  
  187.             <tr>  
  188.               <td>1</td>  
  189.               <td>张三</td>  
  190.             <tr/>  
  191.             <tr>  
  192.               <td>2</td>  
  193.               <td>李四</td>  
  194.             <tr/>  
  195.             -->  
  196.         </table>  
  197.       </td>  
  198.     </tr>  
  199.     <tr height="40">  
  200.       <td></td>  
  201.       <td></td>  
  202.     </tr>  
  203.     <tr>  
  204.       <td>  
  205.         <input type="text" id="messageInput"  style="width:590px" placeholder="巴拉巴拉点什么吧" />  
  206.       </td>  
  207.       <td>  
  208.         <input type="button" id="sendBtn" value=http://www.mamicode.com/"发送" />  
  209.       </td>  
  210.     </tr>  
  211.   </table>  
  212.   
  213. </body>  
  214. </html>  


运行方式:

 

1.运行Lanucher来启动后端的 WebSocket服务

2.打开Resources下的banana.html页面即可在线聊天,如下:

技术分享

当有人上线/下线时,右边的"上线人员"会动态变化

技术分享

 

技术分享

 

技术分享

 

综上,WebSocket 协议用于构建低延迟的服务,如竞拍、股票行情等,使用 Netty 可以方便的构建 WebSocket 服务,需要注意的是,WebSocket 协议基于 Http协议,采用 Http 握手成功后,就可以进行 TCP 全双工通信了。

 

GitHub上源码:https://github.com/leonzm/websocket_demo

 

参考:
《Netty 权威指南》

知乎上关于WebSocket

Websocket使用实例解读 -- tomcat

WebSocket API 接口

HTML5 WebSockets 教程

Netty笔记:使用WebSocket协议开发聊天系统