常 用 的Channel實現(xiàn)類類:
FileChannel , DatagramChannel ,ServerSocketChannel和SocketChannel 。
FileChannel 用于文件的數(shù)據(jù)讀寫,
DatagramChannel 用于 UDP 的數(shù)據(jù)讀寫,
ServerSocketChannel 和SocketChannel 用于 TCP 的數(shù)據(jù)讀寫。
【ServerSocketChanne類似 ServerSocket , SocketChannel 類似 Socket】
SocketChannel 與ServerSocketChannel
類似 Socke和ServerSocket,可以完成客戶端與服務(wù)端數(shù)據(jù)的通信工作.
1.ServerSocketChannel(服務(wù)端)
服務(wù)端:
- 打開一個服務(wù)端通道
- 綁定對應(yīng)的端口號
- 通道默認是阻塞的,需要設(shè)置為非阻塞
- 檢查是否有客戶端連接 有客戶端連接會返回對應(yīng)的通道
- 獲取客戶端傳遞過來的數(shù)據(jù),并把數(shù)據(jù)放在byteBuffer這個緩沖區(qū)中
- 給客戶端回寫數(shù)據(jù)
- 釋放資源
示例代碼
public static void main(String[] args) throws IOException, InterruptedException {
//1. 打開一個服務(wù)端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2. 綁定對應(yīng)的端口號
serverSocketChannel.bind(new InetSocketAddress(9999));
//3. 通道默認是阻塞的,需要設(shè)置為非阻塞
serverSocketChannel.configureBlocking(false);
System.out.println("服務(wù)端啟動成功....");
while (true) {
//4. 檢查是否有客戶端連接 有客戶端連接會返回對應(yīng)的通道
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel == null) {
System.out.println("沒有客戶端連接...我去做別的事情");
Thread.sleep(2000);
continue;
}
//5. 獲取客戶端傳遞過來的數(shù)據(jù),并把數(shù)據(jù)放在byteBuffer這個緩沖區(qū)中
ByteBuffer allocate = ByteBuffer.allocate(1024);
//返回值
//正數(shù): 表示本地讀到有效字節(jié)數(shù)
//0: 表示本次沒有讀到數(shù)據(jù)
//-1: 表示讀到末尾
int read = socketChannel.read(allocate);
System.out.println("客戶端消息:" + new String(allocate.array(), 0,
read, StandardCharsets.UTF_8));
//6. 給客戶端回寫數(shù)據(jù)
socketChannel.write(ByteBuffer.wrap("沒錢".getBytes(StandardCharsets.UTF_8)));
//7. 釋放資源
socketChannel.close();
}
}
2.SocketChannel(客戶端)
- 打開通道
- 設(shè)置連接IP和端口號
- 寫出數(shù)據(jù)
- 讀取服務(wù)器寫回的數(shù)據(jù)
- 釋放資源
public static void main(String[] args) throws IOException {
//1. 打開通道
SocketChannel socketChannel = SocketChannel.open();
//2. 設(shè)置連接IP和端口號
socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
//3. 寫出數(shù)據(jù)
socketChannel.write(ByteBuffer.wrap("老板.還錢吧!".getBytes(StandardCharsets.UTF_8)));
//4. 讀取服務(wù)器寫回的數(shù)據(jù)
ByteBuffer allocate = ByteBuffer.allocate(1024);
int read = socketChannel.read(allocate);
System.out.println("服務(wù)端消息:" +
new String(allocate.array(), 0, read, StandardCharsets.UTF_8));
//5. 釋放資源
socketChannel.close();
}
3. Selector
介紹
可以用一個線程,處理多個的客戶端連接,就會使用到NIO的Selector(選擇器). Selector 能夠檢測
多個注冊的服務(wù)端通道上是否有事件發(fā)生,如果有事件發(fā)生,便獲取事件然后針對每個事件進行相應(yīng)的
處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求。
不使用selector情況下:

在這種沒有選擇器的情況下,對應(yīng)每個連接對應(yīng)一個處理線程. 但是連接并不能馬上就會發(fā)送信息,所以還
會產(chǎn)生資源浪費
使用selector:

只有在通道真正有讀寫事件發(fā)生時,才會進行讀寫,就大大地減少了系統(tǒng)開銷,并且不必為每個連接都
創(chuàng)建一個線程,不用去維護多個線程, 避免了多線程之間的上下文切換導(dǎo)致的開銷
Selector常用方法:
Selector.open() : //得到一個選擇器對象
selector.select() : //阻塞 監(jiān)控所有注冊的通道,當有對應(yīng)的事件操作時, 會將SelectionKey放入
集合內(nèi)部并返回事件數(shù)量
selector.select(1000): //阻塞 1000 毫秒,監(jiān)控所有注冊的通道,當有對應(yīng)的事件操作時, 會將
SelectionKey放入集合內(nèi)部并返回
selector.selectedKeys() : // 返回存有SelectionKey的集合
SelectionKey常用方法
SelectionKey.isAcceptable(): 是否是連接繼續(xù)事件
SelectionKey.isConnectable(): 是否是連接就緒事件
SelectionKey.isReadable(): 是否是讀就緒事件
SelectionKey.isWritable(): 是否是寫就緒事件
SelectionKey中定義的4種事件:
SelectionKey.OP_ACCEPT —— 接收連接繼續(xù)事件,表示服務(wù)器監(jiān)聽到了客戶連接,服務(wù)器
可以接收這個連接了
SelectionKey.OP_CONNECT —— 連接就緒事件,表示客戶端與服務(wù)器的連接已經(jīng)建立成功
SelectionKey.OP_READ —— 讀就緒事件,表示通道中已經(jīng)有了可讀的數(shù)據(jù),可以執(zhí)行讀操
作了(通道目前有數(shù)據(jù),可以進行讀操作了)
SelectionKey.OP_WRITE —— 寫就緒事件,表示已經(jīng)可以向通道寫數(shù)據(jù)了(通道目前可以用
于寫操作)
Selector 編碼
服務(wù)端實現(xiàn)步驟:
- 打開一個服務(wù)端通道
- 綁定對應(yīng)的端口號
- 通道默認是阻塞的,需要設(shè)置為非阻塞
- 創(chuàng)建選擇器
- 將服務(wù)端通道注冊到選擇器上,并指定注冊監(jiān)聽的事件為OP_ACCEPT
- 檢查選擇器是否有事件
- 獲取事件集合
- 判斷事件是否是客戶端連接事件SelectionKey.isAcceptable()
- 得到客戶端通道,并將通道注冊到選擇器上, 并指定監(jiān)聽事件為OP_READ
- 判斷是否是客戶端讀就緒事件SelectionKey.isReadable()
- 得到客戶端通道,讀取數(shù)據(jù)到緩沖區(qū)
- 給客戶端回寫數(shù)據(jù)
- 從集合中刪除對應(yīng)的事件, 因為防止二次處理
代碼示例
public static void main(String[] args) throws IOException {
//1. 打開一個服務(wù)端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2. 綁定對應(yīng)的端口號
serverSocketChannel.bind(new InetSocketAddress(9999));
//3. 通道默認是阻塞的,需要設(shè)置為非阻塞
serverSocketChannel.configureBlocking(false);
//4. 創(chuàng)建選擇器
Selector selector = Selector.open();
//5. 將服務(wù)端通道注冊到選擇器上,并指定注冊監(jiān)聽的事件為OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服務(wù)端啟動成功.....");
while (true) {
//6. 檢查選擇器是否有事件
int select = selector.select(2000);
if (select == 0) {
System.out.println("沒有事件發(fā)生....");
continue;
}
//7. 獲取事件集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//8. 判斷事件是否是客戶端連接事件SelectionKey.isAcceptable()
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
//9. 得到客戶端通道,并將通道注冊到選擇器上, 并指定監(jiān)聽事件為OP_READ
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("有客戶端連接.....");
//將通道必須設(shè)置成非阻塞的狀態(tài).因為selector選擇器需要輪詢監(jiān)聽每個通道的事件
socketChannel.configureBlocking(false);
//指定監(jiān)聽事件為OP_READ 讀就緒事件
socketChannel.register(selector, SelectionKey.OP_READ);
}
//10. 判斷是否是客戶端讀就緒事件SelectionKey.isReadable()
if (key.isReadable()) {
//11.得到客戶端通道,讀取數(shù)據(jù)到緩沖區(qū)
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer allocate = ByteBuffer.allocate(1024);
int read = socketChannel.read(allocate);
if (read > 0) {
System.out.println("客戶端消息:" + new String(allocate.array(), 0, read
, StandardCharsets.UTF_8));
//12. 給客戶端回寫數(shù)據(jù)
socketChannel.write(ByteBuffer.wrap("沒錢".getBytes(StandardCharsets.UTF_8)));
socketChannel.close();
}
}
//13. 從集合中刪除對應(yīng)的事件, 因為防止二次處理.
iterator.remove();
}
}