WebSocket叢集解決方案,不用MQ

十月南城發表於2023-03-28

首先不瞭解WebSocket的可以先看看這篇文章,以及傳統的WebSocket方案是怎麼做的,https://www.cnblogs.com/jeremylai7/p/16875115.html 這是用MQ解決的版本,那麼這種方案存在什麼問題呢。

第一:增加MQ,可能造成訊息擠壓、訊息順序的問題

第二:增加MQ,則還需要保證MQ的可用性

第三:每個socket服務都需要去消費訊息,增加每個服務的壓力(做無用功)

 

那麼,基於以上問題,還有沒有解決方案呢?

當然有!!!

 

首先我們理解一個邏輯,為什麼WebSocket不能直接做叢集,socket是一個長連結,當我們要給socket使用者傳送訊息的時候,我們不知道使用者是連線到哪一個服務上面的,這樣就無法直接傳送訊息了

 

那麼,我們能不能給每一個socket伺服器增加一個標識,然後在使用者連線的時候將使用者與socket伺服器的關係繫結起來,然後在使用的時候再去判斷使用者存在哪,再給指定的伺服器傳送訊息不就解決問題了嗎?

 

那麼,我們來結合springcloud來完成這個工作,根據這個理論,其他方式也可以實現

首先,來看websocket服務,啟動的時候主要注意的問題

@SpringBootApplication
public class WsApplication implements CommandLineRunner {

    public static void main(String[] args) {
        //動態服務名
        System.setProperty("SpringApplicationName", "WS-" + IdUtil.simpleUUID());
        SpringApplication.run(WsApplication.class, args);
    }

    @Override
    public void run(String... args) {
        System.out.println("專案啟動完畢");
    }

}
WebSocket叢集解決方案,不用MQ

需要注意的是動態服務名這裡,每個服務的名字都是不一樣的,這樣就保證了每個服務的一個唯一性

spring:
  application:
    #隨機名字,做ws叢集使用
    name: ${SpringApplicationName}
  #    name: ws
  redis:
    host: 127.0.0.1
  cloud:
    nacos:
      server-addr: 127.0.0.1
      config:
        file-extension: yaml

server:
  port: 9090
WebSocket叢集解決方案,不用MQ

這裡用到了nacos與redis,使用的地方待會會有,其中SpringApplicationName是在啟動的時候傳入的

接下來看WebSocket連結時候需要注意的點

@Component
@ServerEndpoint("/ws/{userId}")
public class WebSocket {

    /**
     * 存放使用者資訊
     */
    private static final ConcurrentHashMap<Long, WebSocket> WEB_SOCKET_MAP = new ConcurrentHashMap<>(16);
    /**
     * session
     */
    private Session session;

    private Long userId;

    private String applicationName = System.getProperty("SpringApplicationName");

    private StringRedisTemplate stringRedisTemplate = SpringUtil.getBean(StringRedisTemplate.class);

    /**
     * 靜態常量
     */
    private static final String SOCKET_USER_SPRING_APPLICATION_NAME = "ws:socket:user:spring:application:name";

    /**
     * 當有新的WebSocket連線完成時
     *
     * @param session
     * @param userId
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") Long userId) {
        System.out.println("new connection");
        System.out.println(userId);
        this.session = session;
        //根據token獲取使用者資訊
        this.userId = userId;
        WEB_SOCKET_MAP.put(this.userId, this);
        this.stringRedisTemplate.opsForHash().put(SOCKET_USER_SPRING_APPLICATION_NAME, userId + "", applicationName);
    }
}
WebSocket叢集解決方案,不用MQ

其中在連結的時候,將使用者ID與socket服務的關係儲存進了redis,這樣我們在使用的時候就可以根據這個關係,找到對應的socket服務從而實現自己的業務邏輯

然後我們定義一個傳送訊息的介面

@RestController
@RequestMapping("push")
public class PushController {


    @PostMapping("{userId}")
    public void pushMessage(@PathVariable Long userId, @RequestBody JSONObject message) {
        WebSocket.sendMessage(userId, message);
    }
}
WebSocket叢集解決方案,不用MQ

再單獨封裝一個介面,供使用方使用feign

@FeignClient(name = "pushFeign", configuration = DynamicRoutingConfig.class)
public interface PushFeign {

    /**
     * 推送訊息
     *
     * @param serviceName 服務名
     * @param userId      使用者
     * @param message     訊息體
     */
    @PostMapping(value = "//{serviceName}/push/{userId}")
    void pushMessage(@PathVariable String serviceName, @PathVariable Long userId, @RequestBody JSONObject message);
}
WebSocket叢集解決方案,不用MQ

再來個Service

@Service
public class PushService {

    @Resource
    private PushFeign pushFeign;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 靜態常量
     */
    private static final String SOCKET_USER_SPRING_APPLICATION_NAME = "ws:socket:user:spring:application:name";

    /**
     * 傳送訊息
     *
     * @param userId
     * @param message
     */
    public void pushMessage(Long userId, JSONObject message) {
        Object serviceName = this.stringRedisTemplate.opsForHash().get(SOCKET_USER_SPRING_APPLICATION_NAME, userId + "");
        if (serviceName != null) {
            this.pushFeign.pushMessage(serviceName.toString(), userId, message);
        }
    }
}
WebSocket叢集解決方案,不用MQ

還有個feign的配置檔案,將連結重寫DynamicRoutingConfig

public class DynamicRoutingConfig {
    @Bean
    public RequestInterceptor cloudContextInterceptor() {
        return template -> {
            String url = template.url();
            if (url.startsWith("//")) {
                url = "http:" + url;
                if (url.contains("?")) {
                    url = url.substring(0, url.indexOf("?"));
                }
                template.target(url);
                template.uri("");
            }
        };
    }
}
WebSocket叢集解決方案,不用MQ

那麼在使用的時候,我們可以直接呼叫PushService.pushMessage方法就可以直接給對應的使用者傳送訊息了

 

那麼可能又有人想問了,每個服務都不一樣,那閘道器這些該怎麼做,專案原始碼已經放在了碼雲上面, https://gitee.com/liupan1230/spring-cloud-websocket-cluster  大家可以參考,同時也有傳送方呼叫示例

相關文章