一、網絡IO
1.1 概述
文件IO用到的FileChannel並不支持非阻塞操作,學習NIO主要就是進行網絡IO,JavaNIO中的網絡通道是非阻塞IO的實現,基於事件驅動,非常適用於服務器需要維持大量連接,但是數據交換量不大的情況,例如一些即時通信的服務等…
在Java中編寫Socket服務器,通常有以下幾種模式:
- 一個客戶 端連接用一個線程,優點:程序編寫簡單;缺點:如果連接非常多,分配的線程也會非常多,服務器可能會因爲資源耗盡而崩潰。
- 把每一個客戶端連接交給一個擁有固定數量線程的連接池,優點:程序編寫相對簡單,可以處理大量的連接。線程的開銷非常大,連接如果非常多,排隊現象會比較嚴重。
- 使用 Java的NIO,用非阻塞的IO方式處理。這種模式可以用一個線程,處理大量的客戶端連接。
1.2 核心API
1.2.1 Selector選擇器
Selector選擇器能夠檢測多個註冊的通道上是否有事件發生,如果有事件發生,便獲取事件然後針對每個事件進行相應的響應處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接。這樣使得只有在連接真正有讀寫事件發生時,纔會調用函數來進行讀寫,就大大地減少了系統開銷,並且不必爲每個連接都創建一個線程,不用去維護多個線程,並且避免了多線程之間的上下文切換導致的開銷。
該類的常用方法如下所示:
1. public static Selector open(),得到一個選擇器對象
2. public int select(long timeout),監控所有註冊的channel,當其中有註冊的IO操作可以進行時,將對應的SelectionKey 加入到內部集合中並返回,參數用來設置超時時間
3. public Set<SelectionKey> selectedKeys(),從內部集合中得到所有的SelectionKey
4. SelectionKey, 代表了Selector 和serverSocketChannel 的註冊關係,一共四種:
5. int OP_ACCEPT:有新的網絡連接可以accept,值爲16
6. int OP_CONNECT:代表連接已經建立,值爲8
1.2.2 SelectionKey
SelectionKey代表了Selector 和serverSocketChannel 的註冊關係,一共四種:
- int OP_ACCEPT:有新的網絡連接可以accept,值爲16
- int OP_CONNECT:代表連接已經建立,值爲8
- int OP_READ和int OP_WRITE:代表了讀、寫操作,值爲1和4
該類的常用方法如下所示:
* public abstract Selector selector(),得到與之,關聯的Selector對象
* public abstract SelectableChannel channel(),得到與之關聯的通道
* public final Object attachment(),得到與之關聯的共享數據
* public abstract SelectionKey interestOps(int ops),設置或改變監聽事件
* public final boolean isAcceptable(), 是否可以accept
* public final boolean isReadable(),是否可以讀
* public final boolean isWritable(),是否可以寫
1.2.3 ServerSocketChannel
用來在服務器端監聽新的客戶端Socket連接,常用方法如下所示:
* public static ServerSocketChannel open(),得到- - 個ServerSocketChannel通道
* public final ServerSocketChannel bind(SocketAddress local),設置服務器端端口號
* public final SelectableChannel configureBlocking(boolean block),設置阻塞或非阻塞模式,取值false 表示採用非阻塞模式
* public SocketChannel accept(),接受-一個連接,返回代表這個連接的通道對象
* public final SelectionKey register(Selector sel, int ops),註冊一個選擇器並設置監聽事件
1.2.4 SocketChannel 網絡IO通道
具體負責進行讀寫操作。NIO總是把緩衝區的數據寫入通道,或者把通道里的數據讀出到緩衝區( buffer)。常用方法如下所示:
* public static SocketChannel open(),得到一個SocketChannel通道
* public final SelectableChannel configureBlocking(boolean block),設置阻塞或非阻塞模式,取值false表示採用非阻塞模式
* public boolean connect(SocketAddress remote),連接服務器
* public boolean finishConnect(),如果上面的方法連接失敗,接下來就要通過該方法完成連接操作
* public int write(ByteBuffer src),往通道里寫數據
* public int read(ByteBuffer dst),從通道里讀數據
* public final SelectionKey register(Selector sel, int ops, Object att),註冊一一個選擇 器並設置監聽事件,最後一個參數可以設置共享數據
* public final void close(),關閉通道
二、簡單的網絡IO的例子(客戶端向服務端發送消息)
2.1 客戶端代碼
package com.example.demo;
import java.net.InetSocketAddress;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @author : pengweiwei
* @date : 2020/1/28 4:54 下午
*/
public class NIOClient {
public static void main(String[] args) throws Exception{
//1.先得到一個網絡通道
SocketChannel socketChannel = SocketChannel.open();
//2.設置阻塞方式爲非阻塞
socketChannel.configureBlocking(false);
//3.設置連接的服務器的IP和端口號
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8888);
//4.連接服務器
if(!socketChannel.connect(inetSocketAddress)){
//如果沒連接上,在等待連接的時候還可以做其他事
while (!socketChannel.finishConnect()){
System.out.println(" do something...");
}
}
//5.得到一個緩衝區並存入數據
String msg = "hello server";
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
//6.發送數據
socketChannel.write(byteBuffer);
//7.不能關閉連接,讓客戶端保持連接
System.in.read();
}
}
2.2 服務端代碼
package com.example.demo;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* @author : pengweiwei
* @date : 2020/1/28 5:17 下午
*/
public class NIOServer {
public static void main(String[] args) throws Exception{
//1.得到一個ServerSocketChannel對象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2.得到一個選擇器Selector對象
Selector selector = Selector.open();
//3.綁定客戶端的端口號
serverSocketChannel.bind(new InetSocketAddress(8888));
//4.設置阻塞方式
serverSocketChannel.configureBlocking(false);
//5.把ServerSocketChannel對象註冊給Selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//6.監控客戶端
while (true){
//表示沒有客戶端嘗試連接
if(selector.select(2000) == 0){
System.out.println("沒有客戶端嘗試連接");
continue;
}
//如果有的話,得到所有的SelectionKey,判斷通道里的事件類型
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
//判斷selectionKey的事件類型
if(selectionKey.isAcceptable()){
//客戶端連接事件
System.out.println("OP_ACCEPT");
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if(selectionKey.isReadable()){
//讀取客戶端數據事件
SocketChannel channel = (SocketChannel)selectionKey.channel();
ByteBuffer buffer = (ByteBuffer)selectionKey.attachment();
channel.read(buffer);
System.out.println("客戶端發來的數據"+new String(buffer.array()));
}
iterator.remove();
}
}
}
}
運行結果:
後面的空格是由於緩衝區設置了1024的大小,而發送的消息只有hello,server 後面都是空格,所以,發送消息之前調用字符串的trim去掉空格就行。