Netty服務端程式碼的hello world
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public static void main(String[]args)throws Exception{
new EchoServer(8888).start();
}
public void start() throws Exception{
final EchoServerHandler handler = new EchoServerHandler();
EventLoopGroup group = new NioEventLoopGroup();
try{
ServerBootstrap b = new ServerBootstrap();
b.group(group).channel(NioServerSocketChannel.class).localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(handler);
}
});
ChannelFuture f = b.bind().sync();
f.channel().closeFuture().sync();
}finally {
group.shutdownGracefully().sync();
}
}
}
複製程式碼
- 初始化EventLoopGroup
所謂的EventLoopGroup,組(group)的概念表現在它自身維護了一個陣列children,預設維護邏輯處理核數2倍的NioEventLoop執行緒,並通過chooser來方便的獲取下一個要執行的執行緒。實際處理的是NioEventLoop,它的部分類結構如下:
實質上的執行緒執行,就是啟動一個java Thread,然後從taskQuene中獲取要執行的任務,在run方法中執行。
- 配置引導類ServerBootstrap作為工具來引導channel建立
- 初始化用於處理連線請求的group(即acceptor)和處理事件的childGroup(即client)
Hello word版程式碼中用的是同一個NioEventLoop,實際中一般各自分配
- 配置要建立channel的型別
- 置服務端監聽的埠
- 配置服務自己的訊息處理器,通過childHandler持有
- 建立並初始化channel
在管道的最後新增ChannelInitializer的方式則會在管道註冊完成之後,往管道中 新增一個ServerBootstrapAcceptor(它是InboundHandler),它持有對childGroup(client)和childHandler的引用,而ChannelInitializer這個InboundHandler在完成它的使命之後,就會從管道中被移除, 至此完成channel的初始化。
ServerBootstrapAcceptor 最開始在客戶端建立連線的時候執行呼叫(後續讀訊息呼叫),入口是 doReadMessages,讀到訊息之後,從Head沿著InBoundHandler到ServerBootstrapAcceptor,觸發讀事件,此時執行註冊childGroup到這個channel,也就是每次都用childGroup來處理讀到的訊息
public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
//管道註冊完成之後觸發
ChannelPipeline pipeline = ctx.pipeline();
boolean success = false;
try {
initChannel((C) ctx.channel()); //執行註冊過程中的方法,在這裡就是往管道中新增ServerBootstrapAcceptor
pipeline.remove(this); //刪除ChannelInitializer本身
ctx.fireChannelRegistered(); //繼續沿著管道傳遞channel註冊完成事件
success = true;
} catch (Throwable t) {
logger.warn("Failed to initialize a channel. Closing: " + ctx.channel(), t);
} finally {
if (pipeline.context(this) != null) {
pipeline.remove(this);
}
if (!success) {
ctx.close();
}
}
}
複製程式碼
新建的NioServerSocketChannel的部分類結構如下:
對於Netty來講channel有”兩個”
- 一個是自身的channel介面,主要負責提供給使用者操作I/O的方法,比如 read、write、connect和bind,真實的資料傳輸都是通過channel介面的內部介面unsafe來實現的,unsafe本身不會暴漏給使用者使用。另外它會在內部維護一個pipeline,用來連線各個handler對資料的處理,本質上pipeline就是維護了handler之間關係的一個雙向連結串列,它會持有Netty自身的channel引用,以便在管道中能夠對IO進行操作,
凡是通過 channel()方法獲取的則是Netty自身的channel
public DefaultChannelPipeline(AbstractChannel channel) {
if (channel == null) {
throw new NullPointerException("channel");
}
this.channel = channel; //Netty自身的channel
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
複製程式碼
- 另一個channel,也就是AbstractNioChannel持有的ch屬性,它在NioServerSocketChannel中由jdk初始化,即ServerSocketChannel物件。Netty內部
凡是通過javachannel()呼叫的獲取到的值即是jdk的channel
,而unsafe本身真正意義上執行的register、bind、connect、write、read操作均通過ServerSocketChannel實現
- 執行channel的註冊
可以看到註冊過程中實際的註冊操作經理了從channel->unsafe->ch的一個過程,實際的註冊操作就是使用jdk完成的。
- 執行channel的繫結
能夠看到的是,繫結操作也是通過jdk來實現繫結的。另外同步阻塞住server,使之不關閉,實際上也就是隻要CloseFuture不完成,那麼server主執行緒永遠阻塞住,由剛開始分配的NioEventLoop一直在執行各自的task
從服務端的hello world版本可以得出什麼結論?
netty nio底層的註冊channel、繫結監聽埠都是通過jdk自身的nio完成的。java nio中的select和channel是怎麼使用的?
附自定義handler的程式碼
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
System.out.printf("Server get:"+in.toString(CharsetUtil.UTF_8));
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//將目前暫存於ChannelOutboundBuffer中的訊息在下一次flush或者writeAndFlush的時候沖刷到遠端並關閉這個channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
複製程式碼
Netty客戶端程式碼的hello world怎麼寫?
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host,int port){
this.host=host;
this.port=port;
}
public void start() throws Exception{
EventLoopGroup group = new NioEventLoopGroup();
try{
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)//指定NIO的傳輸方式
.remoteAddress(new InetSocketAddress(host,port))//指定遠端地址
.handler(new ChannelInitializer<SocketChannel>() {//向channel的pipeline新增handler
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler());//channelHander交給pipeline
}
});
ChannelFuture f = b.connect().sync();//連線到遠端節點,阻塞直到連線完成
System.out.println("wait");
f.channel().closeFuture().sync();//阻塞直到連線關閉
System.out.println("over");
}finally {
System.out.println("shutdown");
group.shutdownGracefully().sync();//關閉執行緒池並且釋放資源
}
}
public static void main(String[]args) throws Exception{
new EchoClient("localhost",8888).start();
}
}
複製程式碼
從程式碼本身可以看到與 server的差異化在於以下兩個部分:
- Bootstrap:功能類似ServerBootstrap,一樣使用builder模式來構建client所需要的引數,包括要連線的遠端地址remoteAddress,以及自定義的handler
- conncet:channel的新建和註冊與服務端差不多,只是初始化channel的時候在pipeline中新增的是自定義的handler,而服務端則是新增了一個ServerBootstrapAcceptor,然後去執行了jdk的connect
注意這裡的實際上是沒有指定本地的地址的
附自定義handler程式碼
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Hello world",CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println("Client get:"+msg.toString(CharsetUtil.UTF_8));
}
}
複製程式碼