SpringBoot+Netty+WebSocket实战

实现CommandLineRunner接口异步启动netty服务



/**
 * netty server async start
 *
 * @author liupeng
 * @version 1.0
 * @description: TODO
 * @date 2023/8/25 10:37
 */
@Component
@Slf4j
@Order
public class NettyService implements CommandLineRunner {

   
    @Resource
    private NettyProperties nettyProperties;

    @Override
    public void run(String... args) {
        Executors.newSingleThreadExecutor().execute(() -> {

            try {
                TimeUnit.MILLISECONDS.sleep(6666 - System.currentTimeMillis() % 1000);
            } catch (InterruptedException e) {
                log.error("netty service delay error");
            }
            log.info("netty service start");
            // 完成处理连接
            EventLoopGroup bossGroup = new NioEventLoopGroup(1);
            // 完成与客户端处理 目前就两个设备端
            EventLoopGroup workerGroup = new NioEventLoopGroup(2);
            try {
                Integer nettyPort = nettyProperties.getPort();
                ServerBootstrap serverBootstrap = new ServerBootstrap();
                serverBootstrap.group(bossGroup, workerGroup)
                        .channel(NioServerSocketChannel.class)
                        //服务器连接队列大小 使用默认的NetUtil.SOMAXCONN
                        .option(ChannelOption.SO_BACKLOG, NetUtil.SOMAXCONN)
                        // set SO_KEEPALIVE
                        .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
                        // 自定义通道处理
                        .childHandler(new CustomChannelInitializer(nettyProperties));
                Channel channel = serverBootstrap.bind(nettyPort).sync().addListener((ChannelFutureListener) future -> {
                    if (future.isSuccess()) {
                        log.info("监听端口: {} 成功 ", nettyPort);
                    } else {
                        log.info("监听端口: {} 失败 ", nettyPort);
                    }
                }).channel();
                // 阻塞等待通道关闭完成
                channel.closeFuture().sync();
            } catch (Exception e) {
                log.info("netty service exception : " + e);
            } finally {
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
                log.info("netty service shutdown");
            }
        });
    }
}

application.yml配置文件中配置netty关键信息:如netty服务器端口号、需要连接的设备SN



/**
 * @author liupeng
 * @version 1.0
 * @description: TODO
 * @date 2023/8/29 10:31
 */
@Data
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "netty")
public class NettyProperties {

    /**
     * port
     */
    private Integer port;

    /**
     * 重试次数
     */
    private Integer retryNum;

    /**
     * 重试等待时长 秒
     */
    private Integer retryWaitTime;

    /**
     *Video设备id
     */
    private List<String> videoServerId;

    /**
     * VA设备id
     */
    private List<String> vaServerId;

}

注册ServerBootstrap中的自定义 ChannelInitializer



/**
 * 自定义 ChannelInitializer
 *
 * @author liupeng
 * @version 1.0
 * @description: TODO
 * @date 2023/8/25 10:51
 */
public class CustomChannelInitializer extends ChannelInitializer {
    NettyProperties nettyProperties;


    public CustomChannelInitializer(NettyProperties nettyProperties) {

        this.nettyProperties = nettyProperties;
    }

    @Override
    protected void initChannel(Channel channel) throws Exception {
        ChannelPipeline pipeline = channel.pipeline();
        // Http消息编码解码
        pipeline.addLast("http-codec", new HttpServerCodec());
        // Http消息组装
        pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
        // 以块的方式来写的处理器
        pipeline.addLast("http-chunked", new ChunkedWriteHandler());
        // todo 登录 认证 handler
        pipeline.addLast("login", new LoginHandler(nettyProperties));
        //编码解码
        pipeline.addLast("codec", new CodecHandler());
        //分发
        pipeline.addLast("dispatcher", new DispatcherHandler());
    }
}

处理HTTP、WebSocket请求的Channel



/**
 * 处理第一次http请求
 *
 * @author liupeng
 * @version 1.0
 * @description: TODO
 * @date 2023/8/25 11:14
 */
@Slf4j
public class LoginHandler extends SimpleChannelInboundHandler<Object> {

    /**
     * handshaker
     */
    private WebSocketServerHandshaker handshaker;
    private NettyProperties nettyProperties;

    public LoginHandler(NettyProperties nettyProperties) {
        this.nettyProperties = nettyProperties;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof FullHttpRequest) {
            // HTTP接入
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        } else if (msg instanceof WebSocketFrame) {
            // WebSocket接入
            handleWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {  // (2)
        Channel incoming = ctx.channel();
        log.info("Client: {} added", incoming.remoteAddress());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {  // (3)
        Channel channel = ctx.channel();
        log.info("Client: {} removed", channel.remoteAddress());
        String uuid = ChannelUtil.getInstance().getId(ctx.channel());
        ChannelUtil.getInstance().unBindChannel(uuid, channel);
        //关闭连接
        handshaker.close(ctx.channel(), new CloseWebSocketFrame(WebSocketCloseStatus.NORMAL_CLOSURE));
        ChannelFuture future = ctx.close();
        future.addListener((ChannelFutureListener) f -> {
            // 额外操作
            log.info("handlerRemoved : handlerRemoved.close channelId: {}", uuid);
        });
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) { // (5)
        Channel incoming = ctx.channel();
        log.info("Client: {} active", incoming.remoteAddress());
//        ctx.channel().writeAndFlush(new PongWebSocketFrame());
    }

    /**
     * ChannelInactive触发场景
     * 客户端发送close帧(FIN包)
     * 客户端关闭进程(RST包)
     * 服务端或客户端主动调用channel.close()
     *
     * @param ctx
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) { // (6)
        Channel incoming = ctx.channel();
        log.info("Client: {} inactive", incoming.remoteAddress());
    }


    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error(cause.toString());
        Channel channel = ctx.channel();
        log.info("Client: {} removed, error: {}", channel.remoteAddress(), cause.getMessage());
        String uuid = ChannelUtil.getInstance().getId(ctx.channel());
        ChannelUtil.getInstance().unBindChannel(uuid, channel);
        // 其他异常,关闭连接
        handshaker.close(ctx.channel(), new CloseWebSocketFrame(WebSocketCloseStatus.NORMAL_CLOSURE));
        ChannelFuture future = ctx.close();
        future.addListener((ChannelFutureListener) f -> {
            // 额外操作
            log.info("exceptionCaught : ChannelHandlerContext.close channelId: {}", uuid);
        });
    }

    /**
     * 处理Http请求,完成WebSocket握手
     * todo 第一次连接中做token校验
     *
     * @param ctx
     * @param request
     */
    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {
        // 如果HTTP解码失败,返回HTTP异常
        if (!request.decoderResult().isSuccess()) {//
            sendHttpResponse(ctx, request, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
            return;
        }
        // 子协议
        String header = request.headers().get(SEC_WEB_SOCKET_PROTOCOL);
        // todo 后期设备过多可查配置表
        List<String> vaServerId = nettyProperties.getVaServerId();
        List<String> videoServerId = nettyProperties.getVideoServerId();
        vaServerId.addAll(videoServerId);
        if (!StringUtils.hasLength(header) || !vaServerId.contains(header)) {
            log.error("header 设备唯一码  不存在 :" + header);
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
            return;
        }
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://" + request.headers().get(HOST) + "/ws"
                , header, false, 65536);
        HttpVersion protocolVersion = request.protocolVersion();
        //HTTP/1.1
        if (protocolVersion.minorVersion() == 1) {
            wsFactory = new WebSocketServerHandshakerFactory("wss://" + request.headers().get(HOST) + "/ws"
                    , header, false, 65536);
        }
//        调试
//        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://" + request.headers().get(HOST) + "/ws"
//                , null, false, 65536 * 10);
        handshaker = wsFactory.newHandshaker(request);
        if (handshaker == null) {
            // 无法处理的websocket版本
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            // 向客户端发送websocket握手,完成握手
            // todo  token 验证
            // 设备唯一码
            ChannelUtil.getInstance().bindChannel(header, ctx.channel());
            log.info(" channel 连接 :" + header);
            handshaker.handshake(ctx.channel(), request);
        }
    }

    /**
     * Http返回
     *
     * @param ctx
     * @param request
     * @param response
     */
    private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) {
        // 返回应答给客户端
        if (response.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(response.status().toString(), CharsetUtil.UTF_8);
            response.content().writeBytes(buf);
            buf.release();
            //允许跨域访问 设置头部信息
            response.headers().set(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            response.headers().set(ACCESS_CONTROL_ALLOW_METHODS, "GET,POST,PUT,DELETE");
            response.headers().set(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
            response.headers().set(ACCESS_CONTROL_ALLOW_HEADERS, "Origin, X-Requested-With, Content-Type, Accept");
            HttpUtil.setContentLength(response, response.content().readableBytes());
        }
        // 如果是非Keep-Alive,关闭连接 保持Keep-Alive
        ChannelFuture f = ctx.channel().writeAndFlush(response);
        if (!HttpUtil.isKeepAlive(request) || response.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }

    private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        //当前管道id
        String id = ChannelUtil.getInstance().getId(ctx.channel());
//        log.info("useUUID {}", id);
        if (frame instanceof CloseWebSocketFrame) {
            //关闭管道
            log.info("channelId: {} CloseWebSocketFrame", id);
            ChannelUtil.getInstance().unBindChannel(id, ctx.channel());
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
        } else if (frame instanceof PingWebSocketFrame) {
            //Ping消息
            log.info("channelId: {} PingWebSocketFrame", id);
            ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
        } else if (frame instanceof TextWebSocketFrame) {

编码解码 Handler,可自定义数据格式



/**
 * 编码解码 Handler
 *
 * @author liupeng
 * @version 1.0
 * @description: TODO
 * @date 2023/8/25 10:53
 */
public class CodecHandler extends MessageToMessageCodec<TextWebSocketFrame, String> {
    @Override
    protected void decode(ChannelHandlerContext ctx, TextWebSocketFrame msg, List<Object> in) throws Exception {
        String text = msg.text();
        in.add(text);
    }

    @Override
    protected void encode(ChannelHandlerContext ctx, String res, List<Object> out) throws Exception {
        out.add(new TextWebSocketFrame(res));
    }
}

多个ChannelInboundHandler的调度器:目前就一种数据格式的处理Handler,后续可按需添加。


/**
 * 调度 handle
 *
 * @author liupeng
 * @version 1.0
 * @description: TODO
 * @date 2023/8/25 11:02
 */
@Slf4j
public class DispatcherHandler extends SimpleChannelInboundHandler<String> {

    /**
     * map
     */
    private static final Map<String, SimpleChannelInboundHandler> HANDLER_MAP = new HashMap<>();

    public DispatcherHandler() {
        HANDLER_MAP.put(LIVE_STREAMING_LIST, new LiveStreamProcessingHandler());
        HANDLER_MAP.put(LIVE_STREAMING_STATUS, new LiveStreamProcessingHandler());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        Request request;
        try {
            request = ChannelUtil.getInstance().verifyMessage(msg);
        }catch (Exception e){
            log.error(" 校验请求参数异常 {} " , e.getMessage());
            ctx.channel().writeAndFlush("Data format exception :" + msg);
            return;
        }
        SimpleChannelInboundHandler handler = HANDLER_MAP.get(request.getExecute());
        if (handler == null) {
            ctx.channel().writeAndFlush("execute command error");
        }
        handler.channelRead(ctx, msg);
    }
}

从spring容器中获取具体的业务bean,上面的LiveStreamProcessingHandler继承AbstractLiveStreamProcess抽象类,各自的具体业务可以自定义实现。



/**
 * @author liupeng
 * @version 1.0
 * @description: TODO
 * @date 2023/8/25 14:12
 */
@Slf4j
public class LiveStreamProcessingHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        log.info("message {}", msg);
        Channel channel = ctx.channel();
        String id = ChannelUtil.getInstance().getId(channel);
        if (StringUtils.isEmpty(id)) {
            //todo 集群可通过消息队列组件进行转发监听
        }
        Request request = ChannelUtil.getInstance().verifyMessage(msg);
        String execute = request.getExecute();
        try {
            if (StringUtils.isNotEmpty(execute)) {
                Object bean = ApplicationContextRegister
                        .getApplicationContext().getBean(execute);
                if (ObjectUtils.isNotEmpty(bean)) {
                    AbstractLiveStreamProcess service = (AbstractLiveStreamProcess) bean;
                    Request process = service.process(request, channel);
                    if (ObjectUtils.isNotEmpty(process)) {
                        log.info(" responseData : {}", request);
                        channel.writeAndFlush(ChannelUtil.getInstance().responseData(request));
                    }
                    return;
                }
            }
            request.setParams("execute command error");
        } catch (Exception e) {
            log.error(" LiveStreamProcessingHandler 处理异常" + e);
            request.setParams("handler 处理异常 " + e.getMessage());
        }
        channel.writeAndFlush(ChannelUtil.getInstance().responseData(request));
    }
}

  • 15
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值