Netty5入門(4),netty5入門
這個類實現SimpleChannelInboundHandler,SimpleChannelInboundHandler是一個抽象類別,實現了中定義的channelRead方法,但同時定義了一個抽象的messageReceived方法,因此我們在MyWebSocketServerHandler類中,不需要實現channelRead方法,但需要實現messageReceived方法。當然,我們還需要覆蓋ChannelHandlerAdapter的channelActive方法和channelInactive方法,因為我們想在用戶端串連和關閉時做一些事情。
1、channelActive和channelInactive方法
這兩個方法分別在新的用戶端串連到服務端時觸發,我們僅僅是在這兩個方法中進行Channel的添加和移除操作,並輸出一些內容到控制台而已。
2、messageReceived方法
真正的商務邏輯在這個方法裡。在這個方法中,我們針對用戶端的請求類型進行處理——因為我們不知道用戶端到底會是什麼樣子以及會以何種方式請求服務端。如果用戶端是以WebSocket的方式(即ws://)請求的,我們調用handlerWebSocketFrame進行處理,否則調用handleHttpRequest方法。
3、handleWebSocketFrame方法
這個方法用於處理WebSocket請求,即WebSocket握手完成後(即handshaker已經初始化)的訊息。
首先需要判斷WebSocket請求(即WebSocketFrame類)的具體類型,以進行不同的操作。
這裡需要介紹一下WebSocket 資料轉送格式中的Opcode定義。Opcode是一個4位作業碼,定義承載資料,如果收到了一個未知的作業碼,串連也必須斷掉,以下是定義的作業碼:
* %x0 表示連續訊息片斷
* %x1 表示簡訊片斷
* %x2 表示二進位訊息片斷
* %x3-7 為將來的非控制訊息片斷保留的作業碼
* %x8 表示串連關閉
* %x9 表示心跳檢查的ping
* %xA 表示心跳檢查的pong
* %xB-F 為將來的控制訊息片斷的保留作業碼
根據這個,我們可以對frame的類型進行判斷:
首先判斷是不是WebSocket關閉操作:
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return;
}
如果是,關閉handshaker,即WebSocket會話。
然後判斷是不是Ping訊息,如果是,則發送一個Pong訊息給用戶端:
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(
new PongWebSocketFrame(frame.content().retain()));
return;
}
WebSocket中,Ping、Pong訊息用於心跳檢查。用戶端使用的叫做Ping,服務端對之進行的響應叫做Pong。
注意,凡是引用到frame時,都要retain一下。因為netty所有io操作都是非同步,這樣做是防止frame在還沒有用完的時候就被釋放掉了。
然後判斷訊息的類型是否是簡訊:
if (!(frame instanceof TextWebSocketFrame)) {
System.out.println("本常式僅支援簡訊,不支援二進位訊息");
throw newUnsupportedOperationException(String.format(
"%s frame types notsupported", frame.getClass().getName()));
}
如果不是,拋出UpsupportedOperationException錯誤。
如果前面三種情況都不是,則frame應該是一個合法的WebSocket簡訊了,我們進行接下來的處理:
String request = ((TextWebSocketFrame) frame).text();
System.out.println("服務端收到:" + request);
if (logger.isLoggable(Level.FINE)) {
logger.fine(String.format("%s received %s", ctx.channel(),request));
}
TextWebSocketFrame tws = new TextWebSocketFrame(new Date().toString()+ctx.channel().id() + ":" + request);
// 群發
Global.group.writeAndFlush(tws);
列印收到的內容,記錄日誌,然後最後一句實現群發。
3、handleHttpRequest方法
這個方法處理HTTP請求。一個WebSocket會話的開始其實是由一個HTTP請求開始的。根據HTTP1.1的定義,這個HTTP請求的頭資訊中必須包含一個Upgrade:websocket的key-value。如果不包含則表明這不是一個標準的WebSocket會話的開始,我們可以調用sendHttpResponse輸出一個Bad Request錯誤:
if (!req.getDecoderResult().isSuccess()
||(!"websocket".equals(req.headers().get("Upgrade")))) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}
接下來開始進行WebSocket串連,這是通過建立一個WebSocket的handshaker對象來完成的:
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:7397/websocket", null, false);
handshaker = wsFactory.newHandshaker(req);
如果handshaker建立失敗,發送錯誤訊息,否則開始進行握手動作:
if (handshaker == null) {
WebSocketServerHandshakerFactory
.sendUnsupportedWebSocketVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), req);
}
4、sendHttpResponse方法
這個方法用於向用戶端輸出一些HTTP訊息。
首先它判斷伺服器要輸出的是不是HTTP200狀態(準備就緒),如果不是,表明出錯了,將HTTP狀態代碼輸出給用戶端:
if (res.getStatus().code() != 200) {
ByteBufbuf = Unpooled.copiedBuffer(res.getStatus().toString(),CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
}
ChannelFuture f =ctx.channel().writeAndFlush(res);
然後關閉串連。當然要判斷一下keep alive 和 HTTP 200標誌:
if (!isKeepAlive(req) || res.getStatus().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
根據HTTP1.1協議的規定,
五、NettyServer.java
這個類代表了服務端主線程。在run方法中,我們將所有類串聯在一起。對於netty用戶端,需要用到兩個group:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
其中bossGroup用於所有channel,workGroup則應用於某個channel。
然後是規定動作ServerBootstrap,group,channel以及handler等等,這些代碼都非常模式化,恐怕不需要再多做說明了:
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workGroup);
b.channel(NioServerSocketChannel.class);
b.childHandler(newChildChannelHandler());
System.out.println("服務端開啟等待用戶端串連 ... ...");
Channel ch = b.bind(7397).sync().channel();
ch.closeFuture().sync();
六、index.html
這個就是所謂的用戶端了,是跑在瀏覽器裡的東東。這個檔案隨便你放在那裡(不需要放到web伺服器上),反正用瀏覽器一開啟,其中的js指令碼就會產生一個WebSocket用戶端:
<!DOCTYPE html PUBLIC "-//W3C//DTDXHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<metahttp-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>無標題文檔</title>
</head>
</head>
<script type="text/javascript">
varsocket;
if(!window.WebSocket){
window.WebSocket = window.MozWebSocket;
}
if(window.WebSocket){
socket = newWebSocket("ws://localhost:7397/websocket");
socket.onmessage = function(event){
varta = document.getElementById('responseText');
ta.value+= event.data+"\r\n";
};
socket.onopen = function(event){
varta = document.getElementById('responseText');
ta.value= "開啟WebSoket 服務正常,瀏覽器支援WebSoket!"+"\r\n";
};
socket.onclose = function(event){
varta = document.getElementById('responseText');
ta.value= "";
ta.value= "WebSocket 關閉"+"\r\n";
};
}else{
alert("您的瀏覽器不支援WebSocket協議!");
}
function send(message){
if(!window.WebSocket){return;}
if(socket.readyState== WebSocket.OPEN){
socket.send(message);
}else{
alert("WebSocket串連沒有建立成功!");
}
}
</script>
<body>
<form onSubmit="return false;">
<input type = "text"name="message" value="Netty The Sinper"/>
<br/><br/>
<input type="button"value="發送 WebSocket 請求訊息" onClick="send(this.form.message.value)"/>
<hr color="blue"/>
<h3>服務端返回的應答訊息</h3>
<textarea id="responseText"style="width: 1024px;height: 300px;"></textarea>
</form>
</body>
</html>
function send(message){
if(!window.WebSocket){return;}
if(socket.readyState== WebSocket.OPEN){
socket.send(message);
}else{
alert("WebSocket串連沒有建立成功!");
}
}