netty 與 webSocket 起因 有個需求需要用到 ,然後最近又正好在學 ,然後合起來走一波。寫篇文章記錄一下,做一個念想。 協議格式 開始 我們先寫一個什麼都不加的 熱熱手,話不多說,代碼如下 常規的netty入門示例,加了個String的編碼和解碼器,還加了一個列印消息的 ,並不是什麼太 ...
netty 與 webSocket
起因
有個需求需要用到webSocket
,然後最近又正好在學netty
,然後合起來走一波。寫篇文章記錄一下,做一個念想。
協議格式
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
具體每一bit的意思
FIN 1bit 表示信息的最後一幀
RSV 1-3 1bit each 以後備用的 預設都為 0
Opcode 4bit 幀類型,稍後細說
Mask 1bit 掩碼,是否加密數據,預設必須置為1
Payload 7bit 數據的長度
Masking-key 1 or 4 bit 掩碼
Payload data (x + y) bytes 數據
Extension data x bytes 擴展數據
Application data y bytes 程式數據
OPCODE:4位
解釋PayloadData,如果接收到未知的opcode,接收端必須關閉連接。
0x0表示附加數據幀
0x1表示文本數據幀
0x2表示二進位數據幀
0x3-7暫時無定義,為以後的非控制幀保留
0x8表示連接關閉
0x9表示ping
0xA表示pong
0xB-F暫時無定義,為以後的控制幀保留
開始
我們先寫一個什麼都不加的 service
熱熱手,話不多說,代碼如下
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
* @author Sean Wu
*/
public class ServiceMain {
public static void main(String[] args) throws Exception {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(boss, worker)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new StringEncoder()).addLast(new StringDecoder()).addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx, msg);
System.out.println(msg.toString());
}
});
}
});
ChannelFuture f = b.bind(8866).sync();
f.channel().closeFuture().sync();
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
常規的netty入門示例,加了個String的編碼和解碼器,還加了一個列印消息的 Handler
,並不是什麼太複雜的代碼。
添加Http的支持
websocket 協議作為 http 協議的一種升級,最好麽我們先順手添加一下對 Http 協議的支持。首先我們寫一個 HTTPRequestHandler
,話不多說,代碼如下
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
/**
* @author Sean Wu
*/
public class HTTPRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
// 創建要返回的內容
byte[] retBytes = "this a simple http response".getBytes();
ByteBuf byteBuf = Unpooled.copiedBuffer(retBytes);
// 由於http並不是我們關心的重點,我們就直接返回好了
DefaultHttpResponse response = new DefaultHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, retBytes.length);
ctx.writeAndFlush(response);
ctx.writeAndFlush(byteBuf);
}
}
這個 Handler 對 http 協議做了一個最簡單的支持,就是不管客戶端傳啥都返回一個 this a simple http response
。什麼keep-alive
,Expect:100-Continue
都先不管好了,跟我們這次要講的websocket 並沒有什麼關係的說。然後我們改一下我們上面的 ServiceMain
這個類,在Channel里添加對http的支持。代碼如下。
import com.jiuyan.xisha.websocket.handler.HTTPRequestHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
* @author Sean Wu
*/
public class ServiceMain {
public static void main(String[] args) throws Exception {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(boss, worker)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpServerCodec())
.addLast(new HttpObjectAggregator(65536))
.addLast(new HTTPRequestHandler());
}
});
ChannelFuture f = b.bind(8866).sync();
f.channel().closeFuture().sync();
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
可以看到也非常的簡單,介紹下我們這裡用到的幾個Handler
ChannelHandler | 作用 |
---|---|
HttpServerCodec | 對位元組碼根據http協議進行編碼和解碼, |
HttpObjectAggregator | 將一個 HttpMessage 和跟隨它的多個 HttpContent 聚合 |
為單個 FullHttpRequest 或者 FullHttpResponse (取
決於它是被用來處理請求還是響應)。安裝了這個之後,
ChannelPipeline 中的下一個 ChannelHandler 將只會
收到完整的 HTTP 請求或響應
HTTPRequestHandler | 處理 HttpObjectAggregator 送過來的 FullHttpRequest 請求
然後我們運行一下 ServiceMain
然後用瀏覽器訪問一下,正常的話,如圖所示。
添加對 websocket 的支持
首先,我們在剛纔的 HTTPRequestHandler 的 channelRead0 方法里添加對 websocket 介面的特殊處理。修改後的代碼如下
/**
* @author Sean Wu
*/
public class HTTPRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
System.out.println(msg.uri());
// 如果尾碼為 ws 的請求,則增加引用計數,將他傳給下一個 ChannelInboundHandler
if ("/ws".equalsIgnoreCase(msg.uri())) {
ctx.fireChannelRead(msg.retain());
return;
}
// 之前的代碼
}
}
然後我們要加一個處理 websocket
協議的 handler
根據WebSocket 協議,netty 定義瞭如下六種幀
幀類型 | 秒速 |
---|---|
BinaryWebSocketFrame | 充滿了二進位數據流的一個幀,大多是多媒體文件 |
TextWebSocketFrame | 充滿了文本的一個幀 |
CloseWebSocketFrame | 用來關閉websocket的幀 |
PingWebSocketFrame | 用來探活的的一個幀 |
PongWebSocketFrame | 用來表示自己還活著的一個幀 |
Netty 里提供了一個叫 WebSocketServerProtocolHandler
的類,他會幫你處理 Ping
,Pong
,Close
之類的服務狀態的幀。這裡我們只需要簡單的用下TextWebSocketFramce
就好了。
/**
* @author Sean Wu
*/
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
ctx.writeAndFlush(new TextWebSocketFrame("client " + ctx.channel() + "join"));
}
super.userEventTriggered(ctx, evt);
}
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println(msg.text());
ctx.writeAndFlush(new TextWebSocketFrame("hello" + msg.text()));
}
}
這裡我們的例子非常的簡單,可以說是網上所有 netty-websocket 的例子里最簡單的了。我們只是在收到了客戶端的消息之後列印了一下然後原封不動的加個 hello 返回回去。
再然後,我們要改一下我們之前的 ChannelPipeline。添加對 websocket
的支持。改完之後的代碼如下
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(boss, worker)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpServerCodec())
.addLast(new HttpObjectAggregator(65536))
.addLast(new HTTPRequestHandler())
.addLast(new WebSocketServerProtocolHandler("/ws"))
.addLast(new TextWebSocketFrameHandler());
}
});
ChannelFuture f = b.bind(8866).sync();
f.channel().closeFuture().sync();
boss.shutdownGracefully();
worker.shutdownGracefully();
}
運行示例
首先,啟動我們的伺服器。然後打開剛纔的那個頁面(http://127.0.0.1:8866/),打開調試模式(f12)。
然後輸入如下 js 代碼
var ws = new WebSocket("ws://127.0.0.1:8866/ws");
ws.onopen = function(evt) {
console.log("鏈接建立了 ...");
};
ws.onmessage = function(evt) {
console.log( "收到了消息: " + evt.data);
};
可以看到,很完美。然後我們再試著用 ws.send("xisha")
發些消息看。發消息的js代碼和結果如下。
我們也可以打開網路面板查看我們的消息內容。
可以看到,只有一個鏈接。
總結
還有很多沒講到,恩。。。問題不大。下次有機會再說。