netty实现websocket推送消息

1,125 阅读3分钟

前言

由于http协议为应答模式的连接,无法保持长连接于是引入了websocket套接字长连接概念,能够保持数据持久性的交互;本篇文章将告知读者如何使用netty实现简单的消息推送功能

websocket请求头

GET / HTTP/1.1
Host: 127.0.0.1:8096
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
Upgrade: websocket
Origin: http://localhost:8056
Sec-WebSocket-Version: 13

websocket请求头 会有 Connection 升级为 Upgrade, 并且Upgrade 属性值为 websocket

引入依赖

引入netty和 引擎模板依赖


    <dependencies>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.55.Final</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

创建WebSocketServer

创建Nio线程组,并在辅助启动器中中注入 自定义处理器;定义套接字端口为8096;

/**
 * @author lsc
 * <p> </p>
 */
@Slf4j
public class WebSocketServer {
​
    public void init(){
        NioEventLoopGroup boss=new NioEventLoopGroup();
        NioEventLoopGroup work=new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap=new ServerBootstrap();
            bootstrap.group(boss,work);
            bootstrap.channel(NioServerSocketChannel.class);
            // 自定义处理器
            bootstrap.childHandler(new SocketChannelInitializer());
            Channel channel = bootstrap.bind(8096).sync().channel();
            log.info("------------webSocket服务器启动成功-----------:"+channel);
            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
            log.info("---------运行出错----------:"+e);
        }finally {
            boss.shutdownGracefully();
            work.shutdownGracefully();
            log.info("------------websocket服务器已关闭----------------");
        }
    }
}

SocketChannelInitializer

SocketChannelInitializer 中定义了聚合器 HttpObjectAggregator 将多个http片段消息聚合成完整的http消息,并且指定大小为65536;最后注入自定义的WebSocketHandler;


/**
 * @author lsc
 * <p> </p>
 */
public class SocketChannelInitializer extends ChannelInitializer<SocketChannel> {
​
    @Override
    protected void initChannel(SocketChannel ch) {
        //设置log监听器
        ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));
        //设置解码器
        ch.pipeline().addLast("http-codec",new HttpServerCodec());
        //聚合器
        ch.pipeline().addLast("aggregator",new HttpObjectAggregator(65536));
        //用于大数据的分区传输
        ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
        //自定义业务handler
        ch.pipeline().addLast("handler",new WebSocketHandler());
    }
}

WebSocketHandler

WebSocketHandler 中对接收的消息进行判定,如果是websocket 消息 则将消息广播给所有通道;

/**
 * @author lsc
 * <p> </p>
 */
@Slf4j
public class WebSocketHandler extends SimpleChannelInboundHandler<Object>  {
​
    // 存放已经连接的通道
    private  static ConcurrentMap<String, Channel> ChannelMap=new ConcurrentHashMap();
​
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof FullHttpRequest){
​
            System.out.println("------------收到http消息--------------"+msg);
            handleHttpRequest(ctx,(FullHttpRequest)msg);
        }else if (msg instanceof WebSocketFrame){
            //处理websocket客户端的消息
            String message = ((TextWebSocketFrame) msg).text();
            System.out.println("------------收到消息--------------"+message);
//            ctx.channel().writeAndFlush(new TextWebSocketFrame(message));
            // 将消息回复给所有连接
            Collection<Channel> values = ChannelMap.values();
            for (Channel channel: values){
                channel.writeAndFlush(new TextWebSocketFrame(message));
            }
        }
​
    }
​
    /**
     * @author lsc
     * <p> 处理http请求升级</p>
     */
    private void handleHttpRequest(ChannelHandlerContext ctx,
                                   FullHttpRequest req) throws Exception {
​
        // 该请求是不是websocket upgrade请求
        if (isWebSocketUpgrade(req)) {
            String ws = "ws://127.0.0.1:8096";
            WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(ws, null, false);
            WebSocketServerHandshaker handshaker = factory.newHandshaker(req);
​
            if (handshaker == null) {// 请求头不合法, 导致handshaker没创建成功
                WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
            } else {
                // 响应该请求
                handshaker.handshake(ctx.channel(), req);
            }
            return;
        }
    }
​
    //n1.GET? 2.Upgrade头 包含websocket字符串?
    private boolean isWebSocketUpgrade(FullHttpRequest req) {
        HttpHeaders headers = req.headers();
        return req.method().equals(HttpMethod.GET)
                && headers.get(HttpHeaderNames.UPGRADE).equals("websocket");
    }
​
​
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //添加连接
        log.debug("客户端加入连接:"+ctx.channel());
        Channel channel = ctx.channel();
        ChannelMap.put(channel.id().asShortText(),channel);
    }
​
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //断开连接
        log.debug("客户端断开连接:"+ctx.channel());
        Channel channel = ctx.channel();
        ChannelMap.remove(channel.id().asShortText());
    }
}

最后将WebSocketServer 注入spring监听器,在服务启动的时候运行;


@Slf4j
@Component
public class ApplicationConfig implements ApplicationListener<ApplicationReadyEvent> {
​
​
​
    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        WebSocketServer webSocketServer = new WebSocketServer();
        webSocketServer.init();
    }
}
​

视图转发

编写 IndexController 对视图进行转发


/**
 * @author lsc
 * <p> </p>
 */
@Controller
public class IndexController {
​
    @GetMapping("index")
    public ModelAndView index(){
        ModelAndView modelAndView = new ModelAndView("socket");
        return modelAndView;
    }
}

html


<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户页面</title>
</head>
<body>
<div id="msg"></div>
<input type="text" id="text">
<input type="submit" value="send" onclick="send()">
</body>
<script>
    var msg = document.getElementById("msg");
    var wsServer = 'ws://127.0.0.1:8096';
    var websocket = new WebSocket(wsServer);
    //监听连接打开
    websocket.onopen = function (evt) {
        msg.innerHTML = "The connection is open";
    };
​
    //监听服务器数据推送
    websocket.onmessage = function (evt) {
        msg.innerHTML += "<br>" + evt.data;
    };
​
    //监听连接关闭
    websocket.onclose = function (evt) {
        alert("连接关闭");
    };
​
    function send() {
        var text = document.getElementById("text").value
        websocket.send(text);
    }
</script>
</html>

附上配置文件


server:
  port: 8056

spring:
  # 引擎模板配置
  thymeleaf:
    cache: false # 关闭缓存
    mode: html # 去除htm5严格校验
    prefix: classpath:/templates/ # 指定 thymeleaf 模板路径
    encoding: UTF-8 # 指定字符集编码
    suffix: .html

运行服务后

前端页面显示消息

服务端打印消息

源码获取:知识追寻者公众号回复:netty

配套教程