轉載自:http://www.importnew.com/28036.html
導語
本篇文章以websocket的原理和落地為核心,來敘述websocket的使用,以及相關應用場景。
websocket概述
http與websocket
如我們所了解,http連接為一次請求一次響應(request->response),必須為同步調用方式。
而websocket為一次連接以后,會建立tcp連接,后續客戶端與服務器交互為全雙工方式的交互方式,客戶端可以發送消息到服務端,服務端也可將消息發送給客戶端。
此圖來源於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最適合在高頻率低延遲的場景下,進行事件的交換和處理
了解以上知識后,我舉出幾個比較常見的場景:
- 游戲中的數據傳輸
- 股票K線圖數據
- 客服系統
根據如上所述,各個系統都來使用websocket不是更好嗎?
其實並不是,websocket建立連接之后,后邊交互都由tcp協議進行交互,故開發的復雜度會較高。當然websocket通訊,本身要考慮的事情要比HTTP協議的通訊考慮的更多.
所以如果不是有特殊要求(即 應用不是”高頻率低延遲”的要求),需要優先考慮HTTP協議是否可以滿足。
比如新聞系統,新聞的數據晚上10分鍾-30分鍾,是可以接受的,那么就可以采用HTTP的方式進行輪詢(polling)操作調用REST接口。
當然有時我們建立了websocket通訊,並且希望通過HTTP提供的REST接口推送給某客戶端,此時需要考慮REST接口接受數據傳送給websocket中,進行廣播式的通訊方式。
至此,我已經講述了三種交互方式的使用場景:
- websocket獨立使用場景
- HTTP獨立使用場景
- 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:
- HTTP: https://xxx.com/path
- WEBSOCKET: wss://xxx.com/path
此圖來源於WebSocket 教程,如有侵權問題,告知后,刪除。
SockJS
正如我們所知,websocket協議雖然已經被制定,當時還有很多版本的瀏覽器或瀏覽器廠商還沒有支持的很好。
所以,SockJS,可以理解為是websocket的一個備選方案。
那它如何規定備選方案的呢?
它大概支持這樣幾個方案:
- Websockets
- Streaming
- Polling
當然,開啟並使用SockJS后,它會優先選用websocket協議作為傳輸協議,如果瀏覽器不支持websocket協議,則會在其他方案中,選擇一個較好的協議進行通訊。
看一下目前瀏覽器的支持情況:
所以,如果使用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的優點如下:
- 不需要自建一套自定義的消息格式
- 現有stomp.js客戶端(瀏覽器中使用)可以直接使用
- 能路由信息到指定消息地點
- 可以直接使用成熟的STOMP代理進行廣播 如:RabbitMQ, ActiveMQ
技術落地
后端技術方案選型
websocket服務端選型:spring websocket
支持SockJS,開啟SockJS后,可應對不同瀏覽器的通訊支持
支持STOMP傳輸協議,可無縫對接STOMP協議下的消息代理器(如:RabbitMQ, ActiveMQ)
前端技術方案選型
前端選型: stomp.js,sockjs.js
后端開啟SOMP和SockJS支持后,前對應有對應的js庫進行支持.
所以選用此兩個庫.
總結
上述所用技術,是這樣的邏輯:
- 開啟socktJS:
如果有瀏覽器不支持websocket協議,可以在其他兩種協議中進行選擇,但是對於應用層來講,使用起來是一樣的。
這是為了支持瀏覽器不支持websocket協議的一種備選方案 - 使用STOMP:
使用STOMP進行交互,前端可以使用stomp.js類庫進行交互,消息一STOMP協議格式進行傳輸,這樣就規定了消息傳輸格式。
消息進入后端以后,可以將消息與實現STOMP格式的代理器進行整合。
這是為了消息統一管理,進行機器擴容時,可進行負載均衡部署 - 使用spring websocket:
使用spring websocket,是因為他提供了STOMP的傳輸自協議的同時,還提供了StockJS的支持。
當然,除此之外,spring websocket還提供了權限整合的功能,還有自帶天生與spring家族等相關框架進行無縫整合。
應用場景
應用背景
2016年,在公司與同事一起討論和開發了公司內部的客服系統,由於前端技能的不足,很多通訊方面的問題,無法親自調試前端來解決問題。
因為公司技術架構體系以前后端分離為主,故前端無法協助后端調試,后端無法協助前端調試
在加上websocket為公司剛啟用的協議,了解的人不多,導致前后端調試問題重重。
一年后的今天,我打算將前端重溫,自己來調試一下前后端,來發掘一下之前聯調的問題.
當然,前端,我只是考慮stomp.js和sockt.js的使用。
代碼階段設計
角色
- 客服
- 客戶
登錄用戶狀態
- 上線
- 下線
分配策略
- 用戶登陸后,應該根據用戶角色進行分配
關系保存策略
- 應該提供關系型保存策略: 考慮內存式策略(可用於測試),redis式策略
備注:優先應該考慮實現Inmemory策略,用於測試,讓關系保存策略與存儲平台無關
通訊層設計
- 歸類topic的廣播設計(通訊方式:1-n)
- 歸類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;
|
可提供:
- 登錄(#login)
- 登出(#logout)
- 獲取對應客服(#getCustomerService)
- 獲取對應用戶列表(#listCustomers)
- 當前用戶登錄狀態(#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消息模型
見模型圖:
可以看出對於同一定於目標都為:/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是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的通訊功能在項目中的使用。
如何實現某一功能其實並不重要,重要的是得了解理論,深入理論之后,再進行開發。