一直以來對實時通信挺感興趣,本週就抽空了解了一下websocket。javascript
WebSocket是一種網絡傳輸協議,可在單個TCP鏈接上進行全雙工通訊,位於OSI模型的應用層。html
WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只須要完成一次握手,二者之間就能夠建立持久性的鏈接,並進行雙向數據傳輸。java
實踐是最好的老師,爲了瞭解websocket
具體實現,打算用websocket
與``java socket進行通訊。java socket使用的是傳輸層協議,而websocket是應用層協議,這就須要咱們手動處理數據。web
首先要了解的就是websocket的握手過程和數據幀格式。算法
websocket使用http協議進行握手,首先使用http協議發送請求報文,主要是詢問服務器是否支持websocket服務,請求頭主要信息以下:數組
GET ws://localhost:7000/ HTTP/1.1 Host: localhost:7000 Connection: Upgrade Sec-WebSocket-Key: kvMm3tIaxXRCmGHuY01eQw== Sec-WebSocket-Version: 13 Upgrade: websocket
這裏最重要的就是Sec-WebSocket-Key
,這是客戶端生成的隨機字符串並base64編碼,服務端要對此編碼進行響應。瀏覽器
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade WebSocket-Location: ws://127.0.0.1:9527 Sec-WebSocket-Accept: Mf7ptCXn+TYF9XtDt8w+j9FCBEg=
最重要的是Sec-WebSocket-Accept
,客戶端會對此進行驗證,不符合驗證規則都會被視爲服務端拒絕鏈接。生成規則爲客戶端Sec-WebSocket-Key
去除首尾空白,鏈接固定字符串(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)以後,使用sha-1進行hash操做,結果再用base64編碼便可。服務器
java代碼實現:websocket
public static final String RESPONSE_HEADERS = "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "WebSocket-Location: ws://127.0.0.1:9527\r\n"; ServerSocket serverSocket = new ServerSocket(7000); while (true) { Socket socket = serverSocket.accept(); // 開啓一個新線程 Thread thread = new Thread(() -> { // 響應握手信息 try { // 讀取請求頭 byte[] bytes = new byte[10000000]; socket.getInputStream().read(bytes); String requestHeaders = new String(bytes, StandardCharsets.UTF_8); // 獲取請求頭中的 String webSocketKey = ""; for (String header : requestHeaders.split("\r\n")) { if (header.startsWith("Sec-WebSocket-Key")) { webSocketKey = header.split(":")[1].trim(); } } // 將webSocketKey 與 magicKey 拼接用sha1加密以後在進行base64編碼 String value = webSocketKey + magicKey; String webSocketAccept = new String(Base64.encodeBase64(DigestUtils.sha1(value.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8); // 寫入返回頭 握手結束 成功創建鏈接 String responseHeaders = RESPONSE_HEADERS + "Sec-WebSocket-Accept: " + webSocketAccept + "\r\n\r\n"; socket.getOutputStream().write(responseHeaders.getBytes(StandardCharsets.UTF_8)); System.out.println("握手成功,成功創建鏈接"); } } }
首先新建ServerSocket監聽7000端口,當tcp鏈接創建時,從inputStream中讀取客戶端發送的http字節數組,將其轉換爲字符串,此時requestHeaders的值應爲http請求頭。
從中提取出Sec-WebSocket-Key
的值,再根據規則生成webSocketAccept
,拼接到定義好的RESPONSE_HEADERS
,將此數據寫入socket的outputStream
,客戶端收到並驗證經過後,即成功創建鏈接。網絡
成功進行握手後,爲了正常通訊,還須要瞭解websocket的數據幀格式:
基於websocket的數據幀,咱們須要實現兩個方法,一是提取數據幀中的數據,二是將數據轉化爲數據幀。
核心思想就是根據控制字段來肯定數據字段的讀取方式,將其讀取並解碼。
/** * 將字節數組解碼爲字符串 * @param bytes websocket幀字節數組 * @return 解析爲字符串 */ public static String decodeMessage(byte[] bytes) { int col = 0; boolean isMask = false; int dataStart = 2; // 提取websocket幀中的mask位 if ((bytes[1] & 0x80) == 0x80) { isMask = true; } // 提取playload len int len = bytes[1] & 0x7f; byte[] maskKey = new byte[4]; if (len == 126) { // 若是爲126 繼續日後兩個字節讀取做爲playload len len = bytes[2]; len = (len << 8) + bytes[3]; // 如mask爲1 向後讀取4個字節做爲maskKey if (isMask) { maskKey[0] = bytes[4]; maskKey[1] = bytes[5]; maskKey[2] = bytes[6]; maskKey[3] = bytes[7]; // payload data 開始的位置在maskKey以後 dataStart = 8; } else { dataStart = 4; } } else if (len == 127) { // 若是爲126 繼續日後八個字節讀取做爲playload len // 這裏跳過bytes[2]~bytes[5] len = bytes[6]; len = (len << 8) + bytes[7]; len = (len << 8) + bytes[8]; len = (len << 8) + bytes[9]; if (isMask) { maskKey[0] = bytes[10]; maskKey[1] = bytes[11]; maskKey[2] = bytes[12]; maskKey[3] = bytes[13]; dataStart = 14; } else { dataStart = 10; } } else { // 既不是126也不是127 說明長度僅佔七位 不用處理 if (isMask) { maskKey[0] = bytes[2]; maskKey[1] = bytes[3]; maskKey[2] = bytes[4]; maskKey[3] = bytes[5]; dataStart = 6; } else { dataStart = 2; } } // 讀取payload data 並根據isMask判讀是否進行mask加密 for (int i = 0, count = 0; i < len; i++) { byte t1 = maskKey[count]; byte t2 = bytes[i + dataStart]; // 從datastart 開始讀取data char c = isMask ? (char) (((~t1) & t2) | (t1 & (~t2))) : (char) t2; // isMask爲真,進行mask加密 bufferRes[col++] = c; count = (count + 1) % 4; } bufferRes[col++] = '\0'; return new String(bufferRes); }
核心思想就是根據信息格式將要發送的數據轉化爲websocket數據幀。
/** * 將message編碼爲websocket幀 * @param message 字符信息 * @param isMask 是否進行mask加密 * @param result 保存幀的字節數組 * @return 字節長度 */ public static int encodeMessage(String message, boolean isMask, byte[] result) { int dataEnd = 0; // 幀的第一個字節爲類型, 設置爲默認類型爲text幀 result[dataEnd++] = (byte) 0x81; byte[] maskKey = new byte[4]; // 獲取message 的字節數組 byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); // isMask爲真 設置mask位爲1 if (isMask) { result[dataEnd] = (byte) 0x80; } // 判斷數據的長度 long dataLen = messageBytes.length; if (dataLen < 126) { // 小於126字節 直接賦值 result[dataEnd++] |= dataLen & 0x7f; } else if (dataLen < 65536) { // 小於65536字節,日後賦值兩個字節 result[dataEnd++] |= 0x7E; result[dataEnd++] = (byte) ((dataLen >> 8) & 0xFF); result[dataEnd++] = (byte) ((dataLen >> 0) & 0xFF); } else if (dataLen < 0xFFFFFFFF) { // 小於0xFFFFFFFF個字節,日後賦值八個字節 // 避免數據過大 跳過4個字節 result[dataEnd++] |= 0x7F; result[dataEnd++] |= 0; result[dataEnd++] |= 0; result[dataEnd++] |= 0; result[dataEnd++] |= 0; result[dataEnd++] = (byte) ((dataLen >> 24) & 0xFF); result[dataEnd++] = (byte) ((dataLen >> 16) & 0xFF); result[dataEnd++] = (byte) ((dataLen >> 8) & 0xFF); result[dataEnd++] = (byte) ((dataLen >> 0) & 0xFF); } if (isMask) { // 若是isMask爲真 將數據進行mask加密再保存到幀中 new Random().nextBytes(maskKey); result[dataEnd++] = maskKey[0]; result[dataEnd++] = maskKey[1]; result[dataEnd++] = maskKey[2]; result[dataEnd++] = maskKey[3]; for (int i = 0, count = 0; i < dataLen; i++) { byte t1 = maskKey[count]; byte t2 = messageBytes[i]; result[dataEnd++] = (byte) (((~t1) & t2) | (t1 & (~t2))); count = (count + 1) % 4; } } else { // 直接保存到幀中 for (int i = 0; i < dataLen; i++) { result[dataEnd++] = messageBytes[i]; } } return dataEnd; }
java
端提供ServerSocket
用於創建socket鏈接,鏈接成功後,對websocket作出握手響應,讀取websocket幀
時解碼讀取信息,發送信息時轉化爲websocket幀
。
public static int userCount = 0; public static char[] bufferRes = new char[131072]; public static Scanner sc = new Scanner(System.in); public static final String RESPONSE_HEADERS = "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "WebSocket-Location: ws://127.0.0.1:9527\r\n"; public static String magicKey = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(7000); while (true) { Socket socket = serverSocket.accept(); userCount++; // 開啓一個新線程 Thread thread = new Thread(() -> { // 響應握手信息 try { // 讀取請求頭 byte[] bytes = new byte[10000000]; socket.getInputStream().read(bytes); String requestHeaders = new String(bytes, StandardCharsets.UTF_8); // 獲取請求頭中的 String webSocketKey = ""; for (String header : requestHeaders.split("\r\n")) { if (header.startsWith("Sec-WebSocket-Key")) { webSocketKey = header.split(":")[1].trim(); } } // 將webSocketKey 與 magicKey 拼接用sha1加密以後在進行base64編碼 String value = webSocketKey + magicKey; String webSocketAccept = new String(Base64.encodeBase64(DigestUtils.sha1(value.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8); // 寫入返回頭 握手結束 成功創建鏈接 String responseHeaders = RESPONSE_HEADERS + "Sec-WebSocket-Accept: " + webSocketAccept + "\r\n\r\n"; socket.getOutputStream().write(responseHeaders.getBytes(StandardCharsets.UTF_8)); System.out.println("握手成功,成功創建鏈接"); // 接受客戶端信息 while (true) { System.out.println("讀取信息"); socket.getInputStream().read(bytes); String message = decodeMessage(bytes); System.out.println("讀取到的信息爲:" + message); System.out.println("請回覆信息"); String res = sc.next(); byte[] result = new byte[10000000]; int len = encodeMessage(res, false, result); socket.getOutputStream().write(result, 0, len); } } catch (IOException e) { e.printStackTrace(); } System.out.println("finish Read"); }); thread.setName("用戶" + userCount); thread.start(); } }
客戶端用簡單js
代碼實現(感謝趙凱強同窗提供)。
<!DOCTYPE html> <html> <head> <title>websocket test</title> </head> <body> <ul id="ul"> </ul> <input id="input" type="input" /> <button onclick="onClick()">發送</button> <script type="text/javascript"> var ws = new WebSocket("ws://localhost:7000"); ws.onopen = function(evt) { console.log("Connection open ..."); }; ws.onmessage = function(evt) { console.log( "Received Message: " + evt.data); addLi('接受: ' + evt.data); }; ws.onclose = function(evt) { console.log("Connection closed."); }; // 點擊發送 讀取input的值打印到控制檯上 function onClick() { var value = '發送: ' + document.getElementById("input").value; ws.send(value); document.getElementById("input").value = ''; addLi(value); } // 一個方法 sring 當調用時, 把字符串插入到列表中 function addLi(value) { document.getElementById("ul").innerHTML += "<li>" + value + "</li>"; } </script> </body> </html>
效果: