java websocket 解读及实现

万俟旭
2023-12-01

1.webSocket简介

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

简单的说,就是一次握手,持续通信。

2.使用场景

采用java实现的websocket客户端与服务端除聊天室实现外,因其交互只需建立一次链接关系,极大的节省了内存与带宽,所以也常用于实时数据传输与获取。
某些业务需要在较短的时间间隔下,不断的去获取或传输数据,便可以考虑采用webSocket。
如:实时公交位置的获取,实时人员位置的获取,暴雨天气中水库的水位,某设备的实时温度等等。

3.工程简介

本项目共分两个模块

  1. websocket服务端,采用java语言实现,继承springboot框架,使用maven依赖
  2. websocket测试用客户端,采用java语言实现,使用maven依赖

不必过分纠结项目中依赖所使用的版本,根据各自项目所需,切换合适的版本即可

3.服务端webSocketServer

首先我们来贴上关键代码,然后再进行解读
以下代码共三部分:

  • 所需pom依赖
  • 开启webSocket所需要的配置支持
  • webSocket服务端

接下来我们依次来看:

所需pom依赖

 <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-websocket</artifactId>
 </dependency>
 <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.47</version>
 </dependency>

springboot集成了对webSocket的操作,此处我们使用的版本为2.3.3,同时涉及到数据通信,难免用到json解析,所以此处我们添加alibaba的fastjson依赖,用作解析json数据

开启webSocket所需要的配置支持

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;


/**
 * @author zhaiLiMing
 * @version 2020-9-16
 * webSocket配置开启websocket支持
 */
@Configuration
public class WebSocketConfig {

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

webSocket服务端

服务端基于5个注解实现,分别是:

  1. @ServerEndpoint("/url") 该注解用于注释服务端的类,被该注解注释的类,将会被标注为webSocket的服务类,参数value为访问的路径
  2. @OnOpen 被该注解注释的方法,将在客户端与服务端建立连接时执行
  3. @OnMessage 被该注解注释的方法,将在服务端收到消息时执行
  4. @OnClose 被该注解注释的方法,将在链接关闭时执行
  5. @OnError 被该注解注释的方法,将在链接发生错误时执行
package com.modules.web;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.modules.service.StudentServiceImpl;
import com.modules.utils.DataTranslate;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
 * @author zhaiLiMing
 * @version 2020-9-16
 * webSocket服务端
 * @ServerEndpoint 将本类注解为webSocket服务端,其value为客户端访问URI
 * @Compoent 使得客户端在spring容器启动时候就被加载
 */
@ServerEndpoint("/endpoint")
@Component
public class WebSocketServer {

    public WebSocketServer()
    {
        System.out.println("EchoSocket:start");
    }

    private Session session;

    /**
     * 实例化service层,此处不能使用autowired等注解自动注入,
     * 因spring的bean是默认单例模式
     */
    private static StudentServiceImpl studentService=new StudentServiceImpl();

    /**
     * 打开连接时执行
     */
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        System.out.println("连接已经打开");
    }

    /**
     * 收到消息时执行
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("从客户端收到的消息:" + message);
   	    sendMessage(JSON.toJSONString(JSONArray.toJSONString(“返回给客户端的消息”)));
    }

    /**
     * 关闭连接时执行
     */
    @OnClose
    public void onClose(Session session) {
        System.out.println("连接已经关闭");
    }

    /**
     * 连接发生错误时执行
     */
    @OnError
    public void onError(Throwable error, Session session) {
        System.out.println("连接发生错误");
    }

    /**
      websocket session发送文本消息有两个方法:getAsyncRemote()和getBasicRemote()
      getAsyncRemote()和getBasicRemote()是异步与同步的区别,
      大部分情况下,推荐使用getAsyncRemote()。
    */
    public void sendMessage(String message) throws IOException {
        this.session.getAsyncRemote().sendText(message);
    }
}

需要注意的问题及常见异常

如果需要在webSocket服务类中调用service层,使用注解(如@Autowired等)自动注入,会抛出空指针异常
此处原因是,websocket每接收到一个客户端的握手请求,就会开启一个新的线程来处理该客户端,然而,spring的bean默认是singleton单例模式,所以就会导致此类问题。
针对其的解决方法,可以采用传统的new方式去创建javaBean,或者修改spring的bean为prototype。

4.客户端webSocketClient

所需pom依赖

  <dependency>
      <groupId>org.java-websocket</groupId>
      <artifactId>Java-Websocket</artifactId>
      <version>1.3.8</version>
  </dependency>
  
  <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.47</version>
  </dependency>

针对所使用的依赖不再过多赘述

webSocket客户端

webSocket客户端的实现基于webSocketClient类实现,实例化webSocketClient并重写以下四个方法:

  1. onOpen 与服务端建立连接时执行
  2. onMessage 收到服务端消息时执行
  3. onClose 连接关闭时执行
  4. onError 发生错误时执行
import com.alibaba.fastjson.JSON;
import modules.entry.student.Student;
import modules.service.StudentService;
import modules.service.StudentServiceImpl;
import modules.utils.JsonFormat;
import org.java_websocket.WebSocket;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author zhaiLiMing
 * @version 2020-9-21
 * webSocketClient客户端
 */
public class WebsocketClient {

    //创建webSocketClient客户端
    private static WebSocketClient client;

    //实例化service层
    private static StudentService studentService=new StudentServiceImpl();

    //创建一个5个线程的线程池,用来接收onMessage
    private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);

    public static void main(String[] args) throws URISyntaxException, InterruptedException {



        //实例化webSocketClient,以ws或wss形式发送请求,重写4个方法
        client=new WebSocketClient(new URI("ws://localhost:8080/endpoint")) {

            //创建连接时执行
            @Override
            public void onOpen(ServerHandshake serverHandshake) {
                System.out.println("建立连接");
            }

            //接收到消息时执行
            @Override
            public void onMessage(String s) {

                Runnable runnable = new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(JSON.parseObject(s).getString("code"));
                        //业务处理,接收返回的消息,解析JSON字符串,存入数据库
                        if (JSON.parseObject(s).getString("code").equals("1100")){
                            String data=JSON.parseObject(s).getString("data");
                             //JsonFormat是自己写的工具类
                            List<Student> studentList= JsonFormat.jsonFormatStudent(data);
                            //调用service接收结果
                            int result=studentService.insert(studentList);
                            System.out.println("插入成功:"+result+"条数据!");
                        }else{
                            System.err.println("服务器出错!");
                        }
                    }
                };

                fixedThreadPool.submit(runnable);
            }

            //连接关闭时执行
            @Override
            public void onClose(int i, String s, boolean b) {
                System.out.println("链接关闭");
            }

            //连接出错时执行
            @Override
            public void onError(Exception e) {
                System.out.println("链接出错");
            }
        };
        client.connect();

        //检测连接状态,重复尝试连接
        while (!client.getReadyState().equals(WebSocket.READYSTATE.OPEN)) {
            System.out.println("before reconnect statte:"+client.getReadyState());
            Thread.sleep(2000);
            if(client.getReadyState().equals(WebSocket.READYSTATE.CLOSING) || client.getReadyState().equals(WebSocket.READYSTATE.CLOSED)){
                client.reconnect();
            }
            System.out.println("After reconnect statte:"+client.getReadyState());
        }

        //发送数据
        client.send("getStudent");

    }
}

需要注意的问题及常见异常

实例化webSocketClient时,有一个参数URI,URI中传入地址,有两种请求方式

  1. ws请求:其类似于http请求,非安全
  2. wss请求:其类似于https请求,安全

请求方式不同时,可能会抛出异常,两种请求方式具体区别在此不做解释,可查阅别的文章

5.继续了解webSocket

在java中webSocket的5种状态

参阅过webSocket API文档的部分朋友或许会疑惑,为什么是5种呢?API文档写的4种呀!
实际上我们仔细看就会发现,java中(基于其他语言的websocket没有研究,所以只说java),webSocket的源码里定义了内部枚举类READYSTATE,其中包含以下5种状态

  1. NOT_YET_CONNECTED 尚未链接
  2. CONNECTING 链接中
  3. OPEN 链接已打开
  4. CLOSING 链接正在关闭
  5. CLOSED 链接已经关闭
public static enum READYSTATE {
        NOT_YET_CONNECTED,
        CONNECTING,
        OPEN,
        CLOSING,
        CLOSED;

        private READYSTATE() {
        }
    }

不难看出,5种状态表明着webSocket的整个生命周期,这对于我们在使用webSocket时解决一些问题是非常关键的

connect()与reconnect()

当webSocketClient初始化完毕之后,webSocketClient提供了两种链接方式,封装为两个方法,分别是

  1. connect()
  2. reconnect()

那么这两者有什么区别呢?这便设计到了上一个问题,webSocket的5种状态。
起始时,webSocket状态为 NOT_YET_CONNECTED ,尚未链接,而当一次链接关闭之后,其状态为 CLOSED 。这两者虽然都是没有链接的状态,但本质上是有区别的。
NOT_YET_CONNECTED 表示该webSocket实例还未开始链接,并处于等待链接的状态,形象的讲,就是初生的婴儿;
而 CLOSED 则表示链接关闭,虽然也不是链接状态,但其表示已经完成了一次生命周期,该webSocket实例到了消亡的时候,形象的讲,就是垂暮的老人。
而webSocket想要链接,则只能在 NOT_YET_CONNECTED 状态下进行,一旦状态改变,则无法再次链接。这便是connect()链接。针对其解决方法,就是reconnect()链接。
reconnect()链接的实现,便是在connect之前调用了reset()方法,重置了当前webSocket,使得状态又改变成了 NOT_YET_CONNECTED ,从而可以再次执行connect()方法,我们看一下源码:

  public void reconnect() {
        this.reset();
        this.connect();
    }

心跳机制及断线重连方法

了解了webSocket的5种状态以及connect()与reconnect()的区别后,就不难理解断线重连和心跳机制。
所谓心跳机制,即为每隔一定时间,由客户端发送特定的心跳包给服务器,服务器也回应消息,双方互相确认对方还"活着"。
例如我们每隔10秒则调用 webSocket.send("心跳包")
同时在onMessage中接收到返回的内容,如果能接收到预期返回的内容,则证明双方都存在,反之则证明有一方挂掉。
至于重连机制,则可以利用reconnect()方法,在检测到断线后,重新尝试链接服务端

//开启一个新线程
new Thread(){
    @Override
    public void run(){
         try{
         //间隔10秒发送心跳
             Thread.sleep(10000);
             webSocketClient.send("心跳包");
         }catch (Exception e){
         //捕获异常进行重连
             webSocketClient.reconnect();
         }
     }
   }.start();
 类似资料: