一:學習前知識儲備
1.1 Socket
- 套接字(socket)是一個抽象層,應用程序可以通過它發送或接收數據,可對其進行像對文件一樣的打開、讀寫和關閉等操作。套接字允許應用程序將I/O插入到網絡中,並與網絡中的其他應用程序進行通信。網絡套接字是IP地址與端口的組合。
- 一種獨立於協議的網絡編程接口
1.2 IO操作
IO操作包括:對硬盤的讀寫、對socket的讀寫以及對外設的讀寫。
基本的IO操作
分爲兩個過程:
- DMA(直接內存存取)把數據讀取到內核空間的緩衝區(讀就緒)
- 內核將數據拷貝到用戶空間。
同步IO、異步IO、阻塞IO、非阻塞IO解釋
- 同步IO:當用戶發出IO請求操作之後,內核會去查看要讀取的數據是否就緒,如果數據沒有就緒,就一直等待。需要通過用戶線程或者內核不斷地去輪詢數據是否就緒,當數據就緒時,再將數據從內核拷貝到用戶空間。
- 異步IO:只有IO請求操作的發出是由用戶線程來進行的,IO操作的兩個階段都是由內核自動完成,然後發送通知告知用戶線程IO操作已經完成。也就是說在異步IO中,不會對用戶線程產生任何阻塞。
- 阻塞IO:當用戶線程發起一個IO請求操作(以讀請求操作爲例),內核查看要讀取的數據還沒就緒,當前線程被掛起,阻塞等待結果返回。
- 非阻塞IO:如果數據沒有就緒,則會返回一個標誌信息告知用戶線程當前要讀的數據沒有就緒。當前線程在拿到此次請求結果的過程中,可以做其它事情。
1.3 Java中的NIO
JAVA NIO有兩種解釋:一種叫非阻塞IO(Non-blocking I/O),另一種也叫新的IO(New I/O),其實是同一個概念。它是一種同步非阻塞的I/O模型,也是I/O多路複用的基礎,已經被越來越多地應用到大型應用服務器,成爲解決高併發與大量連接、I/O處理問題的有效方式。
NIO主要有三大核心部分: Channel、Buffer、Selector
傳統IO是基於字節流和字符流進行操作(基於流),而NIO基於Channel和Buffer(緩衝區)進行操作,數據總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。Selector(選擇區)用於監聽多個通道的事件(比如:連接打開,數據到達)。因此,單個線程可以監聽多個數據通道。
- Channel(通道):表示到實體
- 如硬件設備、文件、網絡套接字或可以執行一個或多個不同 I/O 操作(如讀取或寫入)的程序組件的開放的連接。
- Channel接口的常用實現類有FileChannel(對應文件IO)、DatagramChannel(對應UDP)、SocketChannel和ServerSocketChannel(對應TCP的客戶端和服務器端)。
- Channel和IO中的Stream(流)是差不多一個等級的。只不過Stream是單向的,譬如:InputStream, OutputStream.而Channel是雙向的,既可以用來進行讀操作,又可以用來進行寫操作。
- Buffer(緩衝區):是一個用於存儲特定基本類型數據的容器。
- 除了boolean外,其餘每種基本類型都有一個對應的buffer類。
- Buffer類的子類有ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer 。
- Selector(選擇器):用於監聽多個通道的事件(比如:連接打開,數據到達)。
- 單個的線程可以監聽多個數據通道。即用選擇器,藉助單一線程,就可對數量龐大的活動I/O通道實施監控和維護。
傳統的IO處理模式
好處:由於每個Client連接有一個單獨的處理線程爲其服務,因此可保證良好的響應時間。
缺陷:每一個連接都使用一個線程。當系統負載增大(併發請求增多)時,Server端需要的線程數會增加,對於操作系統來說,線程之間上下文切換的開銷很大。佔用過多資源
非阻塞的IO處理模式,一個線程管理多個網絡連接
NIO服務器端實現非阻塞過程
- 服務器上所有Channel需要向Selector註冊,而Selector則負責監視這些Socket的IO狀態(觀察者)。
- 當其中任意一個或者多個Channel具有可用的IO操作時,該Selector的select()方法將會返回大於0的整數,該整數值就表示該Selector上有多少個Channel具有可用的IO操作,並提供了selectedKeys()方法來返回這些Channel對應的SelectionKey集合(一個SelectionKey對應一個就緒的通道)。
- 正是通過Selector,使得服務器端只需要不斷地調用Selector實例的select()方法即可知道當前所有Channel是否有需要處理的IO操作。注
- :java NIO就是多路複用IO,jdk7之後底層是epoll模型。參考下圖進行理解:
1.4 NIO、BIO、AIO對比
- BIO(IO)
- 同步阻塞,傳統io方式。
- 適用於連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中。
- NIO
- 同步非阻塞,jdk4開始支持。
- 適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器。
- AIO
- 異步非阻塞,jdk7開始支持。
- 適用於連接數目多且連接比較長(重操作)的架構。
形象的理解NIO和AIO:如果把內核比作快遞,NIO就是你要自己時不時到官網查下快遞是否已經到了你所在城市,然後自己去取快遞;AIO就是快遞員送貨上門了。
1.5 服務端案例
package cn.xd.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import com.sun.xml.internal.ws.util.StringUtils;
public class NioServer {
public static void main(String[] args) throws Exception {
// 1、初始化一個ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 9999);
serverSocketChannel.configureBlocking(false);// 設置爲非阻塞模式,後續的accept()方法會立刻返回
serverSocketChannel.socket().bind(inetSocketAddress, 1024);// 監聽本地9999端口的請求,第二個參數限制可以建立的最大連接數
Selector selector = Selector.open();
/**
* 將通道註冊到一個選擇器上(非阻塞模式與選擇器搭配會工作的更好)
* 注意register()方法的第二個參數。這是一個“interest集合”,意思是在通過Selector監聽Channel時對什麼事件感興趣。
* 可以監聽四種不同類型的事件:OP_CONNECT,OP_ACCEPT,OP_READ,OP_WRITE
* 如果你對不止一種事件感興趣,那麼可以用“位或”操作符將常量連接起來:SelectionKey.OP_READ | SelectionKey.OP_WRITE
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 2、監聽連接請求並處理
while (true) {
int connects = selector.select(2000);// 每次最多阻塞2秒
if (connects == 0) {
System.out.println("沒有請求...");
continue;
} else {
System.out.println("請求來了...");
}
// 獲取監聽到有連接請求的channel對應的selectionKey
Set selectedKeys = selector.selectedKeys();
// 遍歷selectionKey來訪問就緒的通道
Iterator selectedKeyIterator = selectedKeys.iterator();
while (selectedKeyIterator.hasNext()) {
SelectionKey selectionKey = selectedKeyIterator.next();
if (selectionKey.isValid()) {
if (selectionKey.isAcceptable()) {// 接收就緒
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
// 返回一個包含新進來的連接SocketChannel,因爲前面設置的非阻塞模式,這裏會立即返回。
SocketChannel socketChannel = channel.accept();
if (socketChannel == null) {
return;
}
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("連接建立完成");
doWrite(socketChannel, "connection is established");// 連接建立完成,給客戶端發消息
} else if (selectionKey.isReadable()) {// 讀就緒
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(10);
while ((socketChannel.read(readBuffer)) > 0) {// // 讀取客戶端發送來的消息
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "utf-8");
doWrite(socketChannel, body);// 將客戶端發送的內容原封不動的發回去
readBuffer.clear();
}
socketChannel.close();//讀取數據完畢後關閉連接,如果不關閉一直處於連接狀態。
}
}
selectedKeyIterator.remove(); // 注意每次必須手動remove(),下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中
}
}
}
private static void doWrite(SocketChannel socketChannel, String response) throws IOException {
if (StringUtils.isNotBlank(response)) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put(bytes);
writeBuffer.flip();
// 發送消息到客戶端
socketChannel.write(writeBuffer);
writeBuffer.clear();
}
}
}