本來想先了解Netty組件,然後再學習組件應用的,然後越學越感覺怪異,總感覺少了啥,組件學起來不知道咋用的,想想還是先從Netty應用開始學算了。
自己的技術學習方法:先學習技術的應用,在應用中逐步拋出問題,比如說這個功能是怎麼實現的,帶着問題去接觸底層原理,然後解決問題。
1. 最基礎的Netty應用實現——實現請求與響應
1. 首先是環境配置(jdk)要保證沒問題,其次,要引入Netty的jar,使用netty-5.0版本的jar。
2. 在使用Netty開始開發之前,先想一想使用jdk中的NIO原生類庫開發服務端時所需要的主要步驟:
- 首先,創建ServerSocketChannel,設置爲非阻塞模式。
- 綁定監聽端口,設置TCP連接參數。
- 創建一個獨立IO線程,用於輪詢多路複用器Selector。
- 創建Selector,將創建的ServerSocketChannel註冊到該Selector中,並且監聽ServerSocketChannel上的SelectionKey.OP_ACCEPT事件。
- 啓動獨立IO線程,在循環體中執行Selector.select()方法,獲取到就緒的Channel。
- 每當獲取到Channel時,就需要判斷Channel的狀態,
- 如果是OP_ACCEPT,那麼就說明是新的客戶端接入,調用ServerSocketChannel的accept()方法,創建新的連接,即SocketChannel對象。創建SocketChannel對象後,可以設置該TCP連接參數,並且要設置爲非阻塞模式,設置完畢後,將該SocketChannel註冊到Selector中,並且監聽OP_READ事件。
- 如果是OP_READ,那麼就說明SocketChannel中有已經就緒的數據可以進行讀取,此時就需要構造ByteBuffer對象進行讀取。
- 如果是OP_WRITE,那麼說明SocketChannel還有數據沒有發送,需要繼續進行發送。
3. 僅僅是一個簡單的數據收發,都已經如此複雜,而Netty真的就簡單很多了。以一個服務端時間信息查詢爲例,客戶端向服務端發送一條字符串信息,服務端返回當前時間數據作爲響應,代碼如下
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* @ClassName TimeServer
* @Description: Netty實現的時間服務器服務端示例
* @Author
* @Date 2019/11/19 14:41
* @Modified By:
* @Version V1.0
*/
public class TimeServer {
public static void main(String[] args) {
try {
new TimeServer().bind(8080);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//綁定端口
public void bind(int port) throws InterruptedException {
//想想Netty的線程模型,我們需要首先創建Reactor模式的線程組,有兩個線程組
EventLoopGroup bossGroup = new NioEventLoopGroup();//主線程組,用於接收連接請求,並建立連接
EventLoopGroup workGroup = new NioEventLoopGroup();//工作線程組,該線程組通過SocketChannel進行IO操作
try {
//Netty服務端啓動類配置
ServerBootstrap serverBootstrap = new ServerBootstrap();//創建服務端啓動類,該類是輔助服務端啓動的類,主要目的是降低開發複雜度,我們只需要配置相關參數即可
serverBootstrap.group(bossGroup, workGroup);//配置線程池
//配置服務端的NioServerSocketChannel,既然是作爲服務端,肯定是需要一個ServerSocketChannel來接收客戶端連接請求並建立連接
serverBootstrap.channel(NioServerSocketChannel.class);
//配置NioServerSocketChannel的參數,或者說NioServerSocketChannel對應的TCP連接的參數,比如該參數爲連接超時參數,爲10000毫秒
serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
//和上面哪行代碼一樣,配置TCP連接的backlog參數爲1024
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
//最後,綁定IO事件的處理類ChildChannelHandler,該類就是Reactor模型中的Handler,用於處理IO事件,不過這裏的Handler是專門處理ACCEPT事件的,也就是建立連接創建SocketChannel
serverBootstrap.childHandler(new ChildChannelHandler());
//服務端輔助啓動類配置完成後,就可以綁定監聽端口了,綁定結果會通過ChannelFuture來獲取
ChannelFuture future1 = serverBootstrap.bind(port);
//這行代碼表示阻塞當前線程,直到future1中能夠獲取綁定操作結果,綁定完成後服務端就已經開始運行了
future1.sync();
//closeFuture表示Channel連接被關閉時(比如客戶端斷開連接),會返回一個ChannelFuture對象
ChannelFuture future2 = future1.channel().closeFuture();
//阻塞當前線程,直到future2總可以獲取到關閉連接操作結果
future2.sync();
} finally {
//釋放線程池資源
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
//這個類主要用於監聽ServerSocketChannel接收連接請求並建立連接事件,也就是相當於NIO中的ACCEPT事件
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//建立連接後創建的的SocketChannel,會在該方法中進行處理,添加響應的handler處理器
ch.pipeline().addLast(new TimeServerHandler());
}
}
//針對於SocketChannel的處理器,處理SocketChannel中的IO操作
private class TimeServerHandler extends ChannelHandlerAdapter {
//讀取數據,並對數據進行處理
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//將讀取到的數據全部取出
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
//編碼解析
String body = new String(req, "utf-8");
System.out.println("server received:" + body);//輸出
//響應數據
String res = "server current time:" + System.currentTimeMillis();
ByteBuf data = Unpooled.copiedBuffer(res.getBytes());
ctx.write(data);//注意,這裏的寫操作不是立即發送的,而是會先保存在一個緩衝區中
}
//該方法表示讀取數據操作完畢,讀取完畢後進行的操作
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();//調用該方法才能將緩衝區中所有的數據發送給客戶端
}
//數據讀取過程中發生異常情況,要進行的後續處理都在該方法中
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.nio.Buffer;
/**
* @ClassName TimeClient
* @Description: 客戶端
* @Author
* @Date 2019/11/19 16:39
* @Modified By:
* @Version V1.0
*/
public class TimeClient {
public void connect(int port, String host) {
//創建客戶端的NIO線程組
EventLoopGroup workGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();//創建客戶端輔助啓動類
try {
bootstrap.group(workGroup);//綁定NIO線程組
bootstrap.channel(NioSocketChannel.class);//初始創建的Channel類型
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeChannelHandler());
}
});//設置Handler
//bootstrap.bind("8081");
ChannelFuture future = bootstrap.connect(host, port);//與服務端建立連接
future.sync();//同步阻塞等待連接建立成功
ChannelFuture f = future.channel().closeFuture();//等待連接被關閉斷開
f.sync();//同步阻塞等待連接關閉完成
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
workGroup.shutdownGracefully();//清理資源
}
}
public static void main(String[] args) {
new TimeClient().connect(8080, "localhost");
}
private class TimeChannelHandler extends ChannelHandlerAdapter{
//該方法會在建立連接後執行,表示觸發了連接建立事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String message = "query server time";
ByteBuf req = Unpooled.copiedBuffer(message.getBytes());
ctx.writeAndFlush(req);//向服務端發送數據
}
//讀取服務端發送的數據,如果服務端發送了數據,Channel觸發了可讀事件,那麼該方法就會執行,讀取數據
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] buffer = new byte[buf.readableBytes()];
buf.readBytes(buffer);
String message = new String(buffer, "utf-8");
System.out.println(message);//讀取服務端發送過來的數據,並編碼輸出
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
4. Netty開發主要步驟:可以參考Netty的線程模型,幫助理解
- 服務端:
- 首先創建兩個處理線程組,也就是Reactor模型中的主從多線程模型。
- 其次,創建輔助啓動類ServerBootstrap,該類的主要作用就是輔助創建並啓動Netty服務端的類,通過該類我們可以設置一系列的配置並啓動服務端,降低開發複雜度。
- 配置綁定線程組,主線程組用來接收並創建新的連接,工作線程組主要進行IO操作。
- 設置初始創建的channel類型爲NioServerSocketChannel,該類對應着NIO中的ServerSocketChannel,用於接收客戶端的連接請求,並創建連接(SocketChannel)。
- 設置NioServerSocketChannel的TCP連接參數,比如CONNECT_TIMEOUT_MILLIS連接超時判斷等。
- 設置對於與客戶端建立連接後創建的NIOSocketChannel對象(所以是childHandler方法,因爲是NioServerSocketChannel“創建”的),綁定自定義的Handler處理鏈,Netty中對於SocketChannel上的數據IO操作採用了類似於Servlet中的Filter機制,使用了責任鏈設計模式的變形。
- 綁定監聽的端口,啓動服務端。
- 如果連接被關閉,那麼就清理資源,退出
2.客戶端:
- 首先,創建一個工作線程組。
- 其次,創建輔助啓動類Bootstrap。
- 指定初始建立連接創建的Channel類型爲NioSocketChannel
- 調用handler方法綁定NioSocketChannel的處理鏈,因爲是爲NioSocketChannel綁定,所以是handler()方法,而不是childHandler()方法。
- 調用connect()方法與服務端建立連接。
- 如果連接被關閉,那麼就清理資源,退出
2. 使用Netty解決半包讀取(粘包與拆包)的問題
1. TCP中的粘包與拆包問題
TCP在網絡模型中,屬於傳輸層協議,即其只負責數據的傳輸,會將要傳輸的數據以流的形式在網絡間進行傳輸,就像一條河流一樣是連續的,對於TCP來說,並不瞭解來自應用層的數據含義,再加上TCP的窗口滑動機制,TCP可能會把一條完整的數據流在讀取時讀到部分數據,也有可能在讀取時一次性讀取到多段數據流(比較難懂的話可以去簡單瞭解一下NIO中設計的零拷貝實現原理,設計到內核緩衝區和應用緩衝區,以及TCP的部分知識)。
形象一點來說,水龍頭就相當於建立的網絡連接,在系統內核底層會有一個緩衝區,也就是相當於有一個“缸”,這個缸直接放在水龍頭下面的,水龍頭打開後(建立連接),水流出來(接收數據)會直接先到缸內暫存(內核緩衝區接收),然後是用戶空間,也就是應用程序會用一個“盆”(用戶空間的緩衝區)去這個缸裏取水(內核緩衝區數據複製到用戶緩衝區),然後在代碼中對這部分數據進行處理(編解碼等)。但是問題在於,數據發送方發送的數據通常都會是一個一個整體的進行發送,對應着數據接收方也要保證接收一個一個完整的整體,這樣才能保證正確的數據編解碼,但是對於TCP協議來說,由於處於傳輸層,無法理解這些數據在上層應用層中的含義,無法進行分割處理,只負責數據的傳輸,在數據接收方,你能保證每次拿“盆”取出的數據是發送方發送的單條完整消息數據嗎?
服務端接收讀取客戶端發送的數據時,有可能會發生以下幾種情況:
(1)服務端接收並讀取了客戶端發送的一條完整消息數據。比如客戶端發送了一條“hello”的字符串數據,那麼服務端也接受到了一條“hello”的消息數據。
(2)客戶端發送了兩次消息數據,兩條消息數據都是“hello”,但是服務端只接收到了一條消息,消息內容爲”hellohello”。(粘包)
(3)客戶端發送了兩次消息數據,兩條消息數據都是“hello”,服務端也接受到了兩條消息,但是第一次接受到的消息數據是“helloh”,第二次接收到了“ello”。(拆包)
如果是發送的不包含漢字字符的字符串,那麼半包讀取還算能夠正常進行,但是一旦包含漢字字符,或者是某類文件的字節碼數據,發生半包讀取後,對數據進行編解碼肯定是會發生異常的。
粘包拆包問題的演示代碼:正常情況下(或者說我們希望不發生拆包粘包的情況下),客戶端發送100條信息,請求服務端時間;服務端接收100條信息,每接收一條就響應一次時間信息,但因爲拆包和粘包,結果顯然不是這樣。
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* @ClassName TimeServer
* @Description: TCP粘包拆包引起問題的演示
* @Author
* @Date 2019/11/20 13:59
* @Modified By:
* @Version V1.0
*/
public class TimeServer {
public static void main(String[] args) {
try {
new netty_demo.demo1.TimeServer().bind(8080);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//綁定端口
public void bind(int port) throws InterruptedException {
//想想Netty的線程模型,我們需要首先創建Reactor模式的線程組,有兩個線程組
EventLoopGroup bossGroup = new NioEventLoopGroup();//主線程組,用於接收連接請求,並建立連接
EventLoopGroup workGroup = new NioEventLoopGroup();//工作線程組,該線程組通過SocketChannel進行IO操作
try {
//Netty服務端啓動類配置
ServerBootstrap serverBootstrap = new ServerBootstrap();//創建服務端啓動類,該類是輔助服務端啓動的類,主要目的是降低開發複雜度,我們只需要配置相關參數即可
serverBootstrap.group(bossGroup, workGroup);//配置線程池
//配置服務端的NioServerSocketChannel,既然是作爲服務端,肯定是需要一個ServerSocketChannel來接收客戶端連接請求並建立連接
serverBootstrap.channel(NioServerSocketChannel.class);
//配置NioServerSocketChannel的參數,或者說NioServerSocketChannel對應的TCP連接的參數,比如該參數爲連接超時參數,爲10000毫秒
serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
//和上面哪行代碼一樣,配置TCP連接的backlog參數爲1024
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
//最後,綁定IO事件的處理類ChildChannelHandler,該類就是Reactor模型中的Handler,用於處理IO事件,不過這裏的Handler是專門處理ACCEPT事件的,也就是建立連接創建SocketChannel
serverBootstrap.childHandler(new ChildChannelHandler());
//服務端輔助啓動類配置完成後,就可以綁定監聽端口了,綁定結果會通過ChannelFuture來獲取
ChannelFuture future1 = serverBootstrap.bind(port);
//這行代碼表示阻塞當前線程,直到future1中能夠獲取綁定操作結果
future1.sync();
//closeFuture表示關閉連接,該方法會返回一個ChannelFuture,關閉連接操作結果會存儲在該對象中
ChannelFuture future2 = future1.channel().closeFuture();
//阻塞當前線程,直到future2總可以獲取到關閉連接操作結果
future2.sync();
} finally {
//釋放線程池資源
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
//這個類主要用於監聽ServerSocketChannel接收連接請求並建立連接事件,也就是相當於NIO中的ACCEPT事件
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//建立連接後創建的的SocketChannel,會在該方法中進行處理,添加響應的handler處理器
ch.pipeline().addLast(new TimeServerHandler());
}
}
//針對於SocketChannel的處理器,處理SocketChannel中的IO操作
private class TimeServerHandler extends ChannelHandlerAdapter {
private int count = 0;
//讀取數據,並對數據進行處理
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//將讀取到的數據全部取出
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
//編碼解析
String body = new String(req, "utf-8");
System.out.println("server received:" + body + ";count="+ ++count);//輸出
//響應數據
if ("query server time".equals(body)) {
String res = "server current time:" + System.currentTimeMillis();
ByteBuf data = Unpooled.copiedBuffer(res.getBytes());
ctx.writeAndFlush(data);//注意,這裏的寫操作不是立即發送的,而是會先保存在一個緩衝區中
} else {
String res = "bad req:" + System.currentTimeMillis();
ByteBuf data = Unpooled.copiedBuffer(res.getBytes());
ctx.writeAndFlush(data);//注意,這裏的寫操作不是立即發送的,而是會先保存在一個緩衝區中
}
}
//數據讀取過程中發生異常情況,要進行的後續處理都在該方法中
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
public class TimeClient {
public void connect(int port, String host) {
//創建客戶端的NIO線程組
EventLoopGroup workGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();//創建客戶輔助啓動類
try {
bootstrap.group(workGroup);//綁定NIO線程組
bootstrap.channel(NioSocketChannel.class);//初始創建的Channel類型
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeChannelHandler());
}
});//設置Handler
//bootstrap.bind("8081");
ChannelFuture future = bootstrap.connect(host, port);//與服務端建立連接
future.sync();//同步阻塞等待連接建立成功
ChannelFuture f = future.channel().closeFuture();//等待連接關閉斷開
f.sync();//同步阻塞等待連接關閉完成
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
workGroup.shutdownGracefully();//清理資源
}
}
public static void main(String[] args) {
new TimeClient().connect(8080, "localhost");
}
private class TimeChannelHandler extends ChannelHandlerAdapter {
private int count=0;
//該方法會在建立連接後執行,表示觸發了連接建立事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for(int i=1;i<=100;i++){
String message = "query server time";
ByteBuf req = Unpooled.copiedBuffer(message.getBytes());
ctx.writeAndFlush(req);//向服務端發送數據
}
}
//讀取服務端發送的數據,如果服務端發送了數據,Channel觸發了可讀事件,那麼該方法就會執行,讀取數據
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] buffer = new byte[buf.readableBytes()];
buf.readBytes(buffer);
String message = new String(buffer, "utf-8");
System.out.println(message+";count="+ ++count);//讀取服務端發送過來的數據,並編碼輸出
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
2. TCP中的粘包與拆包問題的解決方式
粘包與拆包問題的解決方式有以下幾種:
(1)消息定長,即消息數據長度固定。
(2)以特定字符作爲分隔符或者說結束標誌,比如換行符。
(3)將消息分爲消息頭和消息體,消息頭中包含對消息體的描述,比如對於消息體的數據長度等信息。(比如HTTP協議、TCP協議)
(4)定義應用層協議。
1. 指定消息結束標誌字符解決文本消息數據的半包讀取
對於文本消息數據,也就是字符串數據來說,比較容易解決,可以使用上述的第二種方法,指定一個消息結束標誌字符,比如常見的換行符\n,Netty中正好提供了以換行符作爲消息結束標誌的Handler類,通過io.netty.handler.codec.LineBasedFrameDecoder類來解決拆包粘板問題。代碼如下所示
public class TimeClient {
public void connect(int port, String host) {
//創建客戶端的NIO線程組
EventLoopGroup workGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();//創建客戶輔助啓動類
try {
bootstrap.group(workGroup);//綁定NIO線程組
bootstrap.channel(NioSocketChannel.class);//初始創建的Channel類型
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeChannelHandler());
}
});//設置Handler
//bootstrap.bind("8081");
ChannelFuture future = bootstrap.connect(host, port);//與服務端建立連接
future.sync();//同步阻塞等待連接建立成功
ChannelFuture f = future.channel().closeFuture();//等待連接關閉斷開
f.sync();//同步阻塞等待連接關閉完成
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
workGroup.shutdownGracefully();//清理資源
}
}
public static void main(String[] args) {
new TimeClient().connect(8080, "localhost");
}
private class TimeChannelHandler extends ChannelHandlerAdapter{
private int count=0;
//該方法會在建立連接後執行,表示觸發了連接建立事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for(int i=1;i<=100;i++){
String message = "query server time"+i+System.getProperty("line.separator");
ByteBuf req = Unpooled.copiedBuffer(message.getBytes());
ctx.writeAndFlush(req);//向服務端發送數據
}
}
//讀取服務端發送的數據,如果服務端發送了數據,Channel觸發了可讀事件,那麼該方法就會執行,讀取數據
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String message = (String) msg;
System.out.println(message+";count="+ ++count);//讀取服務端發送過來的數據,
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
/**
* Netty實現的時間服務器服務端粘包拆包問題修正版
*/
public class TimeServer {
public static void main(String[] args) {
try {
new TimeServer().bind(8080);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//綁定端口
public void bind(int port) throws InterruptedException {
//想想Netty的線程模型,我們需要首先創建Reactor模式的線程組,有兩個線程組
EventLoopGroup bossGroup = new NioEventLoopGroup();//主線程組,用於接收連接請求,並建立連接
EventLoopGroup workGroup = new NioEventLoopGroup();//工作線程組,該線程組通過SocketChannel進行IO操作
try {
//Netty服務端啓動類配置
ServerBootstrap serverBootstrap = new ServerBootstrap();//創建服務端啓動類,該類是輔助服務端啓動的類,主要目的是降低開發複雜度,我們只需要配置相關參數即可
serverBootstrap.group(bossGroup, workGroup);//配置線程池
//配置服務端的NioServerSocketChannel,既然是作爲服務端,肯定是需要一個ServerSocketChannel來接收客戶端連接請求並建立連接
serverBootstrap.channel(NioServerSocketChannel.class);
//配置NioServerSocketChannel的參數,或者說NioServerSocketChannel對應的TCP連接的參數,比如該參數爲連接超時參數,爲10000毫秒
serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
//和上面哪行代碼一樣,配置TCP連接的backlog參數爲1024
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
//最後,綁定IO事件的處理類ChildChannelHandler,該類就是Reactor模型中的Handler,用於處理IO事件,不過這裏的Handler是專門處理ACCEPT事件的,也就是建立連接創建SocketChannel
serverBootstrap.childHandler(new ChildChannelHandler());
//服務端輔助啓動類配置完成後,就可以綁定監聽端口了,綁定結果會通過ChannelFuture來獲取
ChannelFuture future1 = serverBootstrap.bind(port);
//這行代碼表示阻塞當前線程,直到future1中能夠獲取綁定操作結果
future1.sync();
//closeFuture表示關閉連接,該方法會返回一個ChannelFuture,關閉連接操作結果會存儲在該對象中
ChannelFuture future2 = future1.channel().closeFuture();
//阻塞當前線程,直到future2總可以獲取到關閉連接操作結果
future2.sync();
} finally {
//釋放線程池資源
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
//這個類主要用於監聽ServerSocketChannel接收連接請求並建立連接事件,也就是相當於NIO中的ACCEPT事件
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
//建立連接後創建的的SocketChannel,會在該方法中進行處理,添加響應的handler處理器
ch.pipeline().addLast(new TimeServerHandler());
}
}
//針對於SocketChannel的處理器,處理SocketChannel中的IO操作
private class TimeServerHandler extends ChannelHandlerAdapter {
//讀取數據,並對數據進行處理
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("server received:" + body);//輸出
//響應數據
String res = "server current time:" + System.currentTimeMillis()+System.getProperty("line.separator");
ByteBuf data = Unpooled.copiedBuffer(res.getBytes());
ctx.writeAndFlush(data);
}
//數據讀取過程中發生異常情況,要進行的後續處理都在該方法中
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
該類LineBasedFrameDecoder是已經默認換行符爲消息結束標誌,無法修改;Netty還提供了另外一個類,可以自由指定結束標誌字符,這個類就是DelimiterBasedFrameDecoder,可以通過該類指定字符$爲消息結束標誌字符。代碼如下
public class TimeClient {
public void connect(int port, String host) {
//創建客戶端的NIO線程組
EventLoopGroup workGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();//創建客戶輔助啓動類
try {
bootstrap.group(workGroup);//綁定NIO線程組
bootstrap.channel(NioSocketChannel.class);//初始創建的Channel類型
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ByteBuf buf = Unpooled.copiedBuffer("$".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeChannelHandler());
}
});//設置Handler
//bootstrap.bind("8081");
ChannelFuture future = bootstrap.connect(host, port);//與服務端建立連接
future.sync();//同步阻塞等待連接建立成功
ChannelFuture f = future.channel().closeFuture();//等待連接關閉斷開
f.sync();//同步阻塞等待連接關閉完成
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
workGroup.shutdownGracefully();//清理資源
}
}
public static void main(String[] args) {
new TimeClient().connect(8080, "localhost");
}
private class TimeChannelHandler extends ChannelHandlerAdapter{
private int count=0;
//該方法會在建立連接後執行,表示觸發了連接建立事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for(int i=1;i<=100;i++){
String message = "query server time"+i+"$";
ByteBuf req = Unpooled.copiedBuffer(message.getBytes());
ctx.writeAndFlush(req);//向服務端發送數據
}
}
//讀取服務端發送的數據,如果服務端發送了數據,Channel觸發了可讀事件,那麼該方法就會執行,讀取數據
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String message = (String) msg;
System.out.println(message+";count="+ ++count);//讀取服務端發送過來的數據,
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
public class TimeServer {
public static void main(String[] args) {
try {
new TimeServer().bind(8080);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//綁定端口
public void bind(int port) throws InterruptedException {
//想想Netty的線程模型,我們需要首先創建Reactor模式的線程組,有兩個線程組
EventLoopGroup bossGroup = new NioEventLoopGroup();//主線程組,用於接收連接請求,並建立連接
EventLoopGroup workGroup = new NioEventLoopGroup();//工作線程組,該線程組通過SocketChannel進行IO操作
try {
//Netty服務端啓動類配置
ServerBootstrap serverBootstrap = new ServerBootstrap();//創建服務端啓動類,該類是輔助服務端啓動的類,主要目的是降低開發複雜度,我們只需要配置相關參數即可
serverBootstrap.group(bossGroup, workGroup);//配置線程池
//配置服務端的NioServerSocketChannel,既然是作爲服務端,肯定是需要一個ServerSocketChannel來接收客戶端連接請求並建立連接
serverBootstrap.channel(NioServerSocketChannel.class);
//配置NioServerSocketChannel的參數,或者說NioServerSocketChannel對應的TCP連接的參數,比如該參數爲連接超時參數,爲10000毫秒
serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
//和上面哪行代碼一樣,配置TCP連接的backlog參數爲1024
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
//最後,綁定IO事件的處理類ChildChannelHandler,該類就是Reactor模型中的Handler,用於處理IO事件,不過這裏的Handler是專門處理ACCEPT事件的,也就是建立連接創建SocketChannel
serverBootstrap.childHandler(new ChildChannelHandler());
//服務端輔助啓動類配置完成後,就可以綁定監聽端口了,綁定結果會通過ChannelFuture來獲取
ChannelFuture future1 = serverBootstrap.bind(port);
//這行代碼表示阻塞當前線程,直到future1中能夠獲取綁定操作結果
future1.sync();
//closeFuture表示關閉連接,該方法會返回一個ChannelFuture,關閉連接操作結果會存儲在該對象中
ChannelFuture future2 = future1.channel().closeFuture();
//阻塞當前線程,直到future2總可以獲取到關閉連接操作結果
future2.sync();
} finally {
//釋放線程池資源
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
//這個類主要用於監聽ServerSocketChannel接收連接請求並建立連接事件,也就是相當於NIO中的ACCEPT事件
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ByteBuf buf = Unpooled.copiedBuffer("$".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
ch.pipeline().addLast(new StringDecoder());
//建立連接後創建的的SocketChannel,會在該方法中進行處理,添加響應的handler處理器
ch.pipeline().addLast(new TimeServerHandler());
}
}
//針對於SocketChannel的處理器,處理SocketChannel中的IO操作
private class TimeServerHandler extends ChannelHandlerAdapter {
//讀取數據,並對數據進行處理
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("server received:" + body);//輸出
//響應數據
String res = "server current time:" + System.currentTimeMillis()+"$";
ByteBuf data = Unpooled.copiedBuffer(res.getBytes());
ctx.writeAndFlush(data);
}
//數據讀取過程中發生異常情況,要進行的後續處理都在該方法中
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
2. 消息定長解決消息數據的半包讀取問題
實際上,如果你可以確定每次發送的最大消息長度的話,那麼可以採用消息定長的方式來解決,當然該方式必須保證消息定長的長度不會很大,可以使用Netty提供的FixedLengthFrameDecoder類實現消息定長。代碼如下
public class TimeClient {
public void connect(int port, String host) {
//創建客戶端的NIO線程組
EventLoopGroup workGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();//創建客戶輔助啓動類
try {
bootstrap.group(workGroup);//綁定NIO線程組
bootstrap.channel(NioSocketChannel.class);//初始創建的Channel類型
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new FixedLengthFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeChannelHandler());
}
});//設置Handler
//bootstrap.bind("8081");
ChannelFuture future = bootstrap.connect(host, port);//與服務端建立連接
future.sync();//同步阻塞等待連接建立成功
ChannelFuture f = future.channel().closeFuture();//等待連接關閉斷開
f.sync();//同步阻塞等待連接關閉完成
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
workGroup.shutdownGracefully();//清理資源
}
}
public static void main(String[] args) {
new TimeClient().connect(8080, "localhost");
}
private class TimeChannelHandler extends ChannelHandlerAdapter{
private int count=0;
//該方法會在建立連接後執行,表示觸發了連接建立事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for(int i=1;i<=100;i++){
String message = "query server time"+i+"$";
ByteBuf req = Unpooled.copiedBuffer(message.getBytes());
ctx.writeAndFlush(req);//向服務端發送數據
}
}
//讀取服務端發送的數據,如果服務端發送了數據,Channel觸發了可讀事件,那麼該方法就會執行,讀取數據
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String message = (String) msg;
System.out.println(message+";count="+ ++count);//讀取服務端發送過來的數據,
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
public class TimeServer {
public static void main(String[] args) {
try {
new TimeServer().bind(8080);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//綁定端口
public void bind(int port) throws InterruptedException {
//想想Netty的線程模型,我們需要首先創建Reactor模式的線程組,有兩個線程組
EventLoopGroup bossGroup = new NioEventLoopGroup();//主線程組,用於接收連接請求,並建立連接
EventLoopGroup workGroup = new NioEventLoopGroup();//工作線程組,該線程組通過SocketChannel進行IO操作
try {
//Netty服務端啓動類配置
ServerBootstrap serverBootstrap = new ServerBootstrap();//創建服務端啓動類,該類是輔助服務端啓動的類,主要目的是降低開發複雜度,我們只需要配置相關參數即可
serverBootstrap.group(bossGroup, workGroup);//配置線程池
//配置服務端的NioServerSocketChannel,既然是作爲服務端,肯定是需要一個ServerSocketChannel來接收客戶端連接請求並建立連接
serverBootstrap.channel(NioServerSocketChannel.class);
//配置NioServerSocketChannel的參數,或者說NioServerSocketChannel對應的TCP連接的參數,比如該參數爲連接超時參數,爲10000毫秒
serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
//和上面哪行代碼一樣,配置TCP連接的backlog參數爲1024
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
//最後,綁定IO事件的處理類ChildChannelHandler,該類就是Reactor模型中的Handler,用於處理IO事件,不過這裏的Handler是專門處理ACCEPT事件的,也就是建立連接創建SocketChannel
serverBootstrap.childHandler(new ChildChannelHandler());
//服務端輔助啓動類配置完成後,就可以綁定監聽端口了,綁定結果會通過ChannelFuture來獲取
ChannelFuture future1 = serverBootstrap.bind(port);
//這行代碼表示阻塞當前線程,直到future1中能夠獲取綁定操作結果
future1.sync();
//closeFuture表示關閉連接,該方法會返回一個ChannelFuture,關閉連接操作結果會存儲在該對象中
ChannelFuture future2 = future1.channel().closeFuture();
//阻塞當前線程,直到future2總可以獲取到關閉連接操作結果
future2.sync();
} finally {
//釋放線程池資源
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
//這個類主要用於監聽ServerSocketChannel接收連接請求並建立連接事件,也就是相當於NIO中的ACCEPT事件
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new FixedLengthFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
//建立連接後創建的的SocketChannel,會在該方法中進行處理,添加響應的handler處理器
ch.pipeline().addLast(new TimeServerHandler());
}
}
//針對於SocketChannel的處理器,處理SocketChannel中的IO操作
private class TimeServerHandler extends ChannelHandlerAdapter {
//讀取數據,並對數據進行處理
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("server received:" + body);//輸出
//響應數據
String res = "server current time:" + System.currentTimeMillis()+"$";
ByteBuf data = Unpooled.copiedBuffer(res.getBytes());
ctx.writeAndFlush(data);
}
//數據讀取過程中發生異常情況,要進行的後續處理都在該方法中
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
3. 定義應用層協議,規定消息結構
上面兩種方式基本都適用於比較小的文本消息數據傳輸,但如果針對於複雜的情況,比如既能傳輸文本數據,也能傳輸各種文件,那麼就比較複雜了,因爲TCP協議屬於傳輸層協議,其不會理解傳輸的二進制數據代表着什麼,所以就必須要定義一個上層應用層的協議,來對TCP接收的數據進行解析。
協議就不細說了,簡單來說就是客戶端服務端雙方的約定,協議規定了客戶端發送數據的格式,也規定了服務端解析數據的方式。常見的HTTP協議就是一個很好的例子,其採用了多種方式解決半包讀取的問題,首先就是指定了消息的結構(比如請求頭、請求行、請求體),並且對於其中的請求頭、請求頭和請求行肯定也需要一個結束符表示,消息子結構之間的間隔會添加一個換行符來表示結束。可以去詳細瞭解一下Http協議的格式。
Http協議相應的處理Handler肯定不用說,Netty中已經準備好了,只需直接調用就可以實現一個簡單的Http協議的文件服務器。代碼如下
public class HttpFileServer {
private static final String DEFAULT_URL = "/src/netty_demo/";
public static void main(String[] args) {
int port = 8080;
try {
new HttpFileServer().run(port, DEFAULT_URL);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//服務器啓動方法
public void run(final int port,final String url) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.group(bossGroup, workGroup);
bootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast("http-decoder",
new HttpRequestDecoder());//添加HTTP請求消息解碼器
ch.pipeline().addLast("http-aggregator",
new HttpObjectAggregator(65536));//該解碼器用於將多個Http消息合併爲一個完整的HTTP消息,
ch.pipeline().addLast("http-encoder",
new HttpResponseEncoder());//HTTP響應數據編碼器
ch.pipeline().addLast("http-chunked",
new ChunkedWriteHandler());//該handler用於解決異步傳輸大碼流(如文件傳輸),但不會佔用太多內存,防止內存溢出
ch.pipeline().addLast("fileServerHandler",
new HttpFileServerHandler(url));
}
});
ChannelFuture cf = bootstrap.bind(port).sync();
System.out.println("HTTP文件服務器啓動,地址爲:http://localhost:8080"+url);
cf.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
//返回HTTP響應,表示該請求錯誤
private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
status, Unpooled.copiedBuffer(status.toString()+"\r\n", CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
//設置HTTP響應的響應頭
private static void setContentTypeHeader(HttpResponse response, File file) {
MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimetypesFileTypeMap.getContentType(file.getPath()));
}
//響應一個重定向消息
private static void sendRedirect(ChannelHandlerContext ctx, String uri) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
response.headers().set(HttpHeaderNames.LOCATION, uri);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
//返回文件列表的html頁面
private static void sendFileList(ChannelHandlerContext ctx, File dir) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
StringBuilder html = new StringBuilder();
String dirPath = dir.getPath();
html.append("<!DOCTYPE html>\n" +
"<html>\n" +
"<head>");
html.append("<title>"+dirPath+"目錄:"+"</title>");
html.append("</head>");
html.append("<body>\n");
html.append("<h3>").append(dirPath).append("目錄:").append("</h3>\n");
html.append("<ul>");
html.append("<li>鏈接:<a href=\"../\">..</a></li>\n");
for (File f:dir.listFiles()) {
if (f.isHidden() || !f.canRead()) {
continue;
}
String name = f.getName();
html.append("<li>鏈接:<a href=\""+name+"\">"+name+"</a></li>\n");
}
html.append("</ul>");
html.append("</body>\n");
html.append("</html>");
ByteBuf buf = Unpooled.copiedBuffer(html, CharsetUtil.UTF_8);
response.content().writeBytes(buf);
buf.release();
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
//HTTP請求入棧後進行的一系列操作,包括進行響應消息
private class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private String url;
public HttpFileServerHandler(String url) {
this.url = url;
}
//解碼uri,並轉爲本地文件目錄
private String decodeUri(String uri) {
try {
uri = URLDecoder.decode(uri, "UTF-8");
} catch (UnsupportedEncodingException e) {
try {
uri = URLDecoder.decode(uri, "ISO-8859-1");
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException("請求路徑異常");
}
}
if (!uri.startsWith(DEFAULT_URL)) {
return null;
}
if (!uri.startsWith("/")) {
return null;
}
uri = uri.replace('/', File.separatorChar);
String path = System.getProperty("user.dir")+uri;
return path;
}
//接收到請求消息後進行的處理
@Override
protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
//首先判斷請求路徑正確性
if(request.decoderResult().isFailure()){
sendError(ctx, HttpResponseStatus.BAD_REQUEST);
return;
}
if(request.method()!=HttpMethod.GET) {
sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
return;
}
String uri = request.uri();
String path = decodeUri(uri);
if (path == null) {
sendError(ctx, HttpResponseStatus.FORBIDDEN);
return;
}
File file = new File(path);
if (file.isHidden() || !file.exists()) {
sendError(ctx, HttpResponseStatus.NOT_FOUND);
return;
}
//如果請求路徑對應的本地文件是一個文件夾(目錄)那麼就將該目錄下的文件變成一個列表展示在HTML頁面中,
//並將該頁面作爲響應消息返回給客戶端
if (file.isDirectory()) {
if(uri.endsWith("/")) {
sendFileList(ctx, file);
} else {
sendRedirect(ctx, uri+"/");
}
return;
}
if (!file.isFile()) {
sendError(ctx, HttpResponseStatus.FORBIDDEN);
return;
}
//如果請求路徑對應的目標是文件,那麼就開啓文件傳輸
RandomAccessFile accessFile = null;
try {
accessFile = new RandomAccessFile(file, "r");//以只讀方式打開文件
}catch (FileNotFoundException e) {
sendError(ctx, HttpResponseStatus.NOT_FOUND);
return;
}
long fileLength = accessFile.length();
//創建並設置響應頭和響應行
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
setContentTypeHeader(response, file);
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength+"");
String connection = request.headers().get(HttpHeaderNames.CONNECTION).toString();
if(connection.contentEquals(HttpHeaderValues.KEEP_ALIVE)) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(response);
//創建並設置響應體,即爲文件數據
ChannelFuture sendFileFuture;
sendFileFuture = ctx.write(new ChunkedFile(accessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception {
if (total < 0) {
System.out.println("文件傳輸中,已發送:"+progress+"byte");
}else {
System.out.println("文件傳輸中,已發送:"+progress+"/"+total+"byte");
}
}
@Override
public void operationComplete(ChannelProgressiveFuture future) throws Exception {
System.out.println("文件傳輸完成");
}
});
ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if(connection.contentEquals(HttpHeaderValues.KEEP_ALIVE)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
if (ctx.channel().isActive()) {
sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
}
}
}
如果看完了Http協議服務器代碼,你會覺得似乎nginx中對於反向代理的實現代碼似乎就是這樣的,當然,其中的路徑等參數並不是寫死了,而是通過nginx的配置文件定義,並且定義一系列的負載均衡規則等,實際代碼肯定要複雜的多,但是原理是一樣的。
3. 總結
在對Netty進行使用之後,相信10個人裏10個人都不會願意再去使用NIO的原生類庫了,Netty相較於NIO原生類庫的最大優勢就在這一點上,開發複雜度低,而且類庫齊全,功能強大。