• 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()

    关闭连接

    注:

    博客:
    https://blog.csdn.net/badao_liumang_qizhi
    关注公众号
    霸道的程序猿
    获取编程相关电子书、教程推送与免费下载。

    实现

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

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

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

    然后新建服务端类WebSocketServer

    package com.badao.NettyWebSocket;
    
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import io.netty.handler.logging.LogLevel;
    import io.netty.handler.logging.LoggingHandler;
    
    public class WebSocketServer {
        public static void main(String[] args) throws  Exception
        {
            EventLoopGroup bossGroup = new NioEventLoopGroup();
            EventLoopGroup workerGroup = new NioEventLoopGroup();
            try{
                ServerBootstrap serverBootstrap = new ServerBootstrap();
                serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)
                        .handler(new LoggingHandler(LogLevel.INFO))
                        .childHandler(new WebSocketInitializer());
                //绑定端口
                ChannelFuture channelFuture = serverBootstrap.bind(70).sync();
                channelFuture.channel().closeFuture().sync();
            }finally {
                //关闭事件组
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }
        }
    }

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

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

    所以新建类WebSocketInitializer

    package com.badao.NettyWebSocket;
    
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelPipeline;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.handler.codec.http.HttpObjectAggregator;
    import io.netty.handler.codec.http.HttpServerCodec;
    import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
    import io.netty.handler.stream.ChunkedWriteHandler;
    
    public class WebSocketInitializer  extends ChannelInitializer<SocketChannel> {
    
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
    
            pipeline.addLast(new HttpServerCodec());
            pipeline.addLast(new ChunkedWriteHandler());
            pipeline.addLast(new HttpObjectAggregator(8192));
            pipeline.addLast(new WebSocketServerProtocolHandler("/badao"));
    
            pipeline.addLast(new WebSocketHandler());
        }
    }

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

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

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

    所以新建类WebSocketHandler

    package com.badao.NettyWebSocket;
    
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
    
    import java.time.LocalDateTime;
    
    public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
            System.out.println("收到消息:"+msg.text());
            ctx.channel().writeAndFlush(new TextWebSocketFrame("WebSocket服务端在"+ LocalDateTime.now()+"发送消息(公众号:霸道的程序猿)"));
        }
    
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
            System.out.println("handlerAdded:"+ctx.channel().id().asLongText());
        }
    
        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
            System.out.println("handlerRemoved:"+ctx.channel().id().asLongText());
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            System.out.println("异常发生");
            ctx.close();
        }
    }

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

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

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
            System.out.println("收到消息:"+msg.text());
            ctx.channel().writeAndFlush(new TextWebSocketFrame("WebSocket服务端在"+ LocalDateTime.now()+"发送消息(公众号:霸道的程序猿)"));
        }

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

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

        @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
            System.out.println("handlerAdded:"+ctx.channel().id().asLongText());
        }

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

    然后在服务端输出。

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

        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
            System.out.println("handlerRemoved:"+ctx.channel().id().asLongText());
        }

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

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            System.out.println("异常发生");
            ctx.close();
        }

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

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

    修改html的代码为

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>公众号:霸道的程序猿</title>
    </head>
    <body>
    <script type="text/javascript">
        var socket;
        if(window.WebSocket)
        {
            socket = new WebSocket("ws://localhost:70/badao")
            socket.onmessage=function (ev) {
                var ta = document.getElementById("responseText");
                ta.value = ta.value+"
    "+ev.data;
            }
    
            socket.onopen = function (ev) {
                var ta = document.getElementById("responseText");
                ta.value = "连接开启";
            }
    
            socket.onclose = function (ev) {
                var ta = document.getElementById("responseText");
                ta.value = ta.value+"
    连接关闭";
            }
        }
        else{
            alert("当前浏览器不支持WebSocket")
        }
    
        function send(message) {
            if(!window.WebSocket)
            {
                return;
            }else
            {
                if(socket.readyState = WebSocket.OPEN)
                {
                    socket.send(message);
                }
                else
                {
                    alert("连接尚未开启");
                }
            }
        }
    </script>
    <form>
        <textarea name="message" style=" 400px;height: 200px">
    
        </textarea>
    
        <input type="button" value="发送数据" onclick="send(this.form.message.value)">
    
        <h3>服务端输出:</h3>
    
        <textarea id="responseText" style=" 400px;height: 200px">
    
        </textarea>
    </form>
    </body>
    </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上右击选择运行

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

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

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

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

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

    继续发送也是如此

    示例代码下载

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

    博客园: https://www.cnblogs.com/badaoliumangqizhi/ 关注公众号 霸道的程序猿 获取编程相关电子书、教程推送与免费下载。
  • 相关阅读:
    linux下inotifytools+rsync进行文件同步的使用
    进程间的通讯(IPC)方式
    is_writable的php实现
    PHP IPC函数介绍消息队列
    PHP实现多进程并行执行脚本
    PHP IPC函数介绍共享内存
    不同语言从shell管道获取数据的方法
    解决phpredis 'RedisException' with message 'read error on connection'
    beanstalkd的安装
    haproxy对redis进行负载均衡
  • 原文地址:https://www.cnblogs.com/badaoliumangqizhi/p/13686101.html
Copyright © 2020-2023  润新知