如何使用Tomcat實現WebSocket即時通訊服務服務端
摘要:HTTP協議是“請求-響應”模式,瀏覽器必須先發請求給伺服器,伺服器才會響應該請求。即伺服器不會主動傳送資料給瀏覽器。
本文分享自華為雲社群《Tomcat支援WebSocket嗎?》,作者: JavaEdge 。
HTTP協議是“請求-響應”模式,瀏覽器必須先發請求給伺服器,伺服器才會響應該請求。即伺服器不會主動傳送資料給瀏覽器。
實時性要求高的應用,如線上遊戲、股票實時報價和線上協同編輯等,瀏覽器需實時顯示伺服器的最新資料,因此出現Ajax和Comet技術:
- Ajax本質還是輪詢
- Comet基於HTTP長連線做了一些hack
但它們實時性不高,頻繁請求也會給伺服器巨大壓力,也浪費網路流量和頻寬。於是HTML5推出WebSocket標準,使得瀏覽器和伺服器之間任一方都可主動發訊息給對方,這樣伺服器有新資料時可主動推給瀏覽器。
WebSocket原理
網路上的兩個程式通過一個雙向鏈路進行通訊,這個雙向鏈路的一端稱為一個Socket。一個Socket對應一個IP地址和埠號,應用程式通常通過Socket向網路發出或應答網路請求。
Socket不是協議,是對TCP/IP協議層抽象出來的API。
WebSocket跟HTTP協議一樣,也是應用層協議。為相容HTTP協議,它通過HTTP協議進行一次握手,握手後資料就直接從TCP層的Socket傳輸,與HTTP協議再無關。
這裡的握手指應用協議層,不是TCP層,握手時,TCP連線已建立。即HTTP請求裡帶有websocket的請求頭,服務端回覆也帶有websocket的響應頭。
瀏覽器發給服務端的請求會帶上跟WebSocket有關的請求頭,比如Connection: Upgrade和Upgrade: websocket
若伺服器支援WebSocket,同樣會在HTTP響應加上WebSocket相關的HTTP頭部:
這樣WebSocket連線就建立好了。
WebSocket的資料傳輸以frame形式傳輸,將一條訊息分為幾個frame,按先後順序傳輸出去。為何這樣設計?
- 大資料的傳輸可以分片傳輸,無需考慮資料大小問題
- 和HTTP的chunk一樣,可邊生成資料邊傳輸,提高傳輸效率
Tomcat如何支援WebSocket
WebSocket聊天室案例
瀏覽器端核心程式碼:
var Chat = {}; Chat.socket = null; Chat.connect = (function(host) {//判斷當前瀏覽器是否支援WebSocket if ('WebSocket' in window) { // 若支援,則建立WebSocket JS類 Chat.socket = new WebSocket(host); } else if ('MozWebSocket' in window) { Chat.socket = new MozWebSocket(host); } else { Console.log('WebSocket is not supported by this browser.'); return; } // 再實現幾個回撥方法 // 回撥函式,當和伺服器的WebSocket連線建立起來後,瀏覽器會回撥這個方法 Chat.socket.onopen = function () { Console.log('Info: WebSocket connection opened.'); document.getElementById('chat').onkeydown = function(event) { if (event.keyCode == 13) { Chat.sendMessage(); } }; }; // 回撥函式,當和伺服器的WebSocket連線關閉後,瀏覽器會回撥這個方法 Chat.socket.onclose = function () { document.getElementById('chat').onkeydown = null; Console.log('Info: WebSocket closed.'); }; // 回撥函式,當伺服器有新訊息傳送到瀏覽器,瀏覽器會回撥這個方法 Chat.socket.onmessage = function (message) { Console.log(message.data); }; });
伺服器端Tomcat實現程式碼:
Tomcat端的實現類加上**@ServerEndpoint**註解,value是URL路徑
@ServerEndpoint(value = "/websocket/chat") public class ChatEndpoint { private static final String GUEST_PREFIX = "Guest"; // 記錄當前有多少個使用者加入到了聊天室,它是static全域性變數。為了多執行緒安全使用原子變數AtomicInteger private static final AtomicInteger connectionIds = new AtomicInteger(0); //每個使用者用一個CharAnnotation例項來維護,請你注意它是一個全域性的static變數,所以用到了執行緒安全的CopyOnWriteArraySet private static final Set<ChatEndpoint> connections = new CopyOnWriteArraySet<>(); private final String nickname; private Session session; public ChatEndpoint() { nickname = GUEST_PREFIX + connectionIds.getAndIncrement(); } //新連線到達時,Tomcat會建立一個Session,並回調這個函式 @OnOpen public void start(Session session) { this.session = session; connections.add(this); String message = String.format("* %s %s", nickname, "has joined."); broadcast(message); } //瀏覽器關閉連線時,Tomcat會回撥這個函式 @OnClose public void end() { connections.remove(this); String message = String.format("* %s %s", nickname, "has disconnected."); broadcast(message); } //瀏覽器傳送訊息到伺服器時,Tomcat會回撥這個函式 @OnMessage public void incoming(String message) { // Never trust the client String filteredMessage = String.format("%s: %s", nickname, HTMLFilter.filter(message.toString())); broadcast(filteredMessage); } // WebSocket連接出錯時,Tomcat會回撥這個函式 @OnError public void onError(Throwable t) throws Throwable { log.error("Chat Error: " + t.toString(), t); } // 向聊天室中的每個使用者廣播訊息 private static void broadcast(String msg) { for (ChatAnnotation client : connections) { try { synchronized (client) { client.session.getBasicRemote().sendText(msg); } } catch (IOException e) { ... } } } }
根據Java WebSocket規範的規定,Java WebSocket應用程式由一系列的WebSocket Endpoint組成。Endpoint是一個Java物件,代表WebSocket連線的一端,就好像處理HTTP請求的Servlet一樣,你可以把它看作是處理WebSocket訊息的介面。
跟Servlet不同的地方在於,Tomcat會給每一個WebSocket連線建立一個Endpoint例項。
可以通過兩種方式。
定義和實現Endpoint
程式設計式
編寫一個Java類繼承javax.websocket.Endpoint,並實現它的onOpen、onClose和onError方法。這些方法跟Endpoint的生命週期有關,Tomcat負責管理Endpoint的生命週期並呼叫這些方法。並且當瀏覽器連線到一個Endpoint時,Tomcat會給這個連線建立一個唯一的Session(javax.websocket.Session)。Session在WebSocket連線握手成功之後建立,並在連線關閉時銷燬。當觸發Endpoint各個生命週期事件時,Tomcat會將當前Session作為引數傳給Endpoint的回撥方法,因此一個Endpoint例項對應一個Session,我們通過在Session中新增MessageHandler訊息處理器來接收訊息,MessageHandler中定義了onMessage方法。在這裡Session的本質是對Socket的封裝,Endpoint通過它與瀏覽器通訊。
註解式
實現一個業務類並給它新增WebSocket相關的註解。
@ServerEndpoint(value = "/websocket/chat")
註解,它表明當前業務類ChatEndpoint是個實現了WebSocket規範的Endpoint,並且註解的value值表明ChatEndpoint對映的URL是/websocket/chat。ChatEndpoint類中有@OnOpen、@OnClose、@OnError和在@OnMessage註解的方法,見名知義。
我們只需關心具體的Endpoint實現,比如聊天室,為向所有人群發訊息,ChatEndpoint在內部使用了一個全域性靜態的集合CopyOnWriteArraySet維護所有ChatEndpoint例項,因為每一個ChatEndpoint例項對應一個WebSocket連線,即代表了一個加入聊天室的使用者。
當某個ChatEndpoint例項收到來自瀏覽器的訊息時,這個ChatEndpoint會向集合中其他ChatEndpoint例項背後的WebSocket連線推送訊息。
- Tomcat主要做了哪些事情呢?
Endpoint載入和WebSocket請求處理。
WebSocket載入
Tomcat的WebSocket載入是通過SCI,ServletContainerInitializer,是Servlet 3.0規範中定義的用來接收Web應用啟動事件的介面。
為什麼要監聽Servlet容器的啟動事件呢?這樣就有機會在Web應用啟動時做一些初始化工作,比如WebSocket需要掃描和載入Endpoint類。
將實現ServletContainerInitializer介面的類增加HandlesTypes註解,並且在註解內指定的一系列類和介面集合。比如Tomcat為了掃描和載入Endpoint而定義的SCI類如下:
定義好SCI,Tomcat在啟動階段掃描類時,會將HandlesTypes註解指定的類都掃描出來,作為SCI的onStartup引數,並呼叫SCI#onStartup。
WsSci#HandlesTypes註解定義了ServerEndpoint.class、ServerApplicationConfig.class和Endpoint.class,因此在Tomcat的啟動階段會將這些類的類例項(不是物件例項)傳遞給WsSci#onStartup。
- WsSci的onStartup方法做了什麼呢?
構造一個WebSocketContainer例項,你可以把WebSocketContainer理解成一個專門處理WebSocket請求的Endpoint容器。即Tomcat會把掃描到的Endpoint子類和添加了註解@ServerEndpoint的類註冊到這個容器,並且該容器還維護了URL到Endpoint的對映關係,這樣通過請求URL就能找到具體的Endpoint來處理WebSocket請求。
WebSocket請求處理
- Tomcat聯結器的元件圖
Tomcat用ProtocolHandler元件遮蔽應用層協議的差異,ProtocolHandler兩個關鍵元件:Endpoint和Processor。
這裡的Endpoint跟上文提到的WebSocket中的Endpoint完全是兩回事,聯結器中的Endpoint元件用來處理I/O通訊。WebSocket本質是個應用層協議,不能用HttpProcessor處理WebSocket請求,而要用專門Processor,在Tomcat就是UpgradeProcessor。
因為Tomcat是將HTTP協議升級成WebSocket協議的,因為WebSocket是通過HTTP協議握手的,當WebSocket握手請求到來時,HttpProtocolHandler首先接收到這個請求,在處理這個HTTP請求時,Tomcat通過一個特殊的Filter判斷該當前HTTP請求是否是一個WebSocket Upgrade請求(即包含Upgrade: websocket的HTTP頭資訊),如果是,則在HTTP響應裡新增WebSocket相關的響應頭資訊,並進行協議升級。
就是用UpgradeProtocolHandler替換當前的HttpProtocolHandler,相應的,把當前Socket的Processor替換成UpgradeProcessor,同時Tomcat會建立WebSocket Session例項和Endpoint例項,並跟當前的WebSocket連線一一對應起來。這個WebSocket連線不會立即關閉,並且在請求處理中,不再使用原有的HttpProcessor,而是用專門的UpgradeProcessor,UpgradeProcessor最終會呼叫相應的Endpoint例項來處理請求。
Tomcat對WebSocket請求的處理沒有經過Servlet容器,而是通過UpgradeProcessor元件直接把請求發到ServerEndpoint例項,並且Tomcat的WebSocket實現不需要關注具體I/O模型的細節,從而實現了與具體I/O方式的解耦。
總結
WebSocket技術實現了Tomcat與瀏覽器的雙向通訊,Tomcat可以主動向瀏覽器推送資料,可以用來實現對資料實時性要求比較高的應用。這需要瀏覽器和Web伺服器同時支援WebSocket標準,Tomcat啟動時通過SCI技術來掃描和載入WebSocket的處理類ServerEndpoint,並且建立起了URL到ServerEndpoint的對映關係。
當第一個WebSocket請求到達時,Tomcat將HTTP協議升級成WebSocket協議,並將該Socket連線的Processor替換成UpgradeProcessor。這個Socket不會立即關閉,對接下來的請求,Tomcat通過UpgradeProcessor直接呼叫相應的ServerEndpoint來處理。
還可以通過Spring來實現WebSocket應用。