前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >16-基于Netty开发WebSocket服务器与浏览器实现长链接

16-基于Netty开发WebSocket服务器与浏览器实现长链接

作者头像
彼岸舞
发布2022-02-18 13:57:07
1.2K0
发布2022-02-18 13:57:07
举报
文章被收录于专栏:java开发的那点事

Netty通过WebSocket编程实现服务器与客户端长连接

需求

  1. Http协议是无状态的,浏览器和服务器间的请求响应一次, 下一次会重新创建连接
  2. 要求: 实现基于WebSocket的长链接的全双工的交互
  3. 改变Http协议多次请求的约束, 实现长链接, 服务器可以发送消息给浏览器
  4. 客户端浏览器和服务器端会相互感知, 比如服务器关闭了, 浏览器会感知, 同样浏览器关闭了,服务器也会感知

运行界面

WebSocketServer

代码语言:javascript
复制
package com.dance.netty.netty.websocket;

import com.dance.netty.netty.heartbeat.NettyServerHertBeat;
import com.dance.netty.netty.heartbeat.NettyServerIdleStateHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
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.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

public class WebSocketServer {
    private final int port;

    public WebSocketServer(int port) {
        this.port = port;
    }

    public void run() throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new LoggingHandler(LogLevel.INFO))  // 在BossGroup中增加一个日志处理器 日志级别为INFO
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            // 因为是基于Http协议的所以采用Http的编解码器
                            pipeline.addLast(new HttpServerCodec());
                            // 是以块的方式写, 添加ChunkedWriteHandler(分块写入处理程序)
                            pipeline.addLast(new ChunkedWriteHandler());
                            /*
                             * http 数据在传输过程中是分段的 http Object aggregator 就是可以将多个段聚合
                             * 这就是为什么 当浏览器发送大量数据时, 就会出现多次http请求
                             * 参数为 : 最大内容长度
                             */
                            pipeline.addLast(new HttpObjectAggregator(8192));
                            /*
                             * 对应WebSocket 他的数据时以桢(frame) 形式传递
                             * 可以看到WebSocketFrame下面有6个子类
                             * 浏览器请求时: ws://localhost:7000/xxx 请求的url
                             * 核心功能是将http协议升级为ws协议 保持长链接
                             */
                            pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
                            // 自定义Handler, 处理业务逻辑
                            pipeline.addLast(new WebSocketTextFrameHandler());
                        }
                    });
            System.out.println("netty server is starter......");
            ChannelFuture sync = serverBootstrap.bind(port).sync();
            sync.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        new WebSocketServer(7000).run();
    }
}

WebSocketTextFrameHandler

代码语言:javascript
复制
package com.dance.netty.netty.websocket;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

import java.time.LocalDateTime;

/**
 * TextWebSocketFrame 表示一个文本桢
 */
public class WebSocketTextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("[服务器] : 收到消息 -> " + msg.text());
        // 回复浏览器
        ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间: " + LocalDateTime.now() + "->"+ msg.text()));
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // id 表示唯一的值 LongText是唯一的
        System.out.println("handlerAdded 被调用:" + ctx.channel().id().asLongText());
        // shortText 可能会重复
        System.out.println("handlerAdded 被调用:" + ctx.channel().id().asShortText());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        // id 表示唯一的值 LongText是唯一的
        System.out.println("handlerRemoved 被调用:" + ctx.channel().id().asLongText());
        // shortText 可能会重复
        System.out.println("handlerRemoved 被调用:" + ctx.channel().id().asShortText());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.channel().close();
        cause.printStackTrace();
    }
}

页面

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    var socket;
    // 判断当前浏览器是否支持WebSocket
    if(window.WebSocket){
        socket = new WebSocket("ws://localhost:7000/hello");
        // 相当于ChannelRead ev就是消息回送
        socket.onmessage = function (ev){
            console.log(ev)
            let textArea = document.getElementById("responseText")
            textArea.value = textArea.value + "\n" + ev.data
        }
        // 相当于连接开启 ChannelAdded ev
        socket.onopen = function (ev) {
            console.log(ev)
            let textArea = document.getElementById("responseText")
            textArea.value = "连接开启了"
        }
        // 相当于连接关闭 ChannelRemove ev
        socket.onclose = function (ev) {
            console.log(ev)
            let textArea = document.getElementById("responseText")
            textArea.value = textArea.value + '\n' + "连接关闭了"
        }
    }else{
        alert("当前浏览器不支持WebSocket")
    }
    function send(msg) {
        if(!window.socket){
            return;
        }
        if(socket.readyState === WebSocket.OPEN){
            // 通过WebSocket发送消息
            socket.send(msg)
        }else{
            alert('连接没有开启')
        }
    }
</script>
<form onsubmit="return false">
    <textarea name="message" style="width: 300px; height: 300px;"></textarea>
    <input type="button" value="发送消息" onclick="send(this.form.message.value)">
    <textarea id="responseText" style="width: 300px; height: 300px;"></textarea>
    <input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">

</form>
<script>


</script>

</body>
</html>

测试

启动服务器

代码语言:javascript
复制
netty server is starter......
一月 16, 2022 11:19:27 下午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0xb0f42cfd] REGISTERED
一月 16, 2022 11:19:27 下午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0xb0f42cfd] BIND: 0.0.0.0/0.0.0.0:7000
一月 16, 2022 11:19:27 下午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0xb0f42cfd, L:/0:0:0:0:0:0:0:0:7000] ACTIVE

启动页面

可以看到连接开启了

可以看到浏览器发送了三个请求

第一个ws请求是和IDEA建立连接的不用管

第二个http协议是请求html文件的

我们主要看第三个请求, 这个就是我们自己的ws请求, 状态为101 Switching Protocols切换协议, 并且协议升级为ws协议

并且服务器感知,建立Channel连接

代码语言:javascript
复制
一月 16, 2022 11:20:46 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xb0f42cfd, L:/0:0:0:0:0:0:0:0:7000] READ: [id: 0x9b6bccb3, L:/0:0:0:0:0:0:0:1:7000 - R:/0:0:0:0:0:0:0:1:55639]
一月 16, 2022 11:20:46 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xb0f42cfd, L:/0:0:0:0:0:0:0:0:7000] READ COMPLETE
handlerAdded 被调用:005056fffec00008-00006534-00000002-37865b9aaa734014-9b6bccb3
handlerAdded 被调用:9b6bccb3

页面发送消息, 后端连接返回消息,并且浏览器中并没有新的请求

服务器

代码语言:javascript
复制
[服务器] : 收到消息 -> hi netty

关闭浏览器后服务端感知,同样的关闭服务器浏览器也会感知

代码语言:javascript
复制
handlerRemoved 被调用:005056fffec00008-00006534-00000002-37865b9aaa734014-9b6bccb3
handlerRemoved 被调用:9b6bccb3

并且在建立WebSocket连接时需要请求路径和后端配置路径一致

前端: ws://localhost:7000/hello

后端配置: /hello

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-01-21 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Netty通过WebSocket编程实现服务器与客户端长连接
相关产品与服务
云服务器
云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档