Netty從入門到禿頭: websocket

ShallDid發表於2020-10-18

1. 核心依賴

<dependencies>
    <!--netty的依賴集合,都整合在一個依賴裡面了-->
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.6.Final</version>
    </dependency>
</dependencies>

2. 程式碼

2.1 啟動項

public class NioWebSocketServer {
    private final Logger logger=Logger.getLogger(this.getClass());
    private void init(){
        logger.info("正在啟動websocket伺服器");
        NioEventLoopGroup boss=new NioEventLoopGroup();
        NioEventLoopGroup work=new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap=new ServerBootstrap();
            bootstrap.group(boss,work);
            bootstrap.channel(NioServerSocketChannel.class);
            bootstrap.childHandler(new NioWebSocketChannelInitializer());
            Channel channel = bootstrap.bind(8081).sync().channel();
            logger.info("webSocket伺服器啟動成功:"+channel);
            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
            logger.info("執行出錯:"+e);
        }finally {
            boss.shutdownGracefully();
            work.shutdownGracefully();
            logger.info("websocket伺服器已關閉");
        }
    }

    public static void main(String[] args) {
        new NioWebSocketServer().init();
    }
}

netty搭建的伺服器基本上都是差不多的寫法:

  • 繫結主執行緒組和工作執行緒組,這部分對應架構圖中的事件迴圈組
  • 只有伺服器才需要繫結埠,客戶端是繫結一個地址
  • 配置channel(資料通道)引數,重點就是ChannelInitializer的配置
  • 以非同步的方式啟動,最後是結束關閉兩個執行緒組

2.2 ChannelInitializer寫法

public class NioWebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) {
        ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));//設定log監聽器,並且日誌級別為debug,方便觀察執行流程
        ch.pipeline().addLast("http-codec",new HttpServerCodec());//設定解碼器
        ch.pipeline().addLast("aggregator",new HttpObjectAggregator(65536));//聚合器,使用websocket會用到
        ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());//用於大資料的分割槽傳輸
        ch.pipeline().addLast("handler",new NioWebSocketHandler());//自定義的業務handler
    }
}

2.3 自定義的處理器NioWebSocketHandler

public class NioWebSocketHandler extends SimpleChannelInboundHandler<Object> {

    private final Logger logger=Logger.getLogger(this.getClass());

    private WebSocketServerHandshaker handshaker;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        logger.debug("收到訊息:"+msg);
        if (msg instanceof FullHttpRequest){
            //以http請求形式接入,但是走的是websocket
                handleHttpRequest(ctx, (FullHttpRequest) msg);
        }else if (msg instanceof  WebSocketFrame){
            //處理websocket客戶端的訊息
            handlerWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //新增連線
        logger.debug("客戶端加入連線:"+ctx.channel());
        ChannelSupervise.addChannel(ctx.channel());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //斷開連線
        logger.debug("客戶端斷開連線:"+ctx.channel());
        ChannelSupervise.removeChannel(ctx.channel());
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
    private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame){
        // 判斷是否關閉鏈路的指令
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
            return;
        }
        // 判斷是否ping訊息
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(
                    new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        // 本例程僅支援文字訊息,不支援二進位制訊息
        if (!(frame instanceof TextWebSocketFrame)) {
            logger.debug("本例程僅支援文字訊息,不支援二進位制訊息");
            throw new UnsupportedOperationException(String.format(
                    "%s frame types not supported", frame.getClass().getName()));
        }
        // 返回應答訊息
        String request = ((TextWebSocketFrame) frame).text();
        logger.debug("服務端收到:" + request);
        TextWebSocketFrame tws = new TextWebSocketFrame(new Date().toString()
                + ctx.channel().id() + ":" + request);
        // 群發
        ChannelSupervise.send2All(tws);
        // 返回【誰發的發給誰】
        // ctx.channel().writeAndFlush(tws);
    }
    /**
     * 唯一的一次http請求,用於建立websocket
     * */
    private void handleHttpRequest(ChannelHandlerContext ctx,
                                   FullHttpRequest req) {
        //要求Upgrade為websocket,過濾掉get/Post
        if (!req.decoderResult().isSuccess()
                || (!"websocket".equals(req.headers().get("Upgrade")))) {
            //若不是websocket方式,則建立BAD_REQUEST的req,返回給客戶端
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            return;
        }
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                "ws://localhost:8081/websocket", null, false);
        handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory
                    .sendUnsupportedVersionResponse(ctx.channel());
        } else {
            handshaker.handshake(ctx.channel(), req);
        }
    }
    /**
     * 拒絕不合法的請求,並返回錯誤資訊
     * */
    private static void sendHttpResponse(ChannelHandlerContext ctx,
                                         FullHttpRequest req, DefaultFullHttpResponse res) {
        // 返回應答給客戶端
        if (res.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(),
                    CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
        }
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        // 如果是非Keep-Alive,關閉連線
        if (!isKeepAlive(req) || res.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }
}

執行流程是:

  • web發起一次類似是http的請求,並在channelRead0方法中進行處理,並通過instanceof去判斷幀物件是FullHttpRequest還是WebSocketFrame,建立連線是時候會是FullHttpRequest
  • 在handleHttpRequest方法中去建立websocket,首先是判斷Upgrade是不是websocket協議,若不是則通過sendHttpResponse將錯誤資訊返回給客戶端,緊接著通過WebSocketServerHandshakerFactory建立socket物件並通過handshaker握手建立連線
  • 在連線建立好後的所以訊息流動都是以WebSocketFrame來體現
  • 在handlerWebSocketFrame去處理訊息,也可能是客戶端發起的關閉指令,ping指令等等

2.4 儲存客戶端的資訊

當有客戶端連線時候會被channelActive監聽到,當斷開時會被channelInactive監聽到,一般在這兩個方法中去儲存/移除客戶端的通道資訊,而通道資訊儲存在ChannelSupervise中:

public class ChannelSupervise {
    private   static ChannelGroup GlobalGroup=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    private  static ConcurrentMap<String, ChannelId> ChannelMap=new ConcurrentHashMap();
    public  static void addChannel(Channel channel){
        GlobalGroup.add(channel);
        ChannelMap.put(channel.id().asShortText(),channel.id());
    }
    public static void removeChannel(Channel channel){
        GlobalGroup.remove(channel);
        ChannelMap.remove(channel.id().asShortText());
    }
    public static  Channel findChannel(String id){
        return GlobalGroup.find(ChannelMap.get(id));
    }
    public static void send2All(TextWebSocketFrame tws){
        GlobalGroup.writeAndFlush(tws);
    }
}

ChannelGroup是netty提供用於管理web於伺服器建立的通道channel的,其本質是一個高度封裝的set集合,在伺服器廣播訊息時,可以直接通過它的writeAndFlush將訊息傳送給集合中的所有通道中去。但在查詢某一個客戶端的通道時候比較坑爹,必須通過channelId物件去查詢,而channelId不能人為建立,所有必須通過map將channelId的字串和channel儲存起來。

相關文章