概述
初步瞭解了NIO核心組件的API,也大致知道了如何啓動一個網絡IO服務和客戶端後。本篇在此基礎上做一些補充,把一些必須要理解的
正文
ServerSocketChannel的accept方法和Selecor的select
在ServerSocketChannel的API中我們可以通過accept方法監聽傳入的連接,有傳入連接的時候返回一個SocketChannel。也可以註冊ACCEPT事件到Selector上,然後通過Selector的select方法監聽傳入的連接。
需要特別注意的是,accept方法返回的是一個連接,並不是連接集合,而通過Selector的select方法返回的是符合事件要求數量,然後通過selectedKeys獲得SelectoinKey集合。
進行SelectionKey集合遍歷處理的時候,需要刪除元素
注意到通過selectedKeys獲得SelectoinKey集合後,操作完會進行remove,或者遍歷操作後一次性進行clear操作。因爲Selector維護着selectedKeys,所以在操作完後如果不進行刪除,那麼下次輪詢還會存在,而出現混亂,那麼Selector爲什麼不自己刪除呢,這就不對了,畢竟人家是告訴你現在的事件集合讓你處理,你處理是否結束並不清楚,所以由真正處理方來決定刪除最合理了。
selector.select()
一直返回大於0問題
在前面的例子的Server端代碼中,當client發起連接然後發送消息後斷開連接,服務端通過read方法獲取數據並且執行selectionKeys.remove()
方法,但是在下一次while循環中selector.select()
結果還是大於0,debug代碼可以發現此時selectionKey
任然是isReadable的,只是read方法返回並不大於0,所以不會進入第二個while循環中,雖然如此,這個時候是一直死循環在執行第一個while循環的,並不是我們預期的阻塞在selector.select()
上等待就緒事件的觸發。
客戶端關閉連接的時候,read會返回-1,所以我們可以在第二次循環的時候直接關閉Channel。
ByteBuffer的複用
可以看到在服務端read數據的時候使用ByteBuffer承接,獲得的數據是不會超過ByteBuffer的長度,在使用完後就進行clear
操作切換到讀模式進行復用,否則每次讀事件都需要爲ByteBuffer申請新的內存,非常浪費。當再次需要切換到寫模式的時候使用flip
方法切換。
Selector.wakeup()
有意思的是Selector採用自己和自己建的TCP連接(Windows)或pipe(Linux)來實現wakeup,因爲只要向這個連接或pipe裏發點數據,就可以喚醒select了。
以下兩個文章透析了這部分的知識:
https://blog.csdn.net/haoel/article/details/2224055
https://blog.csdn.net/haoel/article/details/2224069
SelectionKey.attach(Object ob)
SelectionKey關聯一個對象,這個對象可以通過attachment方法取出,這點在前篇已經提過,這裏再說明一下是要強調一遍通過這個方法,可以比較方便的圍繞selectorKey來進行處理IO流程的拆分工作。比如,我們設計一個接口,所有的綁定對象都實現這個接口,接口中定義一個執行方法,在Accept的SelectorKey上綁定專門處理Accept時間的對象,在READ事件綁定專門處理的對象,如此代碼可以是一致的都是從SelectorKey中拿到對象調用執行方法。
關於拆包粘包
前置說明一下這個概念,首先需要理解的是TCP是流式協議,傳輸內容像流水一樣是沒有明確段落的概念的,而實際使用時我們會把通信的消息進行定義,也就會把一段完整內容定義成有業務含義的消息,那麼天然是需要解決這個矛盾的。這也是接觸Netty時很快會了解解決拆包粘包問題的辦法,這裏只是提一下,瞭解是有這個問題的即可。
代碼
在前面的代碼基礎上,進行了改造,實現一個C/S模型的文件傳輸功能,客戶端將本地文件通過Channel傳輸給服務端,服務端將文件寫到自己本地。
Client代碼:
public static void start() throws IOException {
InetSocketAddress inetSocketAddress = new InetSocketAddress(20023);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(inetSocketAddress);
ByteBuffer byteBuffer = ByteBuffer.allocate(204800);
FileChannel fileChannel = new FileInputStream("/Users/dongchao/test").getChannel();
if (socketChannel.finishConnect()) {
while (fileChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
}
fileChannel.close();
}
public static void main(String[] args) throws IOException {
start();
}
Server端代碼:
public class Server {
public static void start() throws IOException {
// 開啓Selector
Selector selector = Selector.open();
// 開啓一個Server Socket Channel 用於監聽連接Accept事件
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 設置成非阻塞模式
serverSocketChannel.configureBlocking(false);
// 監聽端口
serverSocketChannel.bind(new InetSocketAddress(20023));
// 設置Selector 多路複用監聽Accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 產生一個文件名
String localFile = getFileName();
// 獲取一個File Channel
FileOutputStream fos = new FileOutputStream(localFile);
FileChannel outFileChannel = fos.getChannel();
// 記錄文件字節量
long outLength = 0;
ByteBuffer byteBuffer = ByteBuffer.allocate(204800);
// 阻塞選擇準備好進行I/O操作的鍵
while(selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel1.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
int length;
// 對於Socket Channel來說是從Channel上讀取數據,寫入到ByteBuffer
while ((length = socketChannel.read(byteBuffer)) > 0) {
// 切換成讀模式
byteBuffer.flip();
// 對於File Channel來說是讀取ByteBuffer數據,寫入到Channel
outFileChannel.write(byteBuffer);
outLength = outLength + length;
// 切換成寫模式
byteBuffer.clear();
outFileChannel.force(true);
}
// read 返回-1 客戶端關閉連接
if (length < 0) {
socketChannel.close();
}
}
iterator.remove();
}
System.out.print("file length : " + outLength);
}
fos.close();
outFileChannel.close();
}
private static String getFileName() {
long currentTimeMillis = System.currentTimeMillis();
return "/Users/dongchao/" + currentTimeMillis;
}