zoukankan      html  css  js  c++  java
  • 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/ 关注公众号 霸道的程序猿 获取编程相关电子书、教程推送与免费下载。
  • 相关阅读:
    JavaScript实现类的private、protected、public、static以及继承
    OSS网页上传和断点续传(STSToken篇)
    OSS网页上传和断点续传(OSS配置篇)
    Linq sum()时遇到NULL
    SQLSERVER事务日志已满 the transaction log for database 'xx' is full
    笔记本高分辨软件兼容问题,字体太小或模糊
    H5上传图片之canvas
    An error occurred while updating the entries. See the inner exception for details.
    无限级结构SQL查询所有的下级和所有的上级
    SQLserver 进程被死锁问题解决
  • 原文地址:https://www.cnblogs.com/badaoliumangqizhi/p/13686101.html
Copyright © 2011-2022 走看看