使用netty 4.1.x 手写一个简单的websocket服务端

2 篇文章 0 订阅
1 篇文章 0 订阅

1.netty服务创建

public NettyWebsocketServer(LogWebsocketServerConfig config) {
        this.config = config;
        if (useEpoll()) {
            bossGroup = new EpollEventLoopGroup(1,
                    new EventLoopThreadFactory(1, "NettyWsEpollEventAcceptor"));
            workGroup = new EpollEventLoopGroup(config.getWorkCount(),
                    new EventLoopThreadFactory(config.getWorkCount(), "NettyWsEpollEventSelector"));
        } else {
            bossGroup = new NioEventLoopGroup(1,
                    new EventLoopThreadFactory(1, "NettyWsServerNioAcceptor"));
            workGroup = new NioEventLoopGroup(config.getWorkCount(),
                    new EventLoopThreadFactory(config.getWorkCount(), "NettyWsServerNioEventSelector"));
        }
    }

    public void createNettyServer() throws Exception {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workGroup)
                .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
                //自定义处理器见2
                .childHandler(new SimpleServerChannelInitializer(config, bootstrap))
                .option(ChannelOption.SO_BACKLOG, 1024)
                .option(ChannelOption.SO_REUSEADDR, true)
                .childOption(ChannelOption.TCP_NODELAY, true);
        if (useEpoll()) {
            bootstrap.option(EpollChannelOption.TCP_FASTOPEN, 50);
            bootstrap.childOption(EpollChannelOption.TCP_QUICKACK, true);
            bootstrap.option(EpollChannelOption.EPOLL_MODE, EpollMode.EDGE_TRIGGERED);
        }
        ChannelFuture f = bootstrap.bind(config.getPort()).sync();
        f.addListener(v -> {
            if (!v.isSuccess()) {
                throw new RuntimeException("netty server start failed");
            } else {
                log.info("netty server started, connect port:{}, is use epoll: {}", config.getPort(), useEpoll());
            }
        });
        //开启关闭主从线程池的钩子
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            bossGroup.shutdownGracefully().syncUninterruptibly();
            workGroup.shutdownGracefully().syncUninterruptibly();
        }));
    }
    
    //是否启用native epoll
    private boolean useEpoll() {
        return config.getUseEpollNativeSelector() && NettyServerUtil.isLinuxPlatform() && Epoll.isAvailable();
    }

    /**
     * 自定义线程工厂,保证线程名称和别的业务区分
     */
    public class EventLoopThreadFactory implements ThreadFactory {

        private final AtomicInteger threadIndex = new AtomicInteger(0);

        private final int threadTotal;

        private final String threadNamePrefix;

        public EventLoopThreadFactory(int threadTotal, String threadNamePrefix) {
            this.threadTotal = threadTotal;
            this.threadNamePrefix = threadNamePrefix;
        }

        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, String.format("%s-%d-%d", threadNamePrefix, threadTotal, this.threadIndex.incrementAndGet()));
        }
    }

为了考虑实时推送的性能这里做了几点优化:

1.如果系统满足要求,则使用了netty的native epoll,开启了epoll的ET模式,而java的nio使用的LT模式。为了避免出现问题,这里除了判断系统是否支持,还增加了额外是否开启epoll的开关

2.tcp参数调优:tcp参数很多,但是java nio很多tcp参数并未暴露。所以这里在使用native epoll时增加tcp的fastopen和qucikack

2.自定义处理器

2.1 连接初始化器SimpleServerChannelInitializer

封装了创建链接相关操作,这里只摘了部分主要代码:

@ChannelHandler.Sharable
public class SimpleServerChannelInitializer extends ChannelInitializer<SocketChannel> {

...

 protected void initChannel(SocketChannel socketChannel) {
        ChannelPipeline pipeline = socketChannel.pipeline();
        //netty 自带的http解码器
        pipeline.addLast(new HttpServerCodec(config.getMaxInitialLineLength(), config.getMaxHeaderSize(),
                config.getMaxChunkSize()));
        //http聚合器
        pipeline.addLast(new HttpObjectAggregator(config.getMaxContextLength()));
        pipeline.addLast(new ChunkedWriteHandler());
        //压缩协议
        pipeline.addLast(new WebSocketServerCompressionHandler());
        //http处理器 用来握手和执行进一步操作
        pipeline.addLast(new NettyWebsocketHttpHandler(config, listener));
    }


...

}

这里有两个优化:

1.启用了压缩和大报文的聚合处理器,因为一般来说websocket推送都是大报文,可以提高网络通信的性能

2.使用了@ChannelHandler.Sharble 注解保证部分处理器是单例的,防止高并发情况下创建过多处理器对象

注意点:

需要控制maxContextLength,避免服务器遭受大报文网络攻击

2.2 http请求处理器NettyWebsocketHttpHandler

主要处理的是websocket的握手操作,在握手成功后增加自定义处理器

url后跟的clientId参数为鉴权操作预留了入口,在后续版本中实现了jwt token认证

@ChannelHandler.Sharable
public class NettyWebsocketHttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
...

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) {
        handleHandshake(req, ctx);
    }

    private void handleHandshake(FullHttpRequest req, ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        String uri = req.uri();
        if (!channel.isActive()) {
            return;
        }
        //握手前触发自定义事件回调
        listener.onHandshakeBefore(ctx, uri);
        FullHttpResponse res;
        //从uri中获取?后面的token参数
        String clientId = ChannelAttrKey.getParamKeyOne(uri, ChannelAttrKey.CLIENT_ID);
        //鉴权相关,这里暂时只是判断了一下是否为空
        if (StringUtils.isBlank(clientId)) {
            res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN);
            sendHttpResponse(ctx, req, res);
            return;
        }
        // Handshake
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory
                (getWebSocketLocation(req), "", true, config.getMaxContextLength());
        WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(channel);
        } else {
            ChannelPipeline pipeline = ctx.pipeline();
            pipeline.remove(ctx.name());
            handshaker.handshake(channel, req).addListener(future -> {
                if (future.isSuccess()) {
                    //握手成功触发自定义事件回调
                    listener.onHandshakeSuccess(ctx, uri);
                    //增加业务处理器
                    pipeline.addLast(new NettyWebsocketBusinessHandler(listener));
                    //增加心跳检测处理器
                    pipeline.addLast(new IdleStateHandler(0, 0, config.getMaxIdleTime()));
                    pipeline.addLast(new SimpleChannelIdleHandler(listener));
                } else {
                    //握手失败触发自定义事件回调
                    listener.onHandshakeFailed(ctx, uri);
                }
            });
        }
    }

 //发送http消息
 private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
        int statusCode = res.status().code();
        if (statusCode != OK.code() && res.content().readableBytes() == 0) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
        }
        HttpUtil.setContentLength(res, res.content().readableBytes());

        ChannelFuture f = ctx.channel().writeAndFlush(res);
        if (!HttpUtil.isKeepAlive(req) || statusCode != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }

  private static String getWebSocketLocation(FullHttpRequest req) {
        String location = req.headers().get(HttpHeaderNames.HOST) + req.uri();
        return "ws://" + location;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ...
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        ...
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        //关闭上下文
        ...
    }
...
}

握手成功后,会增加一些参数的保存,以及创建websocket会话

    ...

    public void onHandshakeSuccess(ChannelHandlerContext ctx, String uri) {
        Channel channel = ctx.channel();
        log.info("[handshakeComplete event]channel id->{},uri->{},remote ip->{}......", channel.id(), uri,
                NettyServerUtil.getRemoteIpByChannel(channel));
        WebsocketSession session = new WebsocketSession(channel);
        channel.attr(ChannelAttrKey.SESSION_KEY).set(session);
        Optional<String> optional = Arrays.stream(uri.split("\\?")).findFirst();
        //截取?之前的参数
        if (optional.isPresent()) {
            channel.attr(ChannelAttrKey.PATH_KEY).set(optional.get());
        } else {
            channel.attr(ChannelAttrKey.PATH_KEY).set(uri);
        }

    }

    ...

websocketsession 封装了一些发送操作:

public class WebsocketSession {

    private final Channel channel;

    public WebsocketSession(Channel channel) {
        this.channel = channel;
    }

    ...


  public ChannelFuture sendText(String message) {
        return channel.writeAndFlush(new TextWebSocketFrame(message));
    }


  public ChannelFuture sendBinary(byte[] bytes) {
        ByteBuf buffer = channel.alloc().buffer(bytes.length);
        return channel.writeAndFlush(new 
            BinaryWebSocketFrame(buffer.writeBytes(bytes)));
    }

    ...
    
}

2.3 心跳检测处理器SimpleChannelIdleHandle

执行心跳检测逻辑,如果超过一定时间链接都处于空闲状态,会直接关闭上下文:

@Slf4j
@ChannelHandler.Sharable
public class SimpleChannelIdleHandler extends ChannelDuplexHandler {
    ...


    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent) {
            Channel channel = ctx.channel();
            IdleStateEvent stateEvent = (IdleStateEvent) evt;
            if (stateEvent.state() == IdleState.ALL_IDLE) {
                log.warn("[ChannelIdleState event] idle channel will be closed, id->{},remote ip->{}",
                        channel.id(), NettyServerUtil.getRemoteIpByChannel(channel));
                ctx.close();
            }
        }
    }

    ....
}

2.4 业务处理器NettyWebsocketBusinessHandler

这里仅仅对不同的数据类型进行了归类,然后触发对应事件:

@Slf4j
@ChannelHandler.Sharable
public class LogWebsocketBusinessHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
   
...

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
        Channel channel = ctx.channel();
        WebsocketSession session = channel.attr(ChannelAttrKey.SESSION_KEY).get();
        ByteBuf byteBuf = frame.content();
        byte[] bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);
        if (frame instanceof BinaryWebSocketFrame) {
            listener.onMessageBinary(session, bytes);
        } else if (frame instanceof TextWebSocketFrame) {
            listener.onMessageText(session, new String(bytes));
        } else if (frame instanceof PongWebSocketFrame) {
            listener.onMessagePong(session, bytes);
        } else if (frame instanceof PingWebSocketFrame) {
            listener.onMessagePing(session, bytes);
        } else if (frame instanceof ContinuationWebSocketFrame) {
            listener.onMessageContinuation(session, bytes);
        } else if (frame instanceof CloseWebSocketFrame) {
            if(channel.isActive()){
                ctx.close();
            }
        }
    }

...
}

具体的事件处理器是基于了spring的@Service自定义了处理器注解,为了实现类似于@RestController的路径逻辑:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Service
public @interface NettyWebsocketHandlePath {

    /**
     * bean名称
     *
     * @return
     */
    @AliasFor(annotation = Service.class)
    String value() default "";

    /**
     * 路径
     * @return
     */
    String path();
}

这里获取所以加了注解的spring bean 并加入了自己的缓存,相当于路由到对应路径的处理器上处理具体业务逻辑

...

 /**
     * 获取对应bean
     */
    private void setStrategyMap() {
        //从spring获取自定义注解的相关bean
        Map<String, Object> beanMap = applicationContext.getBeansWithAnnotation(NettyWebsocketHandlePath.class);
        beanMap.forEach((k, v) -> {
            //必须是IRouteHandleStrategy接口的实现类(策略模式)
            if (v instanceof IRouteHandleStrategy) {
                IRouteHandleStrategy strategy = (IRouteHandleStrategy) v;
                NettyWebsocketHandlePath path = AnnotationUtils.findAnnotation(strategy.getClass(), NettyWebsocketHandlePath.class);
                //把bean加入自己的缓存
                STRATEGY_MAP.putIfAbsent(path.path(), strategy);
            }
        });
    }

...

    @Override
    public void onMessageBinary(WebsocketSession session, byte[] message) {
        String path = session.getAttribute(ChannelAttrKey.PATH_KEY.name());
        IRouteHandleStrategy strategy = STRATEGY_MAP.get(path);
        if (strategy != null) {
            strategy.onMessageBinary(session, message);
        } else {
            log.warn("cannot find route handler strategy for {}, please check!!!", path);
        }
    }

...

自此使用netty 4.1.x 手写一个简单的websocket服务端就结束了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值