近期公司项目中要对接第三方的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"协议的证书认证部分,耽误了些许时间,还好顺利解决了;