第十章 Netty

10.1 IO模型 10.2 Netty原理源碼 10.3 Netty應用

10.1 IO模型

我覺得這篇IO講得太好了,我自己再解釋一遍就是暴殄天物

Reactor設計模式

所有IO操作都由NIO單線程完成,然後通過這個線程分配handler處理請求。
這樣不會每有新的請求就會創建新的線程。有的線程存活但空閒,不給這些任務分配線程也節約了IO線程空間

10.2 Netty原理源碼

一 原理

Netty主要基於主從Reactor多線程模型

Netty執行流程圖
在這裏插入圖片描述主從線程池具體介紹

1 Netty 抽象2組線程池
2 BossGroup 負責接收客戶端連接
   WorkerGroup 負責網絡讀寫
3 兩者都是NioEventloopGroup類型
4 NioEventloopGroup可有多線程,相當於一個事件循環組,組中多個事件不斷循環,每個事件循環都是一個NioEventloop
5 NioEventloop:不斷循環執行處理任務的線程,每個NioEventloop有一個select用於監聽綁定在其上的socket網絡通訊


6 每個Boss NioEventloopGroup循環執行步驟
	1 輪詢accept事件
	2 處理accept與client連接,生成NioSocketChannel並將其註冊到某個worker NioEventloop上的selector
	3 處理任務隊列任務,即runAllTasks
7 每個Worker NioEventloopGroup循環執行步驟
	1 輪詢讀寫事件
	2 處理I/O事件(即讀寫事件),在對應的NioSocketChannel處理
	3 runAllTasks

8 每個worker NioEventloop處理業務時會使用pipeline管道,理論上channel包含channelpipeline,每個pipeline包含多個ChannelHandlerContext,每個ChannelHandler封裝一個ChannelHandler。每個pipeline也會包含即調出所在channel

bossGroup和WorkerGroup含有的子線程數默認cpu核數×2
子線程輪詢執行
channel和pipeline互相包含
ctx包含了channel和pipeline

Reactor中任務隊列(TaskQueue) runAllTasks

當有一個非常消耗時間的業務
會異步執行(先返回一個結果但IO還沒完成),別的IO處理完再從taskQueue中拿task最後處理)
提交該channel先到對應NioEventloop的taskQueue中

異步模型

ChannelFuture是一個接口 extend Future
可添加監聽器,當監聽事件發生時觸發

異步高併發更穩定更高吞吐量

在這裏插入圖片描述

ChannelFuture cf = bootstrap.bind(6668).syn();
cf.addListener(new ChannelFutureListener(){
	@Override
	~ operationComplete(ChannelFuture future){
		if(cf.isSuccess()){
			System.out.println("監聽成功")}else{
			System.out.println("監聽失敗")}
	}
})

Netty核心組件

Bootstrap,ServerBootstrap 
	配置整個Netty程序,串聯各個組件
	bootstrap.handler(xxx) //對應BossGroup
	bootstrap.childHandler(xxx) //對應workerGroup

Future, ChannelFuture 
	返回異步對象

Channel 
	執行網絡I/O操作
	可通過Channel獲得當前網絡連接通道狀態,網絡連接配置參數,提供異步操作
	不同協議,不同阻塞提供不同類型的Channel

Selector
	1 Netty基於selector實現I/O多路複用,一個selector線程可監聽多個連接的channel事件
	2 當向selector中註冊channel,selector可不斷自動查詢這些註冊的channel是否有就緒狀態

ChannelHandler及其實現類
	ChannelHandler一個接口, 處理或攔截I/O事件,並將其轉發到其ChannelPipeline中下一個

ChannelPipeline
	提供了ChannelHandler鏈的容器,如client爲例,事件從client->server,稱該事件爲出站。即client發送給server的數據會通過pipeline中一系列ChannelOutBoundHandler處理,反之爲入站
	是一個雙向鏈表,入站事件從head往tail傳,出站tail往head傳,兩種類型handler互不干擾
	channelPipeline實現了一種高級形式攔截過濾器模式,使用戶可控制事件處理方式,及Channel中各個handler如何互相交互

ChannelHandlerContext
	保存channel相關所有信息,同時關聯一個channelHandler對象,即context中包含了具體事件處理器channelHandler也有對應的pipeline和channel信息,方便對channelHandler調用

ChannelOption
	創建channel實例後設置channelOption參數,如初始化服務器可用隊列大小,心跳保持連接等

EventLoopGroup
	1 一組Eventloop爲更好利用多核CPU資源,一般會有多個EventLoop同時工作,一個EventLoop維護一個selector
	2 提供next接口,按規則拿其中Eventloop處理任務,如BossEventloop和WorkerEventloop
	3 通常一個服務器端口即一個ServerSocketChannel對應一個selector和一個eventloop線程。BossEventLoop接收client連接並將socketChannel交給workerEventLoop處理IO

Unpooled類
	用來操作緩衝區工具類(Netty數據容器),不用flip反轉,底層維護了readerIndex,writerIndex

Netty零拷貝

heapByteBuffer是JVM heap上分配的Byte緩衝區,簡單看成對這個byte緩衝區的直接byte[]數組封裝,不需要拷貝直接引用封裝寫到DirectByteBuffer(機器內存上實現零拷貝)

二 基本源碼

看完應用,再回來看源碼

Netty對象池源碼

在這裏插入圖片描述在這裏插入圖片描述
核心組件源碼

1 啓動類 main方法中首先創建SSL配置類
2 創建兩個EventLoopGroup對線
	1 bossGroup 接受TCP請求,然後交給workerGroup通信
	2 EventLoopGroup 包含多個Eventlooop,可註冊channel,事件循環中選擇
	3 主 new NioEventloop(1) 1表示有1個線程可指定
	   從 new NioEventloop() 默認 cpu*2數量的線程
	會創建EventExcutor數組 children = new EventExecutor[nThread];
	每個元素類型爲NioEventloop,NioEventloop實現了EventLoop和Executor接口

	EventLoop接口
		1 繼承了ScheduledExecutorService接口,可接受定時任務
		2 繼承了SingleThreadEventExecutor接口,所以是單線程線程池
		3 該接口一旦被channel註冊,就處理該channel對應所有IO操作
		4 它是單線程線程池,一個死循環線程反覆不斷做3件事情
			監聽端口:默認selector調用CAS阻塞1秒監聽,
			處理端口:有任務則喚醒selector調用processSelectKeys處理key
			處理隊列事件:再之後runAllTasks
		5 一個Eventloop可以綁定多個channel,每個channel只能由一個Eventloop處理

	children數組下
		1 實例化單例線程池數組(CPU*2數量)
		2 根據線程選擇工廠創建一個線程選擇器selector
		3 每個單例線程池加一個關閉監聽器
		4 將所有單例線程池加入LinkedHashSet(children)

3 try塊創建ServerBootstrap對象,引導類,用於啓動服務器和引導整個程序初始化。即調用group方法將兩個主從group加入自己字段
4 添加一個channel(服務器自身channel),參數是clas對象,引導類通過這個class反射創建ChannelFactory。再添加一些TCP參數(改channel在bind方法真正創建)
5 加服務器日誌handler
6 加SocketChannel的handler配置主從handler
7 綁定端口並阻塞直到連接成功
	bind底層兩個方法
		initAndRegister 簡而言之就是初始化所有channel,handler,pipeline,context
		dobind 則是select輪詢事件,處理IO,處理任務隊列				
8 mian線程阻塞等待關閉
9 finally塊中代碼在服務器關閉時關閉所有資源

10.3 Netty應用

1 Netty服務端客戶端簡單通訊

服務端
//創建主從線程
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

//創建服務器啓動對象,配置參數
try{
	ServerBootstrap bootstrap = new ~;

	//NioServerSocketChannel.class服務器通道反射實現
	//ChannelOption.SO_BACKLOG,128線程隊列連接個數
	//ChannelOption.SO_KEEPALIVE,true從線程池都保持心跳機制
	//new ChannelInitializer<SocketChannel>()給從線程池的eventloop對應管道設置處理器handler
	bootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG,128).childOption(ChannelOption.SO_KEEPALIVE,true).childHandler(new ChannelInitializer<SocketChannel>(){
		@Override
		~ initChannel(SocketChannel ch){
			ch.pipeline.addLast(new NettyServerHandler());//添加自己的實現類
		}
	})
	//服務器ready了
	//綁定一個端口同步,生成一個ChannelFuture異步對線,啓動server
	ChannelFuture cf = bootstrap.bind(6668).sync();
	//對關閉的通道監聽,監聽到關閉事件才關閉
	cf.channel().closeFuture().sync();
}catch{
	xxx
}finally{
	xxx
}


//自定義一個handler需要繼承netty規定好的適配器
public class NettyServerHandler extends ChannelInboundHanderAdapter{
	//client data實現
	//當通道有讀取事件會觸發
	//ctx上下文對象包含pipeline和channel,msg是client發送的數據
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg){
		//講msg數據轉成ByteBuf(Netty提供的buffer)
		ByteBuffer buf = (ByteBuf) msg;
		System.out.println(buf.toString(CharsetUtil.UTF_8))//顯示發送的數據
		System...("客服端地址: "+ctx.channel().remoteAddress());
	}
	//數據讀取完觸發
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx){
		//講數據寫入緩存並刷新
		ctx.writeAndFlush(Unpooled.copiedBuffer("HelloClient",~UTF_8));
	}
	//異常處理,一般是關閉通道
	~ exceptionCaught(ChannelHandlerContext ctx,~){
		ctx.channel().close(); or ctx.close();
	}
}


客戶端
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

//創建服務器啓動對象,配置參數
try{
	Bootstrap bootstrap = new ~;
	bootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).handler(new ChannelInitializer<SocketChannel>(){
		@Override
		~ initChannel(SocketChannel ch){
			ch.pipeline.addLast(new NettyClientHandler());
		}
	})
	//啓動client連接server
	ChannelFuture cf = bootstrap.connect("127.0.0.1",6668).sync();
	//對關閉的通道監聽,監聽到關閉事件才關閉
	cf.channel().closeFuture().sync();
}catch{
	xxx
}finally{
	xxx
}

public class NettyClientHandler extends ChannelInboundHanderAdapter{
	@Override
	public void channelActive(ChannelHandlerContext ctx){
		ctx.writeAndFlush(Unpooled.copiedBuffer("HelloServer",~UTF-8));
	}
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg){
		//讀取服務器消息
		ByteBuffer buf = (ByteBuf) msg;
		System.out.println(buf.toString(CharsetUtil.UTF_8))//顯示發送的數據
		System...("服務端地址: "+ctx.channel().remoteAddress());
	}
	~ exceptionCaught(ChannelHandlerContext ctx,~){
		ctx.channel().close(); or ctx.close();
	}
}

2 編解碼器

Netty的StringEncoder(String) 和 ObjectEncoder(Java對象) 都使用java序列化技術
體積太大,二進制編碼5倍多。無法跨語言。序列化性能低

解決方案
	TCP+谷歌的 protobuf 效率更高

3 TCP粘包拆包

解決方案:自定義協議+編解碼器
Client端
pipeline.addLast(new MyMessageEncoder());
pipeline.addLast(new MyClientHandler());

Server端
pipeline.addLast(new MyMessageDncoder());
pipeline.addLast(new MyServerHandler());

客戶端
public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol>{
	public void channelActive(ChannelHandlerContext ctx){
		//自定義協議
		MessageProtocol protocol = new ~() //自己定義的類,規定交互數據的長度內容
		protocol.setLen(xxx);
		protocol.setContent(xxx);
		ctx.writeAndFlush(protocol);
	}
}

//客戶端編碼器
public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol>{
	@Override
	~ encode(ChannelHandlerContext ctx,MessageProtocol msg,ByteBuf out){
		out.writeInt(msg.getLen());
		out.writeBytes(msg.getContent());
	}
}

服務端
public class MyMessageDecoder extends ReplayingDecoder<void>{
	@Override
	~ decode(ChannelHandlerContext ctx, ByteBuf in,List<Object> out){
		//講二進制字節碼轉成我們需要的MessageProtocol
		int len = in.readInt();
		byte[] content = new ~;
		in.readBytes(content);
		MessageProtocol protocol = new ~;
		protocol.setLen(len);
		protocol.setContent(content);
		out.add(protocol);
	}
}

public class MyServerHandler ~{
	@Override
	~ channelRead0(ChannelHandlerContext ctx, MessageProtocol msg){
		//拿到數據進行業務處理
	}
}

Netty耗時操作的解決

普通線程池仍同一個線程,會阻塞,所以需要異步

handler中新建一個線程池變量來處理
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章