zoukankan      html  css  js  c++  java
  • 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?在CODE上查看代码片派生到我的代码片
    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?在CODE上查看代码片派生到我的代码片
    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?在CODE上查看代码片派生到我的代码片
    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?在CODE上查看代码片派生到我的代码片
    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?在CODE上查看代码片派生到我的代码片
    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?在CODE上查看代码片派生到我的代码片
    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?在CODE上查看代码片派生到我的代码片
    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?在CODE上查看代码片派生到我的代码片
    1. <!DOCTYPE html>  
    2. <html>  
    3. <head>  
    4. <meta charset="UTF-8">  
    5. <title>Netty WebSocket 聊天实例</title>  
    6. </head>  
    7. <script src="jquery.min.js" type="text/javascript"></script>  
    8. <script src="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="佚名" placeholder="姓名" />  
    163.   <input type="button" id="onlineBtn" value="上线" />  
    164.   <input type="button" id="downlineBtn" value="下线" />  
    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="590px" placeholder="巴拉巴拉点什么吧" />  
    206.       </td>  
    207.       <td>  
    208.         <input type="button" id="sendBtn" value="发送" />  
    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 教程

  • 相关阅读:
    Web持久化存储Web SQL、Local Storage、Cookies(常用)
    浅谈TypeScript
    浅谈JavaScript、ES5、ES6
    AngularJS1.3一些技巧
    AngularJS学习总结
    poj-----Ultra-QuickSort(离散化+树状数组)
    HDUOJ---1241Oil Deposits(dfs)
    HDUOJ---携程员工运动会场地问题
    HDUOJ------2398Savings Account
    HDUOJ-----2399GPA
  • 原文地址:https://www.cnblogs.com/sanhuan/p/6051696.html
Copyright © 2011-2022 走看看