再談 websocket 論架構設計


轉載自:http://www.importnew.com/28036.html

導語

本篇文章以websocket的原理和落地為核心,來敘述websocket的使用,以及相關應用場景。

websocket概述

http與websocket

如我們所了解,http連接為一次請求一次響應(request->response),必須為同步調用方式。
而websocket為一次連接以后,會建立tcp連接,后續客戶端與服務器交互為全雙工方式的交互方式,客戶端可以發送消息到服務端,服務端也可將消息發送給客戶端。

http,websocket對比 

此圖來源於Websocket協議的學習、調研和實現,如有侵權問題,告知后,刪除。

根據上圖,我們大致可以了解到http與websocket之間的區別和不同。

為什么要使用websocket

那么了解http與websocket之間的不同以后,我們為什么要使用websocket呢? 他的應用場景是什么呢?

我找到了一個比較符合websocket使用場景的描述

“The best fit for WebSocket is in web applications where the client and server need to exchange events at high frequency and with low latency.”
翻譯: 在客戶端與服務器端交互的web應用中,websocket最適合在高頻率低延遲的場景下,進行事件的交換和處理

此段來源於spring websocket的官方文檔

了解以上知識后,我舉出幾個比較常見的場景:

  1. 游戲中的數據傳輸
  2. 股票K線圖數據
  3. 客服系統

根據如上所述,各個系統都來使用websocket不是更好嗎?

其實並不是,websocket建立連接之后,后邊交互都由tcp協議進行交互,故開發的復雜度會較高。當然websocket通訊,本身要考慮的事情要比HTTP協議的通訊考慮的更多.

所以如果不是有特殊要求(即 應用不是”高頻率低延遲”的要求),需要優先考慮HTTP協議是否可以滿足。

比如新聞系統,新聞的數據晚上10分鍾-30分鍾,是可以接受的,那么就可以采用HTTP的方式進行輪詢(polling)操作調用REST接口。

當然有時我們建立了websocket通訊,並且希望通過HTTP提供的REST接口推送給某客戶端,此時需要考慮REST接口接受數據傳送給websocket中,進行廣播式的通訊方式。

至此,我已經講述了三種交互方式的使用場景:

  1. websocket獨立使用場景
  2. HTTP獨立使用場景
  3. HTTP中轉websocket使用場景

相關技術概念

websocket

websocket為一次HTTP握手后,后續通訊為tcp協議的通訊方式。

當然,和HTTP一樣,websocket也有一些約定的通訊方式,http通訊方式為http開頭的方式,e.g. http://xxx.com/path ,websocket通訊方式則為ws開頭的方式,e.g. ws://xxx.com/path

SSL:

  1. HTTP: https://xxx.com/path
  2. WEBSOCKET: wss://xxx.com/path

websocket通訊

此圖來源於WebSocket 教程,如有侵權問題,告知后,刪除。

SockJS

正如我們所知,websocket協議雖然已經被制定,當時還有很多版本的瀏覽器或瀏覽器廠商還沒有支持的很好。

所以,SockJS,可以理解為是websocket的一個備選方案。

那它如何規定備選方案的呢?

它大概支持這樣幾個方案:

  1. Websockets
  2. Streaming
  3. Polling

當然,開啟並使用SockJS后,它會優先選用websocket協議作為傳輸協議,如果瀏覽器不支持websocket協議,則會在其他方案中,選擇一個較好的協議進行通訊。

看一下目前瀏覽器的支持情況:

Supported transports, by browser

此圖來源於github: sockjs-client

所以,如果使用SockJS進行通訊,它將在使用上保持一致,底層由它自己去選擇相應的協議。

可以認為SockJS是websocket通訊層上的上層協議。

底層對於開發者來說是透明的。

STOMP

STOMP 中文為: 面向消息的簡單文本協議

websocket定義了兩種傳輸信息類型: 文本信息 和 二進制信息 ( text and binary ).

類型雖然被確定,但是他們的傳輸體是沒有規定的。

當然你可以自己來寫傳輸體,來規定傳輸內容。(當然,這樣的復雜度是很高的)

所以,需要用一種簡單的文本傳輸類型來規定傳輸內容,它可以作為通訊中的文本傳輸協議,即交互中的高級協議來定義交互信息。

STOMP本身可以支持流類型的網絡傳輸協議: websocket協議和tcp協議

它的格式為:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
COMMAND
header1:value1
header2:value2
 
Body^@
 
 
 
 
SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*
 
^@
 
 
 
SEND
destination:/queue/trade
content-type:application/json
content-length:44
 
{"action":"BUY","ticker":"MMM","shares",44}^@

當然STOMP已經應用於很多消息代理中,作為一個傳輸協議的規定,如:RabbitMQ, ActiveMQ

我們皆可以用STOMP和這類MQ進行消息交互.

除了STOMP相關的代理外,實際上還提供了一個stomp.js,用於瀏覽器客戶端使用STOMP消息協議傳輸的js庫。

讓我們很方便的使用stomp.js進行與STOMP協議相關的代理進行交互.

正如我們所知,如果websocket內容傳輸信息使用STOMP來進行交互,websocket也很好的於消息代理器進行交互(如:RabbitMQ, ActiveMQ)

這樣就很好的提供了消息代理的集成方案。

總結,使用STOMP的優點如下:

  1. 不需要自建一套自定義的消息格式
  2. 現有stomp.js客戶端(瀏覽器中使用)可以直接使用
  3. 能路由信息到指定消息地點
  4. 可以直接使用成熟的STOMP代理進行廣播 如:RabbitMQ, ActiveMQ

技術落地

后端技術方案選型

websocket服務端選型:spring websocket

支持SockJS,開啟SockJS后,可應對不同瀏覽器的通訊支持
支持STOMP傳輸協議,可無縫對接STOMP協議下的消息代理器(如:RabbitMQ, ActiveMQ)

前端技術方案選型

前端選型: stomp.js,sockjs.js

后端開啟SOMP和SockJS支持后,前對應有對應的js庫進行支持.
所以選用此兩個庫.

總結

上述所用技術,是這樣的邏輯:

  1. 開啟socktJS:
    如果有瀏覽器不支持websocket協議,可以在其他兩種協議中進行選擇,但是對於應用層來講,使用起來是一樣的。
    這是為了支持瀏覽器不支持websocket協議的一種備選方案
  2. 使用STOMP:
    使用STOMP進行交互,前端可以使用stomp.js類庫進行交互,消息一STOMP協議格式進行傳輸,這樣就規定了消息傳輸格式。
    消息進入后端以后,可以將消息與實現STOMP格式的代理器進行整合。
    這是為了消息統一管理,進行機器擴容時,可進行負載均衡部署
  3. 使用spring websocket:
    使用spring websocket,是因為他提供了STOMP的傳輸自協議的同時,還提供了StockJS的支持。
    當然,除此之外,spring websocket還提供了權限整合的功能,還有自帶天生與spring家族等相關框架進行無縫整合。

應用場景

應用背景

2016年,在公司與同事一起討論和開發了公司內部的客服系統,由於前端技能的不足,很多通訊方面的問題,無法親自調試前端來解決問題。

因為公司技術架構體系以前后端分離為主,故前端無法協助后端調試,后端無法協助前端調試

在加上websocket為公司剛啟用的協議,了解的人不多,導致前后端調試問題重重。

一年后的今天,我打算將前端重溫,自己來調試一下前后端,來發掘一下之前聯調的問題.

當然,前端,我只是考慮stomp.js和sockt.js的使用。

代碼階段設計

角色

  1. 客服
  2. 客戶

登錄用戶狀態

  1. 上線
  2. 下線

分配策略

  1. 用戶登陸后,應該根據用戶角色進行分配

關系保存策略

  1. 應該提供關系型保存策略: 考慮內存式策略(可用於測試),redis式策略
    備注:優先應該考慮實現Inmemory策略,用於測試,讓關系保存策略與存儲平台無關

通訊層設計

  1. 歸類topic的廣播設計(通訊方式:1-n)
  2. 歸類queue的單點設計(通訊方式:1-1)

代碼實現

角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public enum Role {
  CUSTOMER_SERVICE,
  CUSTOMER;
 
 
  public static boolean isCustomer(User user) {
      Collection<GrantedAuthority> authorities = user.getAuthorities();
      SimpleGrantedAuthority customerGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + Role.CUSTOMER.name());
      return authorities.contains(customerGrantedAuthority);
  }
 
  public static boolean isCustomerService(User user) {
      Collection<GrantedAuthority> authorities = user.getAuthorities();
      SimpleGrantedAuthority customerServiceGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + Role.CUSTOMER_SERVICE.name());
      return authorities.contains(customerServiceGrantedAuthority);
  }
}

代碼中User對象,為安全對象,即 spring中org.springframework.security.core.userdetails.User,為UserDetails的實現類。
User對象中,保存了用戶授權后的很多基礎權限信息,和用戶信息。
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface UserDetails extends Serializable {
  Collection<? extends GrantedAuthority> getAuthorities();
 
  String getPassword();
 
  String getUsername();
 
  boolean isAccountNonExpired();
 
  boolean isAccountNonLocked();
 
  boolean isCredentialsNonExpired();
 
  boolean isEnabled();
}

方法 #isCustomer 和 #isCustomerService 用來判斷用戶當前是否是顧客或者是客服。

登錄用戶狀態

1
2
3
4
5
6
7
8
9
10
11
12
public interface StatesManager {
 
    enum StatesManagerEnum{
        ON_LINE,
        OFF_LINE
    }
 
    void changeState(User user , StatesManagerEnum statesManagerEnum);
 
    StatesManagerEnum currentState(User user);
 
}

設計登錄狀態時,應存在登錄狀態管理相關的狀態管理器,此管理器只負責更改用戶狀態和獲取用戶狀態相關操作。
並不涉及其他關聯邏輯,這樣的代碼划分,更有助於面向接口編程的擴展性

分配策略

1
2
3
public interface DistributionUsers {
  void distribution(User user);
}

分配角色接口設計,只關注傳入的用戶,並不關注此用戶是客服或者用戶,具體需要如何去做,由具體的分配策略來決定。

關系保存策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface RelationHandler {
 
  void saveRelation(User customerService,User customer);
 
  List<User> listCustomers(User customerService);
 
  void deleteRelation(User customerService,User customer);
 
  void saveCustomerService(User customerService);
 
  List<User> listCustomerService();
 
  User getCustomerService(User customer);
 
  boolean exist(User user);
 
  User availableNextCustomerService();
 
}

關系保存策略,亦是只關注關系保存相關,並不在乎於保存到哪個存儲介質中。
實現類由Inmemory還是redis還是mysql,它並不專注。
但是,此處需要注意,對於這種關系保存策略,開發測試時,並不涉及高可用,可將Inmemory先做出來用於測試。
開發功能同時,相關同事再來開發其他介質存儲的策略,性能測試以及UAT相關測試時,應切換為此介質存儲的策略再進行測試。

用戶綜合管理

對於不同功能的實現策略,由各個功能自己來實現,在使用上,我們僅僅根據接口編程即可。
所以,要將上述所有功能封裝成一個工具類進行使用,這就是所謂的
設計模式: 門面模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Component
public class UserManagerFacade {
    @Autowired
    private DistributionUsers distributionUsers;
    @Autowired
    private StatesManager statesManager;
    @Autowired
    private RelationHandler relationHandler;
 
 
    public void login(User user) {
        if (roleSemanticsMistiness(user)) {
            throw new SessionAuthenticationException("角色語義不清晰");
        }
 
        distributionUsers.distribution(user);
        statesManager.changeState(user, StatesManager.StatesManagerEnum.ON_LINE);
    }
    private boolean roleSemanticsMistiness(User user) {
        Collection<GrantedAuthority> authorities = user.getAuthorities();
 
        SimpleGrantedAuthority customerGrantedAuthority = new SimpleGrantedAuthority("ROLE_"+Role.CUSTOMER.name());
        SimpleGrantedAuthority customerServiceGrantedAuthority = new SimpleGrantedAuthority("ROLE_"+Role.CUSTOMER_SERVICE.name());
 
        if (authorities.contains(customerGrantedAuthority)
                && authorities.contains(customerServiceGrantedAuthority)) {
            return true;
        }
 
        return false;
    }
 
    public void logout(User user){
        statesManager.changeState(user, StatesManager.StatesManagerEnum.OFF_LINE);
    }
 
 
    public User getCustomerService(User user){
        return relationHandler.getCustomerService(user);
    }
 
    public List<User> listCustomers(User user){
        return relationHandler.listCustomers(user);
    }
 
    public StatesManager.StatesManagerEnum getStates(User user){
        return statesManager.currentState(user);
    }
 
}

UserManagerFacade 中注入三個相關的功能接口:

1
2
3
4
5
6
@Autowired
private DistributionUsers distributionUsers;
@Autowired
private StatesManager statesManager;
@Autowired
private RelationHandler relationHandler;

可提供:

  1. 登錄(#login)
  2. 登出(#logout)
  3. 獲取對應客服(#getCustomerService)
  4. 獲取對應用戶列表(#listCustomers)
  5. 當前用戶登錄狀態(#getStates)

這樣的設計,可保證對於用戶關系的管理都由UserManagerFacade來決定
其他內部的操作類,對於使用者來說,並不關心,對開發來講,不同功能的策略都是透明的。

通訊層設計 – 登錄,授權

spring websocket雖然並沒有要求connect時,必須授權,因為連接以后,會分發給客戶端websocket的session id,來區分客戶端的不同。
但是對於大多數應用來講,登錄授權以后,進行websocket連接是最合理的,我們可以進行權限的分配,和權限相關的管理。

我模擬例子中,使用的是spring security的Inmemory的相關配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
 
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.inMemoryAuthentication().withUser("admin").password("admin").roles(Role.CUSTOMER_SERVICE.name());
      auth.inMemoryAuthentication().withUser("admin1").password("admin").roles(Role.CUSTOMER_SERVICE.name());
 
 
      auth.inMemoryAuthentication().withUser("user").password("user").roles(Role.CUSTOMER.name());
      auth.inMemoryAuthentication().withUser("user1").password("user").roles(Role.CUSTOMER.name());
      auth.inMemoryAuthentication().withUser("user2").password("user").roles(Role.CUSTOMER.name());
      auth.inMemoryAuthentication().withUser("user3").password("user").roles(Role.CUSTOMER.name());
  }
 
  @Override
  protected void configure(HttpSecurity http) throws Exception {
      http.csrf().disable()
              .formLogin()
              .and()
              .authorizeRequests()
              .anyRequest()
              .authenticated();
  }
}

相對較為簡單,創建2個客戶,4個普通用戶。
當認證管理器認證后,會將認證后的合法認證安全對象user(即 認證后的token)放入STOMP的header中.
此例中,認證管理認證之后,認證的token為org.springframework.security.authentication.UsernamePasswordAuthenticationToken,
此token認證后,將放入websocket的header中。(即 后邊會談到的安全對象 java.security.Principal)

通訊層設計 – websocket配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
 
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();
    }
 
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        config.enableSimpleBroker("/topic", "/queue");
 
    }
}

此配置中,有幾點需進行講解:
其中端點”portfolio”,用於socktJs進行websocket連接時使用,只用於建立連接。
“/topic”, “/queue”,則為STOMP的語義約束,topic語義為1-n(廣播機制),queue語義為1-1(單點機制)
“app”,此為應用級別的映射終點前綴,這樣說有些晦澀,一會看一下示例將會清晰很多。

通訊層設計 – 創建連接

用於連接spring websocket的端點為portfolio,它可用於連接,看一下具體實現:

1
2
3
4
5
6
7
8
9
10
<script src="http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.min.js"></script>
<script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.js"></script>
<script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
 
var socket = new SockJS("/portfolio");
stompClient = Stomp.over(socket);
 
stompClient.connect({}, function(frame) {
   showGreeting("登錄用戶: " + frame.headers["user-name"]);
});

這樣便建立了連接。 后續的其他操作就可以通過stompClient句柄進行使用了。

通訊層設計 – spring websocket消息模型

見模型圖:

message-flow-simple-broker

message-flow-simple-broker

 

此圖來源spring-websocket官方文檔

可以看出對於同一定於目標都為:/topic/broadcast,它的發送渠道為兩種:/app/broadcast和/topic/broadcast

如果為/topic/broadcast,直接可將消息體發送給定於目標(/topic/broadcast)。

如果是/app/broadcast,它將消息對應在MessageHandler方法中進行處理,處理后的結果發放到broker channel中,最后再講消息體發送給目標(/topic/broadcast)

當然,這里邊所說的app前綴就是剛才我們在websocket配置中的前綴.

看一個例子:

前端訂閱:

1
2
3
stompClient.subscribe( '/topic/broadcast' , function(greeting){
    showGreeting(greeting.body);
});

后端服務:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class ChatWebSocket extends AbstractWebSocket{
  @MessageMapping ( "broadcast" )
  public String broadcast( @Payload @Validated Message message, Principal principal) {
     return "發送人: " + principal.getName() + " 內容: " + message.toString();
  }
}
 
@Data
public class Message {
    @NotNull (message = "標題不能為空" )
    private String title;
    private String content;
}

前端發送:

1
2
3
function sendBroadcast() {
    stompClient.send( "/app/broadcast" ,{},JSON.stringify({ 'content' : 'message content' }));
}

這種發送將消息發送給后端帶有@MessageMapping注解的方法,然后組合完數據以后,在推送給訂閱/topic/broadcast的前端

1
2
3
function sendBroadcast() {
    stompClient.send( "/topic/broadcast" ,{},JSON.stringify({ 'content' : 'message content' }));
}

這種發送直接將消息發送給訂閱/topic/broadcast的前端,並不通過注解方法進行流轉。

我相信上述這個理解已經解釋清楚了spring websocket的消息模型圖

通訊層設計 – @MessageMapping

帶有這個注解的@Controller下的方法,正是對應websocket中的中轉數據的處理方法。
那么這個注解下的方法究竟可以獲取哪些數據,其中有什么原理呢?

方法說明

我大概說一下:
Message,@Payload,@Header,@Headers,MessageHeaders,MessageHeaderAccessor, SimpMessageHeaderAccessor,StompHeaderAccessor
以上這些都是獲取消息頭,消息體,或整個消息的基本對象模型。

@DestinationVariable
這個注解用於動態監聽路徑,很想rest中的@PathVariable:

e.g.:

1
2
3
4
5
@MessageMapping ( "/queue/chat/{uid}" )
public void chat( @Payload @Validated Message message, @DestinationVariable ( "uid" ) String uid, Principal principal) {
    String msg = "發送人: " + principal.getName() + " chat " ;
    simpMessagingTemplate.convertAndSendToUser(uid, "/queue/chat" ,msg);
}

java.security.Principal

這個對象我需要重點說一下。
他則是spring security認證之后,產生的Token對象,即本例中的UsernamePasswordAuthenticationToken.

UsernamePasswordAuthenticationToken類圖

不難發現UsernamePasswordAuthenticationToken是Principal的一個實現.

可以將Principal直接轉成授權后的token,進行操作:

1
UsernamePasswordAuthenticationToken user = (UsernamePasswordAuthenticationToken) principal;

正如前邊設計章節所說,整個用戶設計都是對org.springframework.security.core.userdetails.User進行操作,那如何拿到User對象呢。
很簡單,如下:

1
2
UsernamePasswordAuthenticationToken user = (UsernamePasswordAuthenticationToken) principal;
User user = (User) user.getPrincipal()

通訊層設計 – 1-1 && 1-n

1-n topic:
此方式,上述消息模型章節已經講過,此處不再贅述

1-1 queue:
客服-用戶溝通為1-1用戶交互的案例

前端:

1
2
3
stompClient.subscribe('/user/queue/chat',function(greeting){
    showGreeting(greeting.body);
});

后端:

1
2
3
4
5
@MessageMapping("/queue/chat/{uid}")
public void chat(@Payload @Validated Message message, @DestinationVariable("uid") String uid, Principal principal) {
    String msg = "發送人: " + principal.getName() + " chat ";
    simpMessagingTemplate.convertAndSendToUser(uid,"/queue/chat",msg);
}

發送端:

1
2
3
function chat(uid) {
    stompClient.send("/app/queue/chat/"+uid,{},JSON.stringify({'title':'hello','content':'message content'}));
}

上述的轉化,看上去沒有topic那樣1-n的廣播要流暢,因為代碼中采用約定的方式進行開發,當然這是由spring約定的。

約定轉化的處理器為UserDestinationMessageHandler。

大概的語義邏輯如下:

“An application can send messages targeting a specific user, and Spring’s STOMP support recognizes destinations prefixed with “/user/“ for this purpose. For example, a client might subscribe to the destination “/user/queue/position-updates”. This destination will be handled by the UserDestinationMessageHandler and transformed into a destination unique to the user session, e.g. “/queue/position-updates-user123”. This provides the convenience of subscribing to a generically named destination while at the same time ensuring no collisions with other users subscribing to the same destination so that each user can receive unique stock position updates.”

大致的意思是說:如果是客戶端訂閱了/user/queue/position-updates,將由UserDestinationMessageHandler轉化為一個基於用戶會話的訂閱地址,比如/queue/position-updates-user123,然后可以進行通訊。

例子中,我們可以把uid當成用戶的會話,因為用戶1-1通訊是通過spring security授權的,所以我們可以把會話當做授權后的token.
如登錄用戶token為: UsernamePasswordAuthenticationToken newToken = new UsernamePasswordAuthenticationToken(“admin”,”user”);
且這個token是合法的,那么/user/queue/chat訂閱則為/queue/chat-admin

發送時,如果通過/user/admin/queue/chat,則不通過@MessageMapping直接進行推送。
如果通過/app/queue/chat/admin,則將消息由@MessageMapping注解處理,最終發送給/user/admin/queue/chat終點

追蹤代碼simpMessagingTemplate.convertAndSendToUser:

1
2
3
4
5
6
7
8
@Override
public void convertAndSendToUser(String user, String destination, Object payload, Map<String, Object> headers,
    MessagePostProcessor postProcessor) throws MessagingException {
 
  Assert.notNull(user, "User must not be null");
  user = StringUtils.replace(user, "/", "%2F");
  super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
}

說明最后的路徑依然是/user/admin/queue/chat終點.

通訊層設計 – @SubscribeMapping

@SubscribeMapping注解可以完成訂閱即返回的功能。
這個很像HTTP的request-response,但不同的是HTTP的請求和響應是同步的,每次請求必須得到響應。
而@SubscribeMapping則是異步的。意思是說:當訂閱時,直到回應可響應時在進行處理。

通訊層設計 – 異常處理

@MessageMapping是支持jsr 303校驗的,它支持@Validated注解,可拋出錯誤異常,如下:

1
2
3
4
@MessageMapping("broadcast")
public String broadcast(@Payload @Validated Message message, Principal principal) {
    return "發送人: " + principal.getName() + " 內容: " + message.toString();
}

那異常如何處理呢

@MessageExceptionHandler,它可以進行消息層的異常處理

1
2
3
4
5
6
7
8
9
10
@MessageExceptionHandler
@SendToUser(value = "/queue/error",broadcast = false)
public String handleException(MethodArgumentNotValidException methodArgumentNotValidException) {
    BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
    if (!bindingResult.hasErrors()) {
        return "未知錯誤";
    }
    List<FieldError> allErrors = bindingResult.getFieldErrors();
    return "jsr 303 錯誤: " + allErrors.iterator().next().getDefaultMessage();
}

其中@SendToUser,是指只將消息發送給當前用戶,當然,當前用戶需要訂閱/user/queue/error地址。
注解中broadcast,則表明消息不進行多會話的傳播(有可能一個用戶登錄3個瀏覽器,有三個會話),如果此broadcast=false,則只傳給當前會話,不進行其他會話傳播

總結

本文從websocket的原理和協議,以及內容相關協議等不同維度進行了詳細介紹。

最終以一個應用場景為例,從項目的結構設計,以及代碼策略設計,設計模式等不同方面展示了websocket的通訊功能在項目中的使用。

如何實現某一功能其實並不重要,重要的是得了解理論,深入理論之后,再進行開發。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM