Netty入站出站事件
入站事件:
通常指I/O線程生成入站數據。(通俗理解:從socket底層往上冒的事件都是入站)比如EventLoop收到selector的OP_READ事件,入站處理器調用socketChannel.read(ByteBuffer)接收到數據後,這將通道ChannelPipeline中包含的下一個channelRead方法被調用。
出站事件:
經常是指I/O線程執行實際的輸出操作。(通俗理解:主動往socket底層操作的事件都是出站)比如bind方法的用意是請求server socket綁定的SocketAddress,這將導致通道的ChannelPipline包含下一個出站處理器中bind方法被調用。
Netty通過責任鏈實現出入站事件處理
我們在使用責任鏈的時候更多的是
netty的責任鏈,與我們我們經常使用到責任鏈相比多了ChannelPipeline來維護這個責任鏈。
ChannelPipeline鏈中存儲的也並不是直接的處理的處理器,而是包裹handler處理器的DefaultChannelHandlerContext。
對於我這邊理解之所以引入ChannelPipeline這個上下文,是因爲在在使用handler處理器的時候,需要有添加和刪除handler的操作,用ChannelPipeline這個這個責任鏈能夠更好管理hanler處理。
DefaultChannelHandlerContext這種包裝的代替handler的原因,是在handler處理的時候,可能會經常遇到另外線程池的處理,而更好關注業務本身用DefaultChannelHandlerContext代替了handler.
那我之所以需要分入站與出站,這個是因爲我們在添加責任鏈的時候,入站事件只會處理入站事件,而出站同樣也只能處理出站事件。
藉助責任鏈簡單解決一下網絡編程中拆包和粘包
拆包和粘包出現的原因:
(1)由於Nagle算法可能會將一些,要發送的數據小於TCP發送緩衝區的大小,TCP將多次寫入緩衝區的數據一次發送出去,將會發生粘包;
(2)接收數據端的應用層沒有及時讀取接收緩衝區中的數據,將發生粘包;
(3)要發送的數據大於TCP發送緩衝區剩餘空間大小,將會發生拆包;
(4)待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。即TCP報文長度-TCP頭部長度>MSS。
拆包和粘包出現的解決方案:
由於底層的TCP無法理解上層的業務數據,所以在底層是無法保證數據包不被拆分和重組的,這個問題只能通過上層的應用協議棧設計來解決,根據業界的主流協議的解決方案,歸納如下:
(1)、使用帶消息頭的協議、消息頭存儲消息開始標識及消息長度信息,服務端獲取消息頭的時候解析出消息長度,然後向後讀取該長度的內容。
(2)、設置定長消息,服務端每次讀取既定長度的內容作爲一條完整消息。
(3)、設置消息邊界,服務端從網絡流中按消息編輯分離出消息內容。
Netty自帶的拆包器:
(1)、固定長度的拆包器FixedLengthFrameDecoder:如果應用層協議非常簡單,每個數據包的長度都是固定的,需要把拆包器加到Pipeline,Netty把指定長度的數據包(ByteBuf)傳遞到下一個ChannelHandler.
(2)、行拆包器LineBasedFrameDecoder:發送端發送數據包的時候,每個數據包之間以換行符作爲分隔,接收端通過LineBasedFrameDecoder將粘包的ByteBuf拆分成一個個完整的應用層數據包.
(3)、分隔符拆包器DelimiterBasedFrameDecoder:行拆包器的通用版本,自定義分隔符.
(4)、基於長度域拆包器LengthFieldBasedFrameDecoder:最通用的一種拆包器,只要自定義協議包含長度域字段均使用此拆包器實現應用層拆包。使用LengthFieldBasedFrameDecoder需要長度域相對整個數據包的偏移量和長度域的長度構造拆包器,Pipeline最前面添加此拆包器。
代碼編寫:
1、這裏通過最簡單 固定發送長度這種最簡單的粗略的方案來演示這個問題與處理。這裏採用每次發送210個字節。
服務端代碼:
public class XNettyServer {
public static void main(String[] args) throws Exception {
// 1、 線程定義
// accept 處理連接的線程池線程大小爲默認當前cpu的兩倍
//DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
EventLoopGroup acceptGroup = new NioEventLoopGroup();
// read io 處理數據的線程池
EventLoopGroup readGroup = new NioEventLoopGroup();
//業務線程,防止處理線程的任務因爲任務阻塞
NioEventLoopGroup bizGroup = new NioEventLoopGroup(4,new DefaultThreadFactory("biz-thread-pool-"));
try {
ServerBootstrap b = new ServerBootstrap();
b.group(acceptGroup, readGroup);
// 2、 選擇TCP協議,NIO的實現方式
b.channel(NioServerSocketChannel.class);
// ChannelOption.SO_KEEPALIVE表示是否開啓TCP底層心跳機制,true爲開啓
b.childOption(ChannelOption.SO_KEEPALIVE, true);
//ChannelOption.SO_REUSEADDR表示端口釋放後立即就可以被再次使用,因爲一般來說,一個端口釋放後會等待兩分鐘之後才能再被使用
b.childOption(ChannelOption.SO_REUSEADDR, true);
// ChannelOption.TCP_NODELAY表示是否開始Nagle算法,true表示關閉,false表示開啓,通俗地說,如果要求高實時性,有數據發送時就馬上發送,就關閉,如果需要減少發送次數減少網絡交互就開啓
b.childOption(ChannelOption.TCP_NODELAY, false);
//設置發送緩衝區大小
// b.childOption(ChannelOption.SO_SNDBUF, 32 * 1024);
//設置接收緩衝區大小
// b.childOption(ChannelOption.SO_RCVBUF, 32 * 1024);
XHandller xHandller = new XHandller();
b.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 3、 職責鏈定義(請求收到後怎麼處理)
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new XDecoder());
pipeline.addLast(bizGroup,xHandller);
}
});
// 4、 綁定端口
System.out.println("啓動成功,端口 9999");
b.bind(9999).sync().channel().closeFuture().sync();
} finally {
acceptGroup.shutdownGracefully();
readGroup.shutdownGracefully();
}
}
}
根據定義每次發送210個字節來進行解碼
public class XDecoder extends ByteToMessageDecoder {
static final int PACKET_SIZE = 220;
// 用來臨時保留沒有處理過的請求報文
ByteBuf tempMsg = Unpooled.buffer();
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("收到了一次數據包,長度是:" + in.readableBytes());
// in 請求的數據
// out 將粘在一起的報文拆分後的結果保留起來
// 1、 合併報文
ByteBuf message = null;
int tmpMsgSize = tempMsg.readableBytes();
// 如果暫存有上一次餘下的請求報文,則合併
if (tmpMsgSize > 0) {
message = Unpooled.buffer();
message.writeBytes(tempMsg);
message.writeBytes(in);
System.out.println("合併:上一數據包餘下的長度爲:" + tmpMsgSize + ",合併後長度爲:" + message.readableBytes());
} else {
message = in;
}
// 2、 拆分報文
int size = message.readableBytes();
int counter = size / PACKET_SIZE;
for (int i = 0; i < counter; i++) {
byte[] request = new byte[PACKET_SIZE];
// 每次從總的消息中讀取210個字節的數據
message.readBytes(request);
// 將拆分後的結果放入out列表中,交由後面的業務邏輯去處理
out.add(Unpooled.copiedBuffer(request));
}
// 3、多餘的報文存起來
// 第一個報文: i+ 暫存
// 第二個報文: 1 與第一次
size = message.readableBytes();
if (size != 0) {
System.out.println("多餘的數據長度:" + size);
// 剩下來的數據放到tempMsg暫存
tempMsg.clear();
tempMsg.writeBytes(message.readBytes(size));
}
}
}
業務處理器:
/**
* 後續處理handdler
*/
@ChannelHandler.Sharable
public class XHandller extends ChannelInboundHandlerAdapter {
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 輸出 bytebuf
ByteBuf buf = (ByteBuf) msg;
byte[] content = new byte[buf.readableBytes()];
buf.readBytes(content);
System.out.println(Thread.currentThread().getName() + ":" + new String(content));
//釋放堆外內存 重複利用,gc釋放
// buf.release(); //也快交流後面處理緩衝區的釋放
ctx.fireChannelRead(msg);
}
// 異常
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
客戶端:
public class TonySocketClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 9999);
OutputStream outputStream = socket.getOutputStream();
// 消息長度固定爲 220字節,包含有
// 2. 消息內容字符串長度最多70。 按一個漢字3字節,內容的最大長度爲210字節
byte[] request = new byte[220];
byte[] userId = "useridtest".getBytes();
byte[] content = "發送內容test23444444444444444444444444445test".getBytes();
System.arraycopy(userId, 0, request, 0, 10);
System.arraycopy(content, 0, request, 10, content.length);
//通過併發方式來展示發送消息,這樣更容易出現粘包
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
outputStream.write(request);
} catch (IOException e) {
e.printStackTrace();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
Thread.sleep(2000L); // 兩秒後退出
socket.close();
}
}
處理結果:
當我註釋掉添加解嗎處理器: