參考視頻:https://www.bilibili.com/video/av76223318?p=5
I/O模型簡單的解釋:用什麼樣的通道進行數據的發送和接收,很大程度上決定了程序通訊的性能
Java共支持三種網絡編程模型:BIO,NIO,AIO
BIO:Blocking IO
同步並阻塞(傳統阻塞型),服務器實現模式爲一個連接一個線程,即客戶端有連接請求時,服務器端就需要啓動一個線程進行處理,如果這個連續不做任何事情會造成不必要的線程開銷。可以通過線程池機制改善(實現多個客戶連接服務器)。
放在java.io包下
適用場景:
連接數目小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中。jdk1.4之前的唯一選擇,但程序簡單易理解
BIO簡單流程:
1 服務器端啓動一個ServerSockert
2 客戶端啓動Socket對服務器進行通訊,默認情況下服務器端需要對每個客戶建立一個線程與之通訊
3 客戶端發送請求後,先諮詢服務器是否有線程響應
3.1 如果沒有響應,則會等待,或者被拒絕
3.2 如果有響應,客戶端線程會等待請求結束後,再繼續執行
public class BioServerSocket {
public static void main(String[] args) throws Exception{
//創建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
//用線程池來管理線程
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
System.out.println("===== 啓動ServerSocket");
//啓用並監聽
while (true){
//等待並監聽
Socket socket = serverSocket.accept();
//獲得監聽後啓用線程來處理
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
byte[] bytes = new byte[1024];
try {
InputStream inputStream = socket.getInputStream();
int read;
while ((read = inputStream.read(bytes)) != -1){
System.out.println(new String(bytes,0,read));
}
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
}
}
利用命令窗口的telnet模仿客戶端發送請求
telnet命令若需要啓動:https://jingyan.baidu.com/article/7908e85c6ec355af491ad265.html
連接命令:telnet 127.0.0.1 6666
作爲客戶端發送命令:Ctrl ]
發送內容命令:send XXXXX
實際發送內容爲XXXX
NIO:Non-blocking/New IO
同步非阻塞,服務器實現爲一個線程處理多個請求,即客戶端發送的連接請求都會註冊到多路複用器(Selector選擇器)上,多路複用器輪詢到連接有I/O請求就進行處理
放在java.nio包下
適用場景:
連接數目多且連接時間短(輕操作)的架構,比如聊天服務器,彈屏系統,服務期間通訊等。編程比較複雜,jdk1.4之後開始
三大核心部分:Channel通道,Buffer緩衝區,Selector選擇器
NIO是面向緩衝區,或者面向塊編程,是Channel的事件Event驅動的
BIO 和 NIO 比較:
1 BIO以流的方式處理數據,NIO以塊的方式處理數據,塊IO的效率比流IO的效率要高很多
2 BIO是阻塞的,NIO是非阻塞的
3 BIO是基於字節流和字符流進行操作,而NIO基於Channel和Buffer進行操作,數據總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。Selector用於監聽多個通道的事件,因此使用單個線程可以監聽多個客戶端通道
Buffer緩衝區:
本質是一個可以讀寫數據的內存塊,可以理解成一個容器對象(含數組),該對象提供一組方法,可以更輕鬆的使用內存塊。緩衝區對象內置了一些機制,可以跟蹤和記錄緩衝區的狀態變化情況。Channel提供可以從文件,網絡讀取數據的渠道,但讀取和寫入的數據必須經由Buffer。
子類都有四個重要屬性:
Capatity: 容量,即可以容納的最大數據量。在緩衝區創建時被設定並且不能改變。
Limit: 表示緩衝區的當前終點,不能對緩衝區超過極限的位置進行讀寫,且極限是可以修改的
Position: 位置,下一個要被讀或者寫的元素的索引,每次讀寫緩衝區時都會改變該值,爲下次讀寫做準備
Mark:標記
static void Test(){
ByteBuffer byteBuffer = ByteBuffer.allocate(5); //創建緩衝區
ByteBuffer direct = ByteBuffer.allocateDirect(5);//創建直接緩衝區
byteBuffer.put("h".getBytes()[0]);
byteBuffer.put("s".getBytes()[0]);
byteBuffer.put("s".getBytes()[0]);
byteBuffer.put("n".getBytes()[0]);
byteBuffer.put("j".getBytes()[0]);
byteBuffer.flip(); //切換讀寫
while (byteBuffer.hasRemaining()){
System.out.println(byteBuffer.get());
}
System.out.println(" ================ ");
byteBuffer.put(1,"k".getBytes()[0]);
System.out.println("第一個元素:"+byteBuffer.get(1));
System.out.println("第二個元素:"+byteBuffer.get(2));
System.out.println("容量:"+byteBuffer.capacity());
System.out.println("位置:"+byteBuffer.position());
System.out.println(".. 具體諸多其他方法搜搜就好了");
}
Channel通道:
類似於流,但有區別
1 通道能同時進行讀寫,而流只能進行讀或者只能寫
2 通道能異步進行讀寫數據
3 通道能從緩衝讀取數據,也能寫入緩衝
Channel在java.nio中是接口,具體常用的實現類:FileChannel(文件數據讀寫), DatagramChannel(UDP的數據讀寫), ServerSocketChannel(TCP數據讀寫), SocketChannel(TCP數據讀寫)
FileChannel:
方法:
read(ByteBuffer des); 通道讀取數據,放到緩存區
write(ByteBuffer tar); 讀取緩衝區數據,放到通道
transferFrom(....); 從目標通道複製數據到當前通道
transferTo(....); 從當前通道複製數據到目標通道
static void test2() throws Exception {
//發送數據
String str = "Hi,女孩";
//封裝的Buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(str.getBytes());
//buffer反轉
byteBuffer.flip();
//最終存儲地方
FileOutputStream fileInputStream = new FileOutputStream("d://hiGirl.text");
//獲得通道
FileChannel channel = fileInputStream.getChannel();
//緩衝區讀取數據到通道
channel.write(byteBuffer);
//關閉流
fileInputStream.close();
}
static void test4() throws Exception{
File file = new File("d://hiGirl.txt");
FileInputStream fileInputStream = new FileInputStream(file);
FileOutputStream fileOutputStream = new FileOutputStream("d://hiGir2.txt");
FileChannel channel = fileInputStream.getChannel();
FileChannel outputStreamChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
while (true){
byteBuffer.clear();
int read = channel.read(byteBuffer);
if (read == -1) break;
byteBuffer.flip();
outputStreamChannel.write(byteBuffer);
byteBuffer.flip();
}
fileInputStream.close();
fileOutputStream.close();
}
static void test5() throws Exception{
File file = new File("d://hiGirl.txt");
FileInputStream fileInputStream = new FileInputStream(file);
FileOutputStream fileOutputStream = new FileOutputStream("d://hiGir4.txt");
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileChannel outputStreamChannel = fileOutputStream.getChannel();
inputStreamChannel.transferTo(0,inputStreamChannel.size(),outputStreamChannel);
// 或 outputStreamChannel.transferFrom(inputStreamChannel,0,inputStreamChannel.size());
inputStreamChannel.close();
outputStreamChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
MapperedByteBuffer:
可以讓文件直接在內存(對外內存)中進行修改(操作系統不需要copy),而如何同步到文件由NIO完成
static void test6() throws Exception{
RandomAccessFile randomAccessFile = new RandomAccessFile("d://hiGirl.txt","rw");
FileChannel channel = randomAccessFile.getChannel();
/**
* FileChannel.MapMode
* READ_ONLY: 只讀
* READ_WRITE:讀寫
* PRIVATE: private (copy-on-write)
*
* 定義可以修改的範圍
* position: 可以修改的起始位置
* size: 映射內存大小
*/
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
map.put(0,(byte) 'H');
map.put(2,(byte) 'L');
randomAccessFile.close();
}
scattering:將數據寫入到Buffer時,可以採用Buffer數組,依次寫入
gatthering:將數據讀取到Buffer時,可以採用Buffer數組,依次讀取
static void test7() throws Exception{
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
SocketChannel accept = serverSocketChannel.accept();
//設置數組Buffer
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(3);
byteBuffers[1] = ByteBuffer.allocate(5);
int mesLength = 3+5;
while (true){
long byteRead = 0;
while (byteRead < mesLength){
long read = accept.read(byteBuffers);
byteRead += read;
System.out.println("byteRead: "+byteRead);
Arrays.asList(byteBuffers).stream().map(buffer -> "position: "+buffer.position()+" ,limit: "+buffer.limit())
.forEach(System.out::println);
}
//將所有buffer進行反轉可以進行其他操作
Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.flip());
//將數據讀出顯示到客戶端
long byteWrite = 0;
while (byteWrite < mesLength){
long write = accept.write(byteBuffers);
byteWrite += write;
}
//復位
Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear());
System.out.println("byteRead = "+byteRead + " byteWrite = "+byteWrite);
}
}
selector:
selector能夠檢測多個註冊的通道上是否有事件發生(多個Channel可以以事件的方式註冊到註冊到同一個Selector),如果有事件發生便獲取事件,然後針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求。
1 Netty的IO線程聚合了Selector選擇器,可以同時併發處理成百上千個併發請求
2 當線程從某客戶端Socket通道進行讀寫數據時,若沒有線程可用時,可進行其他操作
3 線程通常將阻塞IO的空閒時間用於其他通道上執行IO操作,所以單個線程可以管理多個輸入和輸出通道
4 由於讀寫操作都是非阻塞的,這就可以充分提升IO線程的運行效率,避免由於頻繁IO阻塞導致的線程掛起
5 一個IO線程可以併發處理N個客戶端連接和讀寫操作,這從根本上解決了傳統同步阻塞IO一連接一線程模型,架構的性能,彈性伸縮能力和可靠性都得到了極大的提升
Selector抽象類
public abstract class Selector implements Closeable {
//得到一個選擇器對象
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
//監控所有註冊通道,當其中有IO操作可進行時,將對應的SelectionKey加入到內部集合中並返回,參數用來設置超時時間
//select()爲阻塞方法,至少有一個事件發生纔會返回
//select(long timeout) 非阻塞,若無事件發生,超時時間後也會返回
//selectNow() 非阻塞,有事件發生立馬返回
//wakeup() 喚醒selector
public abstract int select(long timeout)
throws IOException;
//從內部集合中得到所有SelectionKey
public abstract Set<SelectionKey> selectedKeys();
}
原理
1 當有客戶端連接時,會通過ServerSocketChannel得到對應的SocketChannel
2 對應的SocketChannel註冊倒Selector上,一個Selector上可以註冊多個SocketChannel
----- SelectionKey register(Selector sel, int ops,Object att)
3 註冊後返回一個SelectionKey,會和該Selector關聯
4 Selector進行監聽,用select()方法 ,會返回有事件發生的通道的個數
5 進一步得到各個SelectionKey
6 再通過SelectionKey反向獲取SocketChannel channel()
7 可以通過得到的channel,完成業務處理
OP_ACCEPT: 有新的網絡連接可以accept, 1<<4 = 16
OP_CONNECT:代表連接已經建立:1<<3=8
OP_READ:代表讀操作:1<<0 = 1
OP_WRITE:代表寫操作:1<<2=4
零拷貝:零拷貝不是不拷貝,而是沒有CPU拷貝
零拷貝是網絡編程的關鍵,很多性能優化都離不開。在java程序中,常用的零拷貝mmap(內存映射)和sendFile。那麼它們在OS裏到底是一個怎樣的設計?
1 我們說的零拷貝,是從操作系統角度來說。因爲內核緩衝區之間,沒有數據是重複的(只有kernel buffer有一份數據)
2 零拷貝不僅帶來更少的數據複製,還能帶來其他的性能優勢,例如更少的上下文切換,更少的CPU緩存僞共享以及無CPU校驗和計算
(用戶態,kernel[內核態],硬件)
mmap:
文件映射,將文件映射到內核緩衝區,同時,用戶空間可以共享內核空間的數據。這樣在進行網絡傳輸時,就可以減少內核空間到用戶控件的拷貝次數
SendFile:
Linux2.1提供了sendFile函數,基本原理:數據不經過用戶態,直接從內核緩衝區進入到SocketBuffer,同時由於和用戶態完全無關,就減少了一次上下文切換
Linux2.4對sendFile函數進行了優化,避免了從內核緩衝區拷貝到SocketBuffer的操作,直接拷貝到協議棧,從而再一次減少了數據拷貝
mmap 和sendfile區別
1 mmap適合小數據量讀寫,sendfile適合大文件傳輸
2 mmap需要3次上下文切換,3次數據拷貝;sendfile需要2次上下文切換,最少2次數據拷貝
3 sendfile可以利用DMA方式,減少CPU拷貝,mmap則不能(必須從內核拷貝到Socket緩衝區)
傳統拷貝:
4次拷貝:
硬件 -> DMA拷貝到 -> 內核態(kernelBuffer)
內核態(kernelBuffer) -> cpu拷貝到 ->用戶buffer
用戶buffer -> cpu拷貝到 -> socket buffer
socket buffer -> DMA拷貝到 -> 協議棧
3次切換:硬件,內核態, 用戶
MMAP拷貝:
3次拷貝:
硬件 -> DMA拷貝到 -> 內核態(kernelBuffer)
內核態(kernelBuffer) -> cpu拷貝到 -> socket buffer
socket buffer -> DMA拷貝到 -> 協議棧
3次切換:硬件,內核態, 用戶
SendFile拷貝:
linux2.1版的sendfile中還有一次CPU拷貝:3次拷貝,2次切換
linux2.4版的纔是零拷貝:2次拷貝,2次切換
零拷貝代碼Test:
server:
public class NewIOServiceSocket {
public static void main(String[] args) throws Exception{
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open().bind(new InetSocketAddress(7000));
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
int readLength = 0;
while (readLength != -1){
readLength = socketChannel.read(byteBuffer);
byteBuffer.rewind();//倒帶
}
}
}
}
client
public class NewIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",7000));
FileInputStream fileInputStream = new FileInputStream(new File("C:\\Users\\chinaoly\\Desktop\\12345.txt"));
FileChannel fileChannel = fileInputStream.getChannel();
long start = System.currentTimeMillis();
/**
* linux 環境下 transferTo 一次即可完成
* windows環境下,transferTo 一次最多傳8M,大於8M分段傳輸,需要記住傳輸時的位置
*/
long count = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println(" 時間 : "+(System.currentTimeMillis() - start));
}
}
AIO(NIO.2):
異步非阻塞,AIO引入異步通道的概念,採用Proactor模式,簡化了程序編寫,有校的請求才啓用線程,它的特點是先有操作系統完成後才通知服務器端啓動線程去處理,一般適用於連接數較多且連接時間較長的應用。jdk1.7以後引入,但目前還未得到廣泛運用
適用場景:
連接數目多且連接時間長(重操作)的架構。比如相冊服務器,充分調用OS參與併發操作。編程負責,jdk7開始