WebSocket從零開始

##WebSocket從零開始 HTML5在前端的發展已經進入到了高潮階段了,特別是在移動端已是跨平臺開發的首選技術,經過hybird相關技術實現和native無縫的對接(這方面本人不是很瞭解,只是聽到過,貌似某寶的移動端app都是HTML5實現)。HTML5裏面除了改進了HTML,CSS和javascript相關前端技術規範,也對前端和後端交互協議提出了新的方式,從而提升前端和後端通訊的效率,它就是WebSocket。 ###What is WebSocket 若是要說什麼是Websocket,那麼一句話就能夠描述它:就是一個瀏覽器端全雙工的TCP長鏈接。這句話裏面兩個詞可以表達WebSocket特性:全雙工,長鏈接。什麼叫全雙工?就是客戶端和服務器之間創建鏈接以後能夠來回的讀寫交互操做,服務器能夠主動發送消息給客戶端,客戶端也能夠主動發送消息給服務端,注意這些操做都是在一次鏈接中。那什麼叫長連接?就是創建鏈接後,不回當即斷開和服務器的鏈接(也能夠理解是一個有狀態的鏈接,和HTTP無狀態鏈接進行區分)。那麼Websocket能夠理解是在服務器和客戶端之間有了一條高速公路,雙方能夠自由的來回運輸「物資」。<br> ###Why need WebSocket 要回答這個問題,須要有特定的業務場景。當前咱們解決web端實時交互,通常是經過ajax輪詢。在設定的時間週期後定時訪問後端獲取最新的數據。好比:咱們要開發一個web版的客服系統,咱們在前端就要定時的ajax請求服務端獲取最新的聊天信息。以上這些都是基於HTTP協議實現的,HTTP協議是基於TCP/IP的短鏈接,雖然在http頭有keep-alive屬性標識鏈接能夠存活一段時間,但也有必定的侷限性;而且HTTP另外一個很累贅的地方是每發起一次HTTP交互都須要帶上HTTP頭信息,通常都在十幾個字節左右,假如咱們每次只是和服務器同步狀態,其實消息就幾個字節,可是仍是須要帶上十幾個字節的消息頭,這嚴重下降了網絡的利用率。<br>javascript

上面介紹了HTTP兩個主要的缺陷(短鏈接,較大的消息頭)。爲了不這兩個缺陷,因而便有了Websocket,上面說過他是一個長TCP鏈接,這就避免了HTTP端每次創建TCP鏈接消耗的時間;Websocket對客戶端和服務端交互的消息格式上面進行了精簡,整個消息頭縮減到兩個字節(16bit)左右,從而提升了整個網絡的利用率。因此能夠說websocket是爲了解決HTTP在實時性比較高的系統上面所遇到的短板而設計出來的。可是也存在必定的缺陷,若是過多的使用WebSocket,因爲是長TCP鏈接,那麼對客戶端和服務器端一直須要保持鏈接開啓,對資源消耗比較厲害。另外一個是雖然Websocket的消息頭雖然很精簡,可是都已經不是明文,可能對調試起來帶來必定的困難,畢竟HTTP只要打印出來均可以懂。前端

###How WebSocket worked 要知道websocket是如何工做的須要知道websocket協議整個過程當中作了哪些事情? ####Shake hand 這是websocket創建鏈接的首要完成的事情,由客戶端向服務端發起一次握手,因而便創建了和服務器之間的TCP長鏈接。這個過程基本上是基於HTTP協議來實現的,創建鏈接以後即是經過websocket協議來通訊。下面看看此次握手客戶端和服務器端是怎麼交流的。<br>java

客戶端發起的握手請求web

GET ws://localhost:8080/ HTTP/1.1<br> Pragma: no-cache<br> Host: localhost:8080<br> Sec-WebSocket-Key: /u/BkxWUx5qWj+HaQemkpg==<br> User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1653.0 Safari/537.36<br> Upgrade: websocket<br> Sec-WebSocket-Extensions: x-webkit-deflate-frame<br> Cache-Control: no-cache<br> Cookie:"c7a8217cbe2e207038395"<br> Connection: Upgrade<br> Sec-WebSocket-Version: 13<br>ajax

上面是一個標準的HTTP頭信息,只是添加了一些擴展的消息頭。其中Connection: Upgrade表示當前是HTTP協議的升級版本,ws://localhost:8080/ 其中「http」被替換成「ws」表示是websocket協議,而非http協議。Upgrade: websocket 表示當前協議升級爲Websocket協議。這些擴展頭信息中 Sec-WebSocket-Key: /u/BkxWUx5qWj+HaQemkpg== 這部份內容是最重要的,這是我是過程當中的密鑰。服務器會根據這個值來產生一個值反饋給客戶端,若是客戶端校驗成功,則鏈接創建成功。那麼看看服務器響應的消息是什麼樣的<br> 服務端響應握手請求後端

HTTP/1.1 101 Switching Protocols<br> Date: Fri Dec 12 09:59:23 CST 2014<br> Access-Control-Allow-Credentials: true<br> Sec-WebSocket-Accept: nnTiIdVxrAUgekCw03CBJRjS6DM=<br> Server: bieber websocket server<br> Connection: Upgrade<br> Access-Control-Allow-Headers: content-type<br> Upgrade: WebSocket<br>數組

能夠看到服務器端響應也是一個標準的HTTP協議。也是在裏面擴展了幾個頭信息,其中只須要關注Sec-WebSocket-Accept: nnTiIdVxrAUgekCw03CBJRjS6DM= 這個消息,這個值是根據客戶端的 Sec-WebSocket-Key 值生成的具體生成規則以下: base64(sha1(Sec-WebSocket-Key+258EAFA5-E914-47DA-95CA-C5AB0DC85B11))瀏覽器

經過上面一次幾乎是標準的HTTP協議的交互則完成的websocket協議的鏈接創建,後面就能夠基於這個鏈接服務端和客戶端進行交互了,而不須要再次簡歷鏈接。服務器

####Send message 經過握手成功,那麼客戶端和服務器端就能夠通訊了。這裏將給你們介紹一下Websocket如何描述一個消息的。先來看看一個消息格式是怎麼樣的。websocket

圖中基本上就兩個字節來描述消息包的內容。下面對這個消息包進行介紹一下:

FIN:1位<br> 表示這是消息的最後一幀(結束幀),一個消息由一個或多個數據幀構成。若消息由一幀構成,起始幀即結束幀。<br> RSV1,RSV2,RSV3:各1位<br> 這幾位是預留的擴展,若是沒有擴展的時候每一個位都爲0,不然爲1<br> OPCODE:4位<br> 解釋PayloadData,若是接收到未知的opcode,接收端必須關閉鏈接。<br> 0x0表示附加數據幀<br> 0x1表示文本數據幀<br> 0x2表示二進制數據幀<br> 0x3-7暫時無定義,爲之後的非控制幀保留<br> 0x8表示鏈接關閉<br> 0x9表示ping<br> 0xA表示pong<br> 0xB-F暫時無定義,爲之後的控制幀保留<br> MASK:1位<br> 用於標識PayloadData是否通過掩碼處理。若是是1,Masking-key域的數據便是掩碼密鑰,用於解碼PayloadData。客戶端發出的數據幀須要進行掩碼處理,因此此位是1。<br> Payload length:7位,7+16位,7+64位<br> PayloadData的長度(以字節爲單位)。<br> 若是其值在0-125,則是payload的真實長度。<br> 若是值是126,則後面2個字節造成的16位無符號整型數的值是payload的真實長度。注意,網絡字節序,須要轉換。<br> 若是值是127,則後面8個字節造成的64位無符號整型數的值是payload的真實長度。注意,網絡字節序,須要轉換。<br> 長度表示遵循一個原則,用最少的字節表示長度(我理解是儘可能減小沒必要要的傳輸)。舉例說,payload真實長度是124,在0-125之間,必須用前7位表示;不容許長度1是126或127,而後長度2是124,這樣違反原則。 Payload長度是ExtensionData長度與ApplicationData長度之和。ExtensionData長度多是0,這種狀況下,Payload長度便是ApplicationData長度。<br>

以上是websocket創建鏈接以後客戶端和服務器之間交互的消息格式。這也是官方給出的解釋。本人也基於官方給出的解釋以及網上收集的資料實現了一個簡單的websocket客戶端,實現裏面沒有考慮Payload length超過125之後的狀況,只考慮在0-125以內的 狀況。

關於實現方面須要注意幾點的是:

服務器端接受客戶端發送的消息的時候因爲瀏覽器端通常都會帶上一個四位的掩碼,那麼咱們接收的消息是經過掩碼計算過的,服務端也必須進行相關的解碼才能獲取真正的消息內容。具體解碼方式是:

for(int i=0;i<messagesize;i++){//messagesize是消息的字節數 realyMessage=receiveMessage[i]^maskKey[i%4]; }

這樣就能夠獲取真正的消息內容。

那麼服務器向服務器發送消息的時候,也是按照上面的消息格式,可是客戶端不接受掩碼計算過的,因此消息包中的Maks位置應該是0,而消息體就是消息內容的字節數組。

既然Websocket這麼好,那麼如今主流的瀏覽器哪些支持websocket呢?下面給出一個列表:

我這裏經過Netty來包裝了一下WebSocket協議,從而實現服務端,下面貼出實現代碼:

<!-- lang:java -->
public class WebSocketServerHandler extends ChannelHandlerAdapter {


private static final String key="258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ByteBuf byteBuf = (ByteBuf) msg;
    ByteArrayOutputStream inputStream = new ByteArrayOutputStream();
    String secKey=null;
    while(byteBuf.isReadable()){
        byte c = byteBuf.readByte();
        if(c=='\n'){
            String content = new String(inputStream.toByteArray());
            System.out.println(content);
            inputStream.reset();
            if(content.startsWith("Sec-WebSocket-Key")){
                secKey=content.replaceAll("Sec-WebSocket-Key:","");
                secKey=secKey.trim();
            }
        }else{
            inputStream.write(c);
        }
    }
    ReferenceCountUtil.release(msg);
    byte[] bytes=null;
    if(secKey!=null){//接受的是握手請求
        MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
        secKey+=key;
        messageDigest.update(secKey.getBytes());
        secKey=Base64.encodeBase64String(messageDigest.digest());
        StringBuffer response = new StringBuffer();
        response.append("HTTP/1.1 101 Switching Protocols\r\n");
        response.append("Connection:Upgrade\r\n");
        response.append("Server:bieber websocket server\r\n");
        response.append("Upgrade:WebSocket\r\n");
        response.append("Date:").append(new Date()).append("\r\n");
        response.append("Access-Control-Allow-Credentials:true\r\n");
        response.append("Access-Control-Allow-Headers:content-type\r\n");
        response.append("Sec-WebSocket-Accept:").append(secKey).append("\r\n");
        response.append("\r\n");
        System.out.println(response.toString());
        bytes = response.toString().getBytes();
        ByteBuf outByte = ctx.alloc().buffer(bytes.length);
        outByte.writeBytes(bytes);
        ctx.writeAndFlush(outByte);
    }else{//接受的是消息
        ByteBuf requestBytes = ctx.alloc().buffer(inputStream.size());
        requestBytes.writeBytes(inputStream.toByteArray());
        requestBytes.readByte();//FIN,RSV1, RSV2, RSV3,Opcode
        byte lengthByte=requestBytes.readByte();//Mask 1,Payload length 7 11010100  01111111
        int lengthInt = lengthByte;
        lengthInt=lengthInt&127;//01111111,能夠屏蔽mask位的內容,從而獲得純消息長度位
        System.out.println("message size "+lengthInt);
        byte[] maskingKeys = new byte[4];
        requestBytes.readBytes(maskingKeys,0,4);
        byte[] clientByte=new byte[lengthInt];
        for(int i=0;i<lengthInt;i++){
            clientByte[i]=(byte)(requestBytes.readByte()^maskingKeys[i%4]);//將接受的消息解碼
        }
        String sendContent = new String(clientByte);
        System.out.println(sendContent);
        byte[] responseBytes = sendContent.getBytes();
        int responseContentSize = responseBytes.length;
        ByteBuf responseByte = ctx.alloc().buffer(2+responseContentSize);//1一個頭,1個length
        responseByte.writeByte(128|1);//FIN,RSV1, RSV2, RSV3,Opcode  10000000|00000001
        responseByte.writeByte(0|responseContentSize);//Mask 1,Payload length 7 00000000|消息長度(在0-125之間),保持MASK位爲0
        responseByte.writeBytes(responseBytes);
        ctx.writeAndFlush(responseByte);
    }

}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
     cause.printStackTrace();
    ctx.close();
}
}






<!-- lang:java -->
public class WebSocketServer {

private int port;

public WebSocketServer(int port){
    this.port = port;
}

public void run() throws InterruptedException {
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workGroup = new NioEventLoopGroup();
    try {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<io.netty.channel.socket.SocketChannel>() {
            @Override
            protected void initChannel(io.netty.channel.socket.SocketChannel ch) throws Exception {
                ch.pipeline().addLast(new WebSocketServerHandler());
            }
        }).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);
        ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
        channelFuture.channel().closeFuture().sync();
    }finally {
        bossGroup.shutdownGracefully();
        workGroup.shutdownGracefully();
    }
}
public static void main(String[] args) throws Exception {
    int port;
    if (args.length > 0) {
        port = Integer.parseInt(args[0]);
    } else {
        port = 8080;
    }
    new WebSocketServer(port).run();
}
}

若是支持websocket的瀏覽器會有WebSocket類。經過在javascript裏面實例化這個類調用它的相關方法便可實現和服務器端經過websocket來實現通訊。我這裏列舉出簡單的使用:

<!-- lang:javascript -->
var ws = new WebSocket("ws://localhost:8080");
ws.onmessage=function(msg){
	console.log(msg.data);	
};
ws.send("hello world!");

想了解websocket協議更多的內容能夠看官方的網站介紹:http://www.websocket.org

相關文章
相關標籤/搜索