(一)websocket協(xié)議概述
假設(shè)我們要實(shí)現(xiàn)一個WEB版的聊天室可以采用哪些方案?
1.Ajax輪詢?nèi)シ?wù)器取消息
客戶端按照某個時間間隔不斷地向服務(wù)端發(fā)送請求,請求服務(wù)端的最新數(shù)據(jù)然后更新客戶端顯示。這種方式實(shí)際上浪費(fèi)了大量流量并且對服務(wù)端造成了很大壓力。
2.Flash XMLSocket
在 html 頁面中內(nèi)嵌入一個使用了 XMLSocket 類的 Flash 程序。JAVAScript 通過調(diào)用此Flash程序提供的套接口接口與服務(wù)器端的套接口進(jìn)行通信。JavaScript 在收到服務(wù)器端以 XML 格式傳送的信
息后可以很容易地控制 HTML 頁面的內(nèi)容顯示。
- 以上方案的弊端
Ajax 輪詢:
- Http為半雙工協(xié)議,也就是說同一時刻,只有一個方向的數(shù)據(jù)傳送。
- Http消息冗長,包含請求行、請求頭、請求體。占用很多的帶寬和服務(wù)器資源。
- 空輪詢問題。
- 政府項(xiàng)目直接用ajax,別搞那么復(fù)雜,它不存在并發(fā)問題。
Flash XMLSocket
- 客戶端必須安裝 Flash 播放器,而且瀏覽器需要授權(quán)。
- 因?yàn)?XMLSocket 沒有 HTTP 隧道功能,XMLSocket 類不能自動穿過防火墻。
- 因?yàn)槭鞘褂锰捉涌冢枰O(shè)置一個通信端口,防火墻、代理服務(wù)器也可能對非 HTTP 通道端口進(jìn)行限制。
為了解決上述弊端,Html5定義了WebSocket協(xié)義能更好的節(jié)省服務(wù)器資源和寬帶達(dá)到實(shí)時通信的目的。
- webSocket 協(xié)議簡介
webSocket 是html5 開始提供的一種瀏覽器與服務(wù)器間進(jìn)行全雙工二進(jìn)制通信協(xié)議,其基于TCP雙向全雙工作進(jìn)行消息傳遞,同一時刻即可以發(fā)又可以接收消息,相比Http的半雙工協(xié)議性能有很大的提升。
- webSocket特點(diǎn)如下:
- 單一TCP長連接,采用全雙工通信模式。
- 對代理、防火墻透明,80端口必須打開吧。
- 無頭部信息、消息更精簡。
- 通過ping/pong 來保活。
- 服務(wù)器可以主動推送消息給客戶端,不在需要客戶輪詢。
- WebSocket 協(xié)議報文格式
任何應(yīng)用協(xié)議都有其特有的報文格式,比如Http協(xié)議通過 空格 換行組成其報文。如http 協(xié)議不同在于WebSocket屬于二進(jìn)制協(xié)議,通過規(guī)范進(jìn)二進(jìn)位來組成其報文。
- 報文說明:
FIN
標(biāo)識是否為此消息的最后一個數(shù)據(jù)包,占 1 bit
RSV1, RSV2, RSV3
用于擴(kuò)展協(xié)議,一般為0,各占1bit
Opcode
數(shù)據(jù)包類型(frame type),占4bits
0x0:標(biāo)識一個中間數(shù)據(jù)包
0x1:標(biāo)識一個text類型數(shù)據(jù)包
0x2:標(biāo)識一個binary類型數(shù)據(jù)包
0x3-7:保留
0x8:標(biāo)識一個斷開連接類型數(shù)據(jù)包
0x9:標(biāo)識一個ping類型數(shù)據(jù)包
0xA:表示一個pong類型數(shù)據(jù)包
0xB-F:保留
MASK
占1bits
用于標(biāo)識PayloadData是否經(jīng)過掩碼處理。如果是1,Masking-key域的數(shù)據(jù)即是掩碼密鑰,用于解碼
PayloadData。客戶端發(fā)出的數(shù)據(jù)幀需要進(jìn)行掩碼處理,所以此位是1。
Payload length
Payload data的長度,占7bits,7+16bits,7+64bits:
如果其值在0-125,則是payload的真實(shí)長度。如果值是126,則后面2個字節(jié)形成的16bits無符號整型數(shù)的值是payload的真實(shí)長度。注意,網(wǎng)絡(luò)字節(jié)序,需要轉(zhuǎn)換。如果值是127,則后面8個字節(jié)形成的64bits無符號整型數(shù)的值是payload的真實(shí)長度。注意,網(wǎng)絡(luò)字節(jié)序,需要轉(zhuǎn)換。
Payload data
應(yīng)用層數(shù)據(jù)
- WebSocket 在瀏覽當(dāng)中的使用
Http 連接與webSocket 連接建立示意圖
通過javaScript 中的API可以直接操作WebSocket 對象
var ws = new WebSocket(“ws://localhost:8080”);
ws.onopen = function()// 建?成功之后觸發(fā)的事件
{
console.log(“打開連接”);
ws.send("ddd"); // 發(fā)送消息
};
ws.onmessage = function(evt) { // 接收服務(wù)器消息
console.log(evt.data);
};
ws.onclose = function(evt) {
console.log(“WebSocketClosed!”); // 關(guān)閉連接
};
ws.onerror = function(evt) {
console.log(“WebSocketError!”); // 連接異常
};
1.申請一個WebSocket對象,并傳入WebSocket地址信息,這時client會通過Http先發(fā)起握手請求
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket //告訴服務(wù)端需要將通信協(xié)議升級到websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== //瀏覽器base64加密的密鑰,server端收到后需 要提取Sec-WebSocket-Key 信息,然后加密。 Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat //表?客?端請求提供的可供選擇的?協(xié)議 Sec-WebSocket-Version: 13 //版本標(biāo)識
2.服務(wù)端響應(yīng)、并建立連接
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: SIEylb7zRYJAEgiqJXaOW3V+ZWQ=
3.握手成功促發(fā)客戶端 onOpen 事件
- 連接狀態(tài)查看
通過ws.readyState 可查看當(dāng)前連接狀態(tài)可選值
- CONNECTING (0):表示還沒建立連接。
- OPEN (1): 已經(jīng)建立連接,可以進(jìn)行通訊。
- CLOSING (2):通過關(guān)閉握手,正在關(guān)閉連接。
- CLOSED (3):連接已經(jīng)關(guān)閉或無法打開。
(二)netty實(shí)現(xiàn)websocket演示
源碼:https://github.com/limingIOS/netFuture/tree/master/源碼/『互聯(lián)網(wǎng)架構(gòu)』軟件架構(gòu)-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協(xié)議進(jìn)行編解碼
pipeline.addLast("httpServerCodec", new HttpServerCodec());
//ChunkedWriteHandler分塊寫處理,文件過大會將內(nèi)存撐爆
pipeline.addLast("chunkedWriteHandler", new ChunkedWriteHandler());
/**
* 作用是將一個Http的消息組裝成一個完成的HttpRequest或者HttpResponse,那么具體的是什么
* 取決于是請求還是響應(yīng), 該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("來自服務(wù)端: " + 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對象
//當(dāng)有消息過來的時候觸發(fā)
websocket.onmessage = function(event){
var respMessage = document.getElementById("respMessage");
respMessage.value = respMessage.value + "n" + event.data;
}
//連接關(guān)閉的時候觸發(fā)
websocket.onclose = function(event){
var respMessage = document.getElementById("respMessage");
respMessage.value = respMessage.value + "n斷開連接";
}
//連接打開的時候觸發(fā)
websocket.onopen = function(event){
var respMessage = document.getElementById("respMessage");
respMessage.value = "建立連接";
}
}else{
alert("瀏覽器不支持WebSocket");
}
function sendMsg(msg) { //發(fā)送消息
if(window.WebSocket){
if(websocket.readyState == WebSocket.OPEN) { //如果WebSocket是打開狀態(tài)
websocket.send(msg); //send()發(fā)送消息
}
}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="發(fā)送"><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的實(shí)現(xiàn)http和websocket基本也就說到這里,具體netty實(shí)現(xiàn)RPC這塊我沒演示,我感覺沒必要成熟的框架都是基于netty實(shí)現(xiàn)的自己在現(xiàn)實(shí)個RPC真沒必要,如果想看netty實(shí)現(xiàn)RPC直接看dubbo源碼就可以了。






