WebSocket 使用

279 阅读9分钟

应用场景

WebSocket常用在那些场景 常见的应用场景如下:

在线聊天:实现实时的文本聊天功能。
直播和视频会议:实现实时的音频和视频传输。
游戏:实现实时的游戏状态同步和控制。
实时监控和控制:实现实时的硬件监控和控制。
数据可视化:实现实时的数据可视化和分析。
在线协作:实现实时的文档协作和编辑。
智能家居:实现实时的智能家居控制。
推送消息:实现实时的推送消息功能 

J2EE(J2EE 是使用 Java 技术开发企业级应用的工业标准\javaee是一系列的规范及实现\ 核心是EJB\servlet\jdbc\jsp)

WS 的操作符代表了 WS 的消息类型,它的消息类型主要有如下六种:

  1. 文本消息
  2. 二进制消息
  3. 分片消息(分片消息代表此消息是一个某个消息中的一部分,想想大文件分片)
  4. 连接关闭消息
  5. PING 消息
  6. PONG 消息(PING的回复就是PONG)

一.J2EE 方式则是指 Tomcat 为 WS 所做的支持,这套代码的包名前缀叫做:javax.websocket

引入 SpringBoot - Web 的依赖,因为这个依赖中引入了内嵌式容器 Tomcat:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

二. 将一个类定义为 WS 服务器, 需要为这个类加上@ServerEndpoint注解 ,在这个注解中比较常用的有三个参数:WS路径、序列化处理类、反序列化处理类。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ServerEndpoint {
    String value();

    String[] subprotocols() default {};

    Class<? extends Decoder>[] decoders() default {};

    Class<? extends Encoder>[] encoders() default {};

    Class<? extends Configurator> configurator() default Configurator.class;
}
 

具体的一个 WS 服务器类示例:

@Component
@ServerEndpoint("/j2ee-ws/{msg}")
public class WebSocketServer {

    //建立连接成功调用
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "msg") String msg){
        System.out.println("WebSocketServer 收到连接: " + session.getId() + ", 当前消息:" + msg);
    }

    //收到客户端信息
    @OnMessage
    public void onMessage(Session session, String message) throws IOException {
        message = "WebSocketServer 收到连接:" + session.getId() +  ",已收到消息:" + message;
        System.out.println(message);
        session.getBasicRemote().sendText(message);
    }

    //连接关闭
    @OnClose
    public void onclose(Session session){
        System.out.println("连接关闭");
    }

}
 

在以上代码中, 着重关心 WS 相关的注解,主要有以下四个:

  1. @ServerEndpoint : 这里就像 RequestMapping 一样,放入一个 WS 服务器监听的 URL。
  2. @OnOpen :这个注解修饰的方法会在 WS 连接开始时执行。
  3. @OnClose :这个注解修饰的方法则会在 WS 关闭时执行。
  4. @OnMessage :这个注解则是修饰消息接受的方法,并且由于消息有文本和二进制两种方式,所以此方法参数上可以使用 String 或者二进制数组的方式,就像下面这样:
    @OnMessage
    public void onMessage(Session session, String message) throws IOException {

    }

    @OnMessage
    public void onMessage(Session session, byte[] message) throws IOException {

    }
 

除了以上这几个以外,常用的功能方面还差一个分片消息、Ping 消息 和 Pong 消息, 示例中的 WebSocketServer 类还有一个 @Component 注解,这是由于我们使用的是内嵌容器,而内嵌容器需要被 Spring 管理并初始化,所以需要给 WebSocketServer 类加上这么一个注解,所以代码中还需要有这么一个配置:

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
 

Tips:在不使用内嵌容器的时候可以不做以上步骤。

image.png

Spring 方式

Spring 作为 Java 开发界的老大哥,几乎封装了一切可以封装的, 比 J2EE 的更易用。

一、 先引入 SpringBoot - WS 依赖,这个依赖包也会隐式依赖 SpringBoot - Web 包:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
 

二、准备一个用来处理 WS 请求的 Handle了,Spring 为此提供了一个接口 WebSocketHandler, 通过实现此接口重写其接口方法的方式自定义逻辑 例子:

@Component
public class SpringSocketHandle implements WebSocketHandler {

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("SpringSocketHandle, 收到新的连接: " + session.getId());
    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        String msg = "SpringSocketHandle, 连接:" + session.getId() +  ",已收到消息。";
        System.out.println(msg);
        session.sendMessage(new TextMessage(msg));
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        System.out.println("WS 连接发生错误");
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        System.out.println("WS 关闭连接");
    }

    // 支持分片消息
    @Override
    public boolean supportsPartialMessages() {
        return false;
    }
}
  

WebSocketHandler 接口中的五个函数, 它具有什么功能:

  1. afterConnectionEstablished:连接成功后调用。
  2. handleMessage:处理发送来的消息。
  3. handleTransportError: WS 连接出错时调用。
  4. afterConnectionClosed:连接关闭后调用。
  5. supportsPartialMessages:是否支持分片消息。

重点可以来看一下 handleMessage 方法,handleMessage 方法中有一个 WebSocketMessage 参数,这也是一个接口,一般不直接使用这个接口而是使用它的实现类,它有以下几个实现类:

  1. BinaryMessage:二进制消息体
  2. TextMessage:文本消息体
  3. PingMessage: Ping ****消息体
  4. PongMessage: Pong ****消息体

但是由于 handleMessage 这个方法参数是WebSocketMessage, 实际使用中需要判断一下当前来的消息具体是它的哪个子类,比如:

    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
       ** if (message instanceof TextMessage) {**
            this.handleTextMessage(session, (TextMessage)message);
      **  } else if (message instanceof BinaryMessage) {**
            this.handleBinaryMessage(session, (BinaryMessage)message);
        }
    }
 

为了避免这些重复性代码,Spring 给我们定义了一个 AbstractWebSocketHandler,它已经封装了这些重复劳动直接继承AbstractWebSocketHandler这个类 重写要处理的消息类型

    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    }

    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
    }

    protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
    }
复制代码

上面都是对于 Handle 的操作,有了 Handle 之后还需要将它绑定在某个 URL 上【监听某个 URL】,需要以下配置:

@Configuration
@EnableWebSocket
public class SpringSocketConfig implements WebSocketConfigurer {

    @Autowired
    private SpringSocketHandle springSocketHandle;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(springSocketHandle, "/spring-ws").setAllowedOrigins("*");
    }
}
 

这里我把我的自定义 Handle 注册到 "/spring-ws" 上面并设置了一下跨域,在整个配置类上还要打上@EnableWebSocket 注解,用于开启 WS 监听。

Spring 所提供的 WS 封装要比 J2EE 的更方便也更全面一些, 只要看 WebSocketHandler 接口就能知道所有常用功能的用法,所以对于 WS 开发来说 比较推荐 Spring 方式的。

SocketIO 方式

Socket.IO 是一个面向实时 web 应用的 JavaScript 库。它使得服务器和客户端之间实时双向的通信成为可能。他有两个部分:在浏览器中运行的客户端库,和一个面向Node.js的服务端库,两者有着几乎一样的API。

Socket.IO 主要使用WebSocket协议。但是如果需要的话,Socket.io可以回退到几种其它方法,例如Adobe Flash Sockets,JSONP拉取,或是传统的AJAX拉取,并且在同时提供完全相同的接口。

所以我觉得使用它更多是因为兼容性,因为 HTML5 之后原生的 WS 应该也够用了,然而它是一个前端库,所以 Java 语言这块并没有官方支持,好在民间大神已经以 Netty 为基础开发了能与它对接的 Java 库: netty-socketio

不过我要先给大家提个醒,不再建议使用它了,不是因为它很久没更新了,而是因为它支持的 Socket-Client 版本太老了,截止到 2022-04-29 日,SocketIO 已经更新到 4.X 了,但是 NettySocketIO 还只支持 2.X 的 Socket-Client 版本。

第一步还是引入最新的依赖:

        <dependency>
            <groupId>com.corundumstudio.socketio</groupId>
            <artifactId>netty-socketio</artifactId>
            <version>1.7.19</version>
        </dependency>
复制代码

第二步就是配置一个 WS 服务:

@Configuration
public class SocketIoConfig {

    @Bean
    public SocketIOServer socketIOServer() {
        com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();

        config.setHostname("127.0.0.1");
        config.setPort(8001);
        config.setContext("/socketio-ws");
        SocketIOServer server = new SocketIOServer(config);
        server.start();
        return server;
    }

    @Bean
    public SpringAnnotationScanner springAnnotationScanner() {
        return new SpringAnnotationScanner(socketIOServer());
    }
}
 

大家在上文的配置中,可以看到设置了一些 Web 服务器参数,比如:端口号和监听的 path,并将这个服务启动起来,服务启动之后日志上会打印这样一句日志:

[ntLoopGroup-2-1] c.c.socketio.SocketIOServer : SocketIO server started at port: 8001
 

这就代表启动成功了,接下来就是要对 WS 消息做一些处理了:

@Component
public class SocketIoHandle {

    /**
     * 客户端连上socket服务器时执行此事件
     * @param client
     */
    @OnConnect
    public void onConnect(SocketIOClient client) {
        System.out.println("SocketIoHandle 收到连接:" + client.getSessionId());
    }

    /**
     * 客户端断开socket服务器时执行此事件
# * @param client
     */
    @OnDisconnect
    public void onDisconnect(SocketIOClient client) {
        System.out.println("当前链接关闭:" + client.getSessionId());
    }

    @OnEvent( value = "onMsg")
    public void onMessage(SocketIOClient client, AckRequest request, Object data) {
        System.out.println("SocketIoHandle 收到消息:" + data);
        request.isAckRequested();
        client.sendEvent("chatMsg", "我是 NettySocketIO 后端服务,已收到连接:" + client.getSessionId());
    }
}

第三个方法:为什么@OnEvent( value = "onMsg")里面这个值是自定义的,这就涉及到 SocketIO 里面发消息的机制了,通过 SocketIO 发消息是要发给某个事件的,所以这里的第三个方法就是监听 发给onMsg事件的所有消息,监听到之后我又给客户端发了一条消息,这次发给的事件是:chatMsg,客户端也需要监听此事件才能接收到这条消息。

Netty

Netty 作为 Java 界大名鼎鼎的开发组件,对于常见协议也全部进行了封装,所以我们可以直接在 Netty 中去很方便的使用 WebSocket

一个 Netty 开发包,我这里为了方便一般都是 All In:

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

第二步:启动一个 Netty 容器了,配置比较关键:

public class WebSocketNettServer {
    public static void main(String[] args) {

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup work = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap
                    .group(boss, work)
                    .channel(NioServerSocketChannel.class)
                    //设置保持活动连接状态
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .localAddress(8080)
                    .handler(new LoggingHandler(LogLevel.DEBUG))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline()
                                    // HTTP 请求解码和响应编码
                                    .addLast(new HttpServerCodec())
                                    // HTTP 压缩支持
                                    .addLast(new HttpContentCompressor())
                                    // HTTP 对象聚合完整对象
                                    .addLast(new HttpObjectAggregator(65536))
                                    // WebSocket支持
                                    .addLast(new WebSocketServerProtocolHandler("/ws"))
                                    .addLast(WsTextInBoundHandle.INSTANCE);
                        }
                    });

            //绑定端口号,启动服务端
            ChannelFuture channelFuture = bootstrap.bind().sync();
            System.out.println("WebSocketNettServer启动成功");

            //对关闭通道进行监听
            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            boss.shutdownGracefully().syncUninterruptibly();
            work.shutdownGracefully().syncUninterruptibly();
        }

    }
}
 

以上代码 主要关心端口号和重写的 ChannelInitializer 就行了,里面定义了五个过滤器(Netty 使用责任链模式),前面三个都是 HTTP 请求的常用过滤器(毕竟 WS 握手是使用 HTTP 头的所以也要配置 HTTP 支持),第四个则是 WS 的支持,它会拦截 /ws 路径, 最关键的就是第五个了过滤器它是我们具体的业务逻辑处理类,效果基本和 Spring 中的 Handle 差不多, 代码:

@ChannelHandler.Sharable
public class WsTextInBoundHandle extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    private WsTextInBoundHandle() {
        super();
        System.out.println("初始化 WsTextInBoundHandle");
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("WsTextInBoundHandle 收到了连接");
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {

        String str = "WsTextInBoundHandle 收到了一条消息, 内容为:" + msg.text();

        System.out.println(str);

        System.out.println("-----------WsTextInBoundHandle 处理业务逻辑-----------");

        String responseStr = "{"status":200, "content":"收到"}";

        ctx.channel().writeAndFlush(new TextWebSocketFrame(responseStr));
        System.out.println("-----------WsTextInBoundHandle 数据回复完毕-----------");
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {

        System.out.println("WsTextInBoundHandle 消息收到完毕");
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("WsTextInBoundHandle 连接逻辑中发生了异常");
        cause.printStackTrace();
        ctx.close();
    }
}
 

主要是看一下这个类的泛型:TextWebSocketFrame,很明显这是一个 WS 文本消息的类,我们顺着它的定义去看发现它继承了 WebSocketFrame,接着我们去看它的子类:

一图胜千言,示例中我们是一定了一个文本 WS 消息的处理类,如果你想处理其他数据类型的消息,可以将泛型中的 TextWebSocketFrame 换成其他 WebSocketFrame 类就可以了

至于为什么没有连接成功后的处理,这个是和 Netty 的相关机制有关,可以在 channelActive 方法中处理

总结:Spring 方式 > Netty 方式 > J2EE 方式 > SocketIO 方式,当然了,如果你的业务存在浏览器兼容性问题,其实只有一种选择:SocketIO。

参考自:一文搞懂四种 WebSocket 使用方式,建议收藏! - 掘金 (juejin.cn)