一:服务端
引入maven依赖
<!-- websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 引入外部SDK-->
<dependency>
<groupId>com.suning.api.sdk</groupId>
<artifactId>api-push-sdk</artifactId>
<version>suning-sdk-java-standard-20200610</version>
<scope>system</scope>
<!--1、某模块根目录下的,src建立一个lib,把jar放入(选)-->
<!--2、把jar放入maven库建立版本,通过maven私库拉取-->
<systemPath>${project.basedir}/lib/suning-sdk-java-standard-20200610.jar</systemPath>
</dependency>
服务端写法,有详细注释
package com.xxx.xx.dispatch.module.platform.websocket;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.suning.api.message.Message;
import com.suning.api.util.EncryptMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@ServerEndpoint("/websocket")
@Component
public class WebSocketServer implements InitializingBean{
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Value("${mmcs.kafka.suning.subscription.topic:suning-subscription-topic}")
private String subscriptionTopic;
private Session session;
private String TOPIC = "suning_virtualcomm_order_create";
private String CLIENT_TYPE = "java";
private String version = "1.0";
private static String SUNING_SUBSCRIPTION_TOPIC;
/**
WebSocket是多对象,在连接时才实例化;无法在启动时被spring(单例模式)bean对象注入
引文1:
引文2:
*/
private static KafkaTemplate<String, String> kafkaTemplateSu;
@Override
public void afterPropertiesSet() throws Exception {
SUNING_SUBSCRIPTION_TOPIC=subscriptionTopic;
kafkaTemplateSu=kafkaTemplate;
}
@OnOpen
public void onOpen(Session session) {
this.session = session;
log.info("start 一个 webSocket连接:{}", session.getId());
}
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
* @param session
* @return
*/
@OnMessage
public String onMessage(String message, Session session) {
log.info("收到客户端发送的信息:{}", message);
//解析xx的消息体
Message messageSu = JSON.parseObject(message,new TypeReference<Message>(){});
//校验topic
if(!TOPIC.equals(messageSu.getTopic())){
//xxxx订单推送主题不合符
return "";
}
String uriStr = session.getRequestURI().getPath();
log.info("客户端回拼的带参路径URI:{}", uriStr);
//从uri中获取
String signSu =getParamByUrl(uriStr,"sign");
String timestampStr=getParamByUrl(uriStr,"timestamp");
long timestamp=Long.parseLong(timestampStr==null?"0":timestampStr);
//校验sign
String sign = this.getSignStr(messageSu.getAppKey(), "messageSu.appSecret", version, CLIENT_TYPE,timestamp);
//存入Kafka的内容
/**
* {
* "orderId": "110022132", --
* "supplierCode": "100101000" --
* }
*/
String msg =messageSu.getMsg();
kafkaTemplateSu.send(SUNING_SUBSCRIPTION_TOPIC,msg);
log.info("success to send kafka msg:{}", msg);
log.info("当前的sessionId:{}", session.getId());
return "SUCCESS";
}
@OnClose
public void onClose(Session session, CloseReason reason) {
log.info("webSocket连接关闭:sessionId:"+session.getId() + "关闭原因是:"+reason.getReasonPhrase() + "code:"+reason.getCloseCode());
}
@OnError
public void onError(Throwable t) {
log.info("webSocket连接发生错误!");
t.printStackTrace();
}
private String getSignStr(String appKey, String appSecret, String version, String clientType, long timestamp) {
StringBuilder signSource = (new StringBuilder()).append(appKey).append(appSecret).append(version).append(clientType).append(timestamp);
return EncryptMessage.encryptMessage("MD5", signSource.toString());
}
/**
* 获取指定url中的某个参数
* @param url
* @param name
* @return
*/
private String getParamByUrl(String url, String name) {
url += "&";
String pattern = "(\\?|&){1}#{0,1}" + name + "=[a-zA-Z0-9]*(&{1})";
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(url);
if (m.find( )) {
return m.group(0).split("=")[1].replace("&", "");
} else {
return null;
}
}
}
测试服务端的结果
1、在线测试 http://coolaf.com/tool/chattest
ws://127.0.0.1:8002/xxxx/websocket 配送的地址 ws://ip:端口/项目名/具体类。 这个项目路径参数这行代码: server.port=8002 server.context-path=/项目名
二:客户端
引入maven依赖
<!--websocket作为客户端-->
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.4.0</version>
</dependency>
- 第一种写法
package com.xxxx.xxx.dispatch.module.platform.websocket;
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import javax.websocket.Session;
import java.net.URI;
@Slf4j
@Component
public class MyWebSocketClient extends WebSocketClient {
//这中写法报错,Java.net.URI无法找到,没有解决
@Value("${mmcs.kafka.suning.subscription.topic:suning-subscription-topic}")
private String subscriptionTopic;
private Session session;
private String TOPIC = "suning_virtualcomm_order_create";
private String CLIENT_TYPE = "java";
private String version = "1.0";
public MyWebSocketClient(URI serverUri) {
super(serverUri);
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
log.info("=====MyWebSocket onOpen======");
}
//@Override
public void onMessage(String s) {
log.info("-------- 接收到服务端数据: " + s + "--------");
}
//@Override
public void onClose(int i, String s, boolean b) {
log.info("=====MyWebSocket onClose======");
}
//@Override
public void onError(Exception e) {
log.info("=====MyWebSocket onError======");
}
}
第二种写法
package com.xxxx.xx.dispatch.module.platform.websocket;
import com.alibaba.fastjson.JSON;
import com.suning.api.message.Message;
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.WebSocket;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.enums.ReadyState;
import org.java_websocket.handshake.ServerHandshake;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.NotYetConnectedException;
@Slf4j
public class MyWebSocketTest{
private String TOPIC = "suning_virtualcomm_order_create";
/*public static void main(String[] arg0){
log.info("=====MyWebSocket onOpen======");
//ws://ip:端口/项目名/websocket/用户id
URI serverUri =URI.create("ws://127.0.0.1:8002/webSocketServer/su");
MyWebSocketClient myClient = new MyWebSocketClient(serverUri);
Message message = new Message();
message.setTopic("suning_virtualcomm_order_create");
message.setMsg("{\n" +
" \"orderId\": \"110022132\",\n" +
" \"supplierCode\": \"100101000\"\n" +
"}");
myClient.send(JSON.toJSON(message).toString());
try {
myClient.connect();
} catch (Exception e) {
e.printStackTrace();
}
}*/
public static WebSocketClient client;
public static void main(String[] args) throws URISyntaxException, NotYetConnectedException, UnsupportedEncodingException {
// ws://localhost:8085/websocket/ 10.1.5.245:8002 10.1.123.19
//IP:10.12.12.77,Port:8050],success:true
// websocket = new WebSocket("ws://192.168.2.107:8085/websocket");
client = new WebSocketClient(new URI("ws://localhost:8002/xxxx/websocket")) {
@Override
public void onOpen(ServerHandshake serverHandshake) {
System.out.println("onOpen");
log.info("=====MyWebSocket onOpen======");
}
@Override
public void onMessage(String message) {
System.out.println("接收到服务端数据");
log.info("-------- 接收到服务端数据: " + message + "--------");
try {
// 主题名(当监听多个主题时,注意根据主题判断进行业务逻辑处理)
System.err.println("topic:" + message.getTopic());
// 消息内容
System.err.println("message:" + message.getMsg());
} catch (Exception e) {
e.printStackTrace();
// 当需要消息重传时,抛出该异常
// 注意:不是所有的异常都需要系统重试。
// 对于字段不全、主键冲突问题,导致写DB异常,
//不可重传,否则消息会一直重传
// 对于,由于网络问题,权限问题导致的失败,可重传。
// 不要滥用,否则会引起系统不稳定
throw new RetransmissionException();
}
}
@Override
public void onClose(int i, String s, boolean b) {
System.out.println("onClose");
log.info("=====MyWebSocket onClose======");
}
@Override
public void onError(Exception e) {
System.out.println("onError");
log.info("=====MyWebSocket onError======");
}
};
client.connect();
while(!client.getReadyState().equals(ReadyState.OPEN)){
System.out.println("还没有打开");
System.out.println("连接中···请稍后");
}
System.out.println("打开了");
//send("hello world");
Message message = new Message();
message.setTopic("suning_virtualcomm_order_create");
message.setMsg("{\n" +
" \"orderId\": \"110022132\",\n" +
" \"supplierCode\": \"100101000\"\n" +
"}");
client.send(JSON.toJSON(message).toString());
}
public static void send(String str){
client.send(str);
}
}
启动服务端(eureka上服务注册上),直接运行test的main方法,全部用debug运行进行调试。
其它资料
Kafka在bin下命令添加主题,在bin下运行如下命令
./kafka-topics.sh --create --zookeeper 10.1.5.244:2181,10.1.5.244:12181,10.1.5.245:2181/kafka --replication-factor 2 --partitions 10 --topic su-subscription-topic
查看kafka topic列表,使用--list参数
./kafka-topics.sh -list -zookeeper 10.1.5.244:2181,10.1.5.244:12181,10.1.5.245:2181/kafka
上线联调中级版本
1、建立长链接(看代码),我们做客户端,对方实时推送
2、保证服务在启动的时候,自动运行某个方法,而且阻断启动流程:符合要求的是implements CommandLineRunner接口
package com.xxx.xxx.service.thirdorder.websocket.service;
import com.xxx.xxx.common.consts.CacheKeyConst;
import com.xxx.xxx.common.consts.MmcsRedisCons;
import com.xxx.xxx.common.consts.NodeUrlTypeCons;
import com.xxx.xxx.common.log.InterfaceTypeEnum;
import com.xxx.xxx.domain.member.NodeUrlVO;
import com.xxx.xxx.domain.member.NodeVO;
import com.xxx.xxx.service.thirdorder.log.RunLoggerChain;
import com.xxx.ncc.api.redis.service.RedisService;
import com.xxx.api.message.Message;
import com.xxx.api.push.MessageListener;
import com.xxx.api.push.MessagePushClient;
import com.xxx.api.push.RetransmissionException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
import java.text.MessageFormat;
@Slf4j
@Component
public class MessagePushClientServer implements CommandLineRunner {
@Autowired
private RunLoggerChain chain;
@Autowired
public RedisService redisService;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Value("${mmcs.kafka.xxx.subscription.topic:xxx-subscription-topic}")
private String subscriptionTopic;
private String TOPIC = "xxx_virtualcomm_order_create";
@Override
public void run(String... args){
try {
messagePushClient();
} catch (Exception e) {
log.error("xxx MessagePushClient message :{} error", e.getMessage());
}
log.info("启动执行webSocket长链接!");
}
public void messagePushClient(){
//获取xxx信息
NodeVO node = getNodeInfo();
if (null == node) {
log.info("查询不到网元信息");
}
NodeUrlVO nodeUrl = getNodeUrl(node.getNodeCode(), this.getUlrType());
if (null == nodeUrl || StringUtils.isBlank(nodeUrl.getOutAddrUrl())) {
log.info("查询网元链接信息失败!,node:{}", node);
}
//推送API连接地址
String uri=nodeUrl.getOutAddrUrl();
String otherAttr=nodeUrl.getNodeOtherAttr();
String[] apps=otherAttr.split(";");
//xxx分配的appKey(注意环境不同appKey不同)
String appKey = getAppAttr(apps[0]);
//xxx分配的appKey对应的appSecret
String appSecret = getAppAttr(apps[1]);
//组名,同组内只有一个连接收到消息,尽量不要使用default,组名通常需有标志意义。
String groupName="default_connection";
MessagePushClient client = new MessagePushClient(uri, appKey, appSecret, groupName);
client.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
try {
putMessageToOrder(message);
// 主题名(当监听多个主题时,注意根据主题判断进行业务逻辑处理)
log.info("topic:" + message.getTopic());
// 消息内容
log.info("message:" + message.getMsg());
} catch (Exception e) {
e.printStackTrace();
// 当需要消息重传时,抛出该异常
// 注意:不是所有的异常都需要系统重试。
// 对于字段不全、主键冲突问题,导致写DB异常,
//不可重传,否则消息会一直重传
// 对于,由于网络问题,权限问题导致的失败,可重传。
// 不要滥用,否则会引起系统不稳定
throw new RetransmissionException();
}
}
});
try {
client.connect();
} catch (Exception e) {
log.error("xxx MessagePushClient message :{} error", e.getMessage());
}
}
public void putMessageToOrder(Message message){
log.info("收到客户端发送的信息:{}", message);
//校验topic
if(!TOPIC.equals(message.getTopic())){
//xxx订单推送主题不合符
return;
}
chain.logRequest(InterfaceTypeEnum.SN_VIRTUAL_ORDER_CREATE, message, message.getMessageId());
//存入Kafka的内容
/**
* {
* "orderId": "110022132",
* "supplierCode": "100101000"
* }
*/
String msg =message.getMsg();
kafkaTemplate.send(subscriptionTopic,msg);
log.info("success topic {} to send kafka msg:{}",subscriptionTopic, msg);
chain.logResponse(InterfaceTypeEnum.SN_VIRTUAL_ORDER_CREATE, message, message.getUser());
}
private NodeVO getNodeInfo() {
String nodeCode = (String) redisService.hget(CacheKeyConst.PARAM_CONFIG, "suning_node_code");
return (NodeVO) redisService.hget(MmcsRedisCons.NODE_HASH_KEY_CODE,
MessageFormat.format(MmcsRedisCons.NODE_OBJECT_KEY, nodeCode));
}
private NodeUrlVO getNodeUrl(String nodeId, String urlType) {
NodeUrlVO nodeUrl = (NodeUrlVO) redisService.hget(MmcsRedisCons.NODE_URL_HASH_KEY,
MessageFormat.format(MmcsRedisCons.NODE_URL_OBJECT_KEY, nodeId, urlType));
if (null == nodeUrl) {
log.info("============查询网元链接信息失败!!!nodeId:{},urlType:{}", nodeId, urlType);
}
return nodeUrl;
}
private String getAppAttr(String appAttr){
String[] apps=appAttr.split("=");
return apps[1];
}
private String getUlrType() {
return NodeUrlTypeCons.SUNING_ORDER_CREATE;
}
}
3、maven打包为jar文件时,解决scope为system的jar包无法被打包进jar:
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!--第三方SDK打无法打进去jar包-->
<configuration>
<fork>true</fork>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
4、多机器部署会,不会每台都发;长链接断开又打开不影响;推送失败的重试机制(对方目前是手动再触发);订单去重;
5、Linux下手动生产消息。(Kafka的主题脚本运行推送(指定group),自测不依赖上方)
1、进入到Kafka的bin目录下,执行下面的脚本
./kafka-console-producer.sh --broker-list 172.42.34.6:2092,172.42.34.7:2092,172.42.34.8:2092 --topic xxx-subscription-topic
2、再执行下面的推送消息(主题内容)
{"orderId":"31451937985","supplierCode":"10311602"}