SpringBoot+Netty整合websocket(一)——客户端和服务端通讯

SpringBoot+Netty整合websocket(一)——客户端和服务端通讯

背景

现在的一般的项目当中一般都有长连接时事通讯的需求,客户端和服务器之间,客户端和客户端之间进行通讯。
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端 ,它是先进行一次Http的连接,连接成功后转为TCP连接。
我们一般会采用websocket技术,但是原生的websocket往往容易发生同步阻塞,导致效率低,所以会采用Netty整合websocket。
这篇博客主要总结SpringBoot+Netty如何整合websocket(和整合原生的websocket其实差不多)。

步骤

准备,引入Maven依赖

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

1.建立服务端WebSocketServer

/**
 * 功能描述:netty整合websocket的服务端
 **/
@Slf4j
@Configuration
public class WebSocketServer {

    @Value("${netty.port}")
    private int port;

    public void run() throws InterruptedException {
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boss,worker)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,1024)
                    .childOption(ChannelOption.TCP_NODELAY,true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //web基于http协议的解码器
                            ch.pipeline().addLast(new HttpServerCodec());
                            //对大数据流的支持
                            ch.pipeline().addLast(new ChunkedWriteHandler());
                            //对http message进行聚合,聚合成FullHttpRequest或FullHttpResponse
                            ch.pipeline().addLast(new HttpObjectAggregator(1024 * 64));
                            //websocket服务器处理对协议,用于指定给客户端连接访问的路径
                            //该handler会帮你处理一些繁重的复杂的事
                            //会帮你处理握手动作:handshaking(close,ping,pong) ping + pong = 心跳
                            //对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同
                            ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
                            //添加我们的自定义channel处理器
                            ch.pipeline().addLast(new WebSocketHandler());
                        }
                    });
            log.info("服务器启动中,websocket的端口为:"+port);
            ChannelFuture future = bootstrap.bind(port).sync();
            future.channel().closeFuture().sync();
        } finally {
            //关闭主从线程池
            worker.shutdownGracefully();
            boss.shutdownGracefully();
        }

    }
}

2.建立channel处理器

@Slf4j
public class WebSocketHandler extends SimpleChannelInboundHandler<Object> {
    //用于记录和管理所有客户端的channel
     //客户端组
    public  static ChannelGroup channelGroup;

    static {
        channelGroup=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    }
    
    /**
     * 接收客户端传来的消息
     */
    @Override
	protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
    //文本消息
        if (msg instanceof TextWebSocketFrame) {
            //第一次连接成功后,给客户端发送消息
            Channel channel = ctx.channel();
            channel.writeAndFlush(new TextWebSocketFrame("连接客户端成功"));
            
            //获取当前channel绑定的IP地址
            InetSocketAddress ipSocket = (InetSocketAddress)ctx.channel().remoteAddress();
            String address = ipSocket.getAddress().getHostAddress();
            System.out.println("address为:"+address);
            //将IP和channel的关系保存
            if (!channelMap.containsKey(address)){
                channelMap.put(address,ctx.channel());
            }
        }
        //二进制消息
        if (msg instanceof BinaryWebSocketFrame) {
            System.out.println("收到二进制消息:" + ((BinaryWebSocketFrame) msg).content().readableBytes());
            BinaryWebSocketFrame binaryWebSocketFrame = new BinaryWebSocketFrame(Unpooled.buffer().writeBytes("hello".getBytes()));
            //给客户端发送的消息
            ctx.channel().writeAndFlush(binaryWebSocketFrame);
        }
        //ping消息
        if (msg instanceof PongWebSocketFrame) {
            System.out.println("客户端ping成功");
        }
        //关闭消息
        if (msg instanceof CloseWebSocketFrame) {
            System.out.println("客户端关闭,通道关闭");
            Channel channel = ctx.channel();
            channel.close();
        }
	}

    /**
     * 当客户端连接服务端之后(打开连接)
     * 获取客户端的channel,并且放到ChannelGroup中去进行管理
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        channelGroup.add(ctx.channel());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        //当触发handlerRemoved,ChannelGroup会自动移除对应的客户端的channel
        //所以下面这条语句可不写
//        channelGroup.remove(ctx.channel());
        log.info("客户端断开,channel对应的长id为:" + ctx.channel().id().asLongText());
        log.info("客户端断开,channel对应的短id为:" + ctx.channel().id().asShortText());
    }

  /**
     * 异常处理
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught ( ChannelHandlerContext ctx, Throwable cause ) throws Exception {
        System.out.println("连接异常:" + cause.getMessage());
        cause.printStackTrace();
        ctx.channel().close();
        channelGroup.remove(ctx.channel());
    }
}

3.在SpringBoot启动时,启动Netty整合的websocket服务

方式一

可以在application.yml配置netty的启动端口

PS:和原生websocket不同,不能共用项目的端口,所以需要新设定端口

netty:
  port: 10101
这里采用的是,启动类实现CommandLineRunner 接口,重写run方法,用来在项目启动时预加载资源
/**
 * 声明CommandLineRunner接口,实现run方法,就能给启动项目同时启动netty服务
 */
@SpringBootApplication
public class WebsocketApplication implements CommandLineRunner {

   @Autowired
   private WebSocketServer webSocketServer;

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


   @Override
   public void run ( String... args ) throws Exception {
      webSocketServer.run();
   }
}
问题

在IDEA中启动类实现CommandLineRunner接口,会造成Running一直在加载,但是不影响正常的使用
在这里插入图片描述

方式二

直接在启动类里,传入启动端口
@SpringBootApplication
public class WebsocketApplication  {

   public static void main(String[] args) throws InterruptedException {
      SpringApplication.run(WebsocketApplication.class, args);
      //服务启动时,启动netty整合websocket服务
      try {
            new WebSocketNettyServer(10101).run();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
   }

}
修改WebSocketServer,添加带port属性的构造方法
@Slf4j
//全参构造方法
@AllArgsConstructor
public class WebSocketServer {

    @Value("${netty.port}")
    private int port;
    
	//其余代码不变
}

4.前端代码

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title></title>
    </head>
    <body>
        <div>发送消息:</div>
        <input type="text" id="msgContent" />
        <input type="button" value="发送" onclick="CHAT.chat()" />
        
        <div>接受消息:</div>
        <div id="receiveMsg" style="background-color: gainsboro;"></div>
        
        <script type="application/javascript">
            window.CHAT = {
                socket: null,
                init: function() {
                    if (window.WebSocket) {
                        CHAT.socket = new WebSocket("ws://127.0.0.1:10101/ws");
                        CHAT.socket.onopen = function() {
                            console.log("连接建立成功");
                        },
                        CHAT.socket.onclose = function() {
                            console.log("连接关闭");
                        },
                        CHAT.socket.onerror = function() {
                            console.log("发生错误");
                        },
                        CHAT.socket.onmessage = function(e) {
                            console.log("接收到消息" + e.data);
                            var receiveMsg = document.getElementById("receiveMsg");
                            var html = receiveMsg.innerHTML;
                            receiveMsg.innerHTML = html + "<br/>" + e.data;
                        }
                    }else {
                        alert("浏览器不支持WebSocket协议...");
                    }
                },
                chat: function() {
                    var msg = document.getElementById("msgContent");
                    CHAT.socket.send(msg.value);
                }
            }
            CHAT.init();
        </script>
    </body>
</html>

也可以使用在线websocket,模拟客户端测试客户端发送数据至服务端

小结

至此SpringBoot+Netty整合websocket就已经实现了。至于具体的业务逻辑就要写到WebSocketHandlerchannelRead0方法里面了。

但是websocket用的最多还是服务器给客户端推送消息和客户端之间进行实时通讯。但是难免会遇到客户端离线的消息接收问题。

本篇博客的功能只是客户端发送消息至服务器端,服务器端可以接收到消息。下篇博客就总结下websocket如何实现客户端实时通讯(一对一聊天),并存储消息到redis或者数据库中。

发布了52 篇原创文章 · 获赞 68 · 访问量 7160

猜你喜欢

转载自blog.csdn.net/qq_42937522/article/details/104883724