WebSocket項目筆記
1. What is WebSocket?
(以下內容來源於百度百科)
- WebSocket是一種在單個TCP連接上進行全雙工通信的協議
 - WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。
 - 在WebSocket API中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。
 - 背景: 
          
- 推送技術的演進發展:輪詢 ---> Comet ---> WebSocket
 
 -  
握手協議:
 
2. Let's try!
因為項目要求,我的小程序需要與后端服務器進行長連接,以傳送數據。所以我需要在我配置好的Springboot框架中添加WebSocket。十分慶幸的是,Springboot已經集成好了WebSocket了。所以過程並不復雜。看了很多博客,內容都大同小異。
我有點懵,因為我發現大家都在說的是,客戶端與服務器建立連接的過程的實現。所以有幾個問題:
- 既然是服務器主動發送消息,那么服務器到底 “到底什么時候發送消息呢?怎么發送?”
 - 如何傳參數,傳的參數如何接收與使用。
 - 我只需要針對某個客戶端發消息,在同時有多個socket連接的時候我怎么標識該客戶端?
 
大概就有這些。下面我們在配置過程中將問題逐個擊破!
- 開發環境:Springboot 1.5.19 Java1.8
 - 配置pom文件
 
        <!-- 引入 websocket 依賴類-->
         <dependency>
           <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency> 
        - 配置Springboot開啟WebSocket支持
 
package com.cuc.happyseat.config.websocket; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * 開啟WebSocket支持 */ @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
- WebSocket服務類編寫
 
這里對照下面的代碼看:
-  
          
- 第20行,@ServerEndpoint("/websocket/{userID}"),括號中的內容就是客戶端請求Socket連接時的訪問路徑,userID是我要求客戶端傳來的參數,我這里算是為了標識該客戶端吧。
 - 第28行,在該類中添加屬性 userID,並添加對應的getUserID()方法。
 - 第46行,在onOpen()方法即建立連接的時候就接收參數userID,需要標識@PathParam("userID") 。接收參數后直接賦值給屬性userID。
 - 第140-157行,是針對特定客戶端發送消息。服務器和客戶端在建立連接成功后就生成了一個WebSocket對象,並存在集合中,對象里特有的屬性是我們設置的userID。所以通過唯一的userID就能標識服務器與該客戶端建立的那個連接啦!這樣要求發送消息時,傳入userID與消息,服務器在自己的WebSocket連接集合中遍歷找到對應客戶端的連接,就可以直接發消息過去啦~~
 
 
1 package com.cuc.happyseat.websocket; 2 3 import java.io.IOException; 4 import java.util.concurrent.CopyOnWriteArraySet; 5 6 import javax.websocket.EncodeException; 7 import javax.websocket.OnClose; 8 import javax.websocket.OnError; 9 import javax.websocket.OnMessage; 10 import javax.websocket.OnOpen; 11 import javax.websocket.Session; 12 import javax.websocket.server.PathParam; 13 import javax.websocket.server.ServerEndpoint; 14 15 import org.springframework.stereotype.Component; 16 17 /*@ServerEndpoint注解是一個類層次的注解,它的功能主要是將目前的類定義成一個websocket服務器端, 18 * 注解的值將被用於監聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務器端 19 */ 20 @ServerEndpoint("/websocket/{userID}") 21 @Component 22 public class WebSocketServer { 23 24 //每個客戶端都會有相應的session,服務端可以發送相關消息 25 private Session session; 26 27 //接收userID 28 private Integer userID; 29 30 //J.U.C包下線程安全的類,主要用來存放每個客戶端對應的webSocket連接 31 private static CopyOnWriteArraySet<WebSocketServer> copyOnWriteArraySet = new CopyOnWriteArraySet<WebSocketServer>(); 32 33 public Integer getUserID() { 34 return userID; 35 } 36 37 /** 38 * @Name:onOpen 39 * @Description:打開連接。進入頁面后會自動發請求到此進行連接 40 * @Author:mYunYu 41 * @Create Date:14:46 2018/11/15 42 * @Parameters:@PathParam("userID") Integer userID 43 * @Return: 44 */ 45 @OnOpen 46 public void onOpen(Session session, @PathParam("userID") Integer userID) { 47 this.session = session; 48 this.userID = userID; 49 System.out.println(this.session.getId()); 50 //System.out.println("userID:" + userID); 51 copyOnWriteArraySet.add(this); 52 System.out.println("websocket有新的連接, 總數:"+ copyOnWriteArraySet.size()); 53 54 } 55 56 /** 57 * @Name:onClose 58 * @Description:用戶關閉頁面,即關閉連接 59 * @Author:mYunYu 60 * @Create Date:14:46 2018/11/15 61 * @Parameters: 62 * @Return: 63 */ 64 @OnClose 65 public void onClose() { 66 copyOnWriteArraySet.remove(this); 67 System.out.println("websocket連接斷開, 總數:"+ copyOnWriteArraySet.size()); 68 } 69 70 /** 71 * @Name:onMessage 72 * @Description:測試客戶端發送消息,測試是否聯通 73 * @Author:mYunYu 74 * @Create Date:14:46 2018/11/15 75 * @Parameters: 76 * @Return: 77 */ 78 @OnMessage 79 public void onMessage(String message) { 80 System.out.println("websocket收到客戶端發來的消息:"+message); 81 } 82 83 /** 84 * @Name:onError 85 * @Description:出現錯誤 86 * @Author:mYunYu 87 * @Create Date:14:46 2018/11/15 88 * @Parameters: 89 * @Return: 90 */ 91 @OnError 92 public void onError(Session session, Throwable error) { 93 System.out.println("發生錯誤:" + error.getMessage() + "; sessionId:" + session.getId()); 94 error.printStackTrace(); 95 } 96 97 public void sendMessage(Object object){ 98 //遍歷客戶端 99 for (WebSocketServer webSocket : copyOnWriteArraySet) { 100 System.out.println("websocket廣播消息:" + object.toString()); 101 try { 102 //服務器主動推送 103 webSocket.session.getBasicRemote().sendObject(object) ; 104 } catch (Exception e) { 105 e.printStackTrace(); 106 } 107 } 108 } 109 110 /** 111 * @Name:sendMessage 112 * @Description:用於發送給客戶端消息(群發) 113 * @Author:mYunYu 114 * @Create Date:14:46 2018/11/15 115 * @Parameters: 116 * @Return: 117 */ 118 public void sendMessage(String message) { 119 //遍歷客戶端 120 for (WebSocketServer webSocket : copyOnWriteArraySet) { 121 System.out.println("websocket廣播消息:" + message); 122 try { 123 //服務器主動推送 124 webSocket.session.getBasicRemote().sendText(message); 125 } catch (Exception e) { 126 e.printStackTrace(); 127 } 128 } 129 } 130 131 /** 132 * @throws Exception 133 * @Name:sendMessage 134 * @Description:用於發送給指定客戶端消息 135 * @Author:mYunYu 136 * @Create Date:14:47 2018/11/15 137 * @Parameters: 138 * @Return: 139 */ 140 public void sendMessage(Integer userID, String message) throws Exception { 141 Session session = null; 142 WebSocketServer tempWebSocket = null; 143 for (WebSocketServer webSocket : copyOnWriteArraySet) { 144 if (webSocket.getUserID() == userID) { 145 tempWebSocket = webSocket; 146 session = webSocket.session; 147 break; 148 } 149 } 150 if (session != null) { 151 //服務器主動推送 152 tempWebSocket.session.getBasicRemote().sendText(message); 153 154 } else { 155 System.out.println("沒有找到你指定ID的會話:{}"+ "; userId:" + userID); 156 } 157 } 158 159 160 161 }
- Controller類的編寫。
 
-  
          
- 我在看博客的時候,發現有的博主寫了Controller類,有的沒寫,我就有點疑惑了。后來,咳咳,發現特地寫了一個Controller類只是為了測試。。
 - 一般在實際項目中,在確保建立連接過程沒有問題的情況下,我們就直接在一些寫好的接口中寫 WebSocketServer.sendMessage(param, message)語句就行了。
 - 也因此,你寫的位置就決定了你什么時候給你的客戶端發消息,這樣也就實現了主動推送消息的功能咯~
 - 前提是:在你的Controller類里,以@Resource的方式注入WebSocket,而不是@Autowired方式哦(⊙o⊙)。
 
 
@Resource
    WebSocketServer webSocket;
@Autowired
     UserService userService; 
         調用示例:
if(userID>0) { boolean location = userService.getLocation(userID); if(location==false) {//驗證用戶當前不在館內 boolean i = userService.modifyLocation(userID, true); if(i==true) { modelMap.put("successEnter", true); //發消息給客戶端 webSocket.sendMessage(userID, "success"); } }else { modelMap.put("successEnter", false); //發消息給客戶端 webSocket.sendMessage(userID, "fail"); } }else { modelMap.put("successEnter", false); //發消息給客戶端 webSocket.sendMessage(userID, "fail"); }
- 前端測試
 
因為我只寫后端,前端部分小姐姐說看微信的官方文檔就可以啦~ 鏈接:在此!微信封裝好了吧,好像不難。
3. Problems
- 前期我看很多博客,的確產生很多問題,想不通,主要就是上面幾個問題。然后我寫了讓前端先連WebSocket試了一下,想自己了解一下連接過程(也就是在控制台輸出的文件中查),后來就慢慢通了。可以說是一帆風順了??
 - 不過我確保我沒問題不算,得前端說了算對吧。所以,,我背了鍋😭。
 
-  
          
- 第一次,她給我截圖!啊喂你連WebSocket給我用https?嗯?

 - 后來換成了ws,又說不行,我就想我哪又有問題了?然后我剛好在我打開的n篇相似的博客中找到了答案。因為小程序是只支持https訪問的,所以得用wss。(感謝博主!)
 
 - 第一次,她給我截圖!啊喂你連WebSocket給我用https?嗯?
 
       
3. 然后終於傳來了喜訊!開心~
前端連接成功:

后端輸出記錄:
ps:紅框的1,2,3,4應該是每次連接時自增長的sessionid,即上面截圖中返回的socketTaskId,話說用這個來標識用戶應該也可以。

服務器主動給該客戶端發消息,成功發送!

4. Summary
前期對WebSocket的知識了解估計還不夠吧,導致在理解問題的過程中花費了不少時間。
不過是不是程序員都會有這種錯覺呢?:當你面前有一座大山,你覺得難以跨越,但當你成功翻山越嶺之后,就會覺得這座山不過爾爾?
嘿嘿嘿,下面是學習WebSocket過程中參考的幾篇博文:
