Springboot实战:Springboot+Netty优雅的创建websocket客户端 (附源码下载)

Springboot-cli 开发脚手架系列

Netty系列:Springboot+Netty优雅的创建websocket客户端 (附源码下载)



前言

首先我们需要使用Netty搭建基础的tcp框架,参考Springboot使用Netty优雅的创建TCP客户端(附源码),接下来我们开始集成websocket。

1. 环境

  • pom.xml
<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>${netty-all.version}</version>
        </dependency>
  • yml开启日记debug级别打印
# 日记配置
logging:
  level:
    # 开启debug日记打印
    com.netty: debug

2. 引入websocket编码解码器

这里我们需要加入websocket编码解码器,因为websocket的握手是通过http完成的,所以我们还需要加入http的编码器。

/**
 * Netty 通道初始化
 *
 * @author qiding
 */
@Component
@RequiredArgsConstructor
public class ChannelInit extends ChannelInitializer<SocketChannel> {

    private final MessageHandler messageHandler;

    @Override
    protected void initChannel(SocketChannel channel) {
        channel.pipeline()
                // 每隔60s的时间触发一次userEventTriggered的方法,并且指定IdleState的状态位是WRITER_IDLE,事件触发给服务器发送ping消息
                .addLast("idle", new IdleStateHandler(0, 60, 0, TimeUnit.SECONDS))
                // 添加解码器
                .addLast(new HttpClientCodec())
                .addLast(new ChunkedWriteHandler())
                .addLast(new HttpObjectAggregator(1024 * 1024 * 10))
                .addLast(new WebSocketFrameAggregator(1024 * 62))
                // 添加消息处理器
                .addLast("messageHandler", messageHandler);
    }
}

3. 编写websocket处理器

  • 修改消息处理器MessageHandler.java
  • 增加两个全局变量,保存当前连接
    /**
     * websocket会话存储
     */
    private WebSocketClientHandshaker handShaker;
    /**
     * 用于回调判断握手是否成功
     */
    private ChannelPromise handshakeFuture;
  • 连接成功开始握手
 @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        // 保存Promise
        this.handshakeFuture = ctx.newPromise();
    }
@Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.debug("\n");
        log.debug("握手开始,channelId:{}", ctx.channel().id());
        handShaker = WebSocketClientHandshakerFactory.newHandshaker(new URI("ws://" + WebsocketClient.connectedIp + ":" + WebsocketClient.connectedPort + "/ws"), WebSocketVersion.V13, null, false, new DefaultHttpHeaders());
        handShaker.handshake(ctx.channel());
        super.channelActive(ctx);
    }
  • 完整代码
/**
 * 消息处理,单例启动
 *
 * @author ding
 */
@Slf4j
@Component
@ChannelHandler.Sharable
@RequiredArgsConstructor
public class MessageHandler extends SimpleChannelInboundHandler<Object> {

    private WebSocketClientHandshaker handShaker;

    private ChannelPromise handshakeFuture;

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        // 连接前执行
        this.handshakeFuture = ctx.newPromise();
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object message) {
        log.debug("\n");
        log.debug("channelId:" + ctx.channel().id());
        // 判断是否正确握手
        if (!this.handShaker.isHandshakeComplete()) {
            try {
                this.handShaker.finishHandshake(ctx.channel(), (FullHttpResponse) message);
                log.debug("websocket Handshake 完成!");
                this.handshakeFuture.setSuccess();
            } catch (WebSocketHandshakeException e) {
                log.debug("websocket连接失败!");
                this.handshakeFuture.setFailure(e);
            }
            return;
        }
        // 握手失败响应
        if (message instanceof FullHttpResponse) {
            FullHttpResponse response = (FullHttpResponse) message;
            log.error("握手失败!code:{},msg:{}", response.status(), response.content().toString(CharsetUtil.UTF_8));
        }
        WebSocketFrame frame = (WebSocketFrame) message;
        // 消息处理
        if (frame instanceof TextWebSocketFrame) {
            TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
            log.debug("收到消息: " + textFrame.text());
        }
        if (frame instanceof PongWebSocketFrame) {
            log.debug("pong消息");
        }
        if (frame instanceof CloseWebSocketFrame) {
            log.debug("服务器主动关闭连接");
            ctx.close();
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        log.debug("\n");
        log.debug("连接断开");
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.debug("\n");
        log.debug("握手开始,channelId:{}", ctx.channel().id());
        handShaker = WebSocketClientHandshakerFactory.newHandshaker(new URI("ws://" + WebsocketClient.connectedIp + ":" + WebsocketClient.connectedPort + "/ws"), WebSocketVersion.V13, null, false, new DefaultHttpHeaders());
        handShaker.handshake(ctx.channel());
        super.channelActive(ctx);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        log.debug("超时事件时触发");
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            // 当我们长时间没有给服务器发消息时,发送ping消息,告诉服务器我们还活跃
            if (event.state().equals(IdleState.WRITER_IDLE)) {
                log.debug("发送心跳");
                ctx.writeAndFlush(new PingWebSocketFrame());
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
}

4. http测试接口编写

  • pom.xml加入web依赖
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • HttpApi .java
/**
 * 模拟发送api
 *
 * @author qiding
 */
@RequiredArgsConstructor
@RestController
@Slf4j
public class HttpApi {

    private final WebsocketClient websocketClient;

    /**
     * 消息发布
     */
    @GetMapping("/send")
    public String send(String message) {
        websocketClient.getSocketChannel().writeAndFlush(new TextWebSocketFrame(message));
        return "发送成功";
    }

    /**
     * 消息发布
     */
    @PostMapping("/send/json")
    public String send(@RequestBody JSONObject body) {

        websocketClient.getSocketChannel().writeAndFlush(new TextWebSocketFrame(body.toJSONString()));
        return "发送成功";
    }

    /**
     * 连接
     */
    @GetMapping("connect")
    public String connect(String ip, Integer port) throws Exception {
        websocketClient.connect(ip, port);
        return "重启指令发送成功";
    }

    /**
     * 重连
     */
    @GetMapping("reconnect")
    public String reconnect() throws Exception {
        websocketClient.reconnect();
        return "重启指令发送成功";
    }
}

5. 效果演示

基础接口 http://localhost:9999

# 1. 发送消息
/send?message=hello
# 2. 连接
/connect?ip=192.168.0.99&port=20000
# 3. 重连
/reconnect
# 5. 发送json
```json
Request URL:  http://localhost:9999/send/json
Request Method: POST
Request Headers:
{
   "Content-Type":"application/json"
}
Request Body:
{
   "msgId": 1,
   "type": 1,
   "data": {
            "message":"hello"
           }
}
  • 效果
    在这里插入图片描述

在这里插入图片描述

6. 源码分享

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

全栈小定

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

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

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

打赏作者

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

抵扣说明:

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

余额充值