乐趣区

关于java:一文搞懂四种-WebSocket-使用方式建议收藏

在上家公司做 IM 音讯零碎的时候,始终是应用 WebSocket 作为收发音讯的根底组件,明天就和大家聊聊在 Java 中,应用 WebSocket 所常见的四种姿态,如果大家当前或者当初碰到有要应用 WebSoocket 的状况能够做个参考。

下面的思维导图曾经给大家列出了三种应用 WebSocket 的形式,下文会对它们的特点进行一一解读,不同的形式具备不同的特点,咱们先按下不表。

在这里,我想让大家思考一下我在思维导图中列举的第四种做 WebScoket 反对的计划可能是什么?不晓得大家能不能猜对,后文将会给出答案。


本文代码:以下仓库中 spring-websocket 模块,拉整个仓库下来后可在 IDEA Maven 工具栏中独自编译此模块。

  • Github
  • Gitee

    WS 简介

    在正式开始之前,我感觉有必要简略介绍一下 WebSocket 协定,引入任何一个货色之前都有必要晓得咱们为什么须要它?

    在 Web 开发畛域,咱们最罕用的协定是 HTTP,HTTP 协定和 WS 协定都是基于 TCP 所做的封装,然而 HTTP 协定从一开始便被设计成申请 -> 响应的模式,所以在很长一段时间内 HTTP 都是只能从客户端发向服务端,并不具备从服务端被动推送音讯的性能,这也导致在浏览器端想要做到服务器被动推送的成果只能用一些轮询和长轮询的计划来做,但因为它们并不是真正的全双工,所以在耗费资源多的同时,实时性也没现实中那么好。

    既然市场有需要,那必定也会有对应的新技术呈现,WebSocket 就是这样的背景下被开发与制订进去的,并且它作为 HTML5 标准的一部分,失去了所有支流浏览器的反对,同时它还兼容了 HTTP 协定,默认应用 HTTP 的 80 端口和 443 端口,同时应用 HTTP header 进行协定降级。

    和 HTTP 相比,WS 至多有以下几个长处:

  1. 应用的资源更少:因为它的头更小。
  2. 实时性更强:服务端能够通过连贯被动向客户端推送音讯。
  3. 有状态:开启链接之后能够不必每次都携带状态信息。

除了这几个长处以外,我感觉对于 WS 咱们开发人员起码还要理解它的握手过程和协定帧的意义,这就像学习 TCP 的时候须要理解 TCP 头每个字节帧对应的意义一样。

像握手过程我就不说了,因为它复用了 HTTP 头只须要在维基百科(阮一峰的文章讲的也很明确)下面看一下就明确了,像协定帧的话无非就是:标识符、操作符、数据、数据长度这些协定通用帧,根本都没有深刻理解的必要,我认为个别只须要关怀 WS 的操作符就能够了。

WS 的操作符代表了 WS 的音讯类型,它的音讯类型次要有如下六种:

  1. 文本音讯
  2. 二进制音讯
  3. 分片音讯(分片音讯代表此音讯是一个某个音讯中的一部分,想想大文件分片)
  4. 连贯敞开音讯
  5. PING 音讯
  6. PONG 音讯(PING 的回复就是 PONG)

那咱们既然晓得了 WS 次要有以上六种操作,那么一个失常的 WS 框架该当能够很轻松的解决以上这几种音讯,所以接下来就是本文的核心内容,看看以下这几种 WS 框架能不能很不便的解决这几种 WS 音讯。

J2EE 形式

先来 J2EE,个别我把 javax 包外面对 JavaWeb 的扩大都叫做 J2EE,这个定义是否完全正确我感觉没必要深究,只是一种集体习惯,而本章节所介绍的 J2EE 形式则是指 Tomcat 为 WS 所做的反对,这套代码的包名前缀叫做:javax.websocket

这套代码中定义了一套实用于 WS 开发的注解和相干反对,咱们能够利用它和 Tomcat 进行 WS 开发,因为当初更多的都是应用 SpringBoot 的内嵌容器了,所以这次咱们就来依照 SpringBoot 内嵌容器的形式来演示。

首先是引入 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 音讯,对于这三个性能我并没有查到相干用法,只在源码的接口列表中看到了一个 PongMessage 接口,有晓得的读者敌人们有晓得的能够在评论区指出。
    仔细的小伙伴们可能发现了,示例中的 WebSocketServer 类还有一个 @Component 注解,这是因为咱们应用的是内嵌容器,而内嵌容器须要被 Spring 治理并初始化,所以须要给 WebSocketServer 类加上这么一个注解,所以代码中还须要有这么一个配置:

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

    Tips:在不应用内嵌容器的时候能够不做以上步骤。
    最初上个简陋的 WS 成果示例图,前端方面间接应用 HTML5 的 WebScoket 规范库,具体能够查看我的仓库代码:

    Spring 形式

    第二局部来说 Spring 形式,Spring 作为 Java 开发界的老大哥,简直封装了所有能够封装的,对于 WS 开发呢 Spring 也提供了一套相干反对,而且从应用方面我感觉要比 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 接口中的五个函数,通过名字咱们就应该晓得它具备什么性能了:

  5. afterConnectionEstablished:连贯胜利后调用。
  6. handleMessage:解决发送来的音讯。
  7. handleTransportError:WS 连贯出错时调用。
  8. afterConnectionClosed:连贯敞开后调用。
  9. 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,它曾经封装了这些重复劳动,咱们能够间接继承这个类而后重写咱们想要解决的音讯类型:

    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 的形式也就以上这些内容了,不晓得大家是否感觉 Spring 所提供的 WS 封装要比 J2EE 的更不便也更全面一些,起码我只有看 WebSocketHandler 接口就能晓得所有罕用性能的用法,所以对于 WS 开发来说我是比拟举荐 Spring 形式的。

最初上个简陋的 WS 成果示例图,前端方面间接应用 HTML5 的 WebScoket 规范库,具体能够查看我的仓库代码:

SocketIO 形式

SocketIO 形式和下面两种有点不太一样,因为 SocketIO 诞生初就是为了兼容性作为考量的,前端的读者们应该对它更相熟,因为它是一个 JS 库,咱们先来看一下维基百科对它的定义:

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());
    }
}

我置信对于以上代码,前两个办法是很好懂的,然而对于第三个办法如果大家没有接触过 SocketIO 就比拟难了解了,为什么 @OnEvent(value = "onMsg") 外面这个值是自定义的,这就波及到 SocketIO 外面发消息的机制了,通过 SocketIO 发消息是要发给某个事件的,所以这里的第三个办法就是监听 发给 onMsg 事件的所有音讯,监听到之后我又给客户端发了一条音讯,这次发给的事件是:chatMsg,客户端也须要监听此事件能力接管到这条音讯。

最初再上一个简陋的效果图:

因为前端代码不再是规范的 HTML5 的连贯形式,所以我这里简要贴一下相干代码,具体更多内容能够看我的代码仓库:

    function changeSocketStatus() {let element = document.getElementById("socketStatus");
        if (socketStatus) {
            element.textContent = "敞开 WebSocket";
            const socketUrl="ws://127.0.0.1:8001";
            socket = io.connect(socketUrl, {transports: ['websocket'],
                path: "/socketio-ws"
            });
            // 关上事件
            socket.on('connect', () => {console.log("websocket 已关上");
            });
            // 取得音讯事件
            socket.on('chatMsg', (msg) => {
                const serverMsg = "收到服务端信息:" + msg;
                pushContent(serverMsg, 2);
            });
            // 敞开事件
            socket.on('disconnect', () => {console.log("websocket 已敞开");
            });
            // 产生了谬误事件
            socket.on('connect_error', () => {console.log("websocket 产生了谬误");
            })
        }
    }

第四种形式?

第四种形式其实就是 Netty 了,Netty 作为 Java 界赫赫有名的开发组件,对于常见协定也全副进行了封装,所以咱们能够间接在 Netty 中去很不便的应用 WebSocket,接下来咱们能够看看 Netty 怎么作为 WS 的服务器进行开发。

留神:以下内容如果没有 Netty 根底可能一脸蒙的进,一脸蒙的出,不过还是倡议大家看看,Netty 其实很简略。

第一步须要先引入一个 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 办法中解决,大家有趣味的能够理解一下 Netty。

最初上个简陋的 WS 成果示例图,前端方面间接应用 HTML5 的 WebScoket 规范库,具体能够查看我的仓库代码:

总结

洋洋洒洒五千字,有了播种别忘赞。

在上文中,我总共介绍了四种在 Java 中应用 WS 的形式,从我集体应用动向来说我感觉应该是这样的:Spring 形式 > Netty 形式 > J2EE 形式 > SocketIO 形式,当然了,如果你的业务存在浏览器兼容性问题,其实只有一种抉择:SocketIO。

最初,我预计某些读者会去具体拉代码看代码,所以我简略说一下代码构造:

├─java
│  └─com
│      └─example
│          └─springwebsocket
│              │  SpringWebsocketApplication.java
│              │  TestController.java
│              │
│              ├─j2ee
│              │      WebSocketConfig.java
│              │      WebSocketServer.java
│              │
│              ├─socketio
│              │      SocketIoConfig.java
│              │      SocketIoHandle.java
│              │
│              └─spring
│                      SpringSocketConfig.java
│                      SpringSocketHandle.java
│
└─resources
    └─templates
            J2eeIndex.html
            SocketIoIndex.html
            SpringIndex.html

代码构造如上所示,利用代码分成了三个文件夹,别离放着三种形式的具体示例代码,在资源文件夹下的 templates 文件夹也有三个 HTML 文件,就是对应三种示例的 HTML 页面,外面的链接地址和端口我都预设好了,拉下来间接独自编译此模块运行即可。

我没有往里面放 Netty 的代码,是因为感觉 Netty 局部内容很少,文章示例中的代码间接复制就能用,前面如果写 Netty 的话会再开一个 Netty 模块用来放 Netty 相干的代码。

好了,明天的内容就到这了,心愿对大家有帮忙的话能够帮我文章 点点赞 ,GitHub 也 点点赞,大家的点赞与评论都是我更新的不懈能源,下期见。

退出移动版