vlambda博客
学习文章列表

Netty中使用WebSocket实现服务端与客户端的长连接通信发送消息

场景

Netty中实现多客户端连接与通信-以实现聊天室群聊功能为例(附代码下载):

https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108623306

上面讲了使用使用Socket搭建多客户端的连接与通信。

那么如果在Netty中使用WebSocket进行长连接通信要怎么实现。

WebSocket

现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

WebSocket是一种在单个TCP连接上进行全双工通信的协议。

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。

WebSocket 属性

以下是 WebSocket 对象的属性。假定我们使用了以上代码创建了 Socket 对象:

 

属性 描述
Socket.readyState

只读属性 readyState 表示连接状态,可以是以下值:

  • 0 - 表示连接尚未建立。

  • 1 - 表示连接已建立,可以进行通信。

  • 2 - 表示连接正在进行关闭。

  • 3 - 表示连接已经关闭或者连接不能打开。

Socket.bufferedAmount

只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。


WebSocket 事件

以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:

 

事件 事件处理程序 描述
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发

WebSocket 方法

以下是 WebSocket 对象的相关方法。假定我们使用了以上代码创建了 Socket 对象:

 

方法 描述
Socket.send()

使用连接发送数据

Socket.close()

关闭连接

 

注:

实现

在IDEA中搭建好Netty的项目并引入环境可以参照如下:

https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108592418

在此基础上,在src下新建包com.badao.NettyWebSocket

然后新建服务端类WebSocketServer


  
    
    
  
  1. package com.badao.NettyWebSocket;

  2.  

  3. import io.netty.bootstrap.ServerBootstrap;

  4. import io.netty.channel.ChannelFuture;

  5. import io.netty.channel.EventLoopGroup;

  6. import io.netty.channel.nio.NioEventLoopGroup;

  7. import io.netty.channel.socket.nio.NioServerSocketChannel;

  8. import io.netty.handler.logging.LogLevel;

  9. import io.netty.handler.logging.LoggingHandler;

  10.  

  11. public class WebSocketServer {

  12.     public static void main(String[] args) throws  Exception

  13.     {

  14.         EventLoopGroup bossGroup = new NioEventLoopGroup();

  15.         EventLoopGroup workerGroup = new NioEventLoopGroup();

  16.         try{

  17.             ServerBootstrap serverBootstrap = new ServerBootstrap();

  18.             serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)

  19.                     .handler(new LoggingHandler(LogLevel.INFO))

  20.                     .childHandler(new WebSocketInitializer());

  21.             //绑定端口

  22.             ChannelFuture channelFuture = serverBootstrap.bind(70).sync();

  23.             channelFuture.channel().closeFuture().sync();

  24.         }finally {

  25.             //关闭事件组

  26.             bossGroup.shutdownGracefully();

  27.             workerGroup.shutdownGracefully();

  28.         }

  29.     }

  30. }

服务端的搭建在上面已经讲解,这里又添加了Netty自带的日志处理器LoggingHandler

然后又添加了自定义的初始化器WebSocketInitializer

所以新建类WebSocketInitializer


  
    
    
  

  1. package com.badao.NettyWebSocket;

  2.  

  3. import io.netty.channel.Channel;

  4. import io.netty.channel.ChannelInitializer;

  5. import io.netty.channel.ChannelPipeline;

  6. import io.netty.channel.socket.SocketChannel;

  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 WebSocketInitializer  extends ChannelInitializer<SocketChannel> {

  13.  

  14.     @Override

  15.     protected void initChannel(SocketChannel ch) throws Exception {

  16.         ChannelPipeline pipeline = ch.pipeline();

  17.  

  18.         pipeline.addLast(new HttpServerCodec());

  19.         pipeline.addLast(new ChunkedWriteHandler());

  20.         pipeline.addLast(new HttpObjectAggregator(8192));

  21.         pipeline.addLast(new WebSocketServerProtocolHandler("/badao"));

  22.  

  23.         pipeline.addLast(new WebSocketHandler());

  24.     }

  25. }

因为Netty也是基于Http的所以这里需要添加HttpServerCodec处理器等。

要想实现WebSocket功能,主要是添加了WebSocketServerProtocolHandler这个WebSocket服务端协议处理器。注意这里的参数需要添加一个WebSocket的路径,这里是/badao

最后添加自定义的处理器WebSocketHandler 用来做具体的处理

所以新建类WebSocketHandler


  
    
    
  
  1. package com.badao.NettyWebSocket;

  2.  

  3. import io.netty.channel.ChannelHandlerContext;

  4. import io.netty.channel.SimpleChannelInboundHandler;

  5. import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

  6.  

  7. import java.time.LocalDateTime;

  8.  

  9. public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

  10.     @Override

  11.     protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {

  12.         System.out.println("收到消息:"+msg.text());

  13.     }

  14.  

  15.     @Override

  16.     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {

  17.         System.out.println("handlerAdded:"+ctx.channel().id().asLongText());

  18.     }

  19.  

  20.     @Override

  21.     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

  22.         System.out.println("handlerRemoved:"+ctx.channel().id().asLongText());

  23.     }

  24.  

  25.     @Override

  26.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

  27.         System.out.println("异常发生");

  28.         ctx.close();

  29.     }

  30. }

使其继承SimpleChannelInboundHandler注意此时的泛型类型为TextWebSocketFrame

然后重写channelRead0方法用来对收到数据时进行处理


  
    
    
  
  1.     @Override

  2.     protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {

  3.         System.out.println("收到消息:"+msg.text());

  4.     }


在服务端将收到的消息进行输出并给客户端发送数据。

然后重写handlerAdded方法,在客户端与服务端建立连接时进行操作


  
    
    
  
  1.   

  2.     @Override

  3.  public void handlerAdded(ChannelHandlerContext ctx) throws Exception {

  4.         System.out.println("handlerAdded:"+ctx.channel().id().asLongText());

  5.     }


这里通过通道的id方法的asLongText方法获取连接的唯一标志。

然后在服务端输出。

同理重写handlerRemoved方法,在断掉连接时将连接的唯一标志进行输出。


  
    
    
  
  1.  

  2.  @Override

    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

  3.         System.out.println("handlerRemoved:"+ctx.channel().id().asLongText());

  4.     }


最后重写出现异常时的处理方法,在出现异常时将连接关闭。


  
    
    
  
  1.     @Override

  2.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

  3.         System.out.println("异常发生");

  4.         ctx.close();

  5.     }

至此WebSocket服务端搭建完成,然后客户端通过JS就能实现。

在项目目录下src下新建webapp目录,在此目录下新建badao.html

 

修改html的代码为


  
    
    
  
  1. <!DOCTYPE html>

  2. <html lang="en">

  3. <head>

  4.     <meta charset="UTF-8">

  5. </head>

  6. <body>

  7. <script type="text/javascript">

  8.     var socket;

  9.     if(window.WebSocket)

  10.     {

  11.         socket = new WebSocket("ws://localhost:70/badao")

  12.         socket.onmessage=function (ev) {

  13.             var ta = document.getElementById("responseText");

  14.             ta.value = ta.value+"\n"+ev.data;

  15.         }

  16.  

  17.         socket.onopen = function (ev) {

  18.             var ta = document.getElementById("responseText");

  19.             ta.value = "连接开启";

  20.         }

  21.  

  22.         socket.onclose = function (ev) {

  23.             var ta = document.getElementById("responseText");

  24.             ta.value = ta.value+"\n连接关闭";

  25.         }

  26.     }

  27.     else{

  28.         alert("当前浏览器不支持WebSocket")

  29.     }

  30.  

  31.     function send(message) {

  32.         if(!window.WebSocket)

  33.         {

  34.             return;

  35.         }else

  36.         {

  37.             if(socket.readyState = WebSocket.OPEN)

  38.             {

  39.                 socket.send(message);

  40.             }

  41.             else

  42.             {

  43.                 alert("连接尚未开启");

  44.             }

  45.         }

  46.     }

  47. </script>

  48. <form>

  49.     <textarea name="message" style="width: 400px;height: 200px">

  50.  

  51.     </textarea>

  52.  

  53.     <input type="button" value="发送数据" onclick="send(this.form.message.value)">

  54.  

  55.     <h3>服务端输出:</h3>

  56.  

  57.     <textarea id="responseText" style="width: 400px;height: 200px">

  58.  

  59.     </textarea>

  60. </form>

  61. </body>

  62. </html>

在js中通过windows.WebSocket判断是否支持WebSocket

如果支持则

socket = new WebSocket("ws://localhost:70/badao")

建立连接,url的写法前面的ws://是固定的类似http://

后面的是跟的ip:端口号/上面配置的WebSocket路径

然后就是以这个WebSocket对象为中心进行连接和数据的显示。

下面的回调方法onopen会在建立连接成功后回调,onclose会在断掉连接后回调,

onmessage会在收到服务端发送的数据时回调并通过ev.data获取数据。

客户端向服务端发送数据时调用的是socket的send方法,通过

if(socket.readyState = WebSocket.OPEN)

判断连接已经成功建立。

实现长连接通信

运行WebSocketServer的main方法,然后在badao.html上右击选择运行

 

Netty中使用WebSocket实现服务端与客户端的长连接通信发送消息

建立连接成功后会在服务端输出连接的id,在客户端会显示连接开启。

此时如果刷新浏览器,服务端会输出一次断开连接和建立连接

 

Netty中使用WebSocket实现服务端与客户端的长连接通信发送消息

再将服务端停掉,客户端会输出连接关闭

 

Netty中使用WebSocket实现服务端与客户端的长连接通信发送消息

再重新启动服务端,在上面的输入框输入内容并点击发送数据

 

服务端会收到消息并输出,并向客户端发送一个消息。

继续发送也是如此

 

示例代码下载

https://download.csdn.net/download/BADAO_LIUMANG_QIZHI/12853829