Netty进阶(二) Springboot集成Netty实现WebSocket服务

前置技术:springboot、netty、websocket的基本概念

WebSocket介绍

在WebSocket概念出来之前,如果页面要不停地显示最新的价格,那么必须不停地刷新页面,或者用一段js代码每隔几秒钟发消息询问服务器数据。 
而使用WebSocket技术之后,当服务器有了新的数据,会主动通知浏览器。

每创建一个浏览器会话就创建一个WebServer对象。 

里面有四个方法:

OnOpen 表示有浏览器链接过来的时候被调用
OnClose 表示浏览器发出关闭请求的时候被调用
OnMessage 表示浏览器发消息的时候被调用
OnError 表示有错误发生,比如网络断开了等等

前端项目代码:整了一个jsp页面模拟客户端

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>



    
    Insert title here


当前价格为:¥10000

Netty服务应用

只贴一下Netty的依赖

     
            io.netty
            netty-all
            4.1.35.Final
        

项目结构:

Netty进阶(二) Springboot集成Netty实现WebSocket服务_第1张图片

springboot启动类是Main这个类,就不做介绍了。

NettyServer

先来看一下Netty核心逻辑,需要添加四个处理器,三个为自带的,一个为自定义处理器,需要自己去实现。

@Slf4j
public class NettyServer {

    private NioEventLoopGroup bossGroup = new NioEventLoopGroup(3);
    private NioEventLoopGroup workGroup = new NioEventLoopGroup();
    private Channel channel;
    private MyNettyServerHandler nettyServerHandler;

    public NettyServer (MyNettyServerHandler nettyServerHandler){
        this.nettyServerHandler=nettyServerHandler;
    }

    public ChannelFuture start(int port){
        try{
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup,workGroup)
                    //使用哪种通道实现,NioServerSocketChannel为异步的服务器端 TCP Socket 连接。
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                    //设置连接队列长度
                    .option(ChannelOption.SO_BACKLOG,1024)
                    //添加拦截处理器
                    .childHandler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            //websocket协议本身是基于http协议的,所以使用http解编码器
                            pipeline.addLast(new HttpServerCodec());
                            //post请求方式
                            pipeline.addLast(new HttpObjectAggregator(65536));
                            //websocket协议
                            pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
                            pipeline.addLast(nettyServerHandler);
                        }
                    });
            ChannelFuture channelFuture = bootstrap.bind().sync();
            channel=channelFuture.channel();
            log.info("netty 服务开启成功!");
            return channelFuture;
        }catch (Exception e){
            log.info("netty启动错误!{}",e);
        }
        return null;
    }
   public void destory(){
        if (channel!=null){
            channel.close();
        }
       bossGroup.shutdownGracefully();
       workGroup.shutdownGracefully();
       log.info("netty 服务关闭!");
   }
}

MyNettyServerHandler 自定义处理器

@Slf4j
@ChannelHandler.Sharable
public class MyNettyServerHandler extends SimpleChannelInboundHandler {

    private ChannelGroup channels;

    public MyNettyServerHandler(ChannelGroup channels){
        this.channels=channels;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("读取客户端消息!");
        if (null==msg){
            log.info("数据为空");
           return;
        }
        //第一次读请求是http连接而不是数据
        if (msg instanceof FullHttpRequest){
            FullHttpRequest fullHttpRequest = (FullHttpRequest) msg;
            log.info("第一次http请求:"+fullHttpRequest);
        }
        //正常的数据
        else if (msg instanceof TextWebSocketFrame){
            TextWebSocketFrame textWebSocketFrame = (TextWebSocketFrame) msg;
            log.info("客户端消息:"+textWebSocketFrame.text());
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("客户端连接成功!");
        channels.add(ctx.channel());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("客户端断开连接!");
        channels.remove(ctx.channel());
    }
}

里面引入了一个ChannelGroup对象,这个对象是其实就是它,用于存放所有channel实例的通道组

public class ChannelPool {

    //用于存所有的channel实例
    public static final ChannelGroup channels=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}

到这里算是单独的Netty服务,怎么将Netty服务和springboot绑定到一起的呢,这时就需要CommandLineRunner接口了。

在springboot项目启动后会自动启动netty服务,netty服务的端口号为springboot服务的端口号加50

public class NettyServerApplication implements CommandLineRunner {

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

    private NettyServer nettyServer;

    public NettyServerApplication(NettyServer nettyServer){
        this.nettyServer=nettyServer;
    }

    @Override
    public void run(String... args) throws Exception {
        //netty服务为springboot项目端口号加50
        ChannelFuture channelFuture = nettyServer.start(serverPort + 50);
        //添加钩子函数,jvm关闭时一并将注册的服务也关闭
        Runtime.getRuntime().addShutdownHook(new Thread(nettyServer::destory));
        //对关闭通道事件进行监听
        channelFuture.channel().closeFuture().sync();
    }
}

最后通过配置类将这些对象注入进来。

@Configuration
public class WebSocketConfig {

    @Bean
    public NettyServer nettyServer(){
        return new NettyServer(myNettyServerHandler());
    }
    @Bean
    public MyNettyServerHandler myNettyServerHandler(){
        return new MyNettyServerHandler(ChannelPool.channels);
    }
    @Bean
    public NettyServerApplication nettyServerApplication(){
        return new NettyServerApplication(nettyServer());
    }
}

加一个测试入口

@RestController
@RequestMapping("/send")
public class sendController {



    @GetMapping("/ws")
    public void ws() throws Exception{
        while (true){
            TextWebSocketFrame textWebSocketFrame = new TextWebSocketFrame(String.valueOf(new Random().nextInt(10000)));
            ChannelPool.channels.writeAndFlush(textWebSocketFrame);
            Thread.sleep(5000);
        }

    }
}

启动服务器端应用(后端项目)和客户端应用(前端项目)。

访问客户端地址,如我的为http://localhost:8081/websocket.jsp

Netty进阶(二) Springboot集成Netty实现WebSocket服务_第2张图片

接着访问服务端接口,我的为http://localhost:8082/send/ws

会发现客户端的金额每五秒钟刷新一次,并且发现客户端并没有主动发起请求。

我们再来打开一个新的页面去访问客户端地址,发现这两个客户端页面会同时刷新,到这里整个项目就结束了。

你可能感兴趣的:(NIO,websocket,netty)