(一)websocket協議概述
假設我們要實現一個WEB版的聊天室可以采用哪些方案?
1.Ajax輪詢去服務器取消息
客戶端按照某個時間間隔不斷地向服務端發送請求,請求服務端的最新數據然后更新客戶端顯示。這種方式實際上浪費了大量流量并且對服務端造成了很大壓力。
2.Flash XMLSocket
在 html 頁面中內嵌入一個使用了 XMLSocket 類的 Flash 程序。JAVAScript 通過調用此Flash程序提供的套接口接口與服務器端的套接口進行通信。JavaScript 在收到服務器端以 XML 格式傳送的信
息后可以很容易地控制 HTML 頁面的內容顯示。
- 以上方案的弊端
Ajax 輪詢:
- Http為半雙工協議,也就是說同一時刻,只有一個方向的數據傳送。
- Http消息冗長,包含請求行、請求頭、請求體。占用很多的帶寬和服務器資源。
- 空輪詢問題。
- 政府項目直接用ajax,別搞那么復雜,它不存在并發問題。
Flash XMLSocket
- 客戶端必須安裝 Flash 播放器,而且瀏覽器需要授權。
- 因為 XMLSocket 沒有 HTTP 隧道功能,XMLSocket 類不能自動穿過防火墻。
- 因為是使用套接口,需要設置一個通信端口,防火墻、代理服務器也可能對非 HTTP 通道端口進行限制。
為了解決上述弊端,Html5定義了WebSocket協義能更好的節省服務器資源和寬帶達到實時通信的目的。
- webSocket 協議簡介
webSocket 是html5 開始提供的一種瀏覽器與服務器間進行全雙工二進制通信協議,其基于TCP雙向全雙工作進行消息傳遞,同一時刻即可以發又可以接收消息,相比Http的半雙工協議性能有很大的提升。
- webSocket特點如下:
- 單一TCP長連接,采用全雙工通信模式。
- 對代理、防火墻透明,80端口必須打開吧。
- 無頭部信息、消息更精簡。
- 通過ping/pong 來保活。
- 服務器可以主動推送消息給客戶端,不在需要客戶輪詢。
- WebSocket 協議報文格式
任何應用協議都有其特有的報文格式,比如Http協議通過 空格 換行組成其報文。如http 協議不同在于WebSocket屬于二進制協議,通過規范進二進位來組成其報文。

- 報文說明:
FIN
標識是否為此消息的最后一個數據包,占 1 bit
RSV1, RSV2, RSV3
用于擴展協議,一般為0,各占1bit
Opcode
數據包類型(frame type),占4bits
0x0:標識一個中間數據包
0x1:標識一個text類型數據包
0x2:標識一個binary類型數據包
0x3-7:保留
0x8:標識一個斷開連接類型數據包
0x9:標識一個ping類型數據包
0xA:表示一個pong類型數據包
0xB-F:保留
MASK
占1bits
用于標識PayloadData是否經過掩碼處理。如果是1,Masking-key域的數據即是掩碼密鑰,用于解碼
PayloadData。客戶端發出的數據幀需要進行掩碼處理,所以此位是1。
Payload length
Payload data的長度,占7bits,7+16bits,7+64bits:
如果其值在0-125,則是payload的真實長度。如果值是126,則后面2個字節形成的16bits無符號整型數的值是payload的真實長度。注意,網絡字節序,需要轉換。如果值是127,則后面8個字節形成的64bits無符號整型數的值是payload的真實長度。注意,網絡字節序,需要轉換。
Payload data
應用層數據
- WebSocket 在瀏覽當中的使用
Http 連接與webSocket 連接建立示意圖

通過javaScript 中的API可以直接操作WebSocket 對象
var ws = new WebSocket(“ws://localhost:8080”); ws.onopen = function()// 建?成功之后觸發的事件 { console.log(“打開連接”); ws.send("ddd"); // 發送消息 }; ws.onmessage = function(evt) { // 接收服務器消息 console.log(evt.data); }; ws.onclose = function(evt) { console.log(“WebSocketClosed!”); // 關閉連接 }; ws.onerror = function(evt) { console.log(“WebSocketError!”); // 連接異常 };
1.申請一個WebSocket對象,并傳入WebSocket地址信息,這時client會通過Http先發起握手請求
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket //告訴服務端需要將通信協議升級到websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== //瀏覽器base64加密的密鑰,server端收到后需 要提取Sec-WebSocket-Key 信息,然后加密。 Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat //表?客?端請求提供的可供選擇的?協議 Sec-WebSocket-Version: 13 //版本標識
2.服務端響應、并建立連接
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: SIEylb7zRYJAEgiqJXaOW3V+ZWQ=
3.握手成功促發客戶端 onOpen 事件
- 連接狀態查看
通過ws.readyState 可查看當前連接狀態可選值
- CONNECTING (0):表示還沒建立連接。
- OPEN (1): 已經建立連接,可以進行通訊。
- CLOSING (2):通過關閉握手,正在關閉連接。
- CLOSED (3):連接已經關閉或無法打開。
(二)netty實現websocket演示
源碼:https://github.com/limingIOS/netFuture/tree/master/源碼/『互聯網架構』軟件架構-io與nio線程模型reactor模型(上)(53)/nio
源碼:websocket
WebsocketServer.java
package com.dig8.websocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class WebsocketServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new WebSocketChannelInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(8989).sync();
channelFuture.channel().closeFuture().sync();
}finally{
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
WebSocketChannelInitializer.java
package com.dig8.websocket;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//HttpServerCodec: 針對http協議進行編解碼
pipeline.addLast("httpServerCodec", new HttpServerCodec());
//ChunkedWriteHandler分塊寫處理,文件過大會將內存撐爆
pipeline.addLast("chunkedWriteHandler", new ChunkedWriteHandler());
/**
* 作用是將一個Http的消息組裝成一個完成的HttpRequest或者HttpResponse,那么具體的是什么
* 取決于是請求還是響應, 該Handler必須放在HttpServerCodec后的后面
*/
pipeline.addLast("httpObjectAggregator", new HttpObjectAggregator(8192));
//用于處理websocket, /ws為訪問websocket時的uri
pipeline.addLast("webSocketServerProtocolHandler", new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast("myWebSocketHandler", new WebSocketHandler());
}
}
WebSocketHandler.java
package com.dig8.websocket;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.util.Date;
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
Channel channel = ctx.channel();
System.out.println(channel.remoteAddress() + ": " + msg.text());
ctx.channel().writeAndFlush(new TextWebSocketFrame("來自服務端: " + new Date().toString()));
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("ChannelId" + ctx.channel().id().asLongText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("用戶下線: " + ctx.channel().id().asLongText());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.channel().close();
}
}
test.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Socket</title> <script type="text/javascript"> var websocket; //如果瀏覽器支持WebSocket if(window.WebSocket){ websocket = new WebSocket("ws://localhost:8989/ws"); //獲得WebSocket對象 //當有消息過來的時候觸發 websocket.onmessage = function(event){ var respMessage = document.getElementById("respMessage"); respMessage.value = respMessage.value + "n" + event.data; } //連接關閉的時候觸發 websocket.onclose = function(event){ var respMessage = document.getElementById("respMessage"); respMessage.value = respMessage.value + "n斷開連接"; } //連接打開的時候觸發 websocket.onopen = function(event){ var respMessage = document.getElementById("respMessage"); respMessage.value = "建立連接"; } }else{ alert("瀏覽器不支持WebSocket"); } function sendMsg(msg) { //發送消息 if(window.WebSocket){ if(websocket.readyState == WebSocket.OPEN) { //如果WebSocket是打開狀態 websocket.send(msg); //send()發送消息 } }else{ return; } } </script> </head> <body> <form onsubmit="return false"> <textarea style="width: 300px; height: 200px;" name="message"></textarea> <input type="button" onclick="sendMsg(this.form.message.value)" value="發送"><br> <h3>信息</h3> <textarea style="width: 300px; height: 200px;" id="respMessage"></textarea> <input type="button" value="清空" onclick="javascript:document.getElementById('respMessage').value = ''"> </form> </body> </html>


PS:netty的實現http和websocket基本也就說到這里,具體netty實現RPC這塊我沒演示,我感覺沒必要成熟的框架都是基于netty實現的自己在現實個RPC真沒必要,如果想看netty實現RPC直接看dubbo源碼就可以了。