Netty客戶端
-
建立一次通信,必然有着兩個對象。發送者和接收者
-
如我們使用微信、qq進行聊天的時候,就是一個通信的過程
-
當我們使用java NIO實現簡單的通信功能時,也必然存在着發送端(Client)和接收端(Server)
-
接下來通過java NIO的實現,去逐步深入理解學習Netty的實現原理
Java BIO簡單通信功能
- 服務端
public class Server{
// 首先定義一個 ServerSocket 對象
private ServerSocket server;
// 在構造方法中進行初始化
public Server(int port) {
// 傳入需要監聽的端口號
server = new ServerSocket(port);
}
public void listen() {
// 輪詢
while(true) {
// server.accept() 是一個阻塞(同步方法),在沒有請求接收的時候—阻塞
Socket socket = server.accept();
// 拿到請求之後,意味着建立了一個Sokdcet連接
// 拿到 client 發送的數據
InputStream is = socket.getInputStream();
byte[] buff = new byte[10];
// 把數據寫入到buff數組中去
int length = is.read(buff);
if(length > 0) {
// 轉化爲 String 字符串
String msg = new String(buff, 0, length);
}
// 關閉連接
socket.close();
}
}
}
- 客戶端
public class Client {
public static void main(String[] args) throws IOException {
// localhost:和誰進行通信 port:服務器端口
Socket client = new Socket("localhost", 8080);
// 客戶端 輸出 數據:創建輸出流對象
OutputStream os = client.getOutputStream();
// 生成一個隨機字符串
String uId = UUID.randomUUID().toString();
System.out.println("客戶端準備開始發送數據:"+uId);
System.out.println("發送數據字節長度:"+uId.getBytes().length);
os.write(uId.getBytes());
os.close();
client.close();
}
}
- 通過簡單的BIO實現,腦海中定下一個基調
- 客戶端發送,服務端接收,通過IP和端口地址進行關聯
- 不管Netty如何複雜,其思想是不變的
Netty是如何發起一次連接的
- 首先提出問題,假如使用Netty去發出一個請求
- 必然會和java的實現類似通過IP和端口進行連接,這點是躲不開的。
BootStrap來了
- 找到Netty中的BootStrap.java,偷來官方註釋瞧一瞧
- 比較清晰明瞭,傳達了兩點
- 客戶端通過Bootstrap使用Channel將會很簡單
- 我們使用UDP協議的時候,可以使用bind()方法來進行綁定。使用TCP的時候,使用connect()方法進行綁定
詳細註釋版本代碼
- 跟進connect方法,最終調用了Bootstrap的doResolveAndConnect方法
private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.isDone()) {
if (!regFuture.isSuccess()) {
return regFuture;
}
return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
} else {
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
promise.setFailure(cause);
} else {
promise.registered();
doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
}
}
});
return promise;
}
}
- 這裏面有幾個關鍵點需要我們注意
- initAndRegister方法具體做了什麼事情
- 客戶端時如何建立連接的
深入理解initAndRegister方法
- 調用到AbstractBootstrap的 initAndRegister 方法
NioSocketChannel初始化
- 這裏走到 ReflectiveChannelFactory 通過反射的方式創建 Channel 對象
- 創建Channel對象的類型 由 Bootstrap 的 channel() 方法指定,這裏實際上調用到了 NioSocketChannel 的構造方法
- 通過 NioSocketChannel 方法的構造器,深入瞭解
- newSocket(privider) 方法通過 SelectorProvider 對象新建一個 SocketChannel 對象
- 通過層層調用到 AbstractNioChannel 的構造方法
- 這裏parent屬性傳入爲null
- 通過 ch 設置 SocketChannel 非阻塞
- 通過 readInterestOp 設置 SelectionKey.OP_READ 爲初始狀態
-
接着調用父類 AbstractChannel 的構造方法
-
這裏把 Channel 對象設置爲 null
-
爲已經創建的 Channel 對象設置一個 唯一 id
-
對 Unsafe 對象進行初始化——其中封裝了Java底層Socket的操作
-
對 Pipeline 對象進行初始化
-
-
至此 我們最開始設置的 NioSocketChannel 對象就初始化完成了
-
接下來是 初始化完成之後的 init(Channel) 方法
NioSocketChannel註冊
- 完成初始化之後調用 init(channel) 方法
- 這裏體現了 Pipeline對象 和 Channel 對象的關聯
void init(Channel channel) throws Exception {
// 這裏通過 Channel 對象獲取已經初始化完成的 Pipeline 對象
ChannelPipeline p = channel.pipeline();
// 把 handler 添加到 Pipeline 對象中
p.addLast(config.handler());
// 這裏獲取一些底層配置,如 ChannelOption.SO_KEEPALIVE 表示是否開啓TCP底層心跳機制,true爲開啓
final Map<ChannelOption<?>, Object> options = options0();
// 爲每一個 channel 進行配置
synchronized (options) {
for (Entry<ChannelOption<?>, Object> e: options.entrySet()) {
try {
if (!channel.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
logger.warn("Unknown channel option: " + e);
}
} catch (Throwable t) {
logger.warn("Failed to set a channel option: " + channel, t);
}
}
}
// 提供自定義屬性功能 存儲在 attrs 中,通過 channel.attr(key)方法可以獲取相對應屬性
final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
}
}
- 接下來會執行以下代碼
ChannelFuture regFuture = config().group().register(channel);
- 調用鏈時序圖
- 最終調用到 AbstractNioChannel 對象的 doRegister 方法
- 到這裏完成了註冊關聯
- channel的註冊過程可以總結爲:和對應的EventLoop對象的Selector相關聯
- 初始化註冊完成之後,接下來就是客戶端連接操作了
客戶端連接實現
- 首先通過時序圖觀察一下客戶端連接的調用過程,通過調用過程尋找關鍵代碼
- 最終調用到 NioSocketChannel 方法完成連接
- javaChannel()方法獲取java NIO SOcketChannel 完成連接