1. WebSocket介紹
WebSocket協議是一種雙向通信協議,它建立在TCP之上,同http一樣通過TCP來傳輸數據,但是它和http最大的不同有兩點:1.WebSocket是一種雙向通信協議,在建立連接后,WebSocket服務器和Browser/UA都能主動的向對方發送或接收數據,就像Socket一樣,不同的是WebSocket是一種建立在Web基礎上的一種簡單模擬Socket的協議;2.WebSocket需要通過握手連接,類似於TCP它也需要客戶端和服務器端進行握手連接,連接成功后才能相互通信。簡單的建立握手的時序圖如下:
握手過程:
- Browser與WebSocket服務器通過TCP三次握手建立連接,如果這個建立連接失敗,那么后面的過程就不會執行,Web應用程序將收到錯誤消息通知。
- 在TCP建立連接成功后,Browser/UA通過http協議傳送WebSocket支持的版本號,協議的字版本號,原始地址,主機地址等等一些列字段給服務器端。
- WebSocket服務器收到Browser/UA發送來的握手請求后,如果數據包數據和格式正確,客戶端和服務器端的協議版本號匹配等等,就接受本次握手連接,並給出相應的數據回復,同樣回復的數據包也是采用http協議傳輸。
- Browser收到服務器回復的數據包后,如果數據包內容、格式都沒有問題的話,就表示本次連接成功,觸發onopen消息,此時Web開發者就可以在此時通過send接口想服務器發送數據。否則,握手連接失敗,Web應用程序會收到onerror消息,並且能知道連接失敗的原因。
2. Tomcat 7中的Websocket架構
如圖所示,因為Websocket通信分為握手和數據傳輸兩個過程,兩個過程中需要用到的處理方式是不一樣的,握手過程是基於HTTP 1.1基礎上的,而數據傳輸是直接基於TCP的流傳輸。
握手過程中,在HttpServletRequest的基礎上,封裝了WsHttpServletRequest類,添加了對Request的失效操作函數invalidate()。而在數據通信時,接受和處理數據過程中,基於org.apache.coyote.http11.upgrade.UpgradeInbound重新封裝了用於處理數據輸入流的類StreamInbound,並在StreamInbound的基礎上擴展生成了用於消息處理的類MessageInbound。在這兩個數據處理類中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函數接口,這些接口將在載入的代碼類中實現業務邏輯。在用於數據輸出流的類WsOutbound則是封裝了UpgradeOutbound對象實例,基於UpgradeOutbound對象的基礎上,添加了websocket響應有關的處理邏輯。這里處理函數均為同步調用的函數,保證websocket響應的時序性。
Tomcat中Websocket的處理流程如下:
- 接收客戶端發來的握手請求,Coyote.http11連接器對socket進行解析,形成HttpServletRequest發送給Container。
- Container中的相應WebsocketServlet處理請求,如不接受連接請求,則返回,如接受連接請求,則對請求作出響應,建立起客戶端和服務器的socket連接。
- 服務器此時可以通過WsOutbound發送數據給客戶端,同時通過StreamInbound監聽socket。
- 如果接收到客戶端發來的數據,則將socket數據解析成frame,判斷frame類型,通過事件分發數據到不同的邏輯處理流程。
- 數據返回時調用WsOutbound對返回的數據進行封裝處理,發送給客戶端
3. 代碼分析
- WebSocketServlet
這個類負責WS的握手過程,通過對HTTP請求頭的判斷確定是否接受連接請求。接受連接請求后則建立websocket數據連接,連接建立過程如下所示:
WsHttpServletRequestWrapper wrapper = new WsHttpServletRequestWrapper(req); //將HttpServletRequest封裝可進行失效操作的WsHttpServletRequestWrapper StreamInbound inbound = createWebSocketInbound(subProtocol, wrapper); //建立數據連接,監聽對應的端口 wrapper.invalidate(); //握手完,對這個Request進行invalidate處理
2. StreamInbound
這個類最關鍵的是onData()函數,即接收到數據后的處理函數。這個函數里對接受的數據進行解析,並根據操作碼分發給不同的處理函數。
WsInputStream wsIs = new WsInputStream(processor, getWsOutbound()); //根據當前的Processor和定制的WsOutbound輸出流對象,構建輸入流的解析對象 try { WsFrame frame = wsIs.nextFrame(true); //查找數據中的下一個Frame while (frame != null) { byte opCode = frame.getOpCode(); //查找Frame中的操作碼 if (opCode == Constants.OPCODE_BINARY) { doOnBinaryData(wsIs); //處理Binary數據 } else if (opCode == Constants.OPCODE_TEXT) { InputStreamReader r = new InputStreamReader(wsIs, new Utf8Decoder()); doOnTextData(r); //處理文本數據 } else if (opCode == Constants.OPCODE_CLOSE){ closeOutboundConnection(frame); //數據發送完畢,發送close frame return SocketState.CLOSED; } else if (opCode == Constants.OPCODE_PING) { getWsOutbound().pong(frame.getPayLoad()); //發送pong frame } else if (opCode == Constants.OPCODE_PONG) { } else { closeOutboundConnection( Constants.STATUS_PROTOCOL_ERROR, null); return SocketState.CLOSED; } frame = wsIs.nextFrame(false); } }
3. MessageInbound
該類是StreamInbound的擴展類,實現了對文本數據的解析函數。文本處理過程中,主要用到了ByteBuffer和CharBuffer,通過對Buffer的操作實現文本數據的解析。
4. WsOutbound
該類是處理Websocket輸出流的類,實現了Websocket幾個close,pong,ping和正常數據響應frame。比如在輸出文本數據的處理函數里:
public synchronized void writeTextData(char c) throws IOException { if (closed) { //數據流已關閉 throw new IOException(sm.getString("outbound.closed")); } if (cb.position() == cb.capacity()) { //沒有數據可以返回 doFlush(false); } if (text == null) { text = Boolean.TRUE; } else if (text == Boolean.FALSE) { //如果已經寫好數據准備傳輸 flush(); //輸出數據 text = Boolean.TRUE; } cb.append(c); //將添加到CharBuffer中 }
5. WsInputStream
這個類主要用於輸入流的解析,將數據從InputStream中解析成websocket的frame。類的關鍵邏輯在read()函數中:
public int read(byte b[], int off, int len) throws IOException { makePayloadDataAvailable(); //確保有Payload數據可供讀取 if (remaining == 0) { //frame數據已經讀到尾 return -1; } if (len > remaining) { //重置可讀取數據長度 len = (int) remaining; } int result = processor.read(true, b, off, len); //調用Processor進行數據讀取 if(result == -1) { return -1; } for (int i = off; i < off + result; i++) { b[i] = (byte) (b[i] ^ frame.getMask()[(int) ((readThisFragment + i - off) % 4)]); //獲取幀的Mask } remaining -= result; readThisFragment += result; //已讀幀數據 return result; }