用JAVA中整合WebSocket

一. 什么是WebSocket?

WebSocket 是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。

二. 怎么建立WebSocket连接?

浏览器在 TCP 三次握手建立连接之后,都统一使用 HTTP 协议先进行一次通信。

  • 如果此时是普通的 HTTP 请求,那后续双方就还是老样子继续用普通 HTTP 协议进行交互。
  • 如果这时候是想建立 WebSocket 连接,就会在 HTTP 请求里带上一些特殊的header 头,如下:
Connection: Upgrade
Upgrade: WebSocket
Sec-WebSocket-Key: T2a6wZlAwhgQNqruZ2YUyg==\r\n

后续流程如下:
在这里插入图片描述

三. 使用Springboot整合WebSocket

  1. 添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  1. 实现WebSocket的文本消息处理器
@Component
@Slf4j
public class HttpAuthHandler extends TextWebSocketHandler {

    /**
     * socket 建立成功事件
     *
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        Object sessionId = session.getAttributes().get("session_id");
        if (sessionId != null) {
            // 用户连接成功,放入在线用户缓存
            WsSessionManager.add(sessionId.toString(), session);
        } else {
            throw new RuntimeException("用户登录已经失效!");
        }
    }
    /**
     * 接收消息事件
     *
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        // 获得客户端传来的消息
        String payload = (String) message.getPayload();
        Object sessionId = session.getAttributes().get("session_id");
        log.info("server 接收到 {} 发送的 {}", sessionId, payload);
        session.sendMessage(new TextMessage("server 发送给 " + sessionId + " 消息 " + payload + " " + LocalDateTime.now().toString()));
    }

    /**
     * socket 断开连接时
     *
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        Object sessionId = session.getAttributes().get("session_id");
        if (sessionId != null) {
            // 用户退出,移除缓存
            WsSessionManager.remove(sessionId.toString());
        }
    }
}
  1. 实现WebSocket握手拦截器
@Component
@Slf4j
public class MyInterceptor implements HandshakeInterceptor {

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        log.info("握手开始");
        String hostName = request.getRemoteAddress().getHostName();
        String sessionId = hostName + String.valueOf((int)(Math.random()*1000));
        if (Strings.isNotBlank(sessionId)) {
            // 放入属性域
            attributes.put("session_id", sessionId);
            log.info("用户 session_id {} 握手成功!", sessionId);
            return true;
        }
        log.info("用户登录已失效");
        return false;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
        log.info("握手结束");
    }
}
  1. 创建WebSocket配置类
@Configuration
@EnableWebSocket
public class WebsocketConfig implements WebSocketConfigurer {
    @Autowired
    private HttpAuthHandler httpAuthHandler;
    @Autowired
    private MyInterceptor myInterceptor;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry
                .addHandler(httpAuthHandler, "/websocket") // ws://域名/websocket
                .addInterceptors(myInterceptor)
                .setAllowedOrigins("*");
    }
}
  1. 使用Apifox测试连接
    建立websocket连接输入:ws://localhost:8080/websocket
    在这里插入图片描述
    在服务器控制台查看日志:
    在这里插入图片描述

四. 使用netty整合WebSocket

  1. 添加依赖
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.76.Final</version>
</dependency>
  1. 编写netty启动类
public class NettyWebSocketStarter {
    private static EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    private static EventLoopGroup workerGroup = new NioEventLoopGroup();

    public static void main(String[] args) {
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup);
            serverBootstrap.channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.DEBUG)).childHandler(new ChannelInitializer() {
                @Override
                protected void initChannel(Channel channel) throws Exception {
                    ChannelPipeline pipeline = channel.pipeline();
                    // 对 http 协议支持
                    pipeline.addLast(new HttpServerCodec());
                    // 聚合解码 httpRequest/httpContent/lastHttpContent到fullHttpRequest
                    // 保证接受的 http 请求完整
                    pipeline.addLast(new HttpObjectAggregator(64 * 1024));
                    // 心跳 long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit
                    // readerIdleTime 读超时时间,即测试端一定时间内未接收到测试端发来消息
                    // writerIdleTime 写超时时间,即测试端一定时间内想被发送消息
                    // allIdleTime 所有超时时间
                    pipeline.addLast(new IdleStateHandler(6L, 0, 0, TimeUnit.SECONDS));
                    pipeline.addLast(new HeartBeatHandler());
                    // 将http协议升级为websocket协议
                    pipeline.addLast(new WebSocketServerProtocolHandler("/webscoket", null, true, 64 * 1024, true, true, 10000L));
                    pipeline.addLast(new WebSocketHandler());
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
            System.out.println("netty 启动成功");
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.err.println("启动 netty 失败" + e.getMessage());
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
  1. 实现心跳Handler
public class HeartBeatHandler extends ChannelDuplexHandler {
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent e) {
            if (e.state() == IdleState.READER_IDLE) // 心跳超时
                ctx.close();
            else if (e.state() == IdleState.WRITER_IDLE)
                ctx.writeAndFlush("heart");
        }
    }
}
  1. 实现WebSocketHandler
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    /**
     * 通道就绪后触发,一般用于初始化
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("有新的连接加入...");
    }

    /**
     * 通道关闭后触发,一般用于释放资源
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("有连接断开...");
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame textWebSocketFrame) throws Exception {
        System.out.printf("来自用户的消息:%s\n", textWebSocketFrame.text());
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // 认证
        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete e) {
            System.out.println("握手成功...");
            String uri = e.requestUri();
            System.out.println("uri: " + uri);
        }
    }

}
  1. 使用Apifox测试连接
    建立websocket连接输入:ws://localhost:8080/websocket
    在这里插入图片描述
    在服务器控制台查看日志:
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值