之前的文章Netty基礎篇:Netty是什麼?介紹了傳統IO編程存在的問題,及Java NIO編程在解決IO編程的問題中的侷限性,由此引出IO編程問題的理想解決方案——Netty。在上篇文章中簡單展示了Netty的基本使用,本篇文章通過一個Netty服務端的demo來了解一下Netty的基本組件。
1. Netty服務端
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public static void main(String[] args) throws InterruptedException {
new EchoServer(8888).start();
}
public void start() throws InterruptedException {
//創建EventLoopGroup,處理事件
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(boss, worker)
//指定所使用的NIO傳輸 Channel
.channel(NioServerSocketChannel.class)
//使用指定的端口設置套接字地址
.localAddress(new InetSocketAddress(port))
//設置ChannelHandler
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
ChannelPipeline channelPipeline = socketChannel.pipeline();
channelPipeline.addLast(new HttpServerCodec());
channelPipeline.addLast(new HttpObjectAggregator(10 * 1024 * 1024));
channelPipeline.addLast(new NettyServerHandler());
}
});
//異步的綁定服務器,調用sync()方法阻塞等待直到綁定完成
ChannelFuture future = b.bind().sync();
future.channel().closeFuture().sync();
} finally {
//關閉EventLoopGroup,釋放所有的資源
boss.shutdownGracefully().sync();
worker.shutdownGracefully().sync();
}
}
}
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 收到客戶端請求,返回信息
* @param ctx
* @param msg
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
String result;
if (!(msg instanceof FullHttpRequest)) {
result = "未知請求!";
send(ctx, result, HttpResponseStatus.BAD_REQUEST);
return;
}
FullHttpRequest httpRequest = (FullHttpRequest) msg;
try {
String path = httpRequest.uri(); //獲取路徑
String body = getBody(httpRequest); //獲取參數
HttpMethod method = httpRequest.method();//獲取請求方法
System.out.println("接收到:" + method + " 請求");
//如果是GET請求
if (HttpMethod.GET.equals(method)) {
System.out.println("body:" + body);
result = "GET請求";
send(ctx, result, HttpResponseStatus.OK);
return;
}
//如果是POST請求
if (HttpMethod.POST.equals(method)) {
System.out.println("body:" + body);
result = "POST請求";
send(ctx, result, HttpResponseStatus.OK);
return;
}
//如果是PUT請求
if (HttpMethod.PUT.equals(method)) {
System.out.println("body:" + body);
result = "PUT請求";
send(ctx, result, HttpResponseStatus.OK);
return;
}
//如果是DELETE請求
if (HttpMethod.DELETE.equals(method)) {
System.out.println("body:" + body);
result = "DELETE請求";
send(ctx, result, HttpResponseStatus.OK);
}
} catch (Exception e) {
System.out.println("處理請求失敗!");
e.printStackTrace();
} finally {
//釋放請求
httpRequest.release();
}
}
/**
* 獲取body參數
*
* @param request
* @return
*/
private String getBody(FullHttpRequest request) {
ByteBuf buf = request.content();
return buf.toString(CharsetUtil.UTF_8);
}
/**
* 發送的返回值
*
* @param ctx 返回
* @param context 消息
* @param status 狀態
*/
private void send(ChannelHandlerContext ctx, String context, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context, CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
/**
* 建立連接時,返回消息
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("連接的客戶端地址:" + ctx.channel().remoteAddress());
ctx.writeAndFlush("客戶端" + InetAddress.getLocalHost().getHostName() + "成功與服務端建立連接! ");
super.channelActive(ctx);
}
}
當啓動main方法,瀏覽器訪問127.0.0.1:8888/hello,可以在瀏覽器獲取響應結果“GET請求”,服務端可以看到請求日誌:
接收到:GET請求
body:
說明Netty實現了服務端並正常運行,下面我們結合上面的demo來看一下Netty的基礎組件。
2. Netty基礎組件
根據demo,我們可以總結出Netty服務端啓動流程:
- 創建 ServerBootStrap實例
- 設置EventLoopGroup線程池
- 通過ServerBootStrap的channel方法設置Channel類型,會在bind方法調用後根據該類型初始化Channel
- 綁定Socket訪問地址
- 設置ChannelHandler(通過ChannelPipeline將ChannelHandler組織爲一個邏輯鏈)
- 調用bind方法啓動服務端
該流程主要涉及如下圖所示的幾個Netty組件:
2.1 BootStrap
BootStrap是Netty提供的啓動輔助類,幫助Netty客戶端或服務端的Netty初始化,客戶端對應的是Bootstrap類,服務端對應的是 ServerBootStrap引導類。
2.2 Channel
Channel是Netty中的網絡操作抽象類,對應JDK底層的Socket,它除了包含基本的I/O操作,如 bind()、connect()、read()、write()之外,還包括了Netty框架相關的一些功能,如獲取 Channel的EventLoop。
2.3 EventLoop & EventLoopGroup
EventLoop定義了Netty的核心抽象,用於處理連接的生命週期中所發生的事件。EventLoop 爲Channel處理I/O操作,下圖是 Channel,EventLoop,Thread以及EventLoopGroup之間的關係(摘自《Netty In Action》):
它們之間的關係是:
- 一個EventLoopGroup 包含一個或者多個EventLoop
- 一個 EventLoop 在它的生命週期內只和一個Thread綁定
- 所有由 EventLoop處理的 I/O事件都將在它專有的Thread上被處理
- 一個 Channel 在它的生命週期內只註冊一個EventLoop
- 一個 EventLoop 可能會被分配給一個或多個 Channel
EventLoopGroup實際上就是處理I/O操作的線程池,負責爲每個新註冊的Channel分配一個EventLoop,Channel在整個生命週期都有其綁定的 EventLoop來服務。
而上面服務端用的 NioEventLoop 就是 EventLoop的一個重要實現類,NioEventLoop 是Netty內部的I/O線程,而 NioEventLoopGroup是擁有 NioEventLoop的線程池,在Netty服務端中一般存在兩個這樣的NioEventLoopGroup線程池,一個 “Boss” 線程池,用於接收客戶端連接,實際上該線程池中只有一個線程,一個 “Worker”線程池用於處理每個連接的讀寫。而Netty客戶端只需一個線程池即可,主要用於處理連接中的讀寫操作。
2.4 ChannelHandler
ChannelHandler主要用於對出站和入站數據進行處理,它有兩個重要的子接口:
- ChannelInboundHandler——處理入站數據
- ChannelOutboundHandler——處理出站數據
2.5 ChannelPipeline
ChannelPipeline是ChannelHandler的容器,通過ChannelPipeline可以將ChannelHandler組織成一個邏輯鏈,該邏輯鏈可以用來攔截流經Channel的入站和出站事件,當 Channel被創建時,它會被自動地分配到它的專屬的 ChannelPipeline。
當一個消息或者任何其他的入站事件被讀取時,那麼它會從 ChannelPipeline的頭部開始流動,並被傳遞給第一個 ChannelInboundHandler,第一個處理完成之後傳遞給下一個 ChannelInboundHandler,一直到ChannelPipeline的尾端,與之對應的是,當數據被寫出時,數據從 ChannelOutboundHandler 鏈的尾端開始流動,直到它到達鏈的頭部爲止。
2.6 ChannelOption
ChannelOption用於對Channel設置TCP層面通用參數,比如TCP長連接設置,比如可以通過如下代碼實現TCP層面的keepAlive機制:
//設置TCP的長連接,默認的keepAlive的心跳時間是兩個小時
.option(ChannelOption.SO_KEEPALIVE, true)
2.7 ChannelFuture
ChannelFuture用於獲取異步IO的處理結果,其 addListener()
方法註冊了一個 ChannelFutureListener,以便在某個操作完成時(無論是否成功)得到通知。比如可以通過如下方式實現Netty客戶端斷線重連:
//客戶端斷線重連邏輯
ChannelFuture future = bootstrap.connect();
future.addListener((ChannelFutureListener) future1 -> {
if (future1.isSuccess()) {
log.info("連接Netty服務端成功");
} else {
log.info("連接失敗,進行斷線重連");
future1.channel().eventLoop().schedule(() -> start(), 20, TimeUnit.SECONDS);
}
});
socketChannel = (SocketChannel) future.channel();
2.8 ByteBuf
在之前的文章中提到,Java IO編程使用字節流來處理輸入輸出,效率較差,Java NIO中使用內存塊爲單位進行數據處理。而ByteBuf就是字節緩衝區,用於高效處理輸入輸出。
參考鏈接: