Websocket原理(摘抄)
一、websocket與http
WebSocket是HTML5出的東西(協議),也就是說HTTP協議沒有變化,或者說沒關系,但HTTP是不支持持久連接的(長連接,循環連接的不算)
首先HTTP有 1.1
和 1.0
之說,也就是所謂的 keep-alive
,把多個HTTP請求合並為一個,但是 Websocket
其實是一個新協議,跟HTTP協議基本沒有關系,只是為了兼容現有瀏覽器的握手規范而已,也就是說它是HTTP協議上的一種補充可以通過這樣一張圖理解
有交集,但是並不是全部。
另外Html5是指的一系列新的API,或者說新規范,新技術。Http協議本身只有1.0和1.1,而且跟Html本身沒有直接關系。。通俗來說,你可以用HTTP協議傳輸非Html數據,就是這樣=。=
再簡單來說,層級不一樣。
二、Websocket是什么樣的協議,具體有什么優點
首先,Websocket是一個持久化的協議,相對於HTTP這種非持久的協議來說。簡單的舉個例子吧,用目前應用比較廣泛的PHP生命周期來解釋。
HTTP的生命周期通過 Request
來界定,也就是一個 Request
一個 Response
,那么在 HTTP1.0
中,這次HTTP請求就結束了。
在HTTP1.1中進行了改進,使得有一個keep-alive,也就是說,在一個HTTP連接中,可以發送多個Request,接收多個Response。但是請記住 Request = Response
, 在HTTP中永遠是這樣,也就是說一個request只能有一個response。而且這個response也是被動的,不能主動發起。
教練,你BB了這么多,跟Websocket有什么關系呢?_(:з」∠)_好吧,我正准備說Websocket呢。。
首先Websocket是基於HTTP協議的,或者說借用了HTTP的協議來完成一部分握手。
首先我們來看個典型的 Websocket
握手(借用Wikipedia的。。)
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com
熟悉HTTP的童鞋可能發現了,這段類似HTTP協議的握手請求中,多了幾個東西。我會順便講解下作用。
Upgrade: websocket Connection: Upgrade
這個就是Websocket的核心了,告訴 Apache
、 Nginx
等服務器:注意啦,我發起的是Websocket協議,快點幫我找到對應的助理處理~不是那個老土的HTTP。
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
首先, Sec-WebSocket-Key
是一個 Base64 encode
的值,這個是瀏覽器隨機生成的,告訴服務器:泥煤,不要忽悠窩,我要驗證尼是不是真的是Websocket助理。
然后, Sec_WebSocket-Protocol
是一個用戶定義的字符串,用來區分同URL下,不同的服務所需要的協議。簡單理解:今晚我要服務A,別搞錯啦~
最后, Sec-WebSocket-Version
是告訴服務器所使用的 Websocket Draft
(協議版本),在最初的時候,Websocket協議還在 Draft
階段,各種奇奇怪怪的協議都有,而且還有很多期奇奇怪怪不同的東西,什么Firefox和Chrome用的不是一個版本之類的,當初Websocket協議太多可是一個大難題。。不過現在還好,已經定下來啦~大家都使用的一個東西~ 脫水: 服務員,我要的是13歲的噢→_→
然后服務器會返回下列東西,表示已經接受到請求, 成功建立Websocket啦!
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
這里開始就是HTTP最后負責的區域了,告訴客戶,我已經成功切換協議啦~
Upgrade: websocket Connection: Upgrade
依然是固定的,告訴客戶端即將升級的是 Websocket
協議,而不是mozillasocket,lurnarsocket或者shitsocket。
然后, Sec-WebSocket-Accept
這個則是經過服務器確認,並且加密過后的 Sec-WebSocket-Key
。 服務器:好啦好啦,知道啦,給你看我的ID CARD來證明行了吧。。
后面的, Sec-WebSocket-Protocol
則是表示最終使用的協議。
至此,HTTP已經完成它所有工作了,接下來就是完全按照Websocket協議進行了。具體的協議就不在這闡述了。
——————技術解析部分完畢——————
你TMD又BBB了這么久,那到底Websocket有什么鬼用, http long poll
,或者ajax輪詢
不都可以實現實時信息傳遞么。
好好好,年輕人,那我們來講一講Websocket有什么用。來給你吃點胡(蘇)蘿(丹)卜(紅)
三、Websocket的作用
在講Websocket之前,我就順帶着講下 long poll
和 ajax輪詢
的原理。
ajax輪詢
ajax輪詢的原理非常簡單,讓瀏覽器隔個幾秒就發送一次請求,詢問服務器是否有新信息。
場景再現:
客戶端:啦啦啦,有沒有新信息(Request)
服務端:沒有(Response)
客戶端:啦啦啦,有沒有新信息(Request)
服務端:沒有。。(Response)
客戶端:啦啦啦,有沒有新信息(Request)
服務端:你好煩啊,沒有啊。。(Response)
客戶端:啦啦啦,有沒有新消息(Request)
服務端:好啦好啦,有啦給你。(Response)
客戶端:啦啦啦,有沒有新消息(Request)
服務端:。。。。。沒。。。。沒。。。沒有(Response) —- loop
long poll
long poll
其實原理跟 ajax輪詢
差不多,都是采用輪詢的方式,不過采取的是阻塞模型(一直打電話,沒收到就不掛電話),也就是說,客戶端發起連接后,如果沒消息,就一直不返回Response給客戶端。直到有消息才返回,返回完之后,客戶端再次建立連接,周而復始。
場景再現:
客戶端:啦啦啦,有沒有新信息,沒有的話就等有了才返回給我吧(Request)
服務端:額。。 等待到有消息的時候。。來 給你(Response)
客戶端:啦啦啦,有沒有新信息,沒有的話就等有了才返回給我吧(Request) -loop
從上面可以看出其實這兩種方式,都是在不斷地建立HTTP連接,然后等待服務端處理,可以體現HTTP協議的另外一個特點,被動性。
何為被動性呢,其實就是,服務端不能主動聯系客戶端,只能有客戶端發起。
簡單地說就是,服務器是一個很懶的冰箱(這是個梗)(不會、不能主動發起連接),但是上司有命令,如果有客戶來,不管多么累都要好好接待。
說完這個,我們再來說一說上面的缺陷(原諒我廢話這么多吧OAQ)
從上面很容易看出來,不管怎么樣,上面這兩種都是非常消耗資源的。
ajax輪詢 需要服務器有很快的處理速度和資源。(速度)long poll 需要有很高的並發,也就是說同時接待客戶的能力。(場地大小)
所以 ajax輪詢
和 long poll
都有可能發生這種情況。
客戶端:啦啦啦啦,有新信息么?
服務端:月線正忙,請稍后再試(503 Server Unavailable)
客戶端:。。。。好吧,啦啦啦,有新信息么?
服務端:月線正忙,請稍后再試(503 Server Unavailable)
客戶端:然后服務端在一旁忙的要死:冰箱,我要更多的冰箱!更多。。更多。。(我錯了。。這又是梗。。)
言歸正傳,我們來說Websocket吧
通過上面這個例子,我們可以看出,這兩種方式都不是最好的方式,需要很多資源。
一種需要更快的速度,一種需要更多的’電話’。這兩種都會導致’電話’的需求越來越高。
哦對了,忘記說了HTTP還是一個狀態協議。
通俗的說就是,服務器因為每天要接待太多客戶了,是個健忘鬼,你一掛電話,他就把你的東西全忘光了,把你的東西全丟掉了。你第二次還得再告訴服務器一遍。
所以在這種情況下出現了,Websocket出現了。他解決了HTTP的這幾個難題。首先,被動性,當服務器完成協議升級后(HTTP->Websocket),服務端就可以主動推送信息給客戶端啦。所以上面的情景可以做如下修改。
客戶端:啦啦啦,我要建立Websocket協議,需要的服務:chat,Websocket協議版本:17(HTTP Request)
服務端:ok,確認,已升級為Websocket協議(HTTP Protocols Switched)
客戶端:麻煩你有信息的時候推送給我噢。。
服務端:ok,有的時候會告訴你的。
服務端:balabalabalabala
服務端:balabalabalabala
服務端:哈哈哈哈哈啊哈哈哈哈
服務端:笑死我了哈哈哈哈哈哈哈
就變成了這樣,只需要經過一次HTTP請求,就可以做到源源不斷的信息傳送了。(在程序設計中,這種設計叫做回調,即:你有信息了再來通知我,而不是我傻乎乎的每次跑來問你 )
這樣的協議解決了上面同步有延遲,而且還非常消耗資源的這種情況。那么為什么他會解決服務器上消耗資源的問題呢?
其實我們所用的程序是要經過兩層代理的,即HTTP協議在Nginx等服務器的解析下,然后再傳送給相應的Handler(PHP等)來處理。簡單地說,我們有一個非常快速的 接線員(Nginx)
,他負責把問題轉交給相應的 客服(Handler)
。
本身接線員基本上速度是足夠的,但是每次都卡在客服(Handler)了,老有客服處理速度太慢。,導致客服不夠。Websocket就解決了這樣一個難題,建立后,可以直接跟接線員建立持久連接,有信息的時候客服想辦法通知接線員,然后接線員在統一轉交給客戶。
這樣就可以解決客服處理速度過慢的問題了。
同時,在傳統的方式上,要不斷的建立,關閉HTTP協議,由於HTTP是非狀態性的,每次都要重新傳輸 identity info
(鑒別信息),來告訴服務端你是誰。
雖然接線員很快速,但是每次都要聽這么一堆,效率也會有所下降的,同時還得不斷把這些信息轉交給客服,不但浪費客服的處理時間,而且還會在網路傳輸中消耗過多的流量/時間。
但是Websocket只需要一次HTTP握手,所以說整個通訊過程是建立在一次連接/狀態中,也就避免了HTTP的非狀態性,服務端會一直知道你的信息,直到你關閉請求,這樣就解決了接線員要反復解析HTTP協議,還要查看identity info的信息。
同時由客戶主動詢問,轉換為服務器(推送)有信息的時候就發送(當然客戶端還是等主動發送信息過來的。。),沒有信息的時候就交給接線員(Nginx),不需要占用本身速度就慢的客服(Handler)了
——————–
至於怎么在不支持Websocket的客戶端上使用Websocket。。答案是: 不能
但是可以通過上面說的 long poll
和 ajax 輪詢
來 模擬出類似的效果。
廢話不多說上代碼
我的項目是maven項目所以
pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
注:spring需要4以上的版本,而且所有spring版本要一致。
spring-mvc.xml
好多人都忘記一點 allowed-origins="*" 是匹配任何一個url 和 setAllowedOrigins("*") 功能相同。而且這一點是必須加上的。否則會報403錯誤。
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:aop="http://www.springframework.org/schema/aop" 5 xmlns:context="http://www.springframework.org/schema/context" 6 xmlns:websocket="http://www.springframework.org/schema/websocket" 7 xmlns:mvc="http://www.springframework.org/schema/mvc" 8 xsi:schemaLocation="http://www.springframework.org/schema/beans 9 http://www.springframework.org/schema/beans/spring-beans.xsd 10 http://www.springframework.org/schema/aop 11 http://www.springframework.org/schema/aop/spring-aop.xsd 12 http://www.springframework.org/schema/context 13 http://www.springframework.org/schema/context/spring-context.xsd 14 http://www.springframework.org/schema/mvc 15 http://www.springframework.org/schema/mvc/spring-mvc.xsd 16 http://www.springframework.org/schema/websocket 17 http://www.springframework.org/schema/websocket/spring-websocket.xsd"> 18 19 20 <!-- 掃描除了service注解的類=controller --> 21 <context:component-scan base-package="com.bcy.acitylion.**.controller"/> 22 <aop:aspectj-autoproxy proxy-target-class="true"/> 23 24 <mvc:annotation-driven> 25 <mvc:message-converters> 26 <bean class="org.springframework.http.converter.StringHttpMessageConverter"/> 27 <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/> 28 </mvc:message-converters> 29 </mvc:annotation-driven> 30 31 32 33 <!-- 開啟spring注解 --> 34 <mvc:annotation-driven/> 35 <!-- 靜態頁面交由默認web servlet處理 --> 36 <mvc:default-servlet-handler/> 37 <!-- 訪問項目根目錄返回的頁面 --> 38 <mvc:view-controller path="/" view-name="login"/> 39 <!-- ViewResolver config --> 40 <!--配置視圖解析器,使頁面能返回頁面邏輯名路徑--> 41 <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> 42 <property name="prefix" value="/WEB-INF/"/> 43 <property name="suffix" value=".jsp"/> 44 </bean> 45 46 <!--配置webSocket--> 47 <bean id="customHandler" class="com.bcy.actiylion.webSocket.controller.WebSocketHander"/> 48 <websocket:handlers allowed-origins="*"> 49 <!--指定webSocket 地址--> 50 <websocket:mapping path="/socket" handler="customHandler" /> 51 <!--webSocket握手--> 52 <websocket:handshake-interceptors> 53 <bean class="com.bcy.actiylion.webSocket.util.WebSocketInterceptor"/> 54 </websocket:handshake-interceptors> 55 </websocket:handlers> 56 57 </beans>
替換websocket xml注冊方式,如果spring-mvc.xml不配置websocket,那么就使用代碼注冊。
setAllowedOrigins(allowsOrigins)--代表匹配所有
@Configuration @EnableWebMvc @EnableWebSocket public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { //允許連接的域,只能以http或https開頭 String[] allowsOrigins = {"*"}; //WebIM WebSocket通道 registry.addHandler(chatWebSocketHandler(),"/webSocketIMServer").setAllowedOrigins(allowsOrigins).addInterceptors(myInterceptor()); } @Bean public ChatWebSocketHandler chatWebSocketHandler() { return new ChatWebSocketHandler(); } @Bean public WebSocketHandshakeInterceptor myInterceptor(){ return new WebSocketHandshakeInterceptor(); } }
web.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xmlns="http://xmlns.jcp.org/xml/ns/javaee" 4 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" 5 version="3.1"> 6 7 8 9 <context-param> 10 <param-name>contextConfigLocation</param-name> 11 <param-value>classpath*:spring/spring-*.xml</param-value> 12 </context-param> 13 14 15 16 <!-- ServletContext監聽器 begin --> 17 <listener> 18 <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> 19 </listener> 20 <!-- Spring內存溢出監聽器 begin --> 21 <listener> 22 <listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class> 23 </listener> 24 25 26 27 <!-- 如果是用mvn命令生成的xml,需要修改servlet版本為3.1 --> 28 <!-- 配置DispatcherServlet --> 29 <servlet> 30 <servlet-name>SpringMVC</servlet-name> 31 <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> 32 <!-- 配置springMVC需要加載的配置文件--> 33 <init-param> 34 <param-name>contextConfigLocation</param-name> 35 <param-value>classpath:spring/springmvc.xml</param-value> 36 </init-param> 37 <load-on-startup>1</load-on-startup> 38 <async-supported>true</async-supported> 39 </servlet> 40 41 42 43 <servlet-mapping> 44 <servlet-name>SpringMVC</servlet-name> 45 <!-- 默認匹配所有的請求 --> 46 <url-pattern>/</url-pattern> 47 </servlet-mapping> 48 49 50 51 <!-- 字符過濾器 --> 52 <filter> 53 <filter-name>encodingFilter</filter-name> 54 <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> 55 <async-supported>true</async-supported> 56 <init-param> 57 <param-name>encoding</param-name> 58 <param-value>UTF-8</param-value> 59 </init-param> 60 <init-param> 61 <param-name>forceEncoding</param-name> 62 <param-value>true</param-value> 63 </init-param> 64 </filter> 65 <filter-mapping> 66 <filter-name>encodingFilter</filter-name> 67 <url-pattern>/*</url-pattern> 68 </filter-mapping> 69 <!-- session超時時間 單位是分鍾 --> 70 <session-config> 71 <session-timeout>30</session-timeout> 72 </session-config> 73 74 </web-app>
login.jsp-----發送json信息
1 <%@ page language="java" contentType="text/html; charset=UTF-8" 2 pageEncoding="UTF-8"%> 3 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> 4 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 5 <html> 6 <head> 7 <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> 8 <title>Insert title here</title> 9 10 </head> 11 <script type="text/javascript"> 12 13 window.onload=function(){ 14 //var websocket = null; 15 16 var ws = new WebSocket("ws://localhost:18080/acitylion/socket"); 17 // 建立 web socket 連接成功觸發事件 18 19 ws.onopen = function () { 20 var message = { 21 time: new Date(), 22 text: "Hello world!", 23 clientId: "asdfp8734rew" 24 }; 25 ws.send(JSON.stringify(message)); 26 alert("數據發送中..."); 27 }; 28 29 30 31 // 接收服務端數據時觸發事件 32 ws.onmessage = function (evt) { 33 var received_msg = evt.data; 34 var bcy = typeof(received_msg); 35 console.log(received_msg); 36 console.log(bcy); 37 38 }; 39 40 41 42 // 斷開 web socket 連接成功觸發事件 43 ws.onclose = function () { 44 alert("連接已關閉..."); 45 }; 46 } 47 48 50 </script> 51 <body> 52 登錄頁面 53 </body> 54 </html>
定義一個攔截器
WebSocketInterceptor.class
1 package com.bcy.actiylion.webSocket.util; 2 3 import java.util.Map; 4 5 6 import org.springframework.http.server.ServerHttpRequest; 7 import org.springframework.http.server.ServerHttpResponse; 8 import org.springframework.http.server.ServletServerHttpRequest; 9 import org.springframework.web.socket.WebSocketHandler; 10 import org.springframework.web.socket.server.HandshakeInterceptor; 11 12 public class WebSocketInterceptor implements HandshakeInterceptor{ 13 14 // 初次握手訪問前 15 public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, 16 Map<String, Object> attributes) throws Exception { 17 System.out.println("攔截器之前"); 18 if (request instanceof ServletServerHttpRequest) { 19 //可以在這里完成你想要的功能。 20 } 21 System.out.println("攔截器之前完成"); 22 return true; 23 } 24 25 //初次握手后 26 public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, 27 Exception exception) { 28 System.out.println("攔截器之后"); 29 30 } 31 32 }
核心代碼
1 package com.bcy.actiylion.webSocket.controller; 2 3 4 5 import java.io.IOException; 6 import java.util.HashMap; 7 import java.util.Timer; 8 import java.util.TimerTask; 9 10 11 12 import org.springframework.web.socket.CloseStatus; 13 import org.springframework.web.socket.TextMessage; 14 import org.springframework.web.socket.WebSocketHandler; 15 import org.springframework.web.socket.WebSocketMessage; 16 import org.springframework.web.socket.WebSocketSession; 17 18 19 20 import com.fasterxml.jackson.databind.ObjectMapper; 21 22 23 24 public class WebSocketHander implements WebSocketHandler { 25 26 27 28 //連接建立后處理 29 public void afterConnectionEstablished(WebSocketSession session) throws Exception { 30 System.out.println("afterConnectionEstablished"); 31 32 } 33 //接收文本消息,並發送出去 34 public void handleMessage(final WebSocketSession session, WebSocketMessage<?> message) throws Exception { 35 String params = (String) message.getPayload(); 36 ObjectMapper mapper = new ObjectMapper(); 37 HashMap<String,Object> map = mapper.readValue(params, HashMap.class); 38 final Object time = map.get("time"); 39 final Object text = map.get("text"); 40 final Object clientId = map.get("clientId"); 41 System.out.println(time.toString()); 42 System.out.println(text.toString()); 43 System.out.println(clientId.toString()); 44 //定時每3秒鍾返回給前台數據 45 Timer timer = new Timer(); 46 timer.schedule(new TimerTask() { 47 @Override 48 public void run() { 49 TextMessage reply = new TextMessage("time : "+time+"text:"+text+"clientId:"+clientId); 50 try { 51 session.sendMessage(reply); 52 } catch (IOException e) { 53 e.printStackTrace(); 54 } 55 56 } 57 }, 0,3 * 1000); 58 59 } 60 //拋出異常時處理 61 public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { 62 System.out.println("handleTransportError"); 63 64 } 65 //連接關閉后處理 66 public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { 67 System.out.println("afterConnectionClosed"); 68 69 } 70 71 72 73 public boolean supportsPartialMessages() { 74 System.out.println("supportsPartialMessages"); 75 return false; 76 } 77 78 }
代碼完成。
有問題聯系我吧