近期公司项目中要对接第三方的WebSocket服务获取数据,本来以为是很简单的工作,但问题是服务方提供的是"wss"协议,需要证书认证,为此查阅了很多博客,都没有解决,

最后还是自己详细看了代码,根据"https"协议的证书认证方式修改了一下,嘿,还真成了!

下面就完整的分享下java端 WebSocketClient 的创建、连接、心跳检测、重连机制以及"wss"协议的证书认证完整示例代码:

一、Spring Boot导入依赖

<!-- websocket start,版本可根据实际修改 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>2.3.4.RELEASE</version>
</dependency>
<!-- Java-WebSocket -->
<dependency>
    <groupId>org.java-websocket</groupId>
    <artifactId>Java-WebSocket</artifactId>
    <version>1.4.0</version>
</dependency>
<!-- websocket end -->

二、导入wss协议证书

参考 “spring boot 使用RestTemplate通过证书认证访问https实现SSL请求” 中的前两步操作;

三、创建WebSocket客户端静态类

项目中可能对接多个WebSocket连接,所以这里我们先定义一个静态类,以便子类实现:

import net.sf.json.JSONObject;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.ssl.SSLContexts;
import org.java_websocket.client.WebSocketClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.util.Map;

/**
 * WebSocket客户端静态类
 */
public abstract class WebSocketClientAbs {

    /**
     * resources 目录下证书路径
     * cerPath = "classpath:/keystore/client_trust.keystore";
     */
    @Value("${cer-path}")
    private String cerPath;

    /**
     * 使用"keytool"命令导入证书时输入的密码
     * cerPwd = "111111";
     */
    @Value("${cer-pwd}")
    private String cerPwd;

    /**
     * 创建WebSocket客户端
     *
     * @param wsUri
     * @param httpHeaders
     * @return
     */
    public abstract WebSocketClient createWebSocketClient(String wsUri, Map<String, String> httpHeaders);

    /**
     * 客户端连接
     *
     * @param uri
     * @param httpHeaders
     * @return
     */
    public abstract WebSocketClient connect(String uri, Map<String, String> httpHeaders);

    /**
     * wss协议证书认证
     *
     * @param webSocketClient
     */
    public void createWebSocketClient(WebSocketClient webSocketClient) {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(resourceLoader(cerPath), cerPwd.toCharArray());
            SSLContext sslContext = SSLContexts.custom()
                    .loadTrustMaterial(keyStore, new TrustSelfSignedStrategy()).build();
            SSLSocketFactory sslfactory = sslContext.getSocketFactory();
            webSocketClient.setSocketFactory(sslfactory);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 添加故障消息类型
     *
     * @param message
     * @param type
     * @return
     */
    public String addTypeOfMsg(String message, String type) {
        try {
            JSONObject json = JSONObject.fromObject(message);
            json.put("msgType", type);
            return json.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 读取文件信息
     *
     * @param fileFullPath
     * @return
     * @throws IOException
     */
    public InputStream resourceLoader(String fileFullPath) throws IOException {
        ResourceLoader resourceLoader = new DefaultResourceLoader();
        return resourceLoader.getResource(fileFullPath).getInputStream();
    }

}

四、创建WebSocketClient实例并连接

import com.example.elasticsearchdemo.enums.BizExceptionCodeEnum;
import com.example.elasticsearchdemo.exception.BizException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;

/**
 * WsSocketClient实例连接,单例模式
 */
@Slf4j
@Component
@Data
public class WsClientOfLocal extends WebSocketClientAbs {

    // 消息处理服务
    @Autowired
    private MessageService messageService;

    private WebSocketClient wsClient;

    // 消息类型
    private String type;

    // 0:链接断开或者异常;1:代表链接中;2:代表正在连接;
    public static int isConnect = 0;

    /**
     * 获取客户端连接实例
     *
     * @param wsUri
     * @param httpHeaders
     * @return
     */
    @Override
    public WebSocketClient createWebSocketClient(String wsUri, Map<String, String> httpHeaders) {

        try {
            //创建客户端连接对象
            WebSocketClient client = new WebSocketClient(new URI(wsUri), httpHeaders) {

                /**
                 * 建立连接调用
                 * @param serverHandshake
                 */
                @Override
                public void onOpen(ServerHandshake serverHandshake) {
                    log.info("WsClientOfLocal -> onOpen -> 客户端建立连接");
                    isConnect = 1;
                }

                /**
                 * 收到服务端消息调用
                 * @param s
                 */
                @Override
                public void onMessage(String s) {
                    log.info("WsClientOfLocal -> onMessage -> 收到服务端消息:{}", s);
                    // 如果对接多个WebSocket服务,且服务消息处理方案一样,
                    // 则可以在接收到消息的第一时间为该消息添加区分类型,
                    // 比如IP地址:200,201,202
                    s = addTypeOfMsg(s, type);
                    if (StringUtils.isNotBlank(s)) {
                        // 统一处理消息
                        messageService.handleMessage(s);
                    }
                }

                /**
                 * 断开连接调用
                 * @param i
                 * @param s
                 * @param b
                 */
                @Override
                public void onClose(int i, String s, boolean b) {
                    log.info("WsClientOfLocal -> onClose -> 客户端关闭连接,i:{},b:{},s:{}", i, b, s);
                    isConnect = 0;
                }

                /**
                 * 连接报错调用
                 * @param e
                 */
                @Override
                public void onError(Exception e) {
                    log.error("WsClientOfLocal -> onError -> 客户端连接异常,异常信息:{}", e.getMessage());
                    if (null != wsClient) {
                        wsClient.close();
                    }
                    isConnect = 0;
                }
            };
            return client;
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 连接websocket服务端
     * 注意 synchronized 关键字,保证多个请求同时连接时,
     * 只有一个连接在创建
     *
     * @param uri
     * @param httpHeaders
     * @return
     */
    @Override
    public synchronized WebSocketClient connect(String uri, Map<String, String> httpHeaders) {
        WebSocketClient oldWsClient = this.getWsClient();
        if (null != oldWsClient) {
            log.info("WsClientOfLocal -> 已存在连接,oldWsClient:{}-{}",
                    oldWsClient.getReadyState(), oldWsClient.getReadyState().ordinal());
            if (1 == oldWsClient.getReadyState().ordinal()) {
                log.info("WsClientOfLocal -> 使用存在且已打开的连接");
                return oldWsClient;
            } else {
                log.info("WsClientOfLocal -> 注销存在且未打开的连接,并重新获取新的连接");
                oldWsClient.close();
            }
        }

        WebSocketClient newWsClient = createWebSocketClient(uri, httpHeaders);
        // 如果是 "wss" 协议,则进行证书认证,认证方法在父类中
        if (uri.startsWith("wss")) {
            createWebSocketClient(newWsClient);
        }
        if (null == newWsClient) {
            // 自定义异常
            throw new BizException(BizExceptionCodeEnum.GET_ERROR, "WsClientOfLocal -> 创建失败!");
        }
        newWsClient.connect();
        // 设置连接状态为正在连接
        isConnect = 2;
        // 连接状态不再是0请求中,判断建立结果是不是1已建立
        long startTime = System.currentTimeMillis();
        while (1 != newWsClient.getReadyState().ordinal()) {
            // 避免网络波动,设置持续等待连接时间
            long endTime = System.currentTimeMillis();
            long waitTime = (endTime - startTime) / 1000;
            if (5L < waitTime) {
                log.info("WsClientOfLocal -> 建立连接异常,请稍后再试");
                break;
            }
        }
        if (1 == newWsClient.getReadyState().ordinal()) {
            this.setWsClient(newWsClient);
            newWsClient.send("WsClientOfLocal -> 客户端连接成功!");
            log.info("WsClientOfLocal -> 客户端连接成功!");
            return newWsClient;
        }
        return null;
    }

}

如果要对接多个WebSocket服务,由于以上配置是单例模式,则需对应写多个实现类即可,保证每个WebSocket服务只有一个连接;

五、简单的消息处理

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * 消息处理服务
 */
@Service
@Slf4j
public class MessageService {

    /**
     * 处理消息
     * 这里可根据具体业务来实现,比如解析入库、再次分发发送MQ等
     *
     * @param message
     */
    public void handleMessage(String message) {
        log.info("handleMessage -> 开始处理消息---");
        System.out.println("handleMessage -> 接收服务端消息: " + message);
        log.info("handleMessage -> 消息处理完成.");
    }

}

六、客户端心跳重连机制

通过SpringBoot中的定时任务来实现

首先在SpringBoot启动主类上添加注解:@EnableScheduling

配置心跳检测重连:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 心跳重连机制
 */
@Component
@Slf4j
public class WebSocketHeartbeatTimer {

    @Autowired
    private WsClientOfLocal wsClientOfLocal;

    @Value("${local-websocket-url}")
    private String localWebSocketUrl;

    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");


    /**
     * Local WebSocket连接心跳检测,重连机制,每20秒触发一次
     * 注意 @Async 注解,要使用异步线程的方式来执行心跳检测,
     * 避免任务线程被其他任务占用
     */
    @Async
    @Scheduled(cron = "0/20 * * * * ?")
    public void wsHeartbeatOfLocal() {
        try {
            int isConnect = wsClientOfLocal.isConnect;
            log.info("心跳检测 -> WsClientOfLocal: {}-{}", isConnect, ((isConnect == 1) ? "连接中" : "未连接"));
            if (1 != wsClientOfLocal.isConnect) {
                String now = DATE_TIME_FORMATTER.format(LocalDateTime.now());
                log.info("心跳检测 -> WsClientOfLocal连接异常,时间:{},尝试重新连接---", now);
                wsClientOfLocal.connect(localWebSocketUrl, null);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

如果对接多个WebSocket服务,则对应写多个定时任务即可;

七、定时任务异步多线程配置类

关于上面说的多个定时任务使用异步多线程的配置方法:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 * 定时任务异步多线程配置类
 */
@Configuration
@EnableAsync
public class AsyncConfig {

    // 设置核心线程数
    @Value("${public-config.schedule.corePoolSize}")
    private int corePoolSize;

    // 设置最大线程数
    @Value("${public-config.schedule.maxPoolSize}")
    private int maxPoolSize;

    // 设置队列容量
    @Value("${public-config.schedule.queueCapacity}")
    private int queueCapacity;

    // 设置线程活跃时间(秒)
    @Value("${public-config.schedule.keepAliveSeconds}")
    private int keepAliveSeconds;

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        // 设置默认线程名称
        executor.setThreadNamePrefix("task-");
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.initialize();
        return executor;
    }
}

八、设置项目启动后自连

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 项目启动后自动连接WebSocket服务
 */
@Component
@Slf4j
public class WbSocketClientInit {

    @Autowired
    private WsClientOfLocal wsClientOfLocal;

    @Autowired
    private WsClientOfRemote wsClientOfRemote;

    @Value("${local-websocket-url}")
    private String localWebSocketUrl;

    @Value("${remote-websocket-url}")
    private String remoteWebSocketUrl;

    @PostConstruct
    public void initHwFaultWebSocketClient() {
        ExecutorService executor = new ThreadPoolExecutor(2, 5,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
        executor.execute(() -> initWebSocketClientOfLocal());
        executor.execute(() -> initWebSocketClientOfRemote());
        executor.shutdown();
    }

    /**
     * local webSocket首次连接
     */
    public void initWebSocketClientOfLocal() {
        log.info("initWebSocketClientOfLocal -> webSocket首次连接开始");
        wsClientOfLocal.connect(localWebSocketUrl, null);
        log.info("initWebSocketClientOfLocal -> webSocket首次连接完毕");
    }

    /**
     * 华为故障-mae,webSocket首次连接
     */
    public void initWebSocketClientOfRemote() {
        log.info("initWebSocketClientOfRemote -> webSocket首次连接开始");
        wsClientOfRemote.connect(remoteWebSocketUrl, null);
        log.info("initWebSocketClientOfRemote -> webSocket首次连接完毕");
    }

}

总结:以上为java WebSocketClient连接的完整示例,可以直接使用,主要麻烦点还是"wss"协议的证书认证部分,耽误了些许时间,还好顺利解决了;