- 前言:作為一個剛踏入職場的實習(xí)生,我很幸運參加了某個政府項目,并且在項目中負責(zé)一個核心模塊功能的開發(fā),而不是從頭到尾對數(shù)據(jù)庫的crud。雖然我一直心里抱怨我的工作范圍根本就不是實習(xí)生干的活,因為沒有前輩帶我、入職就開始改bug。但在這樣的環(huán)境下我卻學(xué)到了很多東西,即便老板有壓榨我的嫌疑,卻給了我如此好的歷練機會(但工資方面真的很虧QAQ),畢竟實習(xí)過后跳槽簡歷上還有亮點。
- 背景:在不同的網(wǎng)絡(luò)環(huán)境、不同的開發(fā)語言組成的閉環(huán)系統(tǒng)中,多個模塊之間要實時獲取硬件設(shè)備的狀態(tài),并以xml報文的格式相互傳遞。在必要的時候解析這個報文獲取其中的關(guān)鍵信息完成相應(yīng)的動作,最后直觀地把信息顯示給前端用戶。如此一來就是涉及到了跨終端通信,并且有的功能模塊既要充當(dāng)客戶端分發(fā)指令,還要作為客戶端從別處獲取信息。很不巧的是,我負責(zé)的模塊正是充當(dāng)消息中轉(zhuǎn)站的作用,為了解決這個需求我才有了了解這種機制的機會。
- 正文:由于涉及到不同模塊之間的通信,其中又包含了web前端,為了保持通信協(xié)議的一致性項目采用了ws協(xié)議,即websocket。
http協(xié)議和ws協(xié)議:最直觀的特點就是訪問地址的寫法上略有不同,兩種訪問分別為http://ip:port、ws://ip:port。http協(xié)議訪問采用一問一答的方式——客戶端發(fā)起請求到服務(wù)端,服務(wù)端收到請求后返回對應(yīng)的信息,通信結(jié)束。雖然可以利用ajax發(fā)起異步的http請求,不過都是一次訪問后連接就斷開(ajax輪詢可以解決此問題,對服務(wù)器資源消耗較大);ws協(xié)議建立于TCP協(xié)議之上,我們觀察ws的頭部信息會發(fā)現(xiàn)一件有意思的事情
HttpObjectAggregator$AggregatedFullHttpRequest(decodeResult: success, version: HTTP/1.1, content: CompositeByteBuf(ridx: 0, widx: 0, cap: 0, components=0))
GET / HTTP/1.1
Host: localhost:8888
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (windows NT 10.0; Win64; x64) AppleWebKit/537.36 (Khtml, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Upgrade: websocket
Origin: file://
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Sec-WebSocket-Key: RLnvly29Zl3sJQOfGFjO9w==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
content-length: 0
websocket建立時是以http協(xié)議握手,當(dāng)建立連接之后就不需要http之間雙向通信。協(xié)議深層次的東西以我目前的能力是無法理解的,將來有機會我會再回過頭來分析底層協(xié)議。那么接下來就進入我們的主題。
引入maven依賴(結(jié)合項目實際情況而定):
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>5.0.0.Alpha2</version>
</dependency>
我們的需求自己既是服務(wù)端向其他模塊推送消息,又是客戶端從別處獲取消息后推送其他模塊。那么我們要在自己項目中集成netty客戶端和服務(wù)端,因為客戶端無法給多個連接推送消息。這里我主要介紹
SimpleChannelInboundHandler類的三個方法:
1. channelActive
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客戶端加入連接:"+ctx.channel());
//UserChannelUtil封裝了channelGroup
UserChannelUtil.getChannelGroup().add(ctx.channel());
}
這個方法會在有客戶端連接上來時執(zhí)行,具體作用很多,通常是獲取客戶端的通道號加入到ChannelGroup中,例如注意這個GlobalEventExecutor.INSTANCE是單例,那么就意味著我們操作的是同一個對象,我們將連接的通道加入后將來就可以實現(xiàn)批量推送。
2.channelInactive
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
//斷開連接
System.out.println("客戶端斷開連接:"+ctx.channel());
UserChannelUtil.getChannelGroup().remove(ctx.channel());
}
它表示在客戶端離開時會觸發(fā)該事件,在這個方法內(nèi)我們可以做一些收尾工作——把通道號去除,避免了通過列表的積累造成管理混亂。
3.channelRead
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("收到客戶端:"+ctx.channel()+"的消息:n"+msg.text())
}
很顯然它就是在收到消息后會執(zhí)行該事件,客戶端和服務(wù)端的方法都是相同的。
重點:netty主要是通過連接時產(chǎn)生的通道channel進行消息的推送,只要拿到這個channe就能對任意的客戶端推送消息,我們分析以下場景:當(dāng)客戶端連接服務(wù)端時會產(chǎn)生一個channel,觀察channelRead()方法里面一個參數(shù)是channel,另一個則是接收的消息 。現(xiàn)在我們的任務(wù)就是獲取服務(wù)端channelRead()的channel,在客戶端的channelRead()中調(diào)用服務(wù)端的channelRead()方法,再把剛剛獲取的服務(wù)端channel傳進去這樣通道就以及打通了,我們只需要把客戶端傳來的消息試試傳入就完成了我們的需求。
首先建立通道號保存的對象
@Data
public class ServerChannel{
private static Channel serverChannel;
}
從服務(wù)端獲取通道號
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ServerChannel.setServerChannel(ctx.channel());
System.out.println("收到客戶端:"+ctx.channel()+"的消息:n"+msg.text())
}
在客戶端調(diào)用服務(wù)端的方法
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("收到服務(wù)端:"+ctx.channel()+"的消息:n"+msg.text())
//通過反射創(chuàng)建服務(wù)端對象
Class<WebSocketServerHandler> SocketHandler=WebSocketServerHandler.class;
WebSocketServerHandler socketHandler=SocketHandler.newInstance();
//獲取服務(wù)端的通道號
ChannelHandlerContext serverChannel=ServerCtx.getContext();
if(serverChannel !=null){
socketHandler.channelRead(serverCtx,msg);
}
- 結(jié)尾:說了一大堆做法就這么簡單,分析問題的過程很漫長,理清思路就能高效解決問題。在這里給大家一個提示:netty握手協(xié)議內(nèi)容的大小寫敏感,很有可能會因為不同語言的websocket類庫不同導(dǎo)致握手不成功,這時就需要重寫netty的頭部握手協(xié)議。如有不足之處請各位大佬們指正。