Netty WebSocket 協(xié)議
1. 前言
上一節(jié),我們主要講解了 Http 協(xié)議,本節(jié)我們來講解 Netty 支持的另外一個(gè) WebSocket 協(xié)議,WebSocket 是 HTML5 開始支持的全雙工協(xié)議。
在真實(shí)的開發(fā)當(dāng)中,其實(shí) WebSocket 使用非常的廣泛,特別是在目前前端技術(shù)越來越完善的情況下,很多應(yīng)用都是基于前端技術(shù)去實(shí)現(xiàn),比如:APP、小程序等主流應(yīng)用,前端的技術(shù)完全可以開發(fā)出類似原生技術(shù)一樣的產(chǎn)品,并且開發(fā)效率上比原生更加的快、用戶體驗(yàn)更好。
這些應(yīng)用涉及通信、聊天、推送等業(yè)務(wù),則可以使用 WebSocket 來實(shí)現(xiàn),因此,WebSocket 已經(jīng)是目前非常主流的瀏覽器和服務(wù)端建立長連接的通信技術(shù)。
WebSocket 協(xié)議架構(gòu)圖如下所示:
2. 學(xué)習(xí)目的
Netty 雖然可以開發(fā)客戶端和服務(wù)端的應(yīng)用,但是 Netty 和 WebSocket 結(jié)合則是另外一個(gè)不同的應(yīng)用方向。
應(yīng)用方向一: 純后端的應(yīng)用,比如:框架、中間件通信等,則完全可以使用 Netty 來開發(fā)客戶端和服務(wù)端,并且雙方通過 TCP 協(xié)議來進(jìn)行通信。
應(yīng)用方向二: 前端(瀏覽器)和服務(wù)端之間通信,并且想實(shí)現(xiàn)類似長連接的效果,那么 WebSocket 和 Netty 則是主流,并且這部分的應(yīng)用場景非常的廣泛和運(yùn)用非常的多。
因此,我們在學(xué)習(xí)完本章內(nèi)容之后,我們就可以掌握這類的需求的開發(fā)(比如:聊天、消息推送),自己可以嘗試去開發(fā)一款小程序在線聊天系統(tǒng)。
當(dāng)真實(shí)的開發(fā)當(dāng)中,也許很多人直接使用 Spring 封裝的 WebSocket 或者其它第三方的框架(比如:netty-socketio)去實(shí)現(xiàn)。本節(jié)我們主要講解原生 Netty 來開發(fā) WebSocket 協(xié)議格式的數(shù)據(jù)請(qǐng)求,畢竟會(huì)用和知道知其所以然還是有區(qū)別的。
3. WebSocket 的 API
創(chuàng)建 WebSocket 對(duì)象,第一個(gè)參數(shù) url, 指定連接的 URL。第二個(gè)參數(shù) protocol 是可選的,指定了可接受的子協(xié)議。
var Socket = new WebSocket(url, [protocol] );
WebSocket 事件
事件 | 事件處理程序 | 描述 |
---|---|---|
open | Socket.onopen | 連接建立時(shí)觸發(fā) |
message | Socket.onmessage | 客戶端接收服務(wù)端數(shù)據(jù)時(shí)觸發(fā) |
error | Socket.onerror | 通信發(fā)生錯(cuò)誤時(shí)觸發(fā) |
close | Socket.onclose | 連接關(guān)閉時(shí)觸發(fā) |
4. 環(huán)境搭建
本節(jié),我們主要開發(fā)一個(gè) Demo,主要功能如下:
- 前端頁面(html)啟動(dòng)的時(shí)候,連接 WebSocket 服務(wù)端;
- 服務(wù)端往前端每隔 5s 推送一條消息。
環(huán)境搭建步驟:
- 創(chuàng)建一個(gè) Maven 項(xiàng)目;
- 導(dǎo)入 Netty 坐標(biāo)。
實(shí)例:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
5. 代碼實(shí)現(xiàn)
5.1. Netty 主啟動(dòng)類
public class MyWebSocketServer {
public static void main(String[] args) throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//因?yàn)榛趆ttp協(xié)議,使用http的編碼和解碼器
pipeline.addLast(new HttpServerCodec());
//是以塊方式寫,添加ChunkedWriteHandler處理器
pipeline.addLast(new ChunkedWriteHandler());
//http數(shù)據(jù)在傳輸過程中是分段, HttpObjectAggregator ,就是可以將多個(gè)段聚合
pipeline.addLast(new HttpObjectAggregator(8192));
//將 http協(xié)議升級(jí)為 ws協(xié)議 , 保持長連接
pipeline.addLast(new WebSocketServerProtocolHandler("/hello2"));
//自定義的handler ,處理業(yè)務(wù)邏輯
pipeline.addLast(new MyWebSocketHandler());
}
});
//啟動(dòng)服務(wù)器
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
代碼說明:
- WebSocket 比 TCP 和 Http 協(xié)議都稍微復(fù)雜有點(diǎn),它其實(shí)是 TCP 和 Http 協(xié)議的結(jié)合,首先是連接之前發(fā)送的是 Http 協(xié)議請(qǐng)求,但是新增 Http 所沒有的附加頭信息
Upgrade: WebSocket
表明是一個(gè)申請(qǐng)協(xié)議升級(jí)的 Http 請(qǐng)求。其次,真正建立連接之后,其實(shí)底層是 TCP 協(xié)議長連接; HttpServerCodec
將請(qǐng)求和應(yīng)答消息解碼為 HTTP 消息;ChunkedWriteHandler
向客戶端發(fā)送 HTML5 文件;- http 數(shù)據(jù)在傳輸過程中是分段,當(dāng)瀏覽器發(fā)送大量數(shù)據(jù)時(shí),就會(huì)發(fā)出多次 http 請(qǐng)求,
HttpObjectAggregator
可以將 HTTP 消息的多個(gè)部分合成一條完整的 HTTP 消息; WebSocketServerProtocolHandler
定義了 WebSocket 的對(duì)外暴露地址。
5.2 WebSocket 業(yè)務(wù)類
public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{
private SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//1.獲取Channel通道
final Channel channel=ctx.channel();
//2.創(chuàng)建一個(gè)定時(shí)線程池
ScheduledExecutorService ses=Executors.newScheduledThreadPool(1);
//3.一秒鐘之后只需,并且每隔5秒往瀏覽器發(fā)送數(shù)據(jù)
ses.scheduleWithFixedDelay(new Runnable() {
public void run() {
String sendTime=format.format(new Date());
channel.writeAndFlush(new TextWebSocketFrame("推送時(shí)間=" + sendTime));
}
},1,5, TimeUnit.SECONDS);
}
//接受瀏覽器消息
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("收到消息 " + msg.text());
}
//當(dāng)web客戶端連接后,觸發(fā)方法
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
}
//當(dāng)web客戶端斷開后,觸發(fā)方法
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
}
}
代碼說明:
其實(shí) WebSocket 對(duì)于的 Handler 跟我們普通業(yè)務(wù)的 Handler 沒有什么區(qū)別,這里主要使用定時(shí)線程池定時(shí)往瀏覽器推送消息,這個(gè)是傳統(tǒng)的 Http+Ajax 請(qǐng)求無法實(shí)現(xiàn)的逆向推送效果。
5.3. 前端代碼
function WebSocketTest() {
if ("WebSocket" in window) {
var ws = new WebSocket("ws://localhost:7000/hello2");
//發(fā)送數(shù)據(jù)
ws.onopen = function() {
ws.send("發(fā)送數(shù)據(jù)");
};
//接受數(shù)據(jù)
ws.onmessage = function(evt) {
var received_msg = evt.data;
console.log(">>>"+received_msg)
};
//監(jiān)聽連接關(guān)閉
ws.onclose = function() {
alert("連接已關(guān)閉...");
};
} else {
alert("您的瀏覽器不支持 WebSocket!");
}
}
5.4 測試效果
瀏覽器控制臺(tái)每隔 5s 鐘就能收到服務(wù)端推送過來的消息,如下圖所示:
6. 小結(jié)
- 我們需要了解 WebSocket 的應(yīng)用方向,主要是前端和服務(wù)端建立長連接通信;
- 掌握 WebSocket 幾個(gè)事件監(jiān)聽方法,open ()、message ()、error ()、close ();
- 掌握 Netty 編寫的服務(wù)端寫法,主要是幾個(gè)類 HttpServerCodec、ChunkedWriteHandler、HttpObjectAggregator、WebSocketServerProtocolHandler,Handler 業(yè)務(wù)跟普通業(yè)務(wù)沒啥區(qū)別。