1、WebSocket協定概述
WebSocket protocol 是HTML5一種新的協定。它實現了瀏覽器與伺服器全雙工通訊(full-duplex)。一開始的握手需要藉助HTTP請求完成。
WebSocket是真正實現了全雙工通訊的伺服器向用戶端推的網際網路技術。
它是一種在單個TCP連線上進行全雙工通訊協定。Websocket通訊協定與2011年倍IETF定爲標準RFC 6455,Websocket API被W3C定爲標準。
2、優點:
可實現瀏覽器與伺服器全雙工通訊(full-duplex),它可以做到:瀏覽器和伺服器只需要做一個握手的動作,然後,瀏覽器和伺服器之間就形成了一條快速通道。兩者之間就直接可以數據互相傳送。這個新的協定的特點正好適合這種線上即時通訊。
傳統的Http協定實現方式:
http協定可以多次請求,因爲每次請求之後,都會關閉鏈接,下次重新請求數據,需要再次開啓鏈接。
說明:
圖解:
傳統socket技術:
長連線
用戶端 --(先連線上去)----- 伺服器端
好處:可以實現用戶端和伺服器端雙向通訊
缺點:如果大家都不說話,是不是資源就浪費了
WebSocket協定實現方式:
它是一種長鏈接,只能通過一次請求來初始化鏈接,然後所有的請求和響應都是通過這個TCP鏈接進行通訊,這意味着它是一種基於事件驅動,非同步的訊息機制 機製
說明:原理和TCP一樣,只需做一個握手動作,就可以形成一條快速通道。
圖解:
詳細的通訊過程:
1)用戶端發起http請求,附加頭資訊爲:「Upgrade Websocket」
2)伺服器端解析,並返回握手資訊,從而建立連線
3)傳輸數據(雙向)
4)用戶端或伺服器端主動斷開連線。用戶端主動斷開:用戶端發起http請求,請求斷開連線,伺服器端收到訊息後斷開WebSocket連線;伺服器端主動斷開:直接斷開WebSocket連線,用戶端的API會立刻得知。
websocket的優越性不言自明,長連線的連線資源(執行緒資源)隨着連線數量的增多,必會耗盡,用戶端輪詢會給服
務器造成很大的壓力,而websocket是在物理層非網路層建立一條用戶端至伺服器的長連線,以此來保證伺服器向客
戶端的即時推播,既不耗費執行緒資源,又不會不斷向伺服器輪詢請求。
webscoket和傳統http協定的區別
傳統的http請求的的rest風格現在很流行,它的作用就是通過rest風格,能夠把不同路徑的請求對映到同一個方法進行處理。http協定可以多次請求,因爲每次請求之後,都會關閉鏈接,下次重新請求數據,需要再次開啓鏈接。而對於webscoket來說,它是一種長鏈接,只能通過一次請求來初始化鏈接,然後所有的請求和響應都是通過這個TCP鏈接進行通訊,這意味着它是一種基於事件驅動,非同步的訊息機制 機製,和JMS, AMQP等訊息機制 機製的應用差不多。
以前不管使用HTTP輪詢或使用TCP長連線等方式製作線上聊天系統,都有天然缺陷,隨着Html5的興起,其中有一個新的協定WebSocket protocol,可實現瀏覽器與伺服器全雙工通訊(full-duplex),它可以做到:瀏覽器和伺服器只需要做一個握手的動作,然後,瀏覽器和伺服器之間就形成了一條快速通道。兩者之間就直接可以數據互相傳送。這個新的協定的特點正好適合這種線上即時通訊。
傳統的用戶端和伺服器端通訊方式:
WebSocket的用戶端和伺服器端通訊方式:
說明:
http協定是一種應用層協定,已經定義了請求的格式,例如請求的頭部的關鍵字,還有也定義了,伺服器響應數據的格式,它對請求和響應的數據格式做了規範,而websocket協定不同,websocket協定還不夠詳細,它沒有規定請求和接收的數據格式,例如,瀏覽器想向伺服器請求進行socket通訊,但伺服器不知道是否要進行socketon通訊,由於這個原因,websocket就定義了一個子協定,也就是瀏覽器用戶端和伺服器在請求握手的時候,他們能根據頭部的Sec-WebSocket-Protocol,決定是否要進行websocket通訊。當然子協定的使用不是必須的,但是如果不使用子協定,那就必須自己定義一種請求和接收的數據格式規範,然後用戶端和伺服器都使用這種規範來進行通訊。
WebSocket通訊的用戶端使用的是瀏覽器,用戶端操作的API是HTML5中新增的API,使用這些API可以讓用戶端(瀏覽器)和伺服器端(伺服器)進行全雙工的通訊。
支援的瀏覽器如下:
瀏覽器型別 |
瀏覽器版本 |
Chrome |
Supported in version 4+ |
Firefox |
Supported in version 4+ |
Internet Explorer |
Supported in version 10+ |
Opera |
Supported in version 10+ |
Safari |
Supported in version 5+ |
問題出現了,Html5 websocket相容性還不是很好,不是所有的瀏覽器都支援這些新的API,特別是在IE10以下。
但幸運的是現在絕大多數主流的瀏覽器都支援這些API,即使不支援的哪些舊的瀏覽器,也有解決方案。如:
爲了處理不同瀏覽器和瀏覽器版本的相容性,spring webscoket基於SockJS protocol提供了一種解決相容性的方法,在底層遮蔽相容性的問題,提供統一的,透明的,可理解性的webscoket解決方案。
SockJS 是一個瀏覽器上執行的 JavaScript 庫,如果瀏覽器不支援 WebSocket,該庫可以模擬對 WebSocket 的支援,實現瀏覽器和 Web 伺服器之間低延遲、全雙工、跨域的通訊通道。
本課程是基於Java語言開發的,因此伺服器只討論JEE伺服器。
新版本的應用伺服器新增了支援的API,如Tomcat 7.0.47+等
伺服器端:Maven+spring mvc+Spring WebSocket+jQuery+Gson
用戶端:html5的WebSocket的api
引入pom.xml設定:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.itcast.projects</groupId> <artifactId>chatroomdemo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <name>chatroomdemo</name> <description>聊天室的demo</description> <!-- 自定義屬性管理 --> <properties> <!-- 編譯等所有操作使用utf-8編碼 --> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- 統一版本維護管理 --> <spring.version>4.2.8.RELEASE</spring.version> <servlet.version>3.1.0</servlet.version> <jsp.version>2.0</jsp.version> <gson.version>2.7</gson.version> <junit.version>4.12</junit.version> </properties> <!-- 依賴管理 --> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-messaging</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${servlet.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>${gson.version}</version> </dependency> </dependencies>
<!-- 構建資訊管理 --> <build> <finalName>chatroom</finalName> <plugins> <!-- 編譯的jdk版本 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <port>8080</port> <path>/chatroom</path> <uriEncoding>UTF-8</uriEncoding> <finalName>chatroom</finalName> <server>tomcat7</server> </configuration> </plugin> </plugins> </build> </project> |
用戶端如何去連線伺服器端?
用戶端需要主動握手,
需要使用html5的一些程式碼
// 建立一個Socket範例(需要瀏覽器支援)ws:WebSocket協定地址開頭 var socket = new WebSocket('ws://localhost:8080');
//下面 下麪有幾個回撥函數,自動呼叫(什麼時候呼叫?) // 開啓Socket socket.onopen = function(event) { //握手成功後,會自動呼叫該函數 }
// 監聽訊息:用來獲取伺服器端的訊息 socket.onmessage = function(event) { console.log('Client received a message',event); };
// 監聽Socket的關閉 socket.onclose = function(event) { console.log('Client notified socket has closed',event); };
// 關閉Socket.... //socket.close() }; |
spring WebSocket:jee:WebSocket的封裝。
用:只需要知道搭建步驟即可。
/** * * 說明:WebScoket設定處理器 * 把處理器和攔截器註冊到spring websocket中 */ @Component("webSocketConfig") //設定開啓WebSocket服務用來接收ws請求 @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer {
//注入處理器 @Autowired private ChatWebSocketHandler webSocketHandler; @Autowired private ChatHandshakeInterceptor chatHandshakeInterceptor;
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { //新增一個處理器還有定義處理器的處理路徑 registry.addHandler(webSocketHandler, "/ws").addInterceptors(chatHandshakeInterceptor); /* * 在這裏我們用到.withSockJS(),SockJS是spring用來處理瀏覽器對websocket的相容性, * 目前瀏覽器支援websocket還不是很好,特別是IE11以下. * SockJS能根據瀏覽器能否支援websocket來提供三種方式用於websocket請求, * 三種方式分別是 WebSocket, HTTP Streaming以及 HTTP Long Polling */ registry.addHandler(webSocketHandler, "/ws/sockjs").addInterceptors(chatHandshakeInterceptor).withSockJS(); }
} |
/** * websocket的鏈接建立是基於http握手協定,我們可以新增一個攔截器處理握手之前和握手之後過程 * @author BoBo * */ @Component public class ChatHandshakeInterceptor implements HandshakeInterceptor{
/** * 握手之前,若返回false,則不建立鏈接 */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; HttpSession session = servletRequest.getServletRequest().getSession(false); //如果使用者已經登錄,允許聊天 if(session.getAttribute("loginUser")!=null){ //獲取登錄的使用者 User loginUser=(User)session.getAttribute("loginUser") ; //將使用者放入socket處理器的對談(WebSocketSession)中 attributes.put("loginUser", loginUser); System.out.println("Websocket:使用者[ID:" + (loginUser.getId() + ",Name:"+loginUser.getNickname()+"]要建立連線")); }else{ //使用者沒有登錄,拒絕聊天 //握手失敗! System.out.println("--------------握手已失敗..."); return false; } } System.out.println("--------------握手開始..."); return true; }
/** * 握手之後 */ @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { System.out.println("--------------握手成功啦..."); }
}
|
@Component("chatWebSocketHandler") public class ChatWebSocketHandler implements WebSocketHandler {
//線上使用者的SOCKETsession(儲存了所有的通訊通道) public static final Map<String, WebSocketSession> USER_SOCKETSESSION_MAP;
//儲存所有的線上使用者 static { USER_SOCKETSESSION_MAP = new HashMap<String, WebSocketSession>(); }
/** * webscoket建立好鏈接之後的處理常式--連線建立後的準備工作 */ @Override public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception { //將當前的連線的使用者對談放入MAP,key是使用者編號 User loginUser=(User) webSocketSession.getAttributes().get("loginUser"); USER_SOCKETSESSION_MAP.put(loginUser.getId(), webSocketSession);
//羣發訊息告知大家 Message msg = new Message(); msg.setText("風騷的【"+loginUser.getNickname()+"】踩着輕盈的步伐來啦。。。大家歡迎!"); msg.setDate(new Date()); //獲取所有線上的WebSocketSession物件集合 Set<Entry<String, WebSocketSession>> entrySet = USER_SOCKETSESSION_MAP.entrySet(); //將最新的所有的線上人列表放入訊息物件的list集閤中,用於頁面顯示 for (Entry<String, WebSocketSession> entry : entrySet) { msg.getUserList().add((User)entry.getValue().getAttributes().get("loginUser")); }
//將訊息轉換爲json TextMessage message = new TextMessage(GsonUtils.toJson(msg)); //羣發訊息 sendMessageToAll(message);
}
@Override /** * 用戶端發送伺服器的訊息時的處理常式,在這裏收到訊息之後可以分發訊息 */ //處理訊息:當一個新的WebSocket到達的時候,會被呼叫(在用戶端通過Websocket API發送的訊息會經過這裏,然後進行相應的處理) public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> message) throws Exception { //如果訊息沒有任何內容,則直接返回 if(message.getPayloadLength()==0)return; //反序列化伺服器端收到的json訊息 Message msg = GsonUtils.fromJson(message.getPayload().toString(), Message.class); msg.setDate(new Date()); //處理html的字元,跳脫: String text = msg.getText(); //轉換爲HTML跳脫字元表示 String htmlEscapeText = HtmlUtils.htmlEscape(text); msg.setText(htmlEscapeText); System.out.println("訊息(可存數據庫作爲歷史記錄):"+message.getPayload().toString()); //判斷是羣發還是單發 if(msg.getTo()==null||msg.getTo().equals("-1")){ //羣發 sendMessageToAll(new TextMessage(GsonUtils.toJson(msg))); }else{ //單發 sendMessageToUser(msg.getTo(), new TextMessage(GsonUtils.toJson(msg))); } }
@Override /** * 訊息傳輸過程中出現的例外處理函數 * 處理傳輸錯誤:處理由底層WebSocket訊息傳輸過程中發生的異常 */ public void handleTransportError(WebSocketSession webSocketSession, Throwable exception) throws Exception { // 記錄日誌,準備關閉連線 System.out.println("Websocket異常斷開:" + webSocketSession.getId() + "已經關閉"); //一旦發生異常,強制使用者下線,關閉session if (webSocketSession.isOpen()) { webSocketSession.close(); }
//羣發訊息告知大家 Message msg = new Message(); msg.setDate(new Date());
//獲取異常的使用者的對談中的使用者編號 User loginUser=(User)webSocketSession.getAttributes().get("loginUser"); //獲取所有的使用者的對談 Set<Entry<String, WebSocketSession>> entrySet = USER_SOCKETSESSION_MAP.entrySet(); //並查詢出線上使用者的WebSocketSession(對談),將其移除(不再對其發訊息了。。) for (Entry<String, WebSocketSession> entry : entrySet) { if(entry.getKey().equals(loginUser.getId())){ msg.setText("萬衆矚目的【"+loginUser.getNickname()+"】已經退出。。。!"); //清除線上對談 USER_SOCKETSESSION_MAP.remove(entry.getKey()); //記錄日誌: System.out.println("Socket對談已經移除:使用者ID" + entry.getKey()); break; } }
//並查詢出線上使用者的WebSocketSession(對談),將其移除(不再對其發訊息了。。) for (Entry<String, WebSocketSession> entry : entrySet) { msg.getUserList().add((User)entry.getValue().getAttributes().get("loginUser")); }
TextMessage message = new TextMessage(GsonUtils.toJson(msg)); sendMessageToAll(message);
}
@Override /** * websocket鏈接關閉的回撥 * 連線關閉後:一般是回收資源等 */ public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception { // 記錄日誌,準備關閉連線 System.out.println("Websocket正常斷開:" + webSocketSession.getId() + "已經關閉");
//羣發訊息告知大家 Message msg = new Message(); msg.setDate(new Date());
//獲取異常的使用者的對談中的使用者編號 User loginUser=(User)webSocketSession.getAttributes().get("loginUser"); Set<Entry<String, WebSocketSession>> entrySet = USER_SOCKETSESSION_MAP.entrySet(); //並查詢出線上使用者的WebSocketSession(對談),將其移除(不再對其發訊息了。。) for (Entry<String, WebSocketSession> entry : entrySet) { if(entry.getKey().equals(loginUser.getId())){ //羣發訊息告知大家 msg.setText("萬衆矚目的【"+loginUser.getNickname()+"】已經有事先走了,大家繼續聊..."); //清除線上對談 USER_SOCKETSESSION_MAP.remove(entry.getKey()); //記錄日誌: System.out.println("Socket對談已經移除:使用者ID" + entry.getKey()); break; } }
//並查詢出線上使用者的WebSocketSession(對談),將其移除(不再對其發訊息了。。) for (Entry<String, WebSocketSession> entry : entrySet) { msg.getUserList().add((User)entry.getValue().getAttributes().get("loginUser")); }
TextMessage message = new TextMessage(GsonUtils.toJson(msg)); sendMessageToAll(message); }
@Override /** * 是否支援處理拆分訊息,返回true返回拆分訊息 */ //是否支援部分訊息:如果設定爲true,那麼一個大的或未知尺寸的訊息將會被分割,並會收到多次訊息(會通過多次呼叫方法handleMessage(WebSocketSession, WebSocketMessage). ) //如果分爲多條訊息,那麼可以通過一個api:org.springframework.web.socket.WebSocketMessage.isLast() 是否是某條訊息的最後一部分。 //預設一般爲false,訊息不分割 public boolean supportsPartialMessages() { return false; }
/** * * 說明:給某個人發資訊 * @param id * @param message * @author 傳智.BoBo老師 * @throws IOException * @time:2016年10月27日 下午10:40:52 */ private void sendMessageToUser(String id, TextMessage message) throws IOException{ //獲取到要接收訊息的使用者的session WebSocketSession webSocketSession = USER_SOCKETSESSION_MAP.get(id); if (webSocketSession != null && webSocketSession.isOpen()) { //發送訊息 webSocketSession.sendMessage(message); } }
/** * * 說明:羣發資訊:給所有線上使用者發送訊息 * @author 傳智.BoBo老師 * @time:2016年10月27日 下午10:40:07 */ private void sendMessageToAll(final TextMessage message){ //對使用者發送的訊息內容進行跳脫
//獲取到所有線上使用者的SocketSession物件 Set<Entry<String, WebSocketSession>> entrySet = USER_SOCKETSESSION_MAP.entrySet(); for (Entry<String, WebSocketSession> entry : entrySet) { //某使用者的WebSocketSession final WebSocketSession webSocketSession = entry.getValue(); //判斷連線是否仍然開啓的 if(webSocketSession.isOpen()){ //開啓多執行緒發送訊息(效率高) new Thread(new Runnable() { public void run() { try { if (webSocketSession.isOpen()) { webSocketSession.sendMessage(message); } } catch (IOException e) { e.printStackTrace(); } }
}).start();
} } }
} |