需求场景:
1. 本地需要搭建一套轮序服务周期去采集平台上的数据保存到当前环境,而平台只能提供websocket方式的数据访问机制。
2.轮询服务需要要建立连接必须携带对应的token,所以要先获取token,其次不能每次轮询都发起新的websocket连接【轮询频率很高】,只需要每次轮询只创建一个客户端,就采用单例的思想去处理连接。【平台对保持连接的sockek连接数有要求,多了就不让连接了。】
3.根据给定的标签去获取对应的数据。
所以要做的事很明确:
第一步,发起轮询服务时先从缓存读取token,如果不存在或者已过期,则重新获取并放入缓存,第二步,查看当前socket客户端是不是已经存在,如果存在则请求登录并获取数据,如果不存在,则重新创建一个新的客户端请求登录并获取数据。
第三步,在onMessage监听方法实现处理相关的业务逻辑,对返回的数据进行分别处理,再调用入库方法。
碰到的问题:
1.一直无法建立连接
2.一直无法写入到数据库
先上核心代码
// 轮询服务调用方法
/*
* properties 相关参数,包括需要用到的账户名、密码、路径等
* clazz 对应的实体类,json转实体类需要使用
*
*/
private <T> T getData(Map<String, Object> properties,Class<T> clazz) throws Exception
{
// 请求头
Map<String, String> headers = new HashMap<>();
headers.put("authorization", TokenHelper.getInstance().getToken(properties));
// 单例模式,防止重复创建,造成资源浪费以及连接数占满
String wss = properties.get("wss").toString();
WebSocketClientSingleton client = WebSocketClientSingleton.getInstance(wss,properties,mapper);
String rspStr = "";
// 解析响应
return JSON.parseObject(rspStr, clazz);
}
// 简单的一个token获取
public synchronized String getToken(Map<String, Object> properties) throws Exception
{
// 没过期的话直接返回
String token = tokenMap.get("token");
String expiration = tokenMap.get("expiration");
if (StringUtils.isNotEmpty(token)
&& DateUtil.parse(expiration, DatePattern.NORM_DATETIME_PATTERN).isAfter(new Date()))
{
return token;
}
// 不存在,则先获取pkey值,再获取token值
String loginUrl = properties.get("pkey").toString();
String loginName = properties.get("user_name").toString();
String password = properties.get("pwd").toString();
String tokenUrl = properties.get("token").toString();
String userName = properties.get("user_name").toString();
String pkey = "";
// 请求体
Map<String, String> parameters = new HashMap<>();
parameters.put("loginName", loginName);
parameters.put("password", password);
String rspStr = HttpUtil.getPkey(loginUrl, parameters);
if (StringUtils.isNotEmpty(rspStr))
{
JSONObject rspJson = JSON.parseObject(rspStr);
if (MESSAGE.equals(rspJson.getString("msg"))) {
pkey = rspJson.getString("data");
// 请求体
Map<String, String> parameters2 = new HashMap<>();
parameters2.put("loginName", userName);
parameters2.put("pkey", pkey);
String body2 = JSON.toJSONString(parameters2);
String rspStr2 = HttpUtil.getToken(tokenUrl, parameters2);
JSONObject rspJson2 = JSON.parseObject(rspStr2);
String msgCode = rspJson2.getString("msg_code");
if (MSGCODE.equals(msgCode)) {
token = rspJson2.getString("token");
} else {
logger.info("token异常,状态码为:{}",rspJson2.getString("msg_code"));
}
if (StringUtils.isNotEmpty(token))
{
// 10小时候过期
Date expDate = DateUtil.offsetHour(new Date(), 10);
tokenMap.put("token", token);
tokenMap.put("expiration", DateUtil.format(expDate, DatePattern.NORM_DATETIME_PATTERN));
return token;
}
} else {
logger.info("pkey异常,状态码为:{}",rspJson.getString("msg_code"));
}
}
throw new BusinessException("获取token失败");
}
@Service
public class WebSocketClientSingleton extends WebSocketClient {
@Autowired
private static StBraceletRMapper mapper;
private static WebSocketClientSingleton instance;
private static final String SERVER_URI = "wss://xxxx";
private static final int RECONNECT_INTERVAL = 5; // 重连间隔时间(秒)
private ScheduledExecutorService executor;
private Map<String, Object> properties;
private final Logger logger = LoggerFactory.getLogger(BraceletDataJob.class);
//用来接收数据
private String excptMessage;
private WebSocketClientSingleton(Map<String, Object> properties) throws URISyntaxException {
// 此处为第一个问题,需要将参数通过调用父类构造函数方式传上去,不然子类会一直获取不到地址,就无法建立链接
super(new URI(SERVER_URI));
executor = Executors.newSingleThreadScheduledExecutor();
this.properties = properties;
excptMessage = null;
}
public static synchronized WebSocketClientSingleton getInstance(String url,Map<String, Object>properties,
StBraceletRMapper mappers) throws Exception {
// 解决第二个问题 mapper一直为null
mapper = mappers;
if (instance == null) {
instance = new WebSocketClientSingleton(properties);
instance.connect();
} else {
// 存在则无需重新创建
instance.reconnectBlocking();
}
return instance;
}
@SneakyThrows
@Override
public void onOpen(ServerHandshake handshakedata) {
System.out.println("Websocket连接成功!");
System.out.println("发送管理员登录报文!");
// 建立完连接后就发送登录请求
sendMessage(makeAdminSend(properties).toJSONString());
}
// 此接口为监听接口,只要有数据过来,这边都能收到
@Override
public void onMessage(String message) {
excptMessage = message;
logger.info("Received message from server: {}",message);
// 如果登录成功则发送请求数据消息
if (message.contains("login") && message.contains("LoginSucc")) {
instance.sendMessage(makeDataSend().toJSONString());
}
// 如果返回的数据包含我们需要的标签,说明成功请求到数据了,只要再对json进行解析,封装成实体类就可以
if(message.contains("devices_data")) {
List<StBraceletR> list = new ArrayList<>();
List<StHelmetR> helmetRList = new ArrayList<>();
BraceletDataResponse dataResponse = new BraceletDataResponse();
try {
dataResponse = JSONObject.parseObject(message, BraceletDataResponse.class);
}catch (Exception e) {
logger.info("实体类转换出错,请检查!");
}
// 这里进行实体类封装,然后调用mybatis方法,出现第二个问题,此处一直无法调用
// 如果仅使用该文件的mapper则一直是null,解决方法为从轮询服务类传过来一个mapper,然后再构造函数中给定义好的mapper重新赋值 即 mapper = mappers;
mapper.xmlAddBatch(list);
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("Websocket连接断开,状态码为:"+code+",原因为:【"+reason+"】 是否由服务器主动关闭: "+remote);
if (code==1000) {
System.out.println(remote==true?"远程":"本地"+"正常关闭");
}else {
System.out.println("异常关闭");
}
}
@Override
public void onError(Exception ex) {
System.err.println("Websocket连接出错:" + ex.getMessage());
}
// 发送消息给服务端
public void sendMessage(String message) {
if (instance.getConnection().isOpen()) {
instance.getConnection().send(message);
} else {
System.err.println("发送消息失败,websocket连接异常");
}
}
private static JSONObject makeAdminSend(Map<String, Object> properties) throws Exception {
// 封装请求体
}
private static JSONObject makeDataSend(){
// 封装请求体
}
}
// 工具类
public class HttpUtil
{
public static String getPkey(String url, Map<String, String> headers) throws IOException, URISyntaxException {
// 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
URIBuilder uriBuilder = null;
uriBuilder = new URIBuilder(url);
uriBuilder.setParameter("user_name",headers.get("loginName"));
uriBuilder.setParameter("pwd",headers.get("password"));
// HttpGet httpRequest = new HttpGet(uriBuilder.build());
HttpPost httpRequest = new HttpPost(uriBuilder.build());
// 执行请求,获取服务器发还的相应对象
CloseableHttpResponse response = httpClient.execute(httpRequest);
// 从相应对象当中取出数据,放到entity当中
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, "UTF-8");
}
public static String getToken(String url, Map<String, String> headers) throws IOException, URISyntaxException {
// 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
URIBuilder uriBuilder = null;
uriBuilder = new URIBuilder(url);
uriBuilder.setParameter("user_name",headers.get("loginName"));
uriBuilder.setParameter("pkey",headers.get("pkey"));
// HttpGet httpRequest = new HttpGet(uriBuilder.build());
HttpPost httpRequest = new HttpPost(uriBuilder.build());
// 执行请求,获取服务器发还的相应对象
CloseableHttpResponse response = httpClient.execute(httpRequest);
// 从相应对象当中取出数据,放到entity当中
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, "UTF-8");
}
public static String doGet(String url, Map<String, String> headers,Map<String, String> params) throws IOException, URISyntaxException {
// 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
URIBuilder uriBuilder = null;
uriBuilder = new URIBuilder(url);
if (params!= null && !params.isEmpty()) {
params.forEach(uriBuilder::setParameter);
}
HttpGet httpRequest = new HttpGet(uriBuilder.build());
// 请求对象
// HttpGet httpRequest = new HttpGet("http://www.0531yun.com/api/getToken?loginName=jnrstest&password=jnrstest321");
// HttpGet httpGet = new HttpGet(url);
//
// 设置请求头
httpRequest.setHeader("Content-Type", "application/json");
if (headers != null && !headers.isEmpty())
{
headers.forEach(httpRequest::setHeader);
}
// 执行请求,获取服务器发还的相应对象
CloseableHttpResponse response = httpClient.execute(httpRequest);
// 从相应对象当中取出数据,放到entity当中
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, "UTF-8");
}
public static String doPost(String url, String body, Map<String, String> headers) throws IOException
{
// 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 请求对象
HttpPost httpRequest = new HttpPost(url);
// 设置请求头
httpRequest.setHeader("Content-Type", "application/json");
if (headers != null && !headers.isEmpty())
{
headers.forEach(httpRequest::setHeader);
}
// 设置请求体
httpRequest.setEntity(new StringEntity(body));
// 执行请求,获取服务器发还的相应对象
CloseableHttpResponse response = httpClient.execute(httpRequest);
// 从相应对象当中取出数据,放到entity当中
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, "UTF-8");
}
}
以上代码还存在不少可以改进的点,比如可以增加一个无用消息请求,保持连接;可以加一个主动检测连接状态,如果断了就发去重连;关于mapper为null的问题这样做有点野蛮,应该有更合适的方法,等后续更新。
附一个在线socket调试地址