前幾天我給大家分享了一些JAVA網絡編程的知識,從基本的JAVA網絡知識開始,我們學習了結合多線程管理的Socket編程,掌握了關於系統內核中的RecBuffer和SendBuffer的概念模型。然後我們開始了NIO編程模型的最佳搭檔,Channel和Buffer,以及Channel的讀寫操作和Buffer的get、put的關係。因為我們有了前面知識的鋪墊,所以今天我打算給大家分享一個不一樣的網絡編程模型,使用Selector實現多路複用網絡編程。(實際上是JAVA NIO系列第五篇文章)
今天分享目錄如下:
1. TCP上的Channel有哪些
2. TCP上Channel的讀寫操作
3. 使用Selector實現TCP多路複用網絡編程
(還是老話,如果對Selector的op_read、op_write很熟悉,就不要往下看了,去看對你更有幫助的文章)
1. TCP上的Channel
1.1 Server Socket上的Channel:ServerSocketChannel
class ServerSocketChannel{
static ServerSocketChannel open();
ServerSocket socket();
SocketChannel accept();
}
從上面的API中我們可以看出,ServerSocketChannel是使用open這個靜態方法中獲得的。並且我們可以中該Channel中獲得ServerSocket對象。
ServerSocketChannel channel = ServerSocketChannel.open();
ServerSocket serverSocket = channel.socket();
這裡需要注意:
上面代碼ServerSocket沒有綁定地址,需要綁定之後才可以使用,例如使用serverSocket.bind(port)
另外我們要使用ServerSocketChannel的accept來獲得SocketChannel,而不是使用ServerSocket的accept獲得Socket。
如果serverSocketChannel是non-blocking的,那麼accept可能返回null,因為沒有連接進來。
使用accept獲得的channel不是null,那麼該channel默認是Blocking模式,需要手動指定成為Non-Blocking
SelectableChannel configureBlocking(boolean block);
1.2 客戶端連接Socket的Channel:SocketChannel
class SocketChannel{
static SocketChannel open();
static SocketChannel open(SocketAddress address);
Socket socket();
}
從上面的API看到,大概和ServerSocketChannel類似,從Channel中獲得的socket仍然沒有連接到服務,需要使用帶有參數的open方法才能打開連接。同樣這裡獲得的channel默認是Blocking模式,需要手動指定成為Non-Blocking。
例如:我們指定ip和端口
SocketChannel channel = SocketChannel.open(new InetSocketAddress(ip,port));
2. Channel的操作
注意channel的Blocking模式,可以使用API設置
SelectableChannel configureBlocking(boolean block);
Channel的Read操作
在阻塞式編程中,TCP Channel的讀操作會阻塞,一直等到有數據可用。而在非阻塞編程中,讀操作不會被阻塞,如果recBuffer中有數據,則read返回數據,如果recBuffer空,就不會有數據返回。這裡需要注意的地方是非阻塞編程讀過程中,如果對方關閉了連接,或者本地關閉了read操作,會有異常拋出,否則read會立刻返回所讀數據數,儘管數據可能是0。
代碼示例稍後。
Channel的Write操作
在阻塞編程中,和讀一樣,寫操作一直等待sendBuffer中有數據。在非阻塞編程中,寫不會被阻塞。這裡和讀是一樣的,如果沒有異常拋出的話,這裡的寫操作會立即返回寫的數據數,這裡可以是0。什麼時候拋出異常?連接被對端關閉了,或者write操作被關閉都會拋異常的。
代碼示例稍後。
3. 多路複用網絡編程模型
終於到了今天重點的地方,希望讀者慢慢看,吸收裡面的知識點。
除了上面的讀、寫操作之外,在多路複用編程中還有兩個準備就緒類的操作,一個是準備好了Accept,另一個是準備連接已就緒。完整的操作是:
OP_ACCEPT:只有ServerSocketChannel才可以有這個操作,如果有連接進來,這裡就會有返回。
OP_CONNECT:SocketChannel在連接過程中的操作,已經出於連接完成的狀態,下一步是finishConnect。
OP_READ:只有連接完成的SocketChannel才有讀操作。返回數據見上面,除非有異常,否則就會有數據個數返回,即使數據返回0.
OP_WRITE:只有連接完成的SocketChannel才有寫操作。除非有異常,否則返回寫出的數據數,或0.
稍後在深入解釋這4個操作。我們先看代碼,混個面熟先。
如果下面代碼你會了,那麼JAVA的網絡編程你就掌握了。
代碼亮點,關鍵點:
非阻塞配置。(nonblocking)
Selector.select方法的使用。(timeout)
使用key的attachement管理每個連接的ByteBuffer。(attach)
好了,我們回到上面的4個操作上,op_accept,connect,read,wirte。實際上,上面代碼中select()操作,返回的只有兩種事件,可讀的事件和可寫的事件兩種。
可讀事件:有數據在recBuffer中或者accept方法有可用連接進來了,換句話說,OP_READ和OP_ACCETP是一個事件。
可寫事件:sendBuffer中有數據了,或者客戶端連接完成了。換句話說,OP_CONNECT 和OP_WRITE是一回事。
我們定義accept和read事件在一起為可讀事件沒有什麼問題,因accept在server端可以時,read還不可用,所有監聽是分開的,不會衝突。但是connect事件和write事件為可寫事件不能同時監聽,因為在client socket中,這倆可以是同時存在的,會出現歧義。因為在一個準備就緒的Socket中,op_write事件幾乎是一直可用的,所有除非有數據明確了,需要寫出了,我們才可以在socket中把write事件添加到select流程中,否則NIO調度線程總是選擇報告可寫事件,CPU壓力很大,因此我們才規定,除非有數據可以寫,我們不要訂閱可寫事件。
總結:
今天只是網絡編程基礎知識的分享,所以並沒有結合實際業務需求,明天我打算結合實際業務需求,實現一個斷點續傳的Socket編程模式。
感謝你的閱讀,希望本文的分享能給你帶來提升,有問題在下方評論中討論,我會及時反饋。
閱讀更多 吳濤分享 的文章