Netty实战: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()
                // 心跳时间
                .addLast("idle", new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS))
                // 对http协议的支持.
                .addLast(new HttpServerCodec())
                // 对大数据流的支持
                .addLast(new ChunkedWriteHandler())
                // 聚合 Http 将多个requestLine、requestHeader、messageBody信息转化成单一的request或者response对象
                .addLast(new HttpObjectAggregator(8192))
                // 聚合 websocket 的数据帧,因为客户端可能分段向服务器端发送数据
                .addLast(new WebSocketFrameAggregator(1024 * 62))
                // 添加消息处理器
                .addLast("messageHandler", messageHandler);
    }

}

3. 编写websocket处理器

  • WebsocketMessageHandler 处理器主要负责处理握手协议和文本交互
/**
 * Websocket 消息处理器
 *
 * @author qiding
 */
@Slf4j
@Component
public class WebsocketMessageHandler {

    /**
     * 对webSocket 首次握手进行解析
     */
    public void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {
        // 首次握手进行校验
        this.isFullHttpRequest(ctx, request);
        // 获取请求uri
        String uri = request.uri();
        // 参数分别是 (ws地址,子协议,是否扩展,最大frame长度)
        WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(getWebSocketLocation(request), null, true, 5 * 1024 * 1024);
        WebSocketServerHandshaker handShaker = factory.newHandshaker(request);
        if (handShaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            handShaker.handshake(ctx.channel(), request);
        }
        WebSocketSession.setChannelShaker(ctx.channel().id(), handShaker);
    }

    /**
     * 处理消息
     */
    public void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 获取webSocket 会话
        WebSocketServerHandshaker handShaker = WebSocketSession.getChannelShaker(ctx.channel().id());
        // 关闭
        if (frame instanceof CloseWebSocketFrame) {
            log.debug("收到关闭请求");
            handShaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
            return;
        }
        // 握手PING/PONG
        if (frame instanceof PingWebSocketFrame) {
            ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        // 文本接收和回复
        if (frame instanceof TextWebSocketFrame) {
            log.debug("收到消息:\n{}", ((TextWebSocketFrame) frame).text());
            ctx.writeAndFlush(new TextWebSocketFrame("服务器接收成功!"));
            return;
        }
        // 二进制文本
        if (frame instanceof BinaryWebSocketFrame) {
            ctx.writeAndFlush(frame.retain());
        }
    }

    /**
     * 判断是否是正确的websocket 握手协议
     */
    private void isFullHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {
        if (!request.decoderResult().isSuccess()) {
            log.error("非webSocket请求");
            this.sendResponse(ctx, request, new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.BAD_REQUEST, ctx.alloc().buffer()));
            ctx.close();
            return;
        }
        if (!HttpMethod.GET.equals(request.method())) {
            log.error("非GET请求");
            this.sendResponse(ctx, request, new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.FORBIDDEN, ctx.alloc().buffer()));
            ctx.close();
        }
    }

    /**
     * SSL支持采用wss:
     */
    private String getWebSocketLocation(FullHttpRequest request) {
        return "ws://" + request.headers().get(HttpHeaderNames.HOST) + "/websocket";
    }
    
    /**
     * http 握手通用响应
     */
    private void sendResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse resp) {
        HttpResponseStatus status = resp.status();
        if (status != HttpResponseStatus.OK) {
            ByteBufUtil.writeUtf8(resp.content(), status.toString());
            HttpUtil.setContentLength(req, resp.content().readableBytes());
        }
        boolean keepAlive = HttpUtil.isKeepAlive(req) && status == HttpResponseStatus.OK;
        HttpUtil.setKeepAlive(req, keepAlive);
        ChannelFuture future = ctx.write(resp);
        if (!keepAlive) {
            future.addListener(ChannelFutureListener.CLOSE);
        }
    }
}

  • 修改主消息处理器MessageHandler.java ,加入握手过程处理
/**
 * 消息处理,单例启动
 *
 * @author qiding
 */
@Slf4j
@Component
@ChannelHandler.Sharable
@RequiredArgsConstructor
public class MessageHandler extends SimpleChannelInboundHandler<WebSocketFrame> {

    private final WebsocketMessageHandler websocketHandler;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof FullHttpRequest) {
            websocketHandler.handleHttpRequest(ctx, (FullHttpRequest) msg);
            log.debug("\n");
            log.debug("客户端{}握手成功!", ctx.channel().id());
        }
        super.channelRead(ctx, msg);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
        log.debug("\n");
        log.debug("channelId:" + ctx.channel().id());
        websocketHandler.handleWebSocketFrame(ctx, frame);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        log.debug("\n");
        log.debug("断开连接");
        // 释放缓存
        ChannelStore.closeAndClean(ctx);
        WebSocketSession.clear(ctx.channel().id());
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.debug("\n");
        log.debug("成功建立连接,channelId:{}", ctx.channel().id());
        super.channelActive(ctx);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        log.debug("心跳事件时触发");
    }
}

4. 效果演示

服务类和启动类基础模块的搭建参考开头提供的连接进行搭建即可,这里就不重复了
这里启动项目,以postman测试为例

  • postman模拟客户端连接服务器在这里插入图片描述

  • 服务器
    在这里插入图片描述

5. 源码分享

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

全栈小定

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

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

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

打赏作者

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

抵扣说明:

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

余额充值