SpringBoot2+netty+webSocket實現前後端互相發消息

1.Maven需要的依賴

   <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.42.Final</version>
        </dependency>

初始化netty設置解碼器,編碼器(可以自定義)

public class WebScoketServerInitialzer  extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();

        //http解碼器(websocket是基於http協議的,所以可以直接用現成的
        http解碼器)
        pipeline.addLast(new HttpServerCodec());
        //對寫大數據流的支持
        pipeline.addLast(new ChunkedWriteHandler());
        //設置單次請求的文件的大小
        pipeline.addLast(new HttpObjectAggregator(1024*1024*10));
        //webscoket 服務器處理的協議,用於指定給客戶端連接訪問的路由 :/ws
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
        //自定義handler(作用類似controller,客戶端和服務器端之間發消息都在這個自定義handler裏面)
        pipeline.addLast("handler",new WebSocketHandler());

        

        }
}

websocket啓動類的實現

@Component
public class WebSocketServer {

    private static EventExecutorGroup group = new DefaultEventExecutorGroup(1024);

    public void run(String... args) throws Exception {

        //創建兩個線程
        EventLoopGroup bossGroup = null;
        EventLoopGroup workerGroup = null;
        try {
            bossGroup = new NioEventLoopGroup();
            workerGroup = new NioEventLoopGroup();
            //服務對象
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            //構建工程線程池
            serverBootstrap.group(bossGroup, workerGroup)
                    //Nio長連接
                    .channel(NioServerSocketChannel.class)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    //初始化 過濾 解碼
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            //配置http解碼組件
                            pipeline.addLast("http-codec", new HttpServerCodec());
                            pipeline.addLast("ping", new IdleStateHandler(2, 1, 3, TimeUnit.MINUTES));
                            //把多個消息轉換爲一個單一 Http請求或響應
                            pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
                            //文件傳輸
                            pipeline.addLast("http-chunked", new ChunkedWriteHandler());
                            //邏輯  連接 服務ing 異常 斷開
                            pipeline.addLast(group, "handler", new WebSocketHandler());
                        }
                    });
            //綁定監聽端口號
            ChannelFuture channelFuture = serverBootstrap.bind(8082).sync();
            //關閉
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bossGroup != null) {
                bossGroup.shutdownGracefully();
            }

            if (workerGroup != null) {
                workerGroup.shutdownGracefully();
            }
        }
    }

websocket處理類

@Slf4j
public class WebSocketHandler extends ChannelInboundHandlerAdapter {
    private WebSocketServerHandshaker handShaker;
    Connections connections = Connections.getInstance();

    /**
     * 打開連接
     *
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("------------頁面建立連接---------------");
        Connections connections = Connections.getInstance();
        connections.saveConnection("1",ctx);
        super.channelActive(ctx);
    }
    /**
     * 斷開連接時 移除管道 刪除map中管道對應的用戶
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        log.info("[" + ctx.channel().remoteAddress().toString() + "] connect lost.");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.info("-----------連接出錯------------");
        cause.printStackTrace();
        ctx.close();
    }


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 傳統的HTTP接入
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(ctx, (FullHttpRequest) msg);
            //websocket建立連接是基於websocket
            log.info("這是http接入了");
        }
        // WebSocket接入
        else if (msg instanceof WebSocketFrame) {
            handleWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

    /**
     * 判斷請求內容
     *
     * @param ctx
     * @param frame
     */
    private void handleWebSocketFrame(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)) {
            throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass().getName()));
        }

        try {

            log.info("收到的客戶端消息是:"+ ((TextWebSocketFrame) frame).text());
            ctx.channel().write(new TextWebSocketFrame("我收到消息啦"));
            ctx.flush();
        } catch (Exception e2) {
            e2.printStackTrace();
            log.info("處理Socket請求異常");

        }


    }

    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {

        // 如果HTTP解碼失敗,返回HTTP異常
        if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
            return;
        }

        String uri = req.uri();
        System.out.println("請求的鏈接地址" + uri);

        // 構造握手響應返回
        WebSocketServerHandshakerFactory wsFactory =
                new WebSocketServerHandshakerFactory("ws://" + req.headers().get(HttpHeaderNames.HOST) + uri, 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, FullHttpResponse res) {

        // 返回應答給客戶端
        if (res.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
            HttpUtil.setContentLength(res, res.content().readableBytes());
        }

        // 如果是非Keep-Alive,關閉連接
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }

配置Application啓動類

@SpringBootApplication
@EnableSwagger2
public class LoginApplication implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(LoginApplication.class, args);
    }

    WorkThreadPool workThreadPool = new WorkThreadPool();
    public void run(String... args) throws Exception {
        workThreadPool.getPool().execute(() -> {
            try {
                new WebSocketServer().run();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });


    }
}

客戶端在線測試工具:websocket在線測試工具
1.啓動SpringBoot項目
2.輸入連接地址連接
在這裏插入圖片描述
在這裏插入圖片描述
可以測試發消息給客戶端
在這裏插入圖片描述
在代碼裏面服務端也可以打印客戶端的消息
在這裏插入圖片描述
在這裏插入圖片描述
這樣就可以通信了,獲取到客戶端消息。
如果需要羣發消息,需要自己手寫前端代碼建立連接的時候傳一個唯一標識,後端用HashMap或者自定義一個類用來存儲來自不同地方的連接,發消息時候遍歷取到每一個連接,使用ChannelHandlerContext.channel.write(new TextWebsocketFrame(“消息內容”));就可以實現羣發消息
還有就是,如果業務需要返回前端的數據來自數據庫(要調用service層代碼情況),實例化的時候有可能使用Autowire註解實例化失敗會報空指針(先檢查Handler類有沒有用@Component),試着換用setter方式注入,就可以解決了.
源碼地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章