IO模型
- 阻塞IO 如果資料沒有準備就緒,就一直等待,直到資料準備就緒;整個程式會被阻塞。
- 非阻塞IO 需不斷詢問核心是否已經準備好資料,非阻塞雖然不用等待但是一直佔用CPU。
- 多路複用IO NIO 多路複用IO,會有一個執行緒不斷地去輪詢多個socket的狀態,當socket有讀寫事件的時候才會呼叫IO讀寫操作。 用一個執行緒管理多個socket,是通過selector.select()查詢每個通道是否有事件到達,如果沒有事件到達,則會一直阻塞在那裡,因此也會帶來執行緒阻塞問題。
- 訊號驅動IO模型 在訊號驅動IO模型中,當使用者發起一個IO請求操作時,會給對應的socket註冊一個訊號函式,執行緒會繼續執行,當資料準備就緒的時候會給執行緒傳送一個訊號,執行緒接受到訊號時,會在訊號函式中進行IO操作。 非阻塞IO、多路複用IO、訊號驅動IO都不會造成IO操作的第一步,檢視資料是否準備就緒而帶來的執行緒阻塞,但是在第二步,對資料進行拷貝都會使執行緒阻塞。
- 非同步IO jdk7AIO 非同步IO是最理想的IO模型,當執行緒發出一個IO請求操作時,接著就去做自己的事情了,核心去檢視資料是否準備就緒和準備就緒後對資料的拷貝,拷貝完以後核心會給執行緒傳送一個通知說整個IO操作已經完成了,資料可以直接使用了。 同步的IO操作在第二個階段,對資料的拷貝階段,都會造成執行緒的阻塞,非同步IO則不會。
非同步IO在IO操作的兩個階段,都不會使執行緒阻塞。 Java 的 I/O 依賴於作業系統的實現。
Java NIO的工作原理
- 由一個專門的執行緒(Selector)來處理所有的IO事件,並負責分發。
- 事件驅動機制:事件到的時候觸發,而不是同步的去監視事件。
- 執行緒通訊:執行緒之間通過 wait,notify 等方式通訊。保證每次上下文切換都是有意義的。減少無謂的執行緒切換。
三大基本元件
Channel
- FileChannel, 從檔案中讀寫資料。
- DatagramChannel,通過UDP讀寫網路中的資料。
- SocketChannel,通過TCP讀寫網路中的資料。
- ServerSocketChannel,可以監聽新進來的TCP連線,對每一個新進來的連線都會建立一個SocketChannel。
Java NIO 的通道類似流,但又有些不同:
- 既可以從通道中讀取資料,又可以寫資料到通道。但流的讀寫通常是單向的。
- 通道可以非同步地讀寫。
- 通道中的資料總是要先讀到一個 Buffer,或者總是要從一個 Buffer 中寫入。
Buffer
關鍵的Buffer實現 ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer
Buffer兩種模式、三個屬性:
capacity
作為一個記憶體塊,Buffer有一個固定的大小值,也叫“capacity”.你只能往裡寫capacity個byte、long,char等型別。一旦Buffer滿了,需要將其清空(通過讀資料或者清除資料)才能繼續寫資料往裡寫資料。
position
當你寫資料到Buffer中時,position表示當前的位置。初始的position值為0.當一個byte、long等資料寫到Buffer後, position會向前移動到下一個可插入資料的Buffer單元。position最大可為capacity – 1.
當讀取資料時,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式,position會被重置為0. 當從Buffer的position處讀取資料時,position向前移動到下一個可讀的位置。
limit
在寫模式下,Buffer的limit表示你最多能往Buffer裡寫多少資料。 寫模式下,limit等於Buffer的capacity。
當切換Buffer到讀模式時, limit表示你最多能讀到多少資料。因此,當切換Buffer到讀模式時,limit會被設定成寫模式下的position值。換句話說,你能讀到之前寫入的所有資料(limit被設定成已寫資料的數量,這個值在寫模式下就是position)
參考連結:Buffer原理 www.cnblogs.com/chenpi/p/64…
Selector
Selector(選擇器)是Java NIO中能夠檢測一到多個NIO通道,並能夠知曉通道是否為諸如讀寫事件做好準備的元件。這樣,一個單獨的執行緒可以管理多個channel,從而管理多個網路連線。
監聽四種事件
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
select()方法
select()阻塞到至少有一個通道在你註冊的事件上就緒了。
select(long timeout)和select()一樣,除了最長會阻塞timeout毫秒(引數)。
selectedKeys()方法
呼叫selector的selectedKeys()方法,訪問“已選擇鍵集(selected key set)”中的就緒通道。
參考連結:作業系統層面分析Selector原理 zhhphappy.iteye.com/blog/203289…
NIO實現
服務端
public class NIOServerSocket {
//儲存SelectionKey的佇列
private static List<SelectionKey> writeQueue = new ArrayList<SelectionKey>();
private static Selector selector = null;
//新增SelectionKey到佇列
public static void addWriteQueue(SelectionKey key){
synchronized (writeQueue) {
writeQueue.add(key);
//喚醒主執行緒
selector.wakeup();
}
}
public static void main(String[] args) throws IOException {
// 1.建立ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2.繫結埠
serverSocketChannel.bind(new InetSocketAddress(60000));
// 3.設定為非阻塞
serverSocketChannel.configureBlocking(false);
// 4.建立通道選擇器
selector = Selector.open();
/*
* 5.註冊事件型別
*
* sel:通道選擇器
* ops:事件型別 ==>SelectionKey:包裝類,包含事件型別和通道本身。四個常量型別表示四種事件型別
* SelectionKey.OP_ACCEPT 獲取報文 SelectionKey.OP_CONNECT 連線
* SelectionKey.OP_READ 讀 SelectionKey.OP_WRITE 寫
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
System.out.println("伺服器端:正在監聽60000埠");
// 6.獲取可用I/O通道,獲得有多少可用的通道
int num = selector.select();
if (num > 0) { // 判斷是否存在可用的通道
// 獲得所有的keys
Set<SelectionKey> selectedKeys = selector.selectedKeys();
// 使用iterator遍歷所有的keys
Iterator<SelectionKey> iterator = selectedKeys.iterator();
// 迭代遍歷當前I/O通道
while (iterator.hasNext()) {
// 獲得當前key
SelectionKey key = iterator.next();
// 呼叫iterator的remove()方法,並不是移除當前I/O通道,標識當前I/O通道已經處理。
iterator.remove();
// 判斷事件型別,做對應的處理
if (key.isAcceptable()) {
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = ssChannel.accept();
System.out.println("處理請求:"+ socketChannel.getRemoteAddress());
// 獲取客戶端的資料
// 設定非阻塞狀態
socketChannel.configureBlocking(false);
// 註冊到selector(通道選擇器)
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
System.out.println("讀事件");
//取消讀事件的監控
key.cancel();
//呼叫讀操作工具類
NIOHandler.read(key);
} else if (key.isWritable()) {
System.out.println("寫事件");
//取消讀事件的監控
key.cancel();
//呼叫寫操作工具類
NIOHandler.write(key);
}
}
}else{
synchronized (writeQueue) {
while(writeQueue.size() > 0){
SelectionKey key = writeQueue.remove(0);
//註冊寫事件
SocketChannel channel = (SocketChannel) key.channel();
Object attachment = key.attachment();
channel.register(selector, SelectionKey.OP_WRITE,attachment);
}
}
}
}
}
}
複製程式碼
訊息處理
public class NIOHandler {
//構造執行緒池
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
public static void read(final SelectionKey key){
//獲得執行緒並執行
executorService.submit(new Runnable() {
@Override
public void run() {
try {
SocketChannel readChannel = (SocketChannel) key.channel();
// I/O讀資料操作
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = 0;
while (true) {
buffer.clear();
len = readChannel.read(buffer);
if (len == -1) break;
buffer.flip();
while (buffer.hasRemaining()) {
baos.write(buffer.get());
}
}
System.out.println("伺服器端接收到的資料:"+ new String(baos.toByteArray()));
//將資料新增到key中
key.attach(baos);
//將註冊寫操作新增到佇列中
NIOServerSocket.addWriteQueue(key);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
public static void write(final SelectionKey key) {
//拿到執行緒並執行
executorService.submit(new Runnable() {
@Override
public void run() {
try {
// 寫操作
SocketChannel writeChannel = (SocketChannel) key.channel();
//拿到客戶端傳遞的資料
ByteArrayOutputStream attachment = (ByteArrayOutputStream)key.attachment();
System.out.println("客戶端傳送來的資料:"+new String(attachment.toByteArray()));
ByteBuffer buffer = ByteBuffer.allocate(1024);
String message = "你好,我是伺服器!!";
buffer.put(message.getBytes());
buffer.flip();
writeChannel.write(buffer);
writeChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
複製程式碼
客戶端
public class NIOClientSocket {
public static void main(String[] args) throws IOException {
//使用執行緒模擬使用者 併發訪問
for (int i = 0; i < 1; i++) {
new Thread(){
public void run() {
try {
//1.建立SocketChannel
SocketChannel socketChannel=SocketChannel.open();
//2.連線伺服器
socketChannel.connect(new InetSocketAddress("localhost",60000));
//寫資料
String msg="我是客戶端"+Thread.currentThread().getId();
ByteBuffer buffer=ByteBuffer.allocate(1024);
buffer.put(msg.getBytes());
buffer.flip();
socketChannel.write(buffer);
socketChannel.shutdownOutput();
//讀資料
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
while (true) {
buffer.clear();
len = socketChannel.read(buffer);
if (len == -1)
break;
buffer.flip();
while (buffer.hasRemaining()) {
bos.write(buffer.get());
}
}
System.out.println("客戶端收到:"+new String(bos.toByteArray()));
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
};
}.start();
}
}
}
複製程式碼
多執行緒NIO Tips
- 示例程式碼僅供學習參考。對於一個已經被監聽到的事件,處理前先取消事件(SelectionKey .cancel())監控。否則selector.selectedKeys()會一直獲取到該事件,但該方法比較粗暴,並且後續register會產生多個SelectionKey。推薦使用selectionKey.interestOps()改變感興趣事件。
- Selector.select()和Channel.register()需同步。
- 當Channel設定為非阻塞(Channel.configureBlocking(false))時,SocketChannel.read 沒讀到資料也會返回,返回引數等於0。
- OP_WRITE事件,寫緩衝區在絕大部分時候都是有空閒空間的,所以如果你註冊了寫事件,這會使得寫事件一直處於就就緒,選擇處理現場就會一直佔用著CPU資源。參考下面的第二個連結。
- 粘包問題。
參考連結:SocketChannel.read blog.csdn.net/cao47820824…
參考連結:NIO坑 www.jianshu.com/p/1af407c04…