从0到1学SpringCloud——16 gateway websocket长链接

目录

一、前言

二、代码实现

1、gateway网关配置

2、socket server 服务端

3、socket client客户端

4、模拟测试

5、集成 web socket


一、前言

gateway网关内置了支持socket长链接的路由转发功能。

本篇主要讲解通过socket客户端、web页面两周方式建立socket,通过gateway网关路由到socket服务端的实现。

因为gateway默认使用netty,我们引入socket时,也适用netty。

二、代码实现

1、gateway网关配置

网关端口设置为9990。

对于网关来讲,只需要在配置文件中添加以下配置:

#服务名称
spring:
  application:
    name: zhufeng-gateway-config
  cloud:
    gateway:
      routes:
        - id: zhufeng-route-socket
          uri: ws://127.0.0.1:8880
          predicates:
            - Path=/nio/info

请求地址以 ws 开头,上述配置意思是当请求 /nio/info 时,将请求转发到 ws:127.0.0.1:8880 

2、socket server 服务端

pom依赖:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <!-- <version>4.1.82.Final</version>-->
    <version>5.0.0.Alpha2</version>
</dependency>

新建一个socket服务,端口为 8880 

public class WebSocketNettyServer {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {

        // 创建两个线程池
        // 主线程池
        NioEventLoopGroup mainGrp = new NioEventLoopGroup();
        // 从线程池
        NioEventLoopGroup subGrp = new NioEventLoopGroup();

        try {
            // 创建Netty服务器启动对象
            ServerBootstrap serverBootstrap = new ServerBootstrap();

            // 初始化服务器启动对象
            serverBootstrap
                    // 指定使用上面创建的两个线程池
                    .group(mainGrp, subGrp)
                    // 指定Netty通道类型
                    .channel(NioServerSocketChannel.class)
                    // 指定通道初始化器用来加载当Channel收到事件消息后,
                    // 如何进行业务处理
                    .childHandler(new WebSocketChannelInitializer());

            // 绑定服务器端口,以同步的方式启动服务器
            ChannelFuture future = serverBootstrap.bind(8880).sync();
            // 等待服务器关闭
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 优雅关闭服务器
            mainGrp.shutdownGracefully();
            subGrp.shutdownGracefully();
        }
    }
}

自定义socket通道:

public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel>{
    // 初始化通道
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        // 获取管道,将一个一个的ChannelHandler添加到管道中
        ChannelPipeline pipeline = ch.pipeline();

        // 添加一个http的编解码器
        pipeline.addLast(new HttpServerCodec());
        // 添加一个用于支持大数据流的支持
        pipeline.addLast(new ChunkedWriteHandler());
        // 添加一个聚合器,这个聚合器主要是将HttpMessage聚合成FullHttpRequest/Response
        pipeline.addLast(new HttpObjectAggregator(1024 * 1024));

        // 需要指定接收请求的路由
        // 必须使用以 /nio/info 后缀结尾的url才能访问
        pipeline.addLast(new WebSocketServerProtocolHandler("/nio/info"));
        // 添加自定义的Handler
        pipeline.addLast(new MyScocketHandler());
    }
}

自定义socket消息处理:

public class MyScocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    /**用来保存所有的客户端连接*/
    public static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    /**时间格式化*/
    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * 当有新的客户端连接服务器之后,会自动调用这个方法
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // 将新的通道加入到clients
        clients.add(ctx.channel());
    }

    @Override
    protected void messageReceived(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame msg) throws Exception {
        StringBuilder resMsg = new StringBuilder();
        // 获取客户端发送过来的文本消息
        String text = msg.text();
        System.out.println("获取客户端发送内容:" + text);
        //发送所有客户端
        /**
        for (Channel client : clients) {
            System.out.println("获取客户端:" + client.id());
            // 将消息发送到所有的客户端
         resMsg.append(text).append(",").append(channelHandlerContext.channel().id()).append(",").append(sdf.format(new Date()));
            client.writeAndFlush(new TextWebSocketFrame(resMsg.toString()));
        }
         */
        //发送指定客户端
        resMsg.append(text).append(",").append(channelHandlerContext.channel().id()).append(",").append(sdf.format(new Date()));
        channelHandlerContext.channel().writeAndFlush(new TextWebSocketFrame(resMsg.toString()));

    }
}

在低版本的netty依赖中 实现 channelRead0 方法来接收消息

高版本中使用 messageReceived 方法来接收消息

3、socket client客户端

public class WebSocketNettyClient {

    public static void main(String[] args)  {

        EventLoopGroup group = new NioEventLoopGroup();
        final ClientHandler clientHandler =new ClientHandler();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .option(ChannelOption.SO_KEEPALIVE,true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            // 添加一个http的编解码器
                            pipeline.addLast(new HttpClientCodec());
                            // 添加一个用于支持大数据流的支持
                            pipeline.addLast(new ChunkedWriteHandler());
                            // 添加一个聚合器,这个聚合器主要是将HttpMessage聚合成FullHttpRequest/Response
                            pipeline.addLast(new HttpObjectAggregator(1024 * 1024));

                            pipeline.addLast(clientHandler);

                        }
                    });

            URI websocketURI = new URI("ws://localhost:9990/nio/info");
            HttpHeaders httpHeaders = new DefaultHttpHeaders();
            //进行握手
            WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker(websocketURI, WebSocketVersion.V13, (String)null, true,httpHeaders);

            final Channel channel=bootstrap.connect(websocketURI.getHost(),websocketURI.getPort()).sync().channel();
            clientHandler.setHandshaker(handshaker);
            handshaker.handshake(channel);
            //阻塞等待是否握手成功
            clientHandler.handshakeFuture().sync();

            //发送消息
            JSONObject userInfo = new JSONObject();
            userInfo.put("userId","u1001");
            userInfo.put("userName","月夜烛峰");
            channel.writeAndFlush(new TextWebSocketFrame(userInfo.toString()));

            // 等待连接被关闭
            channel.closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            group.shutdownGracefully();
        }

    }
}

上述代码中:

URI websocketURI = new URI("ws://localhost:9990/nio/info");

请求的是gateway的地址。

客户端逻辑处理:

public class ClientHandler extends SimpleChannelInboundHandler<Object> {

    private WebSocketClientHandshaker handshaker;
    ChannelPromise handshakeFuture;

    /**
     * 当客户端主动链接服务端的链接后,调用此方法
     *
     * @param channelHandlerContext ChannelHandlerContext
     */
    @Override
    public void channelActive(ChannelHandlerContext channelHandlerContext) {
        System.out.println("客户端Active .....");
        handlerAdded(channelHandlerContext);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("exceptionCaught:异常信息:" + cause.getMessage());
        ctx.close();
    }

    public void setHandshaker(WebSocketClientHandshaker handshaker) {
        this.handshaker = handshaker;
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        this.handshakeFuture = ctx.newPromise();
    }

    public ChannelFuture handshakeFuture() {
        return this.handshakeFuture;
    }

    @Override
    protected void messageReceived(ChannelHandlerContext ctx, Object o) throws Exception {
        // 握手协议返回,设置结束握手
        if (!this.handshaker.isHandshakeComplete()) {
            FullHttpResponse response = (FullHttpResponse) o;
            this.handshaker.finishHandshake(ctx.channel(), response);
            this.handshakeFuture.setSuccess();
            System.out.println("握手成功::messageReceived HandshakeComplete...");
            return;
        } else if (o instanceof TextWebSocketFrame) {
            TextWebSocketFrame textFrame = (TextWebSocketFrame) o;
            System.out.println("接收消息::messageReceived textFrame: " + textFrame.text());
        } else if (o instanceof CloseWebSocketFrame) {
            System.out.println("关闭链接::messageReceived CloseWebSocketFrame");
        }
    }
}

4、模拟测试

分别启动gateway网关、socket服务端、socket客户端

运行客户端后,控制台打印:

客户端Active .....
握手成功::messageReceived HandshakeComplete...
接收消息::messageReceived textFrame: {"userName":"月夜烛峰","userId":"u1001"},20a1a1e5,2022-09-26 15:52:09

服务端控制台:

客户端发送内容,服务端已经收到,说明socket链接已经建立并可以使用。

5、集成 web socket

为了更直观测试,通过web socket来访问

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>websocket</title>
    <style>
        table {
            border-collapse: collapse;
            border-spacing: 0;
            table-layout: fixed;
        }
    </style>
</head>
<body>

<div id="main" style="display: flex;margin-top:20px;">
    <div style="width: 20px"></div>
    <div style="border: 1px solid grey;width:700px;">
        <div class="card_header" style="padding: 0.5rem 1rem;border-bottom: 1px solid grey "><span>消息列表 </span>
        </div>
        <div class="card_header"
             style="padding: 0.5rem 1rem;font-size:15px;border-bottom: 1px dashed grey;height: 21px">
            <table style="width: 100%;">
                <thead>
                <tr>
                    <th>消息内容</th>
                    <th>发送者</th>
                    <th>发送时间</th>
                </tr>
                </thead>
            </table>
        </div>
        <div id="socket_keep_alive" style="height: 300px;overflow-y:auto;padding: 0rem 1rem 1rem 1rem;font-size:16px">
            <table id="socket_keep_alive_table" style="width: 100%;">
                <tbody style="width: 100%;text-align: center;">

                </tbody>
            </table>
        </div>
    </div>
    <div style="width: 20px"></div>
    <form onsubmit="return false">
        <div id="show_jvm_info" style="border: 1px solid grey;width: 450px;">
            <div class="card_header" style="padding: 0.5rem 1rem;border-bottom: 1px solid grey;"><span>消息发送 </span>
                <button style="float: right;margin-right: 20px;"
                        onclick="send(this.form.message.value);this.form.reset()">发送
                </button>

            </div>
            <textarea name="message" style="height: 345px; width: 445px;border: none;"></textarea>
        </div>
    </form>

</div>

</body>
<script src="/js/jquery-1.8.3.min.js"></script>
<script>

    function cleanInfo(id) {
        $("#" + id).empty();
    }

    function appendInfo(data, id) {
        var sinfo = data.split(",");
        var tableData = "<tr>";
        $.each(sinfo, function (k, v) {
            //console.log(k+':'+v);
            tableData = tableData + "<td>" + v + "</td>";
        })
        tableData = tableData + "</tr>";
        console.log(tableData);
        $("#" + id).prepend(tableData);

    }

    var socket;
    //判断当前浏览器是否支持websocket
    if (window.WebSocket) {
        //go on
        socket = new WebSocket("ws://localhost:9990/nio/info");
        //相当于channelReado, ev 收到服务器端回送的消息
        socket.onmessage = function (msg) {

            appendInfo(msg.data, "socket_keep_alive_table");
        }

        //相当于连接开启(感知到连接开启)
        socket.onopen = function (ev) {
            console.log("websocket 已打开");
        }

        //相当于连接关闭(感知到连接关闭)
        socket.onclose = function (ev) {
            console.log("websocket 已关闭");
        }
    } else {
        alert("当前浏览器不支持websocket")
    }

    //发送消息到服务器
    function send(message) {
        if (!window.socket) { //先判断socket是否创建
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            //通过socket 发送消息
            socket.send(message)
        } else {
            alert("连接没有开启");
        }
    }
</script>

</html>

html页面中也访问gateway网关地址,访问页面:

 

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月夜烛峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值