springboot下的websocket服务端和客户端编写

  • 一:服务端

            引入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:https://blog.csdn.net/Programmer__Wang/article/details/88538993
    引文2:https://blog.csdn.net/CoderYin/article/details/90173118
     */
    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"}
好的,下面是一个简单的Spring Boot WebSocket服务端示例: 1. 首先,在 pom.xml 中添加以下依赖: ```xml <dependencies> <!-- Spring Boot WebSocket依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- Spring Boot Web依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> ``` 2. 创建一个 WebSocketHandler 类来处理 WebSocket 连接: ```java import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; import java.io.IOException; import java.util.ArrayList; import java.util.List; @Component public class MyWebSocketHandler implements WebSocketHandler { private static List<WebSocketSession> sessions = new ArrayList<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { sessions.add(session); } @Override public void handleMessage(WebSocketSession session, TextMessage message) throws Exception { for (WebSocketSession s : sessions) { s.sendMessage(message); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { sessions.remove(session); } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { if (session.isOpen()) { session.close(); } sessions.remove(session); } @Override public boolean supportsPartialMessages() { return false; } } ``` 这个类实现了 WebSocketHandler 接口,并重写了其中的几个方法来处理 WebSocket 连接操作。其中,afterConnectionEstablished 方法在建立连接时被调用,handleMessage 方法在接收到消息时被调用,afterConnectionClosed 方法在连接关闭时被调用,handleTransportError 方法在连接出现异常时被调用。 3. 创建一个 WebSocketConfig 类来配置 WebSocket: ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } @Bean public MyWebSocketHandler myWebSocketHandler() { return new MyWebSocketHandler(); } } ``` 这个类使用 @Configuration 注解表示它是一个配置类,同时使用 @Bean 注解将 MyWebSocketHandler 类和 ServerEndpointExporter 类注入到 Spring 容器中。 4. 在控制器中使用 WebSocket: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.socket.WebSocketSession; @Controller public class MyController { @Autowired private MyWebSocketHandler myWebSocketHandler; @GetMapping("/") public String index() { return "index.html"; } @GetMapping("/send") @ResponseBody public String sendMessage(String message) throws IOException { TextMessage textMessage = new TextMessage(message.getBytes()); for (WebSocketSession session : MyWebSocketHandler.sessions) { session.sendMessage(textMessage); } return "success"; } } ``` 这个控制器包含两个方法,index 方法返回一个 HTML 页面,sendMesssage 方法可以向所有客户端发送消息。 5. 编写一个 index.html 页面来测试 WebSocket: ```html <!DOCTYPE html> <html> <head> <title>WebSocket Test</title> <meta charset="UTF-8"> <script type="text/javascript"> var socket = new WebSocket("ws://" + location.host + "/websocket"); socket.onopen = function () { console.log("WebSocket连接成功"); }; socket.onmessage = function (event) { console.log("收到消息:" + event.data); }; socket.onclose = function () { console.log("WebSocket连接关闭"); }; function sendMessage() { var inputMessage = document.getElementById("inputMessage").value; socket.send(inputMessage); console.log("发送消息:" + inputMessage); } </script> </head> <body> <input type="text" id="inputMessage"> <button onclick="sendMessage()">发送</button> </body> </html> ``` 这个页面中使用 WebSocket API 来连接 WebSocket 服务端并发送消息。 6. 启动 Spring Boot 应用程序,访问 http://localhost:8080/ 可以看到 index.html 页面。在多个浏览器标签页中打开该页面,并在其中一个标签页中发送消息,其他标签页都可以收到消息。 以上就是一个简单的 Spring Boot WebSocket 服务端示例。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值