java基础巩固-宇宙第一AiYWM:为了维持生计,四大基础之OS_Part_2整起~IO们那些事【包括五种IO模型:(BIO、NIO、IO多路复用、信号驱动、AIO);零拷贝、事件处理及并发等模型】
PART0.前情提要:
- 通常用戶進(jìn)程的一個(gè)完整的IO分為兩個(gè)階段(IO有內(nèi)存IO、網(wǎng)絡(luò)IO和磁盤IO三種,通常我們說(shuō)的IO指的是后兩者!):【操作系統(tǒng)和驅(qū)動(dòng)程序運(yùn)行在內(nèi)核空間,應(yīng)用程序運(yùn)行在用戶空間,兩者不能使用指針傳遞數(shù)據(jù),因?yàn)長(zhǎng)inux使用的虛擬內(nèi)存機(jī)制,必須通過(guò)系統(tǒng)調(diào)用請(qǐng)求內(nèi)核來(lái)完成IO動(dòng)作。】
- 內(nèi)存IO:
- 磁盤IO:
- 和磁盤打交道就是費(fèi)事,但也沒(méi)辦法,咱們?nèi)比萘垦健T蹅冊(cè)?Java 中 IO 流分為輸入流和輸出流,根據(jù)數(shù)據(jù)的處理方式又分為字節(jié)流和字符流。根據(jù)擒賊先擒王的手法,咱們把Java IO 流的 40 多個(gè)類的如下 4 個(gè)抽象類基類先抓住,【Java IO 流的 40 多個(gè)類都是從如下 4 個(gè)抽象類基類中派生出來(lái)的】。
- InputStream/Reader: 所有的輸入流的基類,InputStream是字節(jié)輸入流【用于從源頭(通常是文件)讀取數(shù)據(jù)(字節(jié)信息)到內(nèi)存中,java.io.InputStream抽象類是所有字節(jié)輸入流的父類。】,Reader是字符輸入流【不管是文件讀寫還是網(wǎng)絡(luò)發(fā)送接收,信息的最小存儲(chǔ)單元都是字節(jié)。 那為什么 I/O 流操作要分為字節(jié)流操作和字符流操作呢?原因主要是有時(shí)候如果我們不知道編碼類型就很容易出現(xiàn)亂碼問(wèn)題,I/O 流就干脆提供了一個(gè)直接操作字符的接口,方便我們平時(shí)對(duì)字符進(jìn)行流操作。如果音頻文件、圖片等媒體文件用字節(jié)流比較好,如果涉及到字符的話使用字符流比較好。字符流默認(rèn)采用的是 Unicode 編碼,我們可以通過(guò)構(gòu)造方法自定義編碼。utf8 :英文占 1 字節(jié),中文占 3 字節(jié),unicode:任何字符都占 2 個(gè)字節(jié),gbk:英文占 1 字節(jié),中文占 2 字節(jié)】
- InputStream
- InputStream 常用方法 :
- 通過(guò) readAllBytes() 讀取輸入流所有字節(jié)并將其直接賦值給一個(gè) String 對(duì)象// 新建一個(gè) BufferedInputStream 對(duì)象 BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt")); // 讀取文件的內(nèi)容并復(fù)制到 String 對(duì)象中 String result = new String(bufferedInputStream.readAllBytes()); System.out.println(result);
- 一般我們是不會(huì)直接單獨(dú)使用 FileInputStream ,通常會(huì)配合 BufferedInputStream字節(jié)緩沖輸入流
- DataInputStream 用于讀取指定類型數(shù)據(jù),不能單獨(dú)使用,必須結(jié)合 FileInputStream
- ObjectInputStream 用于從輸入流中讀取 Java 對(duì)象(反序列化,用于序列化和反序列化的類必須實(shí)現(xiàn) Serializable 接口,對(duì)象中如果有屬性不想被序列化,使用 transient 修飾),ObjectOutputStream 用于將對(duì)象寫入到輸出流(序列化)
- InputStream 常用方法 :
- Reader:Reader 用于讀取文本, InputStream 用于讀取原始字節(jié)。
- Reader常用方法:
- InputStreamReader 是字節(jié)流轉(zhuǎn)換為字符流的橋梁,其子類 FileReader 是基于該基礎(chǔ)上的封裝,可以直接操作字符文件。
- Reader常用方法:
- InputStream
- OutputStream/Writer: 所有輸出流的基類,OutputStream是字節(jié)輸出流【OutputStream用于將數(shù)據(jù)(字節(jié)信息)寫入到目的地(通常是文件),java.io.OutputStream抽象類是所有字節(jié)輸出流的父類。】,Writer是字符輸出流
- FileOutputStream
- FileOutputStream 是最常用的字節(jié)輸出流對(duì)象,可直接指定文件路徑,可以直接輸出單字節(jié)數(shù)據(jù),也可以輸出指定的字節(jié)數(shù)組。FileOutputStream 通常也會(huì)配合 BufferedOutputStream字節(jié)緩沖輸出流
- DataOutputStream 用于寫入指定類型數(shù)據(jù),不能單獨(dú)使用,必須結(jié)合 FileOutputStream
- FileOutputStream 是最常用的字節(jié)輸出流對(duì)象,可直接指定文件路徑,可以直接輸出單字節(jié)數(shù)據(jù),也可以輸出指定的字節(jié)數(shù)組。FileOutputStream 通常也會(huì)配合 BufferedOutputStream字節(jié)緩沖輸出流
- Writer:
- OutputStreamWriter 是字符流轉(zhuǎn)換為字節(jié)流的橋梁,其子類 FileWriter 是基于該基礎(chǔ)上的封裝,可以直接將字符寫入到文件
- FileOutputStream
- 字節(jié)緩沖流:IO 操作是很消耗性能的,所以緩沖流用來(lái)將數(shù)據(jù)加載至緩沖區(qū),一次性讀取/寫入多個(gè)字節(jié),從而避免頻繁的 IO 操作,提高流的傳輸效率
- 字節(jié)緩沖流采用了裝飾器模式 來(lái)增強(qiáng) InputStream 和OutputStream子類對(duì)象的功能。【字節(jié)流和字節(jié)緩沖流的性能差別主要體現(xiàn)在我們使用兩者的時(shí)候都是調(diào)用 write(int b) 和 read() 這兩個(gè)一次只讀取一個(gè)字節(jié)的方法的時(shí)候。由于字節(jié)緩沖流內(nèi)部有緩沖區(qū)(字節(jié)數(shù)組),因此,字節(jié)緩沖流會(huì)先將讀取到的字節(jié)存放在緩存區(qū),大幅減少 IO 次數(shù),提高讀取效率。】
- 如果是調(diào)用 read(byte b[]) 和 write(byte b[], int off, int len) 這兩個(gè)寫入一個(gè)字節(jié)數(shù)組的方法的話,只要字節(jié)數(shù)組的大小合適,兩者的性能差距其實(shí)不大,基本可以忽略。
- BufferedInputStream(字節(jié)緩沖輸入流):
- BufferedInputStream 內(nèi)部維護(hù)了一個(gè)緩沖區(qū),這個(gè)緩沖區(qū)實(shí)際就是一個(gè)字節(jié)數(shù)組
- BufferedInputStream 內(nèi)部維護(hù)了一個(gè)緩沖區(qū),這個(gè)緩沖區(qū)實(shí)際就是一個(gè)字節(jié)數(shù)組
- BufferedOutputStream(字節(jié)緩沖輸入流):
- BufferedOutputStream 內(nèi)部也維護(hù)了一個(gè)緩沖區(qū),并且,這個(gè)緩存區(qū)的大小也是 8192 字節(jié)
- 字符緩沖流:
- BufferedReader (字符緩沖輸入流)和 BufferedWriter(字符緩沖輸出流)類似于 BufferedInputStream(字節(jié)緩沖輸入流)和BufferedOutputStream(字節(jié)緩沖輸入流),內(nèi)部都維護(hù)了一個(gè)字節(jié)數(shù)組作為緩沖區(qū)。不過(guò),前者主要是用來(lái)操作字符信息
- 打印流:
- System.out.println(“Hello!”);System.out 實(shí)際是用于獲取一個(gè) PrintStream 對(duì)象,print方法實(shí)際調(diào)用的是 PrintStream 對(duì)象的 write 方法。PrintStream 屬于字節(jié)打印流,與之對(duì)應(yīng)的是 PrintWriter (字符打印流)。PrintStream 是 OutputStream 的子類,PrintWriter 是 Writer 的子類。
- 隨機(jī)訪問(wèn)流:隨機(jī)訪問(wèn)流指的是 支持隨意跳轉(zhuǎn)到文件的任意位置進(jìn)行讀寫的 RandomAccessFile
- 文件內(nèi)容指的是文件中實(shí)際保存的數(shù)據(jù),元數(shù)據(jù)則是用來(lái)描述文件屬性比如文件的大小信息、創(chuàng)建和修改時(shí)間。
- RandomAccessFile 中 有一個(gè)文件指針 用來(lái)表示 下一個(gè)將要被寫入或者讀取的字節(jié)所處的位置。我們可以通過(guò) RandomAccessFile 的 seek(long pos) 方法來(lái)設(shè)置文件指針的偏移量(距文件開(kāi)頭 pos 個(gè)字節(jié)處)。如果想要獲取文件指針當(dāng)前的位置的話,可以使用 getFilePointer() 方法。
- RandomAccessFile 比較常見(jiàn)的一個(gè)應(yīng)用就是實(shí)現(xiàn)大文件的 斷點(diǎn)續(xù)傳 。何謂斷點(diǎn)續(xù)傳?簡(jiǎn)單來(lái)說(shuō)就是上傳文件中途暫停或失敗(比如遇到網(wǎng)絡(luò)問(wèn)題)之后,不需要重新上傳,只需要上傳那些未成功上傳的文件分片即可。分片(先將文件切分成多個(gè)文件分片)上傳是斷點(diǎn)續(xù)傳的基礎(chǔ)。
- InputStream/Reader: 所有的輸入流的基類,InputStream是字節(jié)輸入流【用于從源頭(通常是文件)讀取數(shù)據(jù)(字節(jié)信息)到內(nèi)存中,java.io.InputStream抽象類是所有字節(jié)輸入流的父類。】,Reader是字符輸入流【不管是文件讀寫還是網(wǎng)絡(luò)發(fā)送接收,信息的最小存儲(chǔ)單元都是字節(jié)。 那為什么 I/O 流操作要分為字節(jié)流操作和字符流操作呢?原因主要是有時(shí)候如果我們不知道編碼類型就很容易出現(xiàn)亂碼問(wèn)題,I/O 流就干脆提供了一個(gè)直接操作字符的接口,方便我們平時(shí)對(duì)字符進(jìn)行流操作。如果音頻文件、圖片等媒體文件用字節(jié)流比較好,如果涉及到字符的話使用字符流比較好。字符流默認(rèn)采用的是 Unicode 編碼,我們可以通過(guò)構(gòu)造方法自定義編碼。utf8 :英文占 1 字節(jié),中文占 3 字節(jié),unicode:任何字符都占 2 個(gè)字節(jié),gbk:英文占 1 字節(jié),中文占 2 字節(jié)】
- 和磁盤打交道就是費(fèi)事,但也沒(méi)辦法,咱們?nèi)比萘垦健T蹅冊(cè)?Java 中 IO 流分為輸入流和輸出流,根據(jù)數(shù)據(jù)的處理方式又分為字節(jié)流和字符流。根據(jù)擒賊先擒王的手法,咱們把Java IO 流的 40 多個(gè)類的如下 4 個(gè)抽象類基類先抓住,【Java IO 流的 40 多個(gè)類都是從如下 4 個(gè)抽象類基類中派生出來(lái)的】。
- 網(wǎng)絡(luò)IO:
- 客戶端和服務(wù)器能在網(wǎng)絡(luò)中通信,那必須得使用 Socket 編程來(lái)支持跨主機(jī)間通信。Socket這個(gè)貨叫插口,其實(shí)我覺(jué)得就是個(gè),比如咱們自己家里有兩臺(tái)路由器要通信,你不能拿一條線直接粘到路由器屁股上吧,肯定是兩臺(tái)路由器屁股上都有口,然后一條線這邊插好那邊插好,然后就可以跨主機(jī)通信了唄,路由器屁股上開(kāi)的口我覺(jué)得就可以看作是Socket。
- 創(chuàng)建 Socket 的時(shí)候,可以指定網(wǎng)絡(luò)層使用的是 IPv4 還是 IPv6,傳輸層使用的是 TCP 還是 UDP。 不管是那種,反正肯定是 服務(wù)器的程序要先跑起來(lái),然后等待客戶端的連接和數(shù)據(jù)
- 我們先來(lái)看看服務(wù)端的 Socket 編程過(guò)程是怎樣的。或者叫**TCP Socket**。它基本只能一對(duì)一通信,因?yàn)槭褂玫氖峭阶枞姆绞?#xff0c;當(dāng)服務(wù)端在還沒(méi)處理完一個(gè)客戶端的網(wǎng)絡(luò) I/O 時(shí),或者 讀寫操作發(fā)生阻塞時(shí),其他客戶端是無(wú)法與服務(wù)端連接的。可如果我們服務(wù)器只能服務(wù)一個(gè)客戶,那這樣就太浪費(fèi)資源了,于是我們要改進(jìn)這個(gè)網(wǎng)絡(luò) I/O 模型,以支持更多的客戶端。
- 或者說(shuō),先看看 客戶端和服務(wù)端的基于 TCP 的通信流程:
- 服務(wù)端的偽代碼:
- 在 LInux 中一切皆文件,socket 也不例外,每個(gè)打開(kāi)的文件都有讀寫緩沖區(qū),對(duì)文件執(zhí)行read()、write()時(shí)的具體流程如下:
- 服務(wù)端的偽代碼:
- 可以看到 傳統(tǒng)的 socket 通信會(huì)阻塞在 connect,accept,read/write 這幾個(gè)操作上,這樣的話如果 server 是單進(jìn)程/線程的話,只要 server 阻塞,就不能再接收其他 client 的處理了,由此可知傳統(tǒng)的 socket 無(wú)法支持 C10K
- 針對(duì)傳統(tǒng) IO 模型缺陷的改進(jìn),主要有兩種:
- 多進(jìn)程/線程模型
- IO 多路程復(fù)用:下面有
- 高并發(fā)即我們所說(shuō)的 C10K(一個(gè)server 服務(wù) 1w 個(gè) client),C10M。高并發(fā)架構(gòu)其實(shí)有一些很通用的架構(gòu)設(shè)計(jì),如無(wú)鎖化,緩存等。
- 經(jīng)典的 C10K 問(wèn)題:如果服務(wù)器的內(nèi)存只有 2 GB,網(wǎng)卡是千兆的,能支持并發(fā) 1 萬(wàn)請(qǐng)求嗎【單機(jī)同時(shí)處理 1 萬(wàn)個(gè)請(qǐng)求的問(wèn)題。】從硬件資源角度看,對(duì)于 2GB 內(nèi)存千兆網(wǎng)卡的服務(wù)器,如果每個(gè)請(qǐng)求處理占用不到 200KB 的內(nèi)存和 100Kbit 的網(wǎng)絡(luò)帶寬就可以滿足并發(fā) 1 萬(wàn)個(gè)請(qǐng)求。不過(guò),要想真正實(shí)現(xiàn) C10K 的服務(wù)器,要考慮的地方在于服務(wù)器的網(wǎng)絡(luò) I/O 模型,效率低的模型,會(huì)加重系統(tǒng)開(kāi)銷。基于進(jìn)程或者線程模型的,其實(shí)還是有問(wèn)題的。新到來(lái)一個(gè) TCP 連接,就需要分配一個(gè)進(jìn)程或者線程,那么如果要達(dá)到 C10K,意味著要一臺(tái)機(jī)器維護(hù) 1 萬(wàn)個(gè)連接,相當(dāng)于要維護(hù) 1 萬(wàn)個(gè)進(jìn)程/線程,操作系統(tǒng)就算死扛也是扛不住的。
- 針對(duì)傳統(tǒng) IO 模型缺陷的改進(jìn),主要有兩種:
- 我們先來(lái)看看服務(wù)端的 Socket 編程過(guò)程是怎樣的。或者叫**TCP Socket**。它基本只能一對(duì)一通信,因?yàn)槭褂玫氖峭阶枞姆绞?#xff0c;當(dāng)服務(wù)端在還沒(méi)處理完一個(gè)客戶端的網(wǎng)絡(luò) I/O 時(shí),或者 讀寫操作發(fā)生阻塞時(shí),其他客戶端是無(wú)法與服務(wù)端連接的。可如果我們服務(wù)器只能服務(wù)一個(gè)客戶,那這樣就太浪費(fèi)資源了,于是我們要改進(jìn)這個(gè)網(wǎng)絡(luò) I/O 模型,以支持更多的客戶端。
- 服務(wù)器單機(jī)理論最大能連接多少個(gè)客戶端?
- TCP 連接是由四元組唯一確認(rèn)的,這個(gè)四元組就是:本機(jī)IP, 本機(jī)端口, 對(duì)端IP, 對(duì)端端口。服務(wù)器作為服務(wù)方,通常會(huì)在本地固定監(jiān)聽(tīng)一個(gè)端口,等待客戶端的連接。 因此服務(wù)器的本地 IP 和端口是固定的,于是對(duì)于服務(wù)端 TCP 連接的四元組只有對(duì)端 IP 和端口是會(huì)變化的,所以最大 TCP 連接數(shù) = 客戶端 IP 數(shù)×客戶端端口數(shù)。對(duì)于 IPv4,客戶端的 IP 數(shù)最多為 2 的 32 次方,客戶端的端口數(shù)最多為 2 的 16 次方,也就是服務(wù)端單機(jī)最大 TCP 連接數(shù)約為 2 的 48 次方。但是服務(wù)器肯定承載不了那么大的連接數(shù),主要會(huì)受兩個(gè)方面的限制:
- fd(文件描述符):Socket 實(shí)際上是一個(gè)文件,也就會(huì)對(duì)應(yīng)一個(gè)文件描述符。在 Linux 下,單個(gè)進(jìn)程打開(kāi)的文件描述符數(shù)是有限制的,沒(méi)有經(jīng)過(guò)修改的值一般都是 1024,不過(guò)我們可以通過(guò) ulimit 增大文件描述符的數(shù)目;
- 在 Linux 中無(wú)論是文件,socket,還是管道,設(shè)備等,一切皆文件,Linux 抽象出了一個(gè) VFS(virtual file system) 層,屏蔽了所有的具體的文件,VFS 提供了統(tǒng)一的接口給上層調(diào)用,這樣應(yīng)用層只與 VFS 打交道,極大地方便了用戶的開(kāi)發(fā),仔細(xì)對(duì)比你會(huì)發(fā)現(xiàn),這和 Java 中的面向接口編程很類似【fd 的值從 0 開(kāi)始,其中 0,1,2 是固定的,分別指向標(biāo)準(zhǔn)輸入(指向鍵盤),標(biāo)準(zhǔn)輸出/標(biāo)準(zhǔn)錯(cuò)誤(指向顯示器),之后每打開(kāi)一個(gè)文件,fd 都會(huì)從 3 開(kāi)始遞增,但需要注意的是 fd 并不一定都是遞增的,如果關(guān)閉了文件,之前的 fd 是可以被回收利用的】
- 在 Linux 中無(wú)論是文件,socket,還是管道,設(shè)備等,一切皆文件,Linux 抽象出了一個(gè) VFS(virtual file system) 層,屏蔽了所有的具體的文件,VFS 提供了統(tǒng)一的接口給上層調(diào)用,這樣應(yīng)用層只與 VFS 打交道,極大地方便了用戶的開(kāi)發(fā),仔細(xì)對(duì)比你會(huì)發(fā)現(xiàn),這和 Java 中的面向接口編程很類似【fd 的值從 0 開(kāi)始,其中 0,1,2 是固定的,分別指向標(biāo)準(zhǔn)輸入(指向鍵盤),標(biāo)準(zhǔn)輸出/標(biāo)準(zhǔn)錯(cuò)誤(指向顯示器),之后每打開(kāi)一個(gè)文件,fd 都會(huì)從 3 開(kāi)始遞增,但需要注意的是 fd 并不一定都是遞增的,如果關(guān)閉了文件,之前的 fd 是可以被回收利用的】
- 系統(tǒng)內(nèi)存,每個(gè) TCP 連接在內(nèi)核中都有對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu),意味著每個(gè)連接都是會(huì)占用一定內(nèi)存的;
- fd(文件描述符):Socket 實(shí)際上是一個(gè)文件,也就會(huì)對(duì)應(yīng)一個(gè)文件描述符。在 Linux 下,單個(gè)進(jìn)程打開(kāi)的文件描述符數(shù)是有限制的,沒(méi)有經(jīng)過(guò)修改的值一般都是 1024,不過(guò)我們可以通過(guò) ulimit 增大文件描述符的數(shù)目;
- TCP 連接是由四元組唯一確認(rèn)的,這個(gè)四元組就是:本機(jī)IP, 本機(jī)端口, 對(duì)端IP, 對(duì)端端口。服務(wù)器作為服務(wù)方,通常會(huì)在本地固定監(jiān)聽(tīng)一個(gè)端口,等待客戶端的連接。 因此服務(wù)器的本地 IP 和端口是固定的,于是對(duì)于服務(wù)端 TCP 連接的四元組只有對(duì)端 IP 和端口是會(huì)變化的,所以最大 TCP 連接數(shù) = 客戶端 IP 數(shù)×客戶端端口數(shù)。對(duì)于 IPv4,客戶端的 IP 數(shù)最多為 2 的 32 次方,客戶端的端口數(shù)最多為 2 的 16 次方,也就是服務(wù)端單機(jī)最大 TCP 連接數(shù)約為 2 的 48 次方。但是服務(wù)器肯定承載不了那么大的連接數(shù),主要會(huì)受兩個(gè)方面的限制:
- 創(chuàng)建 Socket 的時(shí)候,可以指定網(wǎng)絡(luò)層使用的是 IPv4 還是 IPv6,傳輸層使用的是 TCP 還是 UDP。 不管是那種,反正肯定是 服務(wù)器的程序要先跑起來(lái),然后等待客戶端的連接和數(shù)據(jù)
- 基于 Linux 一切皆文件的理念,在內(nèi)核中 Socket 也是以文件的形式存在的,也是有對(duì)應(yīng)的文件描述符,每個(gè)打開(kāi)的文件也都有讀寫緩沖區(qū)。每個(gè)文件都有一個(gè) inode,Socket 文件的 inode 指向了內(nèi)核中的 Socket 結(jié)構(gòu),在這個(gè)結(jié)構(gòu)體里有兩個(gè)隊(duì)列,分別是發(fā)送隊(duì)列和接收隊(duì)列,這個(gè)兩個(gè)隊(duì)列里面保存的是一個(gè)個(gè) struct sk_buff,用鏈表的組織形式串起來(lái)。sk_buff 可以表示各個(gè)層的數(shù)據(jù)包,在應(yīng)用層數(shù)據(jù)包叫 data,在 TCP 層我們稱為 segment,在 IP 層我們叫 packet,在數(shù)據(jù)鏈路層稱為 frame,這不就和計(jì)算機(jī)網(wǎng)絡(luò)穿起來(lái)了嘛。協(xié)議棧采用的是分層結(jié)構(gòu),上層向下層傳遞數(shù)據(jù)時(shí)需要增加包頭,下層向上層數(shù)據(jù)時(shí)又需要去掉包頭,如果每一層都用一個(gè)結(jié)構(gòu)體,那在層之間傳遞數(shù)據(jù)的時(shí)候,就要發(fā)生多次拷貝,這將大大降低 CPU 效率。于是,為了在層級(jí)之間傳遞數(shù)據(jù)時(shí),不發(fā)生拷貝,只用 sk_buff 一個(gè)結(jié)構(gòu)體來(lái)描述所有的網(wǎng)絡(luò)包,那它是如何做到的呢?是通過(guò)調(diào)整 sk_buff 中 data 的指針。【當(dāng)接收?qǐng)?bào)文時(shí),從網(wǎng)卡驅(qū)動(dòng)開(kāi)始,通過(guò)協(xié)議棧層層往上傳送數(shù)據(jù)報(bào),通過(guò)增加 skb->data 的值,來(lái)逐步剝離協(xié)議首部+++++當(dāng)要發(fā)送報(bào)文時(shí),創(chuàng)建 sk_buff 結(jié)構(gòu)體,數(shù)據(jù)緩存區(qū)的頭部預(yù)留足夠的空間,用來(lái)填充各層首部,在經(jīng)過(guò)各下層協(xié)議時(shí),通過(guò)減少 skb->data 的值來(lái)增加協(xié)議首部。】
- 客戶端和服務(wù)器能在網(wǎng)絡(luò)中通信,那必須得使用 Socket 編程來(lái)支持跨主機(jī)間通信。Socket這個(gè)貨叫插口,其實(shí)我覺(jué)得就是個(gè),比如咱們自己家里有兩臺(tái)路由器要通信,你不能拿一條線直接粘到路由器屁股上吧,肯定是兩臺(tái)路由器屁股上都有口,然后一條線這邊插好那邊插好,然后就可以跨主機(jī)通信了唄,路由器屁股上開(kāi)的口我覺(jué)得就可以看作是Socket。
PART1:Unix 常見(jiàn)的五種IO模型:網(wǎng)絡(luò)編程中的五個(gè) I/O 模型:同步阻塞 I/O(BIO)、同步非阻塞 I/O(NIO)、 I/O 多路復(fù)用、信號(hào)驅(qū)動(dòng)、異步非阻塞 I/O(AIO))【只有 AIO 為異步 IO,其他都是同步 IO】,最常用的就是同步阻塞BIO 和 IO 多路復(fù)用
-
Unix 常見(jiàn)的IO模型:對(duì)于一次IO訪問(wèn)(以read舉例),數(shù)據(jù)會(huì)先被 拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后 才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。
- 無(wú)論是阻塞 I/O、非阻塞 I/O,還是基于非阻塞 I/O 的多路復(fù)用以及信號(hào)驅(qū)動(dòng)都是同步調(diào)用。因?yàn)?它們?cè)?read 調(diào)用時(shí),內(nèi)核將數(shù)據(jù)從內(nèi)核空間拷貝到應(yīng)用程序空間,過(guò)程都是需要等待的,也就是說(shuō)這個(gè)過(guò)程是同步的,如果內(nèi)核實(shí)現(xiàn)的拷貝效率不高,read 調(diào)用就會(huì)在這個(gè)同步過(guò)程中等待比較長(zhǎng)的時(shí)間
- 【5中I/O中,I/O 阻塞、I/O非阻塞、I/O復(fù)用、SIGIO 都會(huì)在不同程度上阻塞應(yīng)用程序,而只有異步I/O模型在整個(gè)操作期間都不會(huì)阻塞應(yīng)用程序。】
- 真正的異步 I/O 是「內(nèi)核數(shù)據(jù)準(zhǔn)備好」和「數(shù)據(jù)從內(nèi)核態(tài)拷貝到用戶態(tài)」這兩個(gè)過(guò)程都不用等待
- POSIX 規(guī)范中定義了同步I/O 和異步I/O的術(shù)語(yǔ):
- 同步I/O : 需要進(jìn)程去真正的去操作I/O
- 異步I/O:內(nèi)核在I/O操作完成后再通知應(yīng)用進(jìn)程操作結(jié)果
- 但是如果使用同步的方式來(lái)通信的話,所有的操作都在一個(gè)線程內(nèi)順序執(zhí)行完成,這么做缺點(diǎn)是很明顯的:因?yàn)橥降耐ㄐ挪僮鲿?huì)阻塞同一個(gè)線程的其他任何操作,只有這個(gè)操作完成了之后,后續(xù)的操作才可以完成,所以出現(xiàn)了同步阻塞+多線程(每個(gè)Socket都創(chuàng)建一個(gè)線程對(duì)應(yīng)),但是系統(tǒng)內(nèi)線程數(shù)量是有限制的,同時(shí)線程切換很浪費(fèi)時(shí)間,適合Socket少的情況,因該需要出現(xiàn)IO模型。
- 【5中I/O中,I/O 阻塞、I/O非阻塞、I/O復(fù)用、SIGIO 都會(huì)在不同程度上阻塞應(yīng)用程序,而只有異步I/O模型在整個(gè)操作期間都不會(huì)阻塞應(yīng)用程序。】
- 阻塞 IO 和 IO 多路復(fù)用最為常用,原因如下:
- 在系統(tǒng)內(nèi)核的支持上,現(xiàn)在大多數(shù)系統(tǒng)內(nèi)核都會(huì)支持阻塞 IO、非阻塞 IO 和 IO 多路復(fù)用,但像信號(hào)驅(qū)動(dòng) IO、異步 IO,只有高版本的 Linux 系統(tǒng)內(nèi)核才會(huì)支持。
- 在編程語(yǔ)言上,無(wú)論 C++ 還是 Java,在高性能的網(wǎng)絡(luò)編程框架的編寫上,大多數(shù)都是基于 Reactor 模式,其中最為典型的便是 Java 的 Netty 框架,而 Reactor 模式是基于 IO 多路復(fù)用的。當(dāng)然,在非高并發(fā)場(chǎng)景下,同步阻塞 IO 是最為常見(jiàn)的
- 當(dāng)一個(gè)read操作發(fā)生時(shí),會(huì)經(jīng)歷兩個(gè)階段:或者說(shuō)I/O 是分為兩個(gè)過(guò)程的【或者說(shuō)當(dāng)應(yīng)用程序發(fā)起 I/O 調(diào)用后,會(huì)經(jīng)歷兩個(gè)步驟:】
- 過(guò)程一:內(nèi)核等待數(shù)據(jù)準(zhǔn)備就緒 (Waiting for the data to be ready)【內(nèi)核程序要從磁盤、網(wǎng)卡等讀取數(shù)據(jù)到內(nèi)核空間緩存區(qū);】
- 傳統(tǒng)的IO流程,包括read和write的過(guò)程:
- read:把數(shù)據(jù)從磁盤讀取到內(nèi)核緩沖區(qū),再?gòu)膬?nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū)
- write:先把數(shù)據(jù)寫入到socket緩沖區(qū),最后寫入網(wǎng)卡設(shè)備。
- 傳統(tǒng)的IO流程,包括read和write的過(guò)程:
- 過(guò)程二:內(nèi)核或者說(shuō)用戶程序?qū)?strong>數(shù)據(jù)從內(nèi)核空間緩存拷貝到用戶空間的進(jìn)程中 (Copying the data from the kernel to the process)【大多數(shù)文件系統(tǒng)的默認(rèn) IO 操作都是緩存 IO。緩存 IO 的缺點(diǎn):數(shù)據(jù)在傳輸過(guò)程中需要在應(yīng)用程序地址空間和內(nèi)核空間進(jìn)行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來(lái)的CPU以及內(nèi)存開(kāi)銷非常大。】
- 過(guò)程一:內(nèi)核等待數(shù)據(jù)準(zhǔn)備就緒 (Waiting for the data to be ready)【內(nèi)核程序要從磁盤、網(wǎng)卡等讀取數(shù)據(jù)到內(nèi)核空間緩存區(qū);】
- 正式因?yàn)檫@兩個(gè)階段,linux系統(tǒng)產(chǎn)生了下面五種網(wǎng)絡(luò)模式的方案:【阻塞 I/O 會(huì)阻塞在過(guò)程 1 和過(guò)程 2,非阻塞 I/O 和基于非阻塞 I/O 的多路復(fù)用只會(huì)阻塞在過(guò)程 2,所以這三個(gè)都可以認(rèn)為是同步 I/O;異步 I/O 則不同,過(guò)程 1 和過(guò)程 2 都不會(huì)阻塞。】【主要原因在于socket.accept()、socket.read()、socket.write()三個(gè)主要函數(shù)都是同步阻塞的。當(dāng)一個(gè)連接在處理I/O的時(shí)候,系統(tǒng)是阻塞的,如果是單線程的話必然就阻塞,但CPU是被釋放出來(lái)的,開(kāi)啟多線程,就可以讓CPU去處理更多的事情。利用多核,當(dāng)I/O阻塞時(shí),但CPU空閑的時(shí)候,可以利用多線程使用CPU資源。】
- Unix 常見(jiàn)的五種IO模型之一:同步阻塞式IO模型BIO(blocking IO model):在linux中,默認(rèn)情況下所有的IO操作都是blocking
- 計(jì)算機(jī)有內(nèi)核,內(nèi)核可以接收客戶端來(lái)的連接【客戶端的所有連接是先到達(dá)內(nèi)核】,建立連接就會(huì)產(chǎn)生文件描述符,原來(lái)內(nèi)核中有read函數(shù)可以讀文件描述符,這個(gè)read操作是在一個(gè)線程或者進(jìn)程中讀,而來(lái)一個(gè)客戶端請(qǐng)求后服務(wù)端就會(huì)new一個(gè)新線程,一個(gè)線程對(duì)應(yīng)一個(gè)連接,利用CPU的時(shí)間片輪轉(zhuǎn)去處理當(dāng)前的用戶讀寫操作,但是線程資源畢竟是有限的。socket在這個(gè)時(shí)期是block的,數(shù)據(jù)包不能返回一直在阻塞,這就是對(duì)應(yīng)的早期的BIO。這樣就浪費(fèi)了計(jì)算機(jī)硬件。NIO此時(shí)出來(lái)了,內(nèi)核此時(shí)發(fā)生變化了,內(nèi)核升級(jí)
- yum install man man-pages:這條命令可以看linux中命令的實(shí)現(xiàn)原理。利用man 2 socket可以看到
- 到了NIO之后,此時(shí)說(shuō)明文件描述符可以是nonblock。然后由于是非阻塞的,我就可以單線程或者單進(jìn)程,在這個(gè)單個(gè)里面寫一個(gè)while循環(huán),read fd1,有沒(méi)有數(shù)據(jù),fd1說(shuō)額沒(méi)有。好,那我就繼續(xù)read fd2,fd2說(shuō)有數(shù)據(jù),拿著有的東西進(jìn)行處理,處理完之后繼續(xù)去用戶空間輪詢r(jià)ead fdx【輪詢操作發(fā)生在用戶空間哦】,但是拿出來(lái)處理都由我自己來(lái)弄,就叫做同步+非阻塞,
- 那現(xiàn)在如果有10000個(gè)fd,在用戶空間內(nèi)的用戶進(jìn)程需要輪詢調(diào)用10000次kernel,這么多次系統(tǒng)調(diào)用,太浪費(fèi)了,所以內(nèi)核進(jìn)行升級(jí),增加一個(gè)系統(tǒng)調(diào)用,select
- 然后,繼續(xù)可以man 2 select看一下這個(gè)系統(tǒng)調(diào)用
- 雖然select是,比如1000個(gè)客戶端連接請(qǐng)求,你告訴我里面50個(gè)準(zhǔn)備好數(shù)據(jù)了,我去挨個(gè)讀這50個(gè),節(jié)省了用戶態(tài)到內(nèi)核態(tài)的切換。但是select歸根到底有缺點(diǎn),往下看咯
- yum install man man-pages:這條命令可以看linux中命令的實(shí)現(xiàn)原理。利用man 2 socket可以看到
- 通常把阻塞的文件描述符(file descriptor,fd)稱之為阻塞I/O。默認(rèn)條件下,創(chuàng)建的socket fd是阻塞的,針對(duì)阻塞I/O調(diào)用系統(tǒng)接口,可能因?yàn)榈却氖录](méi)有到達(dá)而被系統(tǒng)掛起,直到等待的事件觸發(fā)調(diào)用接口才返回,例如,tcp socket的connect調(diào)用會(huì)阻塞至第三次握手成功(不考慮socket 出錯(cuò)或系統(tǒng)中斷)
- 在JDK1.4推出JavaNIO之前,基于Java的所有Socket通信都采用了同步阻塞模式(BIO),這種一請(qǐng)求一應(yīng)答的通信模型簡(jiǎn)化了上層的應(yīng)用開(kāi)發(fā),但是BIO在性能和可靠性方面卻存在著巨大的瓶頸。因此,在很長(zhǎng)一段時(shí)間里,大型的應(yīng)用服務(wù)器都采用C或者C++語(yǔ)言開(kāi)發(fā),因?yàn)樗鼈兛梢灾苯邮褂貌僮飨到y(tǒng)提供的異步I/O或者AIO能力。當(dāng)并發(fā)訪問(wèn)量增大、響應(yīng)時(shí)間延遲增大之后,采用JavaBIO開(kāi)發(fā)的服務(wù)端軟件只有通過(guò)硬件的不斷擴(kuò)容來(lái)滿足高并發(fā)和低時(shí)延,它極大地增加了企業(yè)的成本,并且隨著集群規(guī)模的不斷膨脹,系統(tǒng)的可維護(hù)性也面臨巨大的挑戰(zhàn),只能通過(guò)采購(gòu)性能更高的硬件服務(wù)器來(lái)解決問(wèn)題,這會(huì)導(dǎo)致惡性循環(huán)。正是由于Java傳統(tǒng)BIO的拙劣表現(xiàn),才使得Java支持非阻塞I/O的呼聲日漸高漲,最終,JDK1.4版本提供了新的NIO類庫(kù),Java 終于也可以支持非阻塞I/O 了。
- 一個(gè)典型的讀操作流程大概是這樣:
- 當(dāng)用戶進(jìn)程調(diào)用了recvfrom這個(gè)系統(tǒng)調(diào)用,kernel就開(kāi)始了IO的第一個(gè)階段:準(zhǔn)備數(shù)據(jù)(對(duì)于網(wǎng)絡(luò)IO來(lái)說(shuō),很多時(shí)候數(shù)據(jù)在一開(kāi)始還沒(méi)有到達(dá)。比如,還沒(méi)有收到一個(gè)完整的UDP包。這個(gè)時(shí)候kernel就要等待足夠的數(shù)據(jù)到來(lái)),而數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個(gè)過(guò)程的,這個(gè)過(guò)程需要等待。而在用戶進(jìn)程這邊,整個(gè)進(jìn)程會(huì)被阻塞(當(dāng)然,是進(jìn)程自己選擇的阻塞)。當(dāng)kernel一直等到數(shù)據(jù)準(zhǔn)備好了,它就會(huì)將數(shù)據(jù)從kernel中拷貝到用戶空間的緩沖區(qū)以后,然后kernel返回結(jié)果,用戶進(jìn)程才解除block的狀態(tài),重新運(yùn)行起來(lái)。
- 同步阻塞 BIO 模型中,應(yīng)用程序發(fā)起 read 調(diào)用后,會(huì)一直阻塞,直到內(nèi)核把數(shù)據(jù)拷貝到用戶空間。
- 同步阻塞 BIO 模型中,應(yīng)用程序發(fā)起 read 調(diào)用后,會(huì)一直阻塞,直到內(nèi)核把數(shù)據(jù)拷貝到用戶空間。
- 當(dāng)用戶進(jìn)程調(diào)用了recvfrom這個(gè)系統(tǒng)調(diào)用,kernel就開(kāi)始了IO的第一個(gè)階段:準(zhǔn)備數(shù)據(jù)(對(duì)于網(wǎng)絡(luò)IO來(lái)說(shuō),很多時(shí)候數(shù)據(jù)在一開(kāi)始還沒(méi)有到達(dá)。比如,還沒(méi)有收到一個(gè)完整的UDP包。這個(gè)時(shí)候kernel就要等待足夠的數(shù)據(jù)到來(lái)),而數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個(gè)過(guò)程的,這個(gè)過(guò)程需要等待。而在用戶進(jìn)程這邊,整個(gè)進(jìn)程會(huì)被阻塞(當(dāng)然,是進(jìn)程自己選擇的阻塞)。當(dāng)kernel一直等到數(shù)據(jù)準(zhǔn)備好了,它就會(huì)將數(shù)據(jù)從kernel中拷貝到用戶空間的緩沖區(qū)以后,然后kernel返回結(jié)果,用戶進(jìn)程才解除block的狀態(tài),重新運(yùn)行起來(lái)。
- blocking IO的特點(diǎn)就是在IO執(zhí)行的下兩個(gè)階段的時(shí)候都被block了。BIO 模型有兩處阻塞的地方
- 等待數(shù)據(jù)準(zhǔn)備就緒 (Waiting for the data to be ready) 阻塞 (服務(wù)端阻塞等待客戶端發(fā)起連接。也就是通過(guò) serverSocket.accept()方法服務(wù)端等待用戶發(fā)連接請(qǐng)求過(guò)來(lái)。)
- 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process) 阻塞(連接成功后,工作線程阻塞讀取客戶端 Socket 發(fā)送數(shù)據(jù)。也就是服務(wù)端通過(guò) in.readLine() 從網(wǎng)絡(luò)中讀客戶端發(fā)送過(guò)來(lái)的數(shù)據(jù),這個(gè)地方也會(huì)阻塞。如果客戶端已經(jīng)和服務(wù)端建立了一個(gè)連接,但客戶端遲遲不發(fā)送數(shù)據(jù),那么服務(wù)端的 readLine() 操作會(huì)一直阻塞,造成資源浪費(fèi)。)
- BIO模型的特點(diǎn):或者說(shuō)BIO模型的缺點(diǎn):
- (Socket 連接數(shù)量受限,不適用于高并發(fā)場(chǎng)景)缺乏彈性伸縮能力,當(dāng)客戶端并發(fā)訪問(wèn)量增加后,服務(wù)端的線程個(gè)數(shù)和客戶端并發(fā)訪問(wèn)數(shù)呈1 : 1的正比關(guān)系,由于線程是Java虛擬機(jī)非常寶貴的系統(tǒng)資源,當(dāng)線程數(shù)膨脹之后,系統(tǒng)的性能將急劇下降,隨著并發(fā)訪問(wèn)量的繼續(xù)增大,系統(tǒng)會(huì)發(fā)生線程堆棧溢出、創(chuàng)建新線程失敗等問(wèn)題,并最終導(dǎo)致進(jìn)程宕機(jī)或者僵死,不能對(duì)外提供服務(wù)。顯而易見(jiàn),如果我們要構(gòu)建高性能、低時(shí)延、支持大并發(fā)的應(yīng)用系統(tǒng),使用同步阻塞I/O模型是無(wú)法滿足性能線性增長(zhǎng)和可靠性的。當(dāng)面對(duì)十萬(wàn)甚至百萬(wàn)級(jí)連接的時(shí)候,傳統(tǒng)的BIO模型是無(wú)能為力的。隨著移動(dòng)端應(yīng)用的興起和各種網(wǎng)絡(luò)游戲的盛行,百萬(wàn)級(jí)長(zhǎng)連接日趨普遍,此時(shí),必然需要一種更高效的I/O處理模型NIO。
- 有兩處阻塞,分別是等待用戶發(fā)起連接,和等待用戶發(fā)送數(shù)據(jù)。這不是個(gè)好事情,你想呀,你去買東西,別人給你取盒煙讓你等了一個(gè)小時(shí),然后你掃碼時(shí)網(wǎng)不太好,又讓你等了半小時(shí),你不得不付現(xiàn)金,他給你找零錢又讓你等了一小時(shí)…。所以肯定得解決這個(gè)兩個(gè)地方阻塞的問(wèn)題。用NIO網(wǎng)絡(luò)模型(NIO網(wǎng)絡(luò)模型操作上是用一個(gè)線程處理多個(gè)連接,使得每一個(gè)工作線程都可以處理多個(gè)客戶端的 Socket 請(qǐng)求,這樣工作線程的利用率就能得到提升,所需的工作線程數(shù)量也隨之減少。此時(shí) NIO 的線程模型就變?yōu)?1 個(gè)工作線程對(duì)應(yīng)多個(gè)客戶端 Socket 的請(qǐng)求,這就是所謂的 I/O多路復(fù)用。)來(lái)解決。
- 另外補(bǔ)充一點(diǎn),網(wǎng)絡(luò)編程中,通常把可能永遠(yuǎn)阻塞的系統(tǒng)API調(diào)用 稱為慢系統(tǒng)調(diào)用,典型的如 accept、recv、select等。慢系統(tǒng)調(diào)用在阻塞期間可能被信號(hào)中斷而返回錯(cuò)誤,相應(yīng)的errno 被設(shè)置為EINTR,我們需要處理這種錯(cuò)誤,解決辦法有:
- 重啟系統(tǒng)調(diào)用:
- 信號(hào)處理
- 重啟系統(tǒng)調(diào)用:
- 先理一理哈,在計(jì)算機(jī)網(wǎng)絡(luò)這篇中提到了 應(yīng)用程序建立連接后通過(guò)OS實(shí)現(xiàn)的TCP協(xié)議的socket接口給服務(wù)器發(fā)了數(shù)據(jù)(假設(shè)咱們服務(wù)器上用的服務(wù)器軟件是Tomcat),服務(wù)器會(huì)利用自己體內(nèi)的endPoint的實(shí)現(xiàn)去socket(OS實(shí)現(xiàn)的TCP協(xié)議的socket接口)中拿到數(shù)據(jù),然后再解析成為一個(gè)又一個(gè)請(qǐng)求,再交給tomcat去處理,處理完響應(yīng)給客戶端。
- BI/O 模型典型的Java實(shí)現(xiàn): 基于 BIO 的文件復(fù)制程序:字節(jié)流方式、字符流方式、字符緩沖,按行讀取、隨機(jī)讀寫(RandomAccessFile)
public class BIOSever { //在服務(wù)端創(chuàng)建一個(gè) ServerSocket 對(duì)象ServerSocket ss = new ServerSocket();// 綁定端口 9090,然后啟動(dòng)運(yùn)行服務(wù)端,然后阻塞等待客戶端發(fā)起連接請(qǐng)求,直到有客戶端的連接發(fā)送過(guò)來(lái)之后。當(dāng)有客戶端的連接請(qǐng)求后,服務(wù)端會(huì)啟動(dòng)一個(gè)新線程 ServerTaskThread,用新創(chuàng)建的線程去處理當(dāng)前用戶的讀寫操作。ss.bind(new InetSocketAddress("localhost", 9090));System.out.println("server started listening " + PORT);try {Socket s = null;while (true) {// 阻塞等待客戶端發(fā)送連接請(qǐng)求,直到有客戶端的連接發(fā)送過(guò)來(lái)之后,accept() 方法返回Socket s = ss.accept();new Thread(new ServerTaskThread(s)).start();}} catch (Exception e) {...} finally {if (ss != null) {ss.close();ss = null;} } /* *當(dāng)有客戶端的連接請(qǐng)求后,服務(wù)端會(huì)啟動(dòng)一個(gè)新線程 ServerTaskThread,用新創(chuàng)建的線程去處理當(dāng)前用戶的讀寫操作。 */ public class ServerTaskThread implements Runnable {...while (true) {// 阻塞等待客戶端發(fā)請(qǐng)求過(guò)來(lái)String readLine = in.readLine();if (readLine == null) {break;}...}... }- 除了上面這種寫法,RPC這里也可以對(duì)Socket網(wǎng)絡(luò)通信代碼進(jìn)行封裝,體現(xiàn)咱們的封裝性
- 使用 Java NIO 包組成一個(gè)簡(jiǎn)單的客戶端-服務(wù)端網(wǎng)絡(luò)通訊所需要的 ServerSocketChannel、SocketChannel 和 Buffer,一個(gè)完整的可運(yùn)行的例子:(例子來(lái)自javadoop老師)
- SocketHandler:【來(lái)一個(gè)新的連接,我們就新開(kāi)一個(gè)線程來(lái)處理這個(gè)連接,之后的操作全部由那個(gè)線程來(lái)完成。】
- 客戶端 SocketChannel 的使用:
- SocketHandler:【來(lái)一個(gè)新的連接,我們就新開(kāi)一個(gè)線程來(lái)處理這個(gè)連接,之后的操作全部由那個(gè)線程來(lái)完成。】
- 上面這個(gè)例子的性能瓶頸或者說(shuō)問(wèn)題:非阻塞 IO應(yīng)運(yùn)而生
- 計(jì)算機(jī)有內(nèi)核,內(nèi)核可以接收客戶端來(lái)的連接【客戶端的所有連接是先到達(dá)內(nèi)核】,建立連接就會(huì)產(chǎn)生文件描述符,原來(lái)內(nèi)核中有read函數(shù)可以讀文件描述符,這個(gè)read操作是在一個(gè)線程或者進(jìn)程中讀,而來(lái)一個(gè)客戶端請(qǐng)求后服務(wù)端就會(huì)new一個(gè)新線程,一個(gè)線程對(duì)應(yīng)一個(gè)連接,利用CPU的時(shí)間片輪轉(zhuǎn)去處理當(dāng)前的用戶讀寫操作,但是線程資源畢竟是有限的。socket在這個(gè)時(shí)期是block的,數(shù)據(jù)包不能返回一直在阻塞,這就是對(duì)應(yīng)的早期的BIO。這樣就浪費(fèi)了計(jì)算機(jī)硬件。NIO此時(shí)出來(lái)了,內(nèi)核此時(shí)發(fā)生變化了,內(nèi)核升級(jí)
- Unix 常見(jiàn)的五種IO模型之二:同步非阻塞式IO模型(noblocking IO model)NIO(一般很少直接使用這種模型,而是在其他 I/O 模型中使用非阻塞 I/O 這一特性。這種方式對(duì)單個(gè) I/O 請(qǐng)求意義不大,但給 I/O 多路復(fù)用鋪平了道路):linux下,可以通過(guò)設(shè)置socket使其變?yōu)閚on-blocking。NIO 的日常操作,這個(gè)文章有例子:文件復(fù)制、文件復(fù)制—映射方式、文件復(fù)制—零拷貝方式、
- 把非阻塞的文件描述符稱為非阻塞I/O。可以通過(guò)設(shè)置SOCK_NONBLOCK標(biāo)記創(chuàng)建非阻塞的socket fd,或者使用fcntl將fd設(shè)置為非阻塞。
- 對(duì)非阻塞fd調(diào)用系統(tǒng)接口時(shí),不需要等待事件發(fā)生而立即返回,事件沒(méi)有發(fā)生,接口返回-1,此時(shí)需要通過(guò)errno的值來(lái)區(qū)分是否出錯(cuò)。不同的接口,立即返回時(shí)的errno值不盡相同,如,recv、send、accept errno通常被設(shè)置為EAGIN 或者EWOULDBLOCK,connect 則為EINPRO- GRESS 。
- 對(duì)非阻塞fd調(diào)用系統(tǒng)接口時(shí),不需要等待事件發(fā)生而立即返回,事件沒(méi)有發(fā)生,接口返回-1,此時(shí)需要通過(guò)errno的值來(lái)區(qū)分是否出錯(cuò)。不同的接口,立即返回時(shí)的errno值不盡相同,如,recv、send、accept errno通常被設(shè)置為EAGIN 或者EWOULDBLOCK,connect 則為EINPRO- GRESS 。
- NIO 也稱新 IO 或者非阻塞 IO(Non-Blocking IO)【NIO 中的 N 可以理解為 Non-blocking,不單純是 New。】。Java 中的 NIO 于 Java 1.4 中引入,對(duì)應(yīng) java.nio 包,提供了 Channel , Selector,Buffer 等抽象。傳統(tǒng) IO 是面向輸入/輸出流編程的,而 NIO 是面向通道編程的,或者說(shuō)它是支持面向緩沖的,基于通道的 I/O 操作方法。 對(duì)于高負(fù)載、高并發(fā)的(網(wǎng)絡(luò))應(yīng)用,應(yīng)使用 NIO 。
- NIO 的 3 個(gè)核心概念或者說(shuō)Java NIO 中三大組件 Buffer、Channel、Selector:Channel、Buffer、Selector:。
- Channel(通道):
- 所有的 NIO 操作始于通道,通道是數(shù)據(jù)來(lái)源或數(shù)據(jù)寫入的目的地,主要地,我們將關(guān)心 java.nio 包中實(shí)現(xiàn)的以下幾個(gè) Channel:
- FileChannel:
- FileChannel 是不支持非阻塞的。
- FileChannel 是不支持非阻塞的。
- SocketChannel:
- 可以將 SocketChannel 理解成一個(gè) TCP 客戶端。雖然這么理解有點(diǎn)狹隘,因?yàn)槲覀冊(cè)诮榻B ServerSocketChannel 的時(shí)候會(huì)看到另一種使用方式。【SocketChannel 了,它不僅僅是 TCP 客戶端,它代表的是一個(gè)網(wǎng)絡(luò)通道,可讀可寫。】//打開(kāi)一個(gè) TCP 連接: SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("http://aiminhuqaqq.fun/", 80)); /**上面的這行代碼等價(jià)于下面的兩行: *打開(kāi)一個(gè)通道:SocketChannel socketChannel = SocketChannel.open(); *發(fā)起連接:socketChannel.connect(new InetSocketAddress("http://aiminhuqaqq.fun/", 80)); */
- SocketChannel 的讀寫和 FileChannel 沒(méi)什么區(qū)別,就是操作緩沖區(qū)。
- ServerSocketChannel:
- SocketChannel 是 TCP 客戶端,這里說(shuō)的 ServerSocketChannel 就是對(duì)應(yīng)的服務(wù)端。
- ServerSocketChannel 用于監(jiān)聽(tīng)機(jī)器端口,管理從這個(gè)端口進(jìn)來(lái)的 TCP 連接。
- ServerSocketChannel 不和 Buffer 打交道了,因?yàn)樗⒉粚?shí)際處理數(shù)據(jù),它一旦接收到請(qǐng)求后,實(shí)例化 SocketChannel,之后在這個(gè)連接通道上的數(shù)據(jù)傳遞它就不管了,因?yàn)樗枰^續(xù)監(jiān)聽(tīng)端口,等待下一個(gè)連接
- DatagramChannel:
- UDP 和 TCP 不一樣,DatagramChannel 一個(gè)類處理了服務(wù)端和客戶端。
- 監(jiān)聽(tīng)端口:
- 發(fā)送數(shù)據(jù):
- FileChannel:
- Channel 與Buffer 打交道,讀操作的時(shí)候?qū)?Channel 中的數(shù)據(jù)填充到 Buffer 中,而寫操作時(shí)將 Buffer 中的數(shù)據(jù)寫入到 Channel 中。
- channel 實(shí)例的兩個(gè)方法:
- channel.read(buffer);
- channel.write(buffer);
- channel.read(buffer);
- channel 實(shí)例的兩個(gè)方法:
- Channel 是對(duì) IO 輸入/輸出系統(tǒng)的抽象,是 IO 源與目標(biāo)之間的連接通道,NIO 的通道類似于傳統(tǒng) IO 中的各種“流”,用于讀取和寫入。與 InputStream 和 OutputStream 不同的是,Channel 是雙向的,既可以讀,也可以寫,且支持異步操作。這契合了操作系統(tǒng)的特性,比如 linux 底層通道就是雙向的。此外 Channel 還提供了 map() 方法,通過(guò)該方法可以將“一塊”數(shù)據(jù)直接映射到內(nèi)存中。因此也有人說(shuō),NIO 是面向塊處理的,而傳統(tǒng) I/O 是面向流處理的。
- 所有的 NIO 操作始于通道,通道是數(shù)據(jù)來(lái)源或數(shù)據(jù)寫入的目的地,主要地,我們將關(guān)心 java.nio 包中實(shí)現(xiàn)的以下幾個(gè) Channel:
- Buffer(緩沖):
- Buffer 本質(zhì)上就是一個(gè)容器,其底層持有了一個(gè)具體類型的數(shù)組來(lái)存放具體數(shù)據(jù)。或者說(shuō)一個(gè) Buffer 本質(zhì)上是內(nèi)存中的一塊,我們可以將數(shù)據(jù)寫入這塊內(nèi)存,之后從這塊內(nèi)存獲取數(shù)據(jù)。。從 Channel 中取數(shù)據(jù)或者向 Channel 中寫數(shù)據(jù)都需要通過(guò) Buffer。在 Java 中 Buffer 是一個(gè)抽象類,除 boolean 之外的基本數(shù)據(jù)類型都提供了對(duì)應(yīng)的 Buffer 實(shí)現(xiàn)類。比較常用的是 ByteBuffer 和 CharBuffer。
- java.nio定義的幾個(gè)Buffer的實(shí)現(xiàn):
- Buffer 中的幾個(gè)重要屬性和幾個(gè)重要方法:就像數(shù)組有數(shù)組容量,每次訪問(wèn)元素要指定下標(biāo),Buffer 中也有幾個(gè)重要屬性:position、limit、capacity。
- position:position 的初始值是 0,每往 Buffer 中寫入一個(gè)值,position 就自動(dòng)加 1,代表下一次的寫入位置,所以 position 最后會(huì)指向最后一次寫入的位置的后面一個(gè),如果 Buffer 寫滿了,那么 position 等于 capacity(position 從 0 開(kāi)始)。讀操作的時(shí)候也是類似的,每讀一個(gè)值,position 就自動(dòng)加 1。
- 從 寫操作模式到讀操作模式切換的時(shí)候(flip()),position 都會(huì)歸零,這樣就可以從頭開(kāi)始讀寫了
- rewind():會(huì)重置 position 為 0,通常用于重新從頭讀寫 Buffer。和rewind相近的有:
- clear():有點(diǎn)重置 Buffer 的意思,相當(dāng)于重新實(shí)例化了一樣。clear() 方法會(huì)重置幾個(gè)屬性,但是我們要看到,clear() 方法并不會(huì)將 Buffer 中的數(shù)據(jù)清空,只不過(guò)后續(xù)的寫入會(huì)覆蓋掉原來(lái)的數(shù)據(jù),也就相當(dāng)于清空了數(shù)據(jù)了。
- compact():和 clear() 一樣的是,它們都是在準(zhǔn)備往 Buffer 填充新的數(shù)據(jù)之前調(diào)用。compact() 方法有點(diǎn)不一樣,調(diào)用這個(gè)方法以后,會(huì)先處理還沒(méi)有讀取的數(shù)據(jù),也就是 position 到 limit 之間的數(shù)據(jù)(還沒(méi)有讀過(guò)的數(shù)據(jù)),先將這些數(shù)據(jù)移到左邊,然后在這個(gè)基礎(chǔ)上再開(kāi)始寫入。很明顯,此時(shí) limit 還是等于 capacity,position 指向原來(lái)數(shù)據(jù)的右邊
- clear():有點(diǎn)重置 Buffer 的意思,相當(dāng)于重新實(shí)例化了一樣。clear() 方法會(huì)重置幾個(gè)屬性,但是我們要看到,clear() 方法并不會(huì)將 Buffer 中的數(shù)據(jù)清空,只不過(guò)后續(xù)的寫入會(huì)覆蓋掉原來(lái)的數(shù)據(jù),也就相當(dāng)于清空了數(shù)據(jù)了。
- 從 寫操作模式到讀操作模式切換的時(shí)候(flip()),position 都會(huì)歸零,這樣就可以從頭開(kāi)始讀寫了
- limit:寫操作模式下,limit 代表的是最大能寫入的數(shù)據(jù),這個(gè)時(shí)候 limit 等于 capacity。寫結(jié)束后,切換到讀模式,此時(shí)的 limit 等于 Buffer 中實(shí)際的數(shù)據(jù)大小,因?yàn)?Buffer 不一定被寫滿了
- capacity:
- mark:除了 position、limit、capacity 這三個(gè)基本的屬性外,還有一個(gè)常用的屬性就是 mark。
- mark 用于臨時(shí)保存 position 的值,每次調(diào)用 mark() 方法都會(huì)將 mark 設(shè)值為當(dāng)前的 position,便于后續(xù)需要的時(shí)候使用。
- mark 用于臨時(shí)保存 position 的值,每次調(diào)用 mark() 方法都會(huì)將 mark 設(shè)值為當(dāng)前的 position,便于后續(xù)需要的時(shí)候使用。
- position:position 的初始值是 0,每往 Buffer 中寫入一個(gè)值,position 就自動(dòng)加 1,代表下一次的寫入位置,所以 position 最后會(huì)指向最后一次寫入的位置的后面一個(gè),如果 Buffer 寫滿了,那么 position 等于 capacity(position 從 0 開(kāi)始)。讀操作的時(shí)候也是類似的,每讀一個(gè)值,position 就自動(dòng)加 1。
- 初始化 Buffer:
- 每個(gè) Buffer 實(shí)現(xiàn)類都提供了一個(gè)靜態(tài)方法 allocate(int capacity) 幫助我們快速實(shí)例化一個(gè) Buffer
- 另外,我們經(jīng)常使用 wrap 方法來(lái)初始化一個(gè) Buffer。
- 每個(gè) Buffer 實(shí)現(xiàn)類都提供了一個(gè)靜態(tài)方法 allocate(int capacity) 幫助我們快速實(shí)例化一個(gè) Buffer
- 填充 Buffer:
- 各個(gè) Buffer 類都提供了一些 put 方法用于將數(shù)據(jù)填充到 Buffer 中,如 ByteBuffer 中的幾個(gè) put 方法:
- 對(duì)于 Buffer 來(lái)說(shuō),另一個(gè)常見(jiàn)的操作中就是,我們要將來(lái)自 Channel 的數(shù)據(jù)填充到 Buffer 中,在系統(tǒng)層面上,這個(gè)操作我們稱為讀操作,因?yàn)閿?shù)據(jù)是從外部(文件或網(wǎng)絡(luò)等)讀到內(nèi)存中。
- 提取 Buffer 中的值:
- 每往 Buffer 中寫入一個(gè)值,position 就自動(dòng)加 1,代表下一次的寫入位置,所以 position 最后會(huì)指向最后一次寫入的位置的后面一個(gè),如果 Buffer 寫滿了,那么 position 等于 capacity(position 從 0 開(kāi)始)
- 如果要讀 Buffer 中的值,需要切換模式,從寫入模式切換到讀出模式。注意,通常在說(shuō) NIO 的讀操作的時(shí)候,我們說(shuō)的是從 Channel 中讀數(shù)據(jù)到 Buffer 中,對(duì)應(yīng)的是對(duì) Buffer 的寫入操作
- 每往 Buffer 中寫入一個(gè)值,position 就自動(dòng)加 1,代表下一次的寫入位置,所以 position 最后會(huì)指向最后一次寫入的位置的后面一個(gè),如果 Buffer 寫滿了,那么 position 等于 capacity(position 從 0 開(kāi)始)
- 各個(gè) Buffer 類都提供了一些 put 方法用于將數(shù)據(jù)填充到 Buffer 中,如 ByteBuffer 中的幾個(gè) put 方法:
- Buffer 本質(zhì)上就是一個(gè)容器,其底層持有了一個(gè)具體類型的數(shù)組來(lái)存放具體數(shù)據(jù)。或者說(shuō)一個(gè) Buffer 本質(zhì)上是內(nèi)存中的一塊,我們可以將數(shù)據(jù)寫入這塊內(nèi)存,之后從這塊內(nèi)存獲取數(shù)據(jù)。。從 Channel 中取數(shù)據(jù)或者向 Channel 中寫數(shù)據(jù)都需要通過(guò) Buffer。在 Java 中 Buffer 是一個(gè)抽象類,除 boolean 之外的基本數(shù)據(jù)類型都提供了對(duì)應(yīng)的 Buffer 實(shí)現(xiàn)類。比較常用的是 ByteBuffer 和 CharBuffer。
- Selector:JDK1.4開(kāi)始引入了NIO類庫(kù),主要是使用Selector多路復(fù)用器來(lái)實(shí)現(xiàn)。Selector在Linux等主流操作系統(tǒng)上是通過(guò)IO復(fù)用Epoll實(shí)現(xiàn)的。通過(guò)Selector多路復(fù)用器,只需要一個(gè)線程便可以管理多個(gè)客戶端連接【非阻塞 IO 的核心在于使用一個(gè) Selector 來(lái)管理多個(gè)通道,可以是 SocketChannel,也可以是 ServerSocketChannel,將各個(gè)通道注冊(cè)到 Selector 上,指定監(jiān)聽(tīng)的事件。之后可以只用一個(gè)線程來(lái)輪詢這個(gè) Selector,看看上面是否有通道是準(zhǔn)備好的,當(dāng)通道準(zhǔn)備好可讀或可寫,然后才去開(kāi)始真正的讀寫,這樣速度就很快了。我們就完全沒(méi)有必要給每個(gè)通道都起一個(gè)線程。】。主要點(diǎn)見(jiàn)下述內(nèi)容。
- Selector 建立在非阻塞的基礎(chǔ)之上,大家經(jīng)常聽(tīng)到的 多路復(fù)用 在 Java 世界中指的就是Selector,用于實(shí)現(xiàn)一個(gè)線程管理多個(gè) Channel。
- NIO 中 Selector 是對(duì)底層操作系統(tǒng)實(shí)現(xiàn)的一個(gè)抽象,管理通道狀態(tài)其實(shí)都是底層系統(tǒng)實(shí)現(xiàn)的
- select:上世紀(jì) 80 年代就實(shí)現(xiàn)了,它支持注冊(cè) FD_SETSIZE(1024) 個(gè) socket,在那個(gè)年代肯定是夠用的,不過(guò)現(xiàn)在嘛,肯定是不行了
- poll:1997 年,出現(xiàn)了 poll 作為 select 的替代者,最大的區(qū)別就是,poll 不再限制 socket 數(shù)量。
- select 和 poll 都有一個(gè)共同的問(wèn)題,那就是它們都只會(huì)告訴你有幾個(gè)通道準(zhǔn)備好了,但是不會(huì)告訴你具體是哪幾個(gè)通道。所以,一旦知道有通道準(zhǔn)備好以后,自己還是需要進(jìn)行一次掃描,顯然這個(gè)不太好,通道少的時(shí)候還行,一旦通道的數(shù)量是幾十萬(wàn)個(gè)以上的時(shí)候,掃描一次的時(shí)間都很可觀了,時(shí)間復(fù)雜度 O(n)。所以,后來(lái)才催生了以下epoll實(shí)現(xiàn)。
- epoll:2002 年隨 Linux 內(nèi)核 2.5.44 發(fā)布,epoll 能直接返回具體的準(zhǔn)備好的通道,時(shí)間復(fù)雜度 O(1)。
- 除了 Linux 中的 epoll,2000 年 FreeBSD 出現(xiàn)了 Kqueue,還有就是,Solaris 中有 /dev/poll。Windows 平臺(tái)的非阻塞 IO 使用 select,我們也不必覺(jué)得 Windows 很落后,在 Windows 中 IOCP 提供的異步 IO 是比較強(qiáng)大的。
- Selector一些基本的接口操作:
- 首先,我們 開(kāi)啟一個(gè) Selector選擇器或者叫多路復(fù)用器:Selector selector = Selector.open();
- 將 Channel 注冊(cè)到 Selector 上。Selector 建立在非阻塞模式之上,所以注冊(cè)到 Selector 的 Channel 必須要支持非阻塞模式,FileChannel 不支持非阻塞,我們這里討論最常見(jiàn)的 SocketChannel 和 ServerSocketChannel。
- Selector常用的幾個(gè)方法:
- Channel(通道):
- NIO 網(wǎng)絡(luò)模型,非阻塞IO,操作上是用一個(gè)線程處理多個(gè)連接,使得每一個(gè)工作線程都可以處理多個(gè)客戶端的 Socket 請(qǐng)求,這樣工作線程的利用率就能得到提升,所需的工作線程數(shù)量也隨之減少。此時(shí) NIO 的線程模型就變?yōu)?1 個(gè)工作線程對(duì)應(yīng)多個(gè)客戶端 Socket 的請(qǐng)求,這就是所謂的 I/O多路復(fù)用。使用 I/O 多路復(fù)用時(shí),最好搭配非阻塞 I/O 一起使用【多路復(fù)用 API 返回的事件并不一定可讀寫的,如果使用阻塞 I/O, 那么在調(diào)用 read/write 時(shí)則會(huì)發(fā)生程序阻塞,因此最好搭配非阻塞 I/O,以便應(yīng)對(duì)極少數(shù)的特殊情況】
- JDK1.4開(kāi)始引入了NIO類庫(kù),主要是使用Selector多路復(fù)用器來(lái)實(shí)現(xiàn)。Selector在Linux等主流操作系統(tǒng)上是通過(guò)IO復(fù)用Epoll實(shí)現(xiàn)的。
- 通過(guò)Selector多路復(fù)用器,只需要一個(gè)線程便可以管理多個(gè)客戶端連接。當(dāng)客戶端數(shù)據(jù)到了之后,才會(huì)為其服務(wù)。
- NIO 比 BIO 提高了服務(wù)端工作線程的利用率,并增加了一個(gè)調(diào)度者,來(lái)實(shí)現(xiàn) Socket 連接與 Socket 數(shù)據(jù)讀寫之間的分離。
- JDK 1.4提供了對(duì)非阻塞I/O (NIO)的支持,JDK1.5_ update10版本使用epoll替代了傳統(tǒng)的select/poll,極大地提升了NIO通信的性能。與Socket類和ServerSocket類相對(duì)應(yīng),NIO也提供了SocketChannel和ServerSocketChannel兩種不同的套接字通道實(shí)現(xiàn),這兩種新增的通道都支持阻塞和非阻塞兩種模式。阻塞模式使用非常簡(jiǎn)單,但是性能和可靠性都不好,非阻塞模式則正好相反。開(kāi)發(fā)人員一般可以根據(jù)自己的需要來(lái)選擇合適的模式,一般來(lái)說(shuō),低負(fù)載、低并發(fā)的應(yīng)用程序可以選擇同步阻塞I/O以降低編程復(fù)雜度,但是對(duì)于高負(fù)載、高并發(fā)的網(wǎng)絡(luò)應(yīng)用,需要使用NIO的非阻塞模式進(jìn)行開(kāi)發(fā)。
- NIO采用多路復(fù)用技術(shù),一個(gè)多路復(fù)用器Selector可以同時(shí)輪詢多個(gè)Channel,由于JDK使用了epoll(0代替?zhèn)鹘y(tǒng)的select實(shí)現(xiàn),所以它并沒(méi)有最大連接句柄1024/2048的限制。這也就意味著只需要一個(gè)線程負(fù)責(zé)Selector的輪詢,就可以接入成千上萬(wàn)的客戶端,這確實(shí)是個(gè)非常巨大的進(jìn)步。
- NIO采用多路復(fù)用技術(shù),一個(gè)多路復(fù)用器Selector可以同時(shí)輪詢多個(gè)Channel,由于JDK使用了epoll(0代替?zhèn)鹘y(tǒng)的select實(shí)現(xiàn),所以它并沒(méi)有最大連接句柄1024/2048的限制。這也就意味著只需要一個(gè)線程負(fù)責(zé)Selector的輪詢,就可以接入成千上萬(wàn)的客戶端,這確實(shí)是個(gè)非常巨大的進(jìn)步。
- NIO的實(shí)現(xiàn)流程,類似于Select:【新事件到來(lái)的時(shí)候,會(huì)在Selector上注冊(cè)標(biāo)記位,標(biāo)示可讀、可寫或者有連接到來(lái)。NIO由原來(lái)的阻塞讀寫(占用線程)變成了單線程輪詢事件,找到可以進(jìn)行讀寫的網(wǎng)絡(luò)描述符進(jìn)行讀寫。除了事件的輪詢是阻塞的(沒(méi)有可干的事情必須要阻塞),剩余的I/O操作都是純CPU操作,沒(méi)有必要開(kāi)啟多線程。并且由于線程的節(jié)約,連接數(shù)大的時(shí)候因?yàn)榫€程切換帶來(lái)的問(wèn)題也隨之解決,進(jìn)而為處理海量連接提供了可能。】
- 創(chuàng)建ServerSocketChannel監(jiān)聽(tīng)客戶端連接并綁定監(jiān)聽(tīng)端口,設(shè)置為非阻塞模式
- 創(chuàng)建Reactor線程,創(chuàng)建多路復(fù)用器(Selector)并啟動(dòng)線程
- 將ServerSocketChannel注冊(cè)到Reactor線程的Selector上,監(jiān)聽(tīng)Accept事件
- Selector在線程run方法中無(wú)線循環(huán)輪詢準(zhǔn)備就緒的Key
- Selector監(jiān)聽(tīng)到新的客戶端接入,處理新的請(qǐng)求,完成TCP三次握手,建立物理連接
- 將新的客戶端連接注冊(cè)到Selector上,監(jiān)聽(tīng)讀操作,讀取客戶端發(fā)送的網(wǎng)絡(luò)消息
- 客戶端發(fā)送的數(shù)據(jù)就緒則讀取客戶端請(qǐng)求,進(jìn)行處理
- 既然服務(wù)端的工作線程可以服務(wù)于多個(gè)客戶端的連接請(qǐng)求,那么具體由哪個(gè)工作線程服務(wù)于哪個(gè)客戶端請(qǐng)求呢?
- 這時(shí)就需要一個(gè)調(diào)度者去監(jiān)控所有的客戶端連接,比如當(dāng)圖中的客戶端 A 的輸入已經(jīng)準(zhǔn)備好后,就由這個(gè)調(diào)度者 ,也就是Selector 選擇器去通知服務(wù)端的工作線程,告訴它們由工作線程 1 去服務(wù)于客戶端 A 的請(qǐng)求。這種思路就是 NIO 編程模型的基本原理,調(diào)度者就是 Selector 選擇器【selector的作用就是配合一個(gè)線程來(lái)管理多個(gè)channel,獲取這些channel.上發(fā)生的事件,這些channel工作在非阻塞模式下,不會(huì)讓線程吊死在一個(gè)channel上。適合連接數(shù)特別多,但流量低的場(chǎng)景(low traffic)】。
- 升級(jí)為線程池版,解決上面問(wèn)題,阻塞式I/O
- 線程升級(jí)為線程池版,線程池再升級(jí)為selector版:selector的作用就是配合一個(gè)線程來(lái)管理多個(gè)channel,獲取這些channel.上發(fā)生的事件,這些channel工作在非阻塞模式下,不會(huì)讓線程吊死在一個(gè)channel上。所以用了selector后防止了線程吊死在同一顆樹(shù)上。適合連接數(shù)特別多,但流量低的場(chǎng)景(low traffic)】
- 升級(jí)為線程池版,解決上面問(wèn)題,阻塞式I/O
- 這時(shí)就需要一個(gè)調(diào)度者去監(jiān)控所有的客戶端連接,比如當(dāng)圖中的客戶端 A 的輸入已經(jīng)準(zhǔn)備好后,就由這個(gè)調(diào)度者 ,也就是Selector 選擇器去通知服務(wù)端的工作線程,告訴它們由工作線程 1 去服務(wù)于客戶端 A 的請(qǐng)求。這種思路就是 NIO 編程模型的基本原理,調(diào)度者就是 Selector 選擇器【selector的作用就是配合一個(gè)線程來(lái)管理多個(gè)channel,獲取這些channel.上發(fā)生的事件,這些channel工作在非阻塞模式下,不會(huì)讓線程吊死在一個(gè)channel上。適合連接數(shù)特別多,但流量低的場(chǎng)景(low traffic)】。
- 通過(guò)Selector多路復(fù)用器,只需要一個(gè)線程便可以管理多個(gè)客戶端連接。當(dāng)客戶端數(shù)據(jù)到了之后,才會(huì)為其服務(wù)。
- socket設(shè)置為 NONBLOCK(非阻塞)就是告訴內(nèi)核,當(dāng)所請(qǐng)求的I/O操作無(wú)法完成時(shí),不要將進(jìn)程睡眠,而是返回一個(gè)錯(cuò)誤碼(EWOULDBLOCK) ,這樣請(qǐng)求就不會(huì)阻塞
- 當(dāng)用戶進(jìn)程調(diào)用了recvfrom這個(gè)系統(tǒng)調(diào)用,如果kernel中的數(shù)據(jù)還沒(méi)有準(zhǔn)備好,那么它并不會(huì)block用戶進(jìn)程,而是立刻返回一個(gè)EWOULDBLOCK error。從用戶進(jìn)程角度講 ,它發(fā)起一個(gè)read操作后,并不需要等待,而是馬上就得到了一個(gè)結(jié)果。用戶進(jìn)程判斷結(jié)果是一個(gè)EWOULDBLOCK error時(shí),它就知道數(shù)據(jù)還沒(méi)有準(zhǔn)備好,于是它 可以再次發(fā)送read操作。一旦kernel中的數(shù)據(jù)準(zhǔn)備好了,并且又再次收到了用戶進(jìn)程的system call,那么它馬上就將數(shù)據(jù)拷貝到了用戶空間緩沖區(qū),然后返回。可以看到,I/O 操作函數(shù)將不斷的測(cè)試數(shù)據(jù)是否已經(jīng)準(zhǔn)備好,如果沒(méi)有準(zhǔn)備好,繼續(xù)輪詢,直到數(shù)據(jù)準(zhǔn)備好為止。整個(gè) I/O 請(qǐng)求的過(guò)程中,雖然用戶線程每次發(fā)起 I/O 請(qǐng)求后可以立即返回,但是為了等到數(shù)據(jù),仍需要不斷地輪詢、重復(fù)請(qǐng)求,消耗了大量的 CPU 的資源
- non blocking IO的特點(diǎn)是用戶進(jìn)程需要不斷的主動(dòng)詢問(wèn)kernel數(shù)據(jù)好了沒(méi)有:
- 等待數(shù)據(jù)準(zhǔn)備就緒 (Waiting for the data to be ready) 「這一步是非阻塞的」
- 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process) 「這一步是阻塞的」
- 等待數(shù)據(jù)準(zhǔn)備就緒 (Waiting for the data to be ready) 「這一步是非阻塞的」
- 將BIO那個(gè)例子改裝為NIO的。
- 客戶端 SocketChannel 的使用:
- 客戶端 SocketChannel 的使用:
- 把非阻塞的文件描述符稱為非阻塞I/O。可以通過(guò)設(shè)置SOCK_NONBLOCK標(biāo)記創(chuàng)建非阻塞的socket fd,或者使用fcntl將fd設(shè)置為非阻塞。
- Unix 常見(jiàn)的五種IO模型之三:IO復(fù)用式IO模型(IO multiplexing model):也叫,I/O 多路復(fù)用( IO multiplexing):NIO不停問(wèn)問(wèn)問(wèn),給人煩壞了,把CPU資源也消耗的差不多了,然后神獸繼續(xù)究極進(jìn)化,從BIO—>NIO—>多路復(fù)用
- 在I/O編程過(guò)程中,當(dāng)需要同時(shí)處理多個(gè)客戶端接入請(qǐng)求時(shí),可以利用多線程或者I/O多路復(fù)用技術(shù)進(jìn)行處理。I/O多路復(fù)用技術(shù)通過(guò)把多個(gè)I/O的阻塞復(fù)用到同一個(gè)select的阻塞上,從而使得系統(tǒng)在單線程的情況下可以同時(shí)處理多個(gè)客戶端請(qǐng)求。
- 與傳統(tǒng)的多線程/多進(jìn)程模型相比,I/O 多路復(fù)用的最大優(yōu)勢(shì)是系統(tǒng)開(kāi)銷小,系統(tǒng)不需要?jiǎng)?chuàng)建新的額外進(jìn)程或者線程,也不需要維護(hù)這些進(jìn)程和線程的運(yùn)行,降低了系統(tǒng)的維護(hù)工作量,節(jié)省了系統(tǒng)資源。
- IO 多路復(fù)用模型中,線程首先發(fā)起 select 調(diào)用,詢問(wèn)內(nèi)核數(shù)據(jù)是否準(zhǔn)備就緒,等內(nèi)核把數(shù)據(jù)準(zhǔn)備好了,用戶線程再發(fā)起 read 調(diào)用,相當(dāng)于IO 多路復(fù)用模型,通過(guò)減少無(wú)效的系統(tǒng)調(diào)用,減少了對(duì) CPU 資源的消耗。read 調(diào)用的過(guò)程(數(shù)據(jù)從內(nèi)核空間 -> 用戶空間)還是阻塞的。【相當(dāng)于 IO復(fù)用模型核心思路:系統(tǒng)給我們提供一類函數(shù)(如我們耳濡目染的select、poll、epoll函數(shù)),它們可以同時(shí)監(jiān)控多個(gè)fd的操作,任何一個(gè)返回內(nèi)核數(shù)據(jù)就緒,應(yīng)用進(jìn)程再發(fā)起recvfrom系統(tǒng)調(diào)用】
- IO多路復(fù)用是指 通過(guò)一種機(jī)制監(jiān)視多個(gè)文件描述符fd【文件描述符在形式上是一個(gè)非負(fù)整數(shù),實(shí)際上,它是一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開(kāi)文件的記錄表。當(dāng)程序打開(kāi)一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符】,一旦某個(gè)文件描述符fd就緒(一般是讀就緒或者寫就緒),或者說(shuō)內(nèi)核一旦發(fā)現(xiàn)進(jìn)程指定的一個(gè)或者多個(gè)IO條件準(zhǔn)備讀取,它就通知該進(jìn)程,進(jìn)行相應(yīng)的讀寫操作。
- (IO多路復(fù)用模型指的是:使用 單個(gè)進(jìn)程同時(shí)處理多個(gè)網(wǎng)絡(luò)連接IO,他的原理就是select、poll、epoll 不斷輪詢所負(fù)責(zé)的所有 socket,當(dāng)某個(gè)socket有數(shù)據(jù)到達(dá)了,就通知用戶進(jìn)程。該模型的優(yōu)勢(shì)并不是對(duì)于單個(gè)連接能處理得更快,而是在于能處理更多的連接。)
- 多路指的是多個(gè) Socket 連接,就是指多個(gè)通道,也就是多個(gè)網(wǎng)絡(luò)連接的 IO
- 復(fù)用指的是復(fù)用一個(gè)線程,多個(gè)通道或者多個(gè)網(wǎng)絡(luò)連接的IO可以注冊(cè)到或這說(shuō)復(fù)用在一個(gè)復(fù)用器上
- (IO多路復(fù)用模型指的是:使用 單個(gè)進(jìn)程同時(shí)處理多個(gè)網(wǎng)絡(luò)連接IO,他的原理就是select、poll、epoll 不斷輪詢所負(fù)責(zé)的所有 socket,當(dāng)某個(gè)socket有數(shù)據(jù)到達(dá)了,就通知用戶進(jìn)程。該模型的優(yōu)勢(shì)并不是對(duì)于單個(gè)連接能處理得更快,而是在于能處理更多的連接。)
- I/O多路復(fù)用有兩種事件觸發(fā)模式,分別是 邊緣觸發(fā)(edge-triggered,ET) 和 水平觸發(fā)(level-triggered,LT)。邊緣觸發(fā)的效率比水平觸發(fā)的效率要高,因?yàn)檫吘売|發(fā)可以減少 epoll_wait 的系統(tǒng)調(diào)用次數(shù),系統(tǒng)調(diào)用也是有一定的開(kāi)銷的的,畢竟也存在上下文的切換。【可以看看騰訊云社區(qū)的范蠡老師的epoll LT 模式和 ET 模式詳解】
- 使用邊緣觸發(fā)模式時(shí),當(dāng)被監(jiān)控的 Socket 描述符上有可讀事件發(fā)生時(shí),服務(wù)器端只會(huì)從 epoll_wait 中蘇醒一次,即使進(jìn)程沒(méi)有調(diào)用 read 函數(shù)從內(nèi)核讀取數(shù)據(jù),也依然只蘇醒一次,因此 我們程序要保證一次性將內(nèi)核緩沖區(qū)的數(shù)據(jù)讀取完【驛站只發(fā)一條短信不會(huì)發(fā)第二條第三條讓你去取快遞的模式就叫邊緣觸發(fā)】。
- 如果使用邊緣觸發(fā)模式,I/O 事件發(fā)生時(shí)只會(huì)通知一次,而且我們不知道到底能讀寫多少數(shù)據(jù),所以在收到通知后應(yīng)盡可能地讀寫數(shù)據(jù),以免錯(cuò)失讀寫的機(jī)會(huì)。因此,我們會(huì)循環(huán)從文件描述符讀寫數(shù)據(jù),那么如果文件描述符是阻塞的,沒(méi)有數(shù)據(jù)可讀寫時(shí),進(jìn)程會(huì)阻塞在讀寫函數(shù)那里,程序就沒(méi)辦法繼續(xù)往下執(zhí)行。所以,邊緣觸發(fā)模式一般和非阻塞 I/O 搭配使用,程序會(huì)一直執(zhí)行 I/O 操作,直到系統(tǒng)調(diào)用(如 read 和 write)返回錯(cuò)誤,錯(cuò)誤類型為 EAGAIN 或 EWOULDBLOCK。
- 使用水平觸發(fā)模式時(shí),當(dāng)被監(jiān)控的 Socket 上有可讀事件發(fā)生時(shí),服務(wù)器端不斷地從 epoll_wait 中蘇醒,直到內(nèi)核緩沖區(qū)數(shù)據(jù)被 read 函數(shù)讀完才結(jié)束,目的是告訴我們有數(shù)據(jù)需要讀取。如果使用水平觸發(fā)模式,當(dāng)內(nèi)核通知文件描述符可讀寫時(shí),接下來(lái)還可以繼續(xù)去檢測(cè)它的狀態(tài),看它是否依然可讀或可寫。所以在收到通知后,沒(méi)必要一次執(zhí)行盡可能多的讀寫操作【如果快遞箱發(fā)現(xiàn)你的快遞沒(méi)有被取出,它就會(huì)不停地發(fā)短信通知你,直到你取出了快遞,它才消停,這個(gè)就是水平觸發(fā)的方式】
- 使用邊緣觸發(fā)模式時(shí),當(dāng)被監(jiān)控的 Socket 描述符上有可讀事件發(fā)生時(shí),服務(wù)器端只會(huì)從 epoll_wait 中蘇醒一次,即使進(jìn)程沒(méi)有調(diào)用 read 函數(shù)從內(nèi)核讀取數(shù)據(jù),也依然只蘇醒一次,因此 我們程序要保證一次性將內(nèi)核緩沖區(qū)的數(shù)據(jù)讀取完【驛站只發(fā)一條短信不會(huì)發(fā)第二條第三條讓你去取快遞的模式就叫邊緣觸發(fā)】。
- Linux 系統(tǒng)中的 select、poll、epoll等系統(tǒng)調(diào)用都是 I/O 多路復(fù)用的機(jī)制。(IO multiplexing就是我們常說(shuō)的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。)
- select/poll/epol獲取網(wǎng)絡(luò)事件的過(guò)程:在獲取事件時(shí),先把我們要關(guān)心的連接傳給內(nèi)核,再由內(nèi)核檢測(cè):
- 如果沒(méi)有事件發(fā)生,線程只需阻塞在這個(gè)系統(tǒng)調(diào)用,而無(wú)需像線程池那樣輪訓(xùn)調(diào)用 read 操作來(lái)判斷是否有數(shù)據(jù)
- 如果有事件發(fā)生,內(nèi)核會(huì)返回產(chǎn)生了事件的連接,線程就會(huì)從阻塞狀態(tài)返回,然后在用戶態(tài)中再處理這些連接對(duì)應(yīng)的業(yè)務(wù)即可。
- select/epoll的好處就在于單個(gè)process就可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的IO。它的基本原理就是select,poll,epoll這些個(gè)function會(huì)不斷的輪詢所負(fù)責(zé)的所有socket,當(dāng)某個(gè)socket有數(shù)據(jù)到達(dá)了,就通知用戶進(jìn)程
- 目前支持 IO 多路復(fù)用的系統(tǒng)調(diào)用有 select,epoll 等等。select 系統(tǒng)調(diào)用,目前幾乎在所有的操作系統(tǒng)上都有支持。
- select 調(diào)用 :內(nèi)核提供的系統(tǒng)調(diào)用,select 它支持一次查詢多個(gè)系統(tǒng)調(diào)用的可用狀態(tài)。幾乎所有的操作系統(tǒng)都支持
- 應(yīng)用進(jìn)程通過(guò)調(diào)用select函數(shù),可以同時(shí)監(jiān)控多個(gè)fd,在select函數(shù)監(jiān)控的fd中,只要有任何一個(gè)數(shù)據(jù)狀態(tài)準(zhǔn)備就緒了,select函數(shù)就會(huì)返回可讀狀態(tài),這時(shí)應(yīng)用進(jìn)程再發(fā)起recvfrom請(qǐng)求去讀取數(shù)據(jù)。
- epoll 調(diào)用 :linux 2.6 內(nèi)核,epoll 屬于 select 調(diào)用的增強(qiáng)版本,優(yōu)化了 IO 的執(zhí)行效率
- select 調(diào)用 :內(nèi)核提供的系統(tǒng)調(diào)用,select 它支持一次查詢多個(gè)系統(tǒng)調(diào)用的可用狀態(tài)。幾乎所有的操作系統(tǒng)都支持
- 目前支持 IO 多路復(fù)用的系統(tǒng)調(diào)用有 select,epoll 等等。select 系統(tǒng)調(diào)用,目前幾乎在所有的操作系統(tǒng)上都有支持。
- select、poll 和 epoll 之間的區(qū)別:(select,poll,epoll 都是 IO 多路復(fù)用的機(jī)制, select,poll,epoll 本質(zhì)上都是同步 I/O,因?yàn)樗麄兌?strong>需要在讀寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說(shuō)這個(gè)讀寫過(guò)程是阻塞的,而異步 I/O 則無(wú)需自己負(fù)責(zé)進(jìn)行讀寫,異步 I/O 的實(shí)現(xiàn)會(huì)負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。)
- select:【多個(gè)網(wǎng)絡(luò)連接的 IO 可以注冊(cè)到一個(gè)復(fù)用器(select)上,當(dāng)用戶進(jìn)程調(diào)用了 select復(fù)用器,那么整個(gè)進(jìn)程會(huì)被阻塞。同時(shí),內(nèi)核會(huì)“監(jiān)視”所有 select復(fù)用器 負(fù)責(zé)的 socket,當(dāng)任何一個(gè) socket 中的數(shù)據(jù)準(zhǔn)備好了,select 復(fù)用器就會(huì)返回再?gòu)膬?nèi)核中拿數(shù)據(jù)。這個(gè)時(shí)候用戶進(jìn)程再調(diào)用 read 操作,將數(shù)據(jù)從內(nèi)核中拷貝到用戶進(jìn)程。用戶可以注冊(cè)多個(gè) socket,然后不斷地調(diào)用 select 讀取被激活的 socket,即可達(dá)到在同一個(gè)線程內(nèi)同時(shí)處理多個(gè) IO 請(qǐng)求的目的。而在同步阻塞模型中,必須通過(guò)多線程的方式才能達(dá)到這個(gè)目的。好比我們?nèi)ゲ蛷d吃飯,這次我們是幾個(gè)人一起去的,我們專門留了一個(gè)人在餐廳排號(hào)等位,其他人就去逛街了,等排號(hào)的朋友通知我們可以吃飯了,我們就直接去享用了。】
- 時(shí)間復(fù)雜度 O(n)。
- select/poll 只有水平觸發(fā)模式
- select 僅僅知道有 I/O 事件發(fā)生,但 并不知道是哪幾個(gè)流,所以 只能無(wú)差別輪詢所有流,找出能讀出數(shù)據(jù)或者寫入數(shù)據(jù)的流,并對(duì)其進(jìn)行操作。所以 select 具有 O(n) 的無(wú)差別輪詢復(fù)雜度,同時(shí)處理的流越多,無(wú)差別輪詢時(shí)間就越長(zhǎng)。除了這個(gè),select還有幾個(gè)缺點(diǎn):
- poll:因?yàn)榇嬖谶B接數(shù)限制,所以后來(lái)又提出了poll。與select相比,poll解決了連接數(shù)限制問(wèn)題。但是呢,select和poll一樣,還是需要通過(guò)遍歷文件描述符來(lái)獲取已經(jīng)就緒的socket。如果同時(shí)連接的大量客戶端,在一時(shí)刻可能只有極少處于就緒狀態(tài),伴隨著監(jiān)視的描述符數(shù)量的增長(zhǎng),效率也會(huì)線性下降。
- 時(shí)間復(fù)雜度 O(n)
- select/poll 只有水平觸發(fā)模式
- poll 本質(zhì)上和 select 沒(méi)有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后**查詢每個(gè) fd 對(duì)應(yīng)的設(shè)備狀態(tài), 但是它沒(méi)有最大連接數(shù)的限制**,原因是它是基于鏈表來(lái)存儲(chǔ)的。
- epoll:為了解決select/poll存在的問(wèn)題,多路復(fù)用模型epoll誕生,epoll采用事件驅(qū)動(dòng)來(lái)實(shí)現(xiàn)【epoll先通過(guò)epoll_ctl()來(lái)注冊(cè)一個(gè)fd(文件描述符),一旦基于某個(gè)fd就緒時(shí),內(nèi)核會(huì)采用回調(diào)機(jī)制,迅速激活這個(gè)fd,當(dāng)進(jìn)程調(diào)用epoll_wait()時(shí)便得到通知。這里去掉了遍歷文件描述符的坑爹操作,而是采用監(jiān)聽(tīng)事件回調(diào)的機(jī)制。這就是epoll的亮點(diǎn)。】int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...)int epfd = epoll_create(...);//先用e poll_create 創(chuàng)建一個(gè) epoll對(duì)象 epfd
epoll_ctl(epfd, ...); //再通過(guò) epoll_ctl 將所有需要監(jiān)聽(tīng)的socket添加到epfd中。epoll 在內(nèi)核里使用紅黑樹(shù)來(lái)跟蹤進(jìn)程所有待檢測(cè)的文件描述字,把需要監(jiān)控的 socket 通過(guò) epoll_ctl() 函數(shù)加入內(nèi)核中的紅黑樹(shù)里while(1) {int n = epoll_wait(...);//最后調(diào)用 epoll_wait 等待數(shù)據(jù)。epoll 使用事件驅(qū)動(dòng)的機(jī)制,內(nèi)核里維護(hù)了一個(gè)鏈表來(lái)記錄就緒事件,當(dāng)某個(gè) socket 有事件發(fā)生時(shí),通過(guò)回調(diào)函數(shù)內(nèi)核會(huì)將其加入到這個(gè)就緒事件列表中,當(dāng)用戶調(diào)用 epoll_wait() 函數(shù)時(shí),只會(huì)返回有事件發(fā)生的文件描述符的個(gè)數(shù),不需要像 select/poll 那樣輪詢掃描整個(gè) socket 集合,大大提高了檢測(cè)的效率。for(接收到數(shù)據(jù)的socket){//處理}
}
- 時(shí)間復(fù)雜度 O(1)
- epoll 可以理解為 event poll,不同于忙輪詢和無(wú)差別輪詢,epoll 會(huì)把哪個(gè)流發(fā)生了怎樣的 I/O 事件通知我們。所以說(shuō) epoll 實(shí)際上是事件驅(qū)動(dòng)(每個(gè)事件關(guān)聯(lián)上 fd)的。
- epoll 默認(rèn)的觸發(fā)模式是水平觸發(fā),但是可以根據(jù)應(yīng)用場(chǎng)景設(shè)置為邊緣觸發(fā)模式。
- 經(jīng)典的 C10K 問(wèn)題:如果服務(wù)器的內(nèi)存只有 2 GB,網(wǎng)卡是千兆的,能支持并發(fā) 1 萬(wàn)請(qǐng)求嗎【單機(jī)同時(shí)處理 1 萬(wàn)個(gè)請(qǐng)求的問(wèn)題。】從硬件資源角度看,對(duì)于 2GB 內(nèi)存千兆網(wǎng)卡的服務(wù)器,如果每個(gè)請(qǐng)求處理占用不到 200KB 的內(nèi)存和 100Kbit 的網(wǎng)絡(luò)帶寬就可以滿足并發(fā) 1 萬(wàn)個(gè)請(qǐng)求。不過(guò),要想真正實(shí)現(xiàn) C10K 的服務(wù)器,要考慮的地方在于服務(wù)器的網(wǎng)絡(luò) I/O 模型,效率低的模型,會(huì)加重系統(tǒng)開(kāi)銷。基于進(jìn)程或者線程模型的,其實(shí)還是有問(wèn)題的。新到來(lái)一個(gè) TCP 連接,就需要分配一個(gè)進(jìn)程或者線程,那么如果要達(dá)到 C10K,意味著要一臺(tái)機(jī)器維護(hù) 1 萬(wàn)個(gè)連接,相當(dāng)于要維護(hù) 1 萬(wàn)個(gè)進(jìn)程/線程,操作系統(tǒng)就算死扛也是扛不住的。
- select:【多個(gè)網(wǎng)絡(luò)連接的 IO 可以注冊(cè)到一個(gè)復(fù)用器(select)上,當(dāng)用戶進(jìn)程調(diào)用了 select復(fù)用器,那么整個(gè)進(jìn)程會(huì)被阻塞。同時(shí),內(nèi)核會(huì)“監(jiān)視”所有 select復(fù)用器 負(fù)責(zé)的 socket,當(dāng)任何一個(gè) socket 中的數(shù)據(jù)準(zhǔn)備好了,select 復(fù)用器就會(huì)返回再?gòu)膬?nèi)核中拿數(shù)據(jù)。這個(gè)時(shí)候用戶進(jìn)程再調(diào)用 read 操作,將數(shù)據(jù)從內(nèi)核中拷貝到用戶進(jìn)程。用戶可以注冊(cè)多個(gè) socket,然后不斷地調(diào)用 select 讀取被激活的 socket,即可達(dá)到在同一個(gè)線程內(nèi)同時(shí)處理多個(gè) IO 請(qǐng)求的目的。而在同步阻塞模型中,必須通過(guò)多線程的方式才能達(dá)到這個(gè)目的。好比我們?nèi)ゲ蛷d吃飯,這次我們是幾個(gè)人一起去的,我們專門留了一個(gè)人在餐廳排號(hào)等位,其他人就去逛街了,等排號(hào)的朋友通知我們可以吃飯了,我們就直接去享用了。】
- select/poll/epol獲取網(wǎng)絡(luò)事件的過(guò)程:在獲取事件時(shí),先把我們要關(guān)心的連接傳給內(nèi)核,再由內(nèi)核檢測(cè):
- IO多路復(fù)用適用如下場(chǎng)合:
- 多路復(fù)用 IO 是在高并發(fā)場(chǎng)景中使用最為廣泛的一種 IO 模型,如 Java 的 NIO、Redis、Nginx 的底層實(shí)現(xiàn)就是此類 IO 模型的應(yīng)用,經(jīng)典的 Reactor 模式也是基于此類 IO 模型。
- 最常用的I/O事件通知機(jī)制就是I/O復(fù)用(I/O multiplexing)。Linux 環(huán)境中使用select/poll/epoll 實(shí)現(xiàn)I/O復(fù)用,I/O復(fù)用接口本身是阻塞的,在應(yīng)用程序中通過(guò)I/O復(fù)用接口向內(nèi)核注冊(cè)fd所關(guān)注的事件,當(dāng)關(guān)注事件觸發(fā)時(shí),通過(guò)I/O復(fù)用接口的返回值通知到應(yīng)用程序。
- 以recv為例。I/O復(fù)用接口可以同時(shí)監(jiān)聽(tīng)多個(gè)I/O事件以提高事件處理效率。
- 以recv為例。I/O復(fù)用接口可以同時(shí)監(jiān)聽(tīng)多個(gè)I/O事件以提高事件處理效率。
- 當(dāng)客戶處理多個(gè)描述字時(shí)(一般是交互式輸入和網(wǎng)絡(luò)套接口),必須使用I/O復(fù)用
- 當(dāng)一個(gè)客戶同時(shí)處理多個(gè)套接口時(shí),而這種情況是可能的,但很少出現(xiàn)
- 如果一個(gè)TCP服務(wù)器既要處理監(jiān)聽(tīng)套接口,又要處理已連接套接口,一般也要用到I/O復(fù)用
- 如果一個(gè)服務(wù)器即要處理TCP,又要處理UDP,一般要使用I/O復(fù)用
- 如果一個(gè)服務(wù)器要處理多個(gè)服務(wù)或多個(gè)協(xié)議,一般要使用I/O復(fù)用
- 與多進(jìn)程和多線程技術(shù)相比,I/O多路復(fù)用技術(shù)的最大優(yōu)勢(shì)是系統(tǒng)開(kāi)銷小,系統(tǒng)不必創(chuàng)建進(jìn)程/線程,也不必維護(hù)這些進(jìn)程/線程,從而大大減小了系統(tǒng)的開(kāi)銷
- 當(dāng)用戶進(jìn)程調(diào)用了select,那么整個(gè)進(jìn)程會(huì)被block,而同時(shí),kernel會(huì)“監(jiān)視”所有select負(fù)責(zé)的socket,當(dāng)任何一個(gè)socket中的數(shù)據(jù)準(zhǔn)備好了,select就會(huì)返回。這個(gè)時(shí)候用戶進(jìn)程再調(diào)用read操作,將數(shù)據(jù)從kernel拷貝到用戶進(jìn)程。所以,I/O 多路復(fù)用的特點(diǎn)是通過(guò)一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待多個(gè)文件描述符,而這些文件描述符(套接字描述符)其中的任意一個(gè)進(jìn)入讀就緒狀態(tài),select()函數(shù)就可以返回。這個(gè)圖和blocking IO的圖其實(shí)并沒(méi)有太大的不同,事實(shí)上因?yàn)镮O多路復(fù)用多了添加監(jiān)視 socket,以及調(diào)用 select 函數(shù)的額外操作,效率更差。還更差一些。因?yàn)檫@里需要使用兩個(gè)system call (select 和 recvfrom),而blocking IO只調(diào)用了一個(gè)system call (recvfrom)。但是,使用 select 以后最大的優(yōu)勢(shì)是用戶可以在一個(gè)線程內(nèi)同時(shí)處理多個(gè) socket 的 I/O 請(qǐng)求。用戶可以注冊(cè)多個(gè) socket,然后不斷地調(diào)用 select 讀取被激活的 socket,即可達(dá)到在同一個(gè)線程內(nèi)同時(shí)處理多個(gè) I/O 請(qǐng)求的目的。而在同步阻塞模型中,必須通過(guò)多線程的方式才能達(dá)到這個(gè)目的。所以,如果處理的連接數(shù)不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優(yōu)勢(shì)并不是對(duì)于單個(gè)連接能處理得更快,而是在于能處理更多的連接。)在IO multiplexing Model中,實(shí)際中,對(duì)于每一個(gè)socket,一般都設(shè)置成為non-blocking,但是,如上圖所示,整個(gè)用戶的process其實(shí)是一直被block的。只不過(guò)process是被select這個(gè)函數(shù)block,而不是被socket IO給block。
- 因此對(duì)于IO多路復(fù)用模型來(lái)說(shuō):
- 等待數(shù)據(jù)準(zhǔn)備就緒 (Waiting for the data to be ready) 「阻塞」
- 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process) 「阻塞」
- 在I/O編程過(guò)程中,當(dāng)需要同時(shí)處理多個(gè)客戶端接入請(qǐng)求時(shí),可以利用多線程或者I/O多路復(fù)用技術(shù)進(jìn)行處理。I/O多路復(fù)用技術(shù)通過(guò)把多個(gè)I/O的阻塞復(fù)用到同一個(gè)select的阻塞上,從而使得系統(tǒng)在單線程的情況下可以同時(shí)處理多個(gè)客戶端請(qǐng)求。
- Unix 常見(jiàn)的五種IO模型之四:異步非阻塞 I/O(asynchronous IO)AIO:linux下的asynchronous IO的流程
- POSIX規(guī)范定義了一組異步操作I/O的接口,不用關(guān)心fd 是阻塞還是非阻塞,異步I/O是由內(nèi)核接管應(yīng)用層對(duì)fd的I/O操作。異步I/O向應(yīng)用層通知I/O操作完成的事件,這與前面介紹的I/O 復(fù)用模型、SIGIO模型通知事件就緒的方式明顯不同。以aio_read 實(shí)現(xiàn)異步讀取IO數(shù)據(jù)為例,在等待I/O操作完成期間,不會(huì)阻塞應(yīng)用程序。
- 異步其實(shí)之前咱們就接觸過(guò):通常,我們會(huì)有一個(gè)線程池用于執(zhí)行異步任務(wù),提交任務(wù)的線程將任務(wù)提交到線程池就可以立馬返回,不必等到任務(wù)真正完成。如果想要知道任務(wù)的執(zhí)行結(jié)果,通常是通過(guò)傳遞一個(gè)回調(diào)函數(shù)的方式,任務(wù)結(jié)束后去調(diào)用這個(gè)函數(shù)。同樣的原理,Java 中的異步 IO 也是一樣的,都是由一個(gè)線程池來(lái)負(fù)責(zé)執(zhí)行任務(wù),然后使用回調(diào)或自己去查詢結(jié)果。異步 IO 主要是為了控制線程數(shù)量,減少過(guò)多的線程帶來(lái)的內(nèi)存消耗和 CPU 在線程調(diào)度上的開(kāi)銷。
- 異步 IO 一定存在一個(gè)線程池,這個(gè)線程池負(fù)責(zé)接收任務(wù)、處理 IO 事件、回調(diào)等。這個(gè)線程池就在 group 內(nèi)部【AsynchronousChannelGroup 這個(gè)類】,group 一旦關(guān)閉,那么相應(yīng)的線程池就會(huì)關(guān)閉。AsynchronousServerSocketChannels 和 AsynchronousSocketChannels 是屬于 group 的,當(dāng)我們調(diào)用 AsynchronousServerSocketChannel 或 AsynchronousSocketChannel 的 open() 方法的時(shí)候,相應(yīng)的 channel 就屬于默認(rèn)的 group,這個(gè) group 由 JVM 自動(dòng)構(gòu)造并管理。
- 配置這個(gè)默認(rèn)的 group,可以在 JVM 啟動(dòng)參數(shù)中指定以下系統(tǒng)變量:
- 使用自己定義的 group,這樣可以對(duì)其中的線程進(jìn)行更多的控制,使用以下幾個(gè)方法:
- group 的使用:
- 配置這個(gè)默認(rèn)的 group,可以在 JVM 啟動(dòng)參數(shù)中指定以下系統(tǒng)變量:
- AsynchronousFileChannels 不屬于 group。但是它們也是關(guān)聯(lián)到一個(gè)線程池的,如果不指定,會(huì)使用系統(tǒng)默認(rèn)的線程池,如果想要使用指定的線程池,可以在實(shí)例化的時(shí)候使用以下方法:
- 異步 IO 一定存在一個(gè)線程池,這個(gè)線程池負(fù)責(zé)接收任務(wù)、處理 IO 事件、回調(diào)等。這個(gè)線程池就在 group 內(nèi)部【AsynchronousChannelGroup 這個(gè)類】,group 一旦關(guān)閉,那么相應(yīng)的線程池就會(huì)關(guān)閉。AsynchronousServerSocketChannels 和 AsynchronousSocketChannels 是屬于 group 的,當(dāng)我們調(diào)用 AsynchronousServerSocketChannel 或 AsynchronousSocketChannel 的 open() 方法的時(shí)候,相應(yīng)的 channel 就屬于默認(rèn)的 group,這個(gè) group 由 JVM 自動(dòng)構(gòu)造并管理。
- 異步其實(shí)之前咱們就接觸過(guò):通常,我們會(huì)有一個(gè)線程池用于執(zhí)行異步任務(wù),提交任務(wù)的線程將任務(wù)提交到線程池就可以立馬返回,不必等到任務(wù)真正完成。如果想要知道任務(wù)的執(zhí)行結(jié)果,通常是通過(guò)傳遞一個(gè)回調(diào)函數(shù)的方式,任務(wù)結(jié)束后去調(diào)用這個(gè)函數(shù)。同樣的原理,Java 中的異步 IO 也是一樣的,都是由一個(gè)線程池來(lái)負(fù)責(zé)執(zhí)行任務(wù),然后使用回調(diào)或自己去查詢結(jié)果。異步 IO 主要是為了控制線程數(shù)量,減少過(guò)多的線程帶來(lái)的內(nèi)存消耗和 CPU 在線程調(diào)度上的開(kāi)銷。
- AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改進(jìn)版 NIO 2,它是異步 IO 模型。【JDK 7 對(duì)原有的 NIO 進(jìn)行了改進(jìn)。第一個(gè)改進(jìn)是提供了全面的文件 I/O 相關(guān) API。第二個(gè)改進(jìn)是增加了異步的基于 Channel 的 IO 機(jī)制】
- 異步 IO 是基于事件和回調(diào)機(jī)制實(shí)現(xiàn)的,也就是應(yīng)用操作之后會(huì)直接返回,不會(huì)堵塞在那里,當(dāng)后臺(tái)處理完成,操作系統(tǒng)會(huì)通知相應(yīng)的線程進(jìn)行后續(xù)的操作。
- 異步 IO 是基于事件和回調(diào)機(jī)制實(shí)現(xiàn)的,也就是應(yīng)用操作之后會(huì)直接返回,不會(huì)堵塞在那里,當(dāng)后臺(tái)處理完成,操作系統(tǒng)會(huì)通知相應(yīng)的線程進(jìn)行后續(xù)的操作。
- 用戶進(jìn)程發(fā)起aio_read調(diào)用之后,立刻就可以開(kāi)始去做其它的事。而另一方面,從kernel的角度,當(dāng)它發(fā)現(xiàn)一個(gè)asynchronous read之后,首先它會(huì)立刻返回,所以不會(huì)對(duì)用戶進(jìn)程產(chǎn)生任何block。然后,kernel會(huì)等待數(shù)據(jù)準(zhǔn)備完成,然后將數(shù)據(jù)拷貝到用戶內(nèi)存,當(dāng)這一切都完成之后,kernel會(huì)給用戶進(jìn)程發(fā)送一個(gè)signal,告訴它read操作完成了
- 異步 I/O 模型使用了 Proactor 設(shè)計(jì)模式實(shí)現(xiàn)了這一機(jī)制。因此對(duì)異步IO模型來(lái)說(shuō):
- 等待數(shù)據(jù)準(zhǔn)備就緒 (Waiting for the data to be ready) 「非阻塞」
- 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process) 「非阻塞」
- 異步 I/O 模型使用了 Proactor 設(shè)計(jì)模式實(shí)現(xiàn)了這一機(jī)制。因此對(duì)異步IO模型來(lái)說(shuō):
- AIO:JDK1.7引入NIO2.0,提供了異步文件通道和異步套接字通道的實(shí)現(xiàn)。其底層在Windows上是通過(guò)IOCP實(shí)現(xiàn),在Linux上是通過(guò)IO復(fù)用Epoll來(lái)模擬實(shí)現(xiàn)的。在JAVA NIO框架中,Selector它負(fù)責(zé)代替應(yīng)用查詢中所有已注冊(cè)的通道到操作系統(tǒng)中進(jìn)行IO事件輪詢、管理當(dāng)前注冊(cè)的通道集合,定位發(fā)生事件的通道等操作。但是在JAVA AIO框架中,由于應(yīng)用程序不是輪詢方式,而是訂閱-通知方式,所以不再需要Selector(選擇器)了,改由Channel通道直接到操作系統(tǒng)注冊(cè)監(jiān)聽(tīng) 。【More New IO,或稱 NIO.2,隨 JDK 1.7 發(fā)布,包括了引入異步 IO 接口和 Paths 等文件訪問(wèn)接口。】
- JAVA AIO框架中,只實(shí)現(xiàn)了兩種網(wǎng)絡(luò)IO通道:
- AsynchronousServerSocketChannel(服務(wù)器監(jiān)聽(tīng)通道)
- 這個(gè)類對(duì)應(yīng)的是非阻塞 IO 的 ServerSocketChannel。
- 用回調(diào)函數(shù)的方式寫一個(gè)簡(jiǎn)單的服務(wù)端:
- ChannelHandler 類
- 自定義的 Attachment 類:
- 接下來(lái)可以接收客戶端請(qǐng)求了
- ChannelHandler 類
- AsynchronousSocketChannel(Socket套接字通道)
- 使用 AsynchronousSocketChannel 的方式和非阻塞 IO 基本類似。
- AsynchronousFileChannel:異步的文件 IO
- 文件 IO 在所有的操作系統(tǒng)中都不支持非阻塞模式,但是我們可以對(duì)文件 IO 采用異步的方式來(lái)提高性能。
- AsynchronousFileChannel 里面的一些重要的接口:
- AsynchronousServerSocketChannel(服務(wù)器監(jiān)聽(tīng)通道)
- Java 異步 IO 提供了兩種使用方式,分別是 返回 java.util.concurrent.Future 實(shí)例和使用CompletionHandler 回調(diào)函數(shù)。
- 返回 java.util.concurrent.Future 實(shí)例:JDK 線程池就是這么使用的
- Future 接口的幾個(gè)方法語(yǔ)義:
- Future 接口的幾個(gè)方法語(yǔ)義:
- 提供 CompletionHandler 回調(diào)函數(shù):
- java.nio.channels.CompletionHandler 接口定義:
- java.nio.channels.CompletionHandler 接口定義:
- 返回 java.util.concurrent.Future 實(shí)例:JDK 線程池就是這么使用的
- JAVA AIO框架中,只實(shí)現(xiàn)了兩種網(wǎng)絡(luò)IO通道:
- 異步 I/O 并沒(méi)有涉及到 PageCache,所以使用異步 I/O 就意味著要繞開(kāi) PageCache。繞開(kāi) PageCache 的 I/O 叫 直接 I/O,使用 PageCache 的 I/O 則叫緩存 I/O。通常,對(duì)于磁盤,異步 I/O 只支持直接 I/O。在高并發(fā)的場(chǎng)景下,針對(duì)大文件的傳輸?shù)姆绞?#xff0c;應(yīng)該使用「異步 I/O + 直接 I/O」來(lái)替代零拷貝技術(shù)就可以無(wú)阻塞地讀取文件了。
- 傳輸文件的時(shí)候,我們要根據(jù)文件的大小來(lái)使用不同的方式:
- 傳輸 大文件 的時(shí)候,使用「異步 I/O + 直接 I/O」
- 傳輸小文件的時(shí)候,則使用「零拷貝技術(shù)」
- 傳輸文件的時(shí)候,我們要根據(jù)文件的大小來(lái)使用不同的方式:
- POSIX規(guī)范定義了一組異步操作I/O的接口,不用關(guān)心fd 是阻塞還是非阻塞,異步I/O是由內(nèi)核接管應(yīng)用層對(duì)fd的I/O操作。異步I/O向應(yīng)用層通知I/O操作完成的事件,這與前面介紹的I/O 復(fù)用模型、SIGIO模型通知事件就緒的方式明顯不同。以aio_read 實(shí)現(xiàn)異步讀取IO數(shù)據(jù)為例,在等待I/O操作完成期間,不會(huì)阻塞應(yīng)用程序。
- Unix 常見(jiàn)的五種IO模型之五:信號(hào)驅(qū)動(dòng)式IO模型(signal-driven IO model)
- 除了I/O復(fù)用方式通知I/O事件,還可以通過(guò)SIGIO信號(hào)來(lái)通知I/O事件。兩者不同的是,在等待數(shù)據(jù)達(dá)到期間,I/O復(fù)用是會(huì)阻塞應(yīng)用程序,而SIGIO方式是不會(huì)阻塞應(yīng)用程序的。
- 首先我們?cè)试S socket 進(jìn)行信號(hào)驅(qū)動(dòng) I/O,并安裝一個(gè)信號(hào)處理函數(shù),進(jìn)程繼續(xù)運(yùn)行并不阻塞。當(dāng)數(shù)據(jù)準(zhǔn)備好時(shí),進(jìn)程會(huì)收到一個(gè)SIGIO信號(hào),可以在信號(hào)處理函數(shù)中調(diào)用 I/O 操作函數(shù)處理數(shù)據(jù)。
- 除了I/O復(fù)用方式通知I/O事件,還可以通過(guò)SIGIO信號(hào)來(lái)通知I/O事件。兩者不同的是,在等待數(shù)據(jù)達(dá)到期間,I/O復(fù)用是會(huì)阻塞應(yīng)用程序,而SIGIO方式是不會(huì)阻塞應(yīng)用程序的。
- Unix 常見(jiàn)的五種IO模型之一:同步阻塞式IO模型BIO(blocking IO model):在linux中,默認(rèn)情況下所有的IO操作都是blocking
- 無(wú)論是阻塞 I/O、非阻塞 I/O,還是基于非阻塞 I/O 的多路復(fù)用以及信號(hào)驅(qū)動(dòng)都是同步調(diào)用。因?yàn)?它們?cè)?read 調(diào)用時(shí),內(nèi)核將數(shù)據(jù)從內(nèi)核空間拷貝到應(yīng)用程序空間,過(guò)程都是需要等待的,也就是說(shuō)這個(gè)過(guò)程是同步的,如果內(nèi)核實(shí)現(xiàn)的拷貝效率不高,read 調(diào)用就會(huì)在這個(gè)同步過(guò)程中等待比較長(zhǎng)的時(shí)間
-
零拷貝技術(shù):上面咱們看到了 應(yīng)用進(jìn)程的一次完整的讀寫操作,都需要在用戶空間與內(nèi)核空間中來(lái)回拷貝,并且每一次拷貝,都需要 CPU 進(jìn)行一次上下文切換(由用戶進(jìn)程切換到系統(tǒng)內(nèi)核,或由系統(tǒng)內(nèi)核切換到用戶進(jìn)程),這樣是不是很浪費(fèi) CPU 和性能呢?那有沒(méi)有什么方式,可以減少進(jìn)程間的數(shù)據(jù)拷貝,提高數(shù)據(jù)傳輸?shù)男誓?/strong>?========零拷貝技術(shù)【零拷貝是指計(jì)算機(jī)執(zhí)行IO操作時(shí),CPU不需要將數(shù)據(jù)從一個(gè)存儲(chǔ)區(qū)域復(fù)制到另一個(gè)存儲(chǔ)區(qū)域,從而可以減少上下文切換以及CPU的拷貝時(shí)間。它是一種I/O操作優(yōu)化技術(shù)。取消用戶空間與內(nèi)核空間之間的數(shù)據(jù)拷貝操作,應(yīng)用進(jìn)程每一次的讀寫操作,都可以通過(guò)一種方式,讓應(yīng)用進(jìn)程直接向用戶空間寫入或者讀取數(shù)據(jù),(效果就如同直接向內(nèi)核空間寫入或者讀取數(shù)據(jù)一樣,然后再通過(guò) DMA 將內(nèi)核中的數(shù)據(jù)拷貝到網(wǎng)卡,或?qū)⒕W(wǎng)卡中的數(shù)據(jù) copy 到內(nèi)核)】
- 傳統(tǒng)IO的讀寫流程,包括了4次上下文切換(4次用戶態(tài)和內(nèi)核態(tài)的切換),4次數(shù)據(jù)拷貝(兩次CPU拷貝以及兩次的DMA拷貝)。零拷貝只是減少了用戶態(tài)/內(nèi)核態(tài)的切換次數(shù)以及CPU拷貝的次數(shù)
- 升級(jí)到epoll后,為了溝通有沒(méi)有數(shù)據(jù)還是得用戶態(tài)內(nèi)核態(tài)把fd相關(guān)數(shù)據(jù)拷來(lái)拷去,所以為了減少拷貝的次數(shù),并且用mmap這個(gè)系統(tǒng)調(diào)用實(shí)現(xiàn)了一個(gè)用戶態(tài)和內(nèi)核態(tài)共享的空間,
- 是不是用戶空間與內(nèi)核空間都將數(shù)據(jù)寫到一個(gè)地方,就不需要拷貝了?此時(shí)有沒(méi)有想到虛擬內(nèi)存?零拷貝有兩種解決方式,分別是 mmap+write 方式和 sendfile 方式,mmap+write 方式的核心原理就是通過(guò)虛擬內(nèi)存來(lái)解決的
- 實(shí)現(xiàn)零拷貝的兩種方式:
- mmap + write:
- mmap就是用了虛擬內(nèi)存這個(gè)特點(diǎn),mmap將內(nèi)核中的讀緩沖區(qū)與用戶空間的緩沖區(qū)進(jìn)行映射,以減少數(shù)據(jù)拷貝次數(shù)!
- mmap+write實(shí)現(xiàn)的零拷貝,I/O發(fā)生了4次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝(包括了2次DMA拷貝和1次CPU拷貝)。
- mmap+write實(shí)現(xiàn)的零拷貝,I/O發(fā)生了4次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝(包括了2次DMA拷貝和1次CPU拷貝)。
- read() 系統(tǒng)調(diào)用的過(guò)程中會(huì)把內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到用戶的緩沖區(qū)里,于是為了減少這一步開(kāi)銷,我們可以用 mmap() 替換 read() 系統(tǒng)調(diào)用函數(shù)。mmap() 系統(tǒng)調(diào)用函數(shù)會(huì)直接把內(nèi)核緩沖區(qū)里的數(shù)據(jù)映射到用戶空間,這樣,操作系統(tǒng)內(nèi)核與用戶空間就不需要再進(jìn)行任何的數(shù)據(jù)拷貝操作。
- mmap就是用了虛擬內(nèi)存這個(gè)特點(diǎn),mmap將內(nèi)核中的讀緩沖區(qū)與用戶空間的緩沖區(qū)進(jìn)行映射,以減少數(shù)據(jù)拷貝次數(shù)!
- sendfile
- 在 Linux 內(nèi)核版本 2.1 中,提供了一個(gè)專門發(fā)送文件的系統(tǒng)調(diào)用函數(shù) sendfile():ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);它的前兩個(gè)參數(shù)分別是目的端和源端的文件描述符,后面兩個(gè)參數(shù)是源端的偏移量和復(fù)制數(shù)據(jù)的長(zhǎng)度,返回值是實(shí)際復(fù)制數(shù)據(jù)的長(zhǎng)度。首先,sendfile可以替代前面的 read() 和 write() 這兩個(gè)系統(tǒng)調(diào)用,這樣就可以減少一次系統(tǒng)調(diào)用,也就減少了 2 次上下文切換的開(kāi)銷。其次,該sendfile系統(tǒng)調(diào)用和mmap一樣,可以直接把內(nèi)核緩沖區(qū)里的數(shù)據(jù)拷貝到 socket 緩沖區(qū)里,不再拷貝到用戶態(tài),這樣就只有 2 次上下文切換,和 3 次數(shù)據(jù)拷貝。
- sendfile實(shí)現(xiàn)的零拷貝,I/O發(fā)生了2次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝。其中3次數(shù)據(jù)拷貝中和mmap+write一樣,包括了2次DMA拷貝和1次CPU拷貝。
- 帶有DMA收集拷貝功能的sendfile:但是這還不是真正的零拷貝技術(shù),如果網(wǎng)卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技術(shù)(和普通的 DMA 有所不同),我們可以進(jìn)一步減少通過(guò) CPU 把內(nèi)核緩沖區(qū)里的數(shù)據(jù)拷貝到 socket 緩沖區(qū)的過(guò)程。【linux 2.4版本之后,對(duì)sendfile做了優(yōu)化升級(jí),引入SG-DMA技術(shù),SG-DMA技術(shù)其實(shí)就是對(duì)DMA拷貝加入了scatter/gather操作,它可以直接從內(nèi)核空間緩沖區(qū)中將數(shù)據(jù)讀取到網(wǎng)卡。使用這個(gè)特點(diǎn)搞零拷貝,即還可以多省去一次CPU拷貝。】
- sendfile+DMA scatter/gather實(shí)現(xiàn)的零拷貝,I/O發(fā)生了2次用戶空間與內(nèi)核空間的上下文切換,以及2次數(shù)據(jù)拷貝。其中2次數(shù)據(jù)拷貝都是包DMA拷貝。這就是真正的 零拷貝(Zero-copy) 技術(shù),全程都沒(méi)有通過(guò)CPU來(lái)搬運(yùn)數(shù)據(jù),所有的數(shù)據(jù)都是通過(guò)DMA來(lái)進(jìn)行傳輸?shù)?/strong>。
- 從 Linux 內(nèi)核 2.4 版本開(kāi)始起,對(duì)于支持網(wǎng)卡支持 SG-DMA 技術(shù)的情況下, sendfile() 系統(tǒng)調(diào)用的過(guò)程發(fā)生了點(diǎn)變化:采用了零拷貝
- sendfile+DMA scatter/gather實(shí)現(xiàn)的零拷貝,I/O發(fā)生了2次用戶空間與內(nèi)核空間的上下文切換,以及2次數(shù)據(jù)拷貝。其中2次數(shù)據(jù)拷貝都是包DMA拷貝。這就是真正的 零拷貝(Zero-copy) 技術(shù),全程都沒(méi)有通過(guò)CPU來(lái)搬運(yùn)數(shù)據(jù),所有的數(shù)據(jù)都是通過(guò)DMA來(lái)進(jìn)行傳輸?shù)?/strong>。
- sendfile實(shí)現(xiàn)的零拷貝,I/O發(fā)生了2次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝。其中3次數(shù)據(jù)拷貝中和mmap+write一樣,包括了2次DMA拷貝和1次CPU拷貝。
- Kafka 這個(gè)開(kāi)源項(xiàng)目,就利用了零拷貝技術(shù),從而大幅提升了 I/O 的吞吐率,這也是 Kafka 在處理海量數(shù)據(jù)為什么這么快的原因之一,它調(diào)用了 Java NIO 庫(kù)里的 transferTo 方法。如果 Linux 系統(tǒng)支持 sendfile() 系統(tǒng)調(diào)用,那么 transferTo() 實(shí)際上最后就會(huì)使用到 sendfile() 系統(tǒng)調(diào)用函數(shù)。
- Nginx 也支持零拷貝技術(shù),一般默認(rèn)是開(kāi)啟零拷貝技術(shù),這樣有利于提高文件傳輸?shù)男?/li>
- 在 Linux 內(nèi)核版本 2.1 中,提供了一個(gè)專門發(fā)送文件的系統(tǒng)調(diào)用函數(shù) sendfile():ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);它的前兩個(gè)參數(shù)分別是目的端和源端的文件描述符,后面兩個(gè)參數(shù)是源端的偏移量和復(fù)制數(shù)據(jù)的長(zhǎng)度,返回值是實(shí)際復(fù)制數(shù)據(jù)的長(zhǎng)度。首先,sendfile可以替代前面的 read() 和 write() 這兩個(gè)系統(tǒng)調(diào)用,這樣就可以減少一次系統(tǒng)調(diào)用,也就減少了 2 次上下文切換的開(kāi)銷。其次,該sendfile系統(tǒng)調(diào)用和mmap一樣,可以直接把內(nèi)核緩沖區(qū)里的數(shù)據(jù)拷貝到 socket 緩沖區(qū)里,不再拷貝到用戶態(tài),這樣就只有 2 次上下文切換,和 3 次數(shù)據(jù)拷貝。
- mmap + write:
- 零拷貝更細(xì)致的理論點(diǎn)這里,然后ctrl+F,搜零拷貝
- 這個(gè)零拷貝是操作系統(tǒng)層面上的零拷貝,主要目標(biāo)是避免用戶空間與內(nèi)核空間之間的數(shù)據(jù)拷貝操作,可以提升 CPU 的利用率。
- Netty 相關(guān)的零拷貝技術(shù)
- RPC 框架在網(wǎng)絡(luò)通信框架的選型上,我們最優(yōu)的選擇是基于 Reactor 模式實(shí)現(xiàn)的框架,如 Java 語(yǔ)言,首選的便是 Netty 框架。
- Netty 的零拷貝則不大一樣,他完全站在了用戶空間上,也就是 JVM 上,Netty 的零拷貝主要是偏向于數(shù)據(jù)操作的優(yōu)化上。
- 在傳輸過(guò)程中,RPC 并不會(huì)把請(qǐng)求參數(shù)的所有二進(jìn)制數(shù)據(jù)整體一下子發(fā)送到對(duì)端機(jī)器上,中間可能會(huì)拆分成好幾個(gè)數(shù)據(jù)包,也可能會(huì)合并其他請(qǐng)求的數(shù)據(jù)包,所以消息都需要有邊界。那么一端的機(jī)器收到消息之后,就需要對(duì)數(shù)據(jù)包進(jìn)行處理,根據(jù)邊界對(duì)數(shù)據(jù)包進(jìn)行分割和合并,最終獲得一條完整的消息。收到消息后,對(duì)數(shù)據(jù)包的分割和合并是在用戶空間【因?yàn)閷?duì)數(shù)據(jù)包的處理工作都是由應(yīng)用程序來(lái)處理的】。這里也是會(huì)存在拷貝操作的,但是 不是在用戶空間與內(nèi)核空間之間的拷貝,是用戶空間內(nèi)部?jī)?nèi)存中的拷貝處理操作。Netty 的零拷貝就是為了解決這個(gè)問(wèn)題,在用戶空間對(duì)數(shù)據(jù)操作進(jìn)行優(yōu)化
- Netty 的零拷貝則不大一樣,他完全站在了用戶空間上,也就是 JVM 上,Netty 的零拷貝主要是偏向于數(shù)據(jù)操作的優(yōu)化上。
- Netty 是怎么對(duì)數(shù)據(jù)操作進(jìn)行優(yōu)化的呢?
- Netty 提供了 CompositeByteBuf 類,它可以 將多個(gè) ByteBuf 合并為一個(gè)邏輯上的 ByteBuf,避免了各個(gè) ByteBuf 之間的拷貝【ByteBuf 支持 slice 操作,因此可以將 ByteBuf 分解為多個(gè)共享同一個(gè)存儲(chǔ)區(qū)域的 ByteBuf,避免了內(nèi)存的拷貝】。
- 通過(guò) wrap 操作,我們可以將 byte[] 數(shù)組、ByteBuf、ByteBuffer 等包裝成一個(gè) Netty ByteBuf 對(duì)象, 進(jìn)而避免拷貝操作。
- Netty 框架中很多內(nèi)部的 ChannelHandler 實(shí)現(xiàn)類,都是通過(guò) CompositeByteBuf、slice、wrap 操作來(lái)處理 TCP 傳輸中的拆包與粘包問(wèn)題的。etty 的 ByteBuffer 可以采用 Direct Buffers,使用堆外直接內(nèi)存進(jìn)行 Socket 的讀寫操作,最終的效果與虛擬內(nèi)存所實(shí)現(xiàn)的效果是一樣的。Netty 還提供 FileRegion 中包裝 NIO 的 FileChannel.transferTo() 方法實(shí)現(xiàn)了零拷貝,這與 Linux 中的 sendfile 方式在原理上也是一樣的。
- 傳統(tǒng)IO的讀寫流程,包括了4次上下文切換(4次用戶態(tài)和內(nèi)核態(tài)的切換),4次數(shù)據(jù)拷貝(兩次CPU拷貝以及兩次的DMA拷貝)。零拷貝只是減少了用戶態(tài)/內(nèi)核態(tài)的切換次數(shù)以及CPU拷貝的次數(shù)
-
除了上面基本的I/O模型之外,還有如下集中模型:
- 事件處理模型:Reactor 和Proactor兩種事件處理模型
- 網(wǎng)絡(luò)設(shè)計(jì)模式中,如何處理各種I/O事件是其非常重要的一部分,Reactor 和Proactor兩種事件處理模型應(yīng)運(yùn)而生。上面提到將I/O分為同步I/O 和 異步I/O,可以使用同步I/O實(shí)現(xiàn)Reactor模型,使用異步I/O實(shí)現(xiàn)Proactor模型。
- Reactor事件處理模型:Reactor模型是同步I/O事件處理的一種常見(jiàn)模型
- 一個(gè)典型的Reactor模型類圖結(jié)構(gòu)
- Reactor的核心思想:將關(guān)注的I/O事件注冊(cè)到多路復(fù)用器上,一旦有I/O事件觸發(fā),將事件分發(fā)到事件處理器中、執(zhí)行就緒I/O事件對(duì)應(yīng)的處理函數(shù)中。模型中有三個(gè)重要的組件:
- 多路復(fù)用器:由操作系統(tǒng)提供接口,Linux提供的I/O復(fù)用接口有select、poll、epoll;
- 事件分離器:將多路復(fù)用器返回的就緒事件分發(fā)到事件處理器中;
- 事件處理器:處理就緒事件處理函數(shù)
- Reactor模型工作的簡(jiǎn)化流程:
- 一個(gè)典型的Reactor模型類圖結(jié)構(gòu)
- Proactor事件處理模型:
- 與Reactor不同的是,Proactor使用異步I/O系統(tǒng)接口將I/O操作托管給操作系統(tǒng),Proactor模型中分發(fā)處理異步I/O完成事件,并調(diào)用相應(yīng)的事件處理接口來(lái)處理業(yè)務(wù)邏輯
- Proactor類結(jié)構(gòu):
- Proactor模型的簡(jiǎn)化的工作流程:
- 同步I/O模擬Proactor:
- 并發(fā)模式:多線程、多進(jìn)程的編程的模式【多進(jìn)程/線程模型】
- 在I/O密集型的程序,采用并發(fā)方式可以提高CPU的使用率,可采用多進(jìn)程和多線程兩種方式實(shí)現(xiàn)并發(fā)。當(dāng)前有高效的兩種并發(fā)模式,半同步/半異步模式、Follower/Leader模式。
- 多進(jìn)程/多線程模型:
- 通過(guò)這種方式確實(shí)解決了單進(jìn)程 server 阻塞無(wú)法處理其他 client 請(qǐng)求的問(wèn)題,但眾所周知 fork 創(chuàng)建子進(jìn)程是非常耗時(shí)的,包括頁(yè)表的復(fù)制,進(jìn)程切換時(shí)頁(yè)表的切換等都非常耗時(shí),每來(lái)一個(gè)請(qǐng)求就創(chuàng)建一個(gè)進(jìn)程顯然是無(wú)法接受的。為了節(jié)省進(jìn)程創(chuàng)建的開(kāi)銷,于是有人提出把多進(jìn)程改成多線程,創(chuàng)建線程(使用 pthread_create)的開(kāi)銷確實(shí)小了很多,但同樣的,線程與進(jìn)程一樣,都需要占用堆棧等資源,而且碰到阻塞,喚醒等都涉及到用戶態(tài),內(nèi)核態(tài)的切換,這些都極大地消耗了性能
- 多進(jìn)程/多線程模型:
- 半同步/半異步模式:
- 并發(fā)模式中的“同步”、“異步”與 I/O模型中的“同步”、“異步”是兩個(gè)不同的概念:
- 并發(fā)模式中,“同步”指程序按照代碼順序執(zhí)行,“異步”指程序依賴事件驅(qū)動(dòng),如圖12 所示并發(fā)模式的“同步”執(zhí)行和“異步”執(zhí)行的讀操作;
- 同步讀操作示意圖
- 異步讀操作示意圖
- 同步讀操作示意圖
- I/O模型中,“同步”、“異步”用來(lái)區(qū)分I/O操作的方式,是主動(dòng)通過(guò)I/O操作拿到結(jié)果,還是由內(nèi)核異步的返回操作結(jié)果。
- 并發(fā)模式中,“同步”指程序按照代碼順序執(zhí)行,“異步”指程序依賴事件驅(qū)動(dòng),如圖12 所示并發(fā)模式的“同步”執(zhí)行和“異步”執(zhí)行的讀操作;
- 半同步/半異步工作流程
- 半同步/半反應(yīng)堆模式
- 考慮將兩種事件處理模型,即Reactor和Proactor,與幾種I/O模型結(jié)合在一起,那么半同步/半異步模式就演變?yōu)?strong>半同步/半反應(yīng)堆模式。
- 使用Reactor的方式:
- 工作流程:
- 工作流程:
- 將Reactor替換為Proactor
- 工作流程:
- 工作流程:
- 半同步/半反應(yīng)堆模式有明顯的缺點(diǎn):
- 半同步/半反應(yīng)堆模式的演變模式:
- 考慮將兩種事件處理模型,即Reactor和Proactor,與幾種I/O模型結(jié)合在一起,那么半同步/半異步模式就演變?yōu)?strong>半同步/半反應(yīng)堆模式。
- 并發(fā)模式中的“同步”、“異步”與 I/O模型中的“同步”、“異步”是兩個(gè)不同的概念:
- Follower/Leader模式
- Follower/Leader是多個(gè)工作線程輪流進(jìn)行事件監(jiān)聽(tīng)、事件分發(fā)、處理事件的模式。在Follower/Leader模式工作的任何一個(gè)時(shí)間點(diǎn),只有一個(gè)工作線程處理成為L(zhǎng)eader ,負(fù)責(zé)I/O事件監(jiān)聽(tīng),而其他線程都是Follower,并等待成為L(zhǎng)eader。
- Leader/Follow模式的工作線程的三種狀態(tài)的轉(zhuǎn)移關(guān)系
- Follower/Leader是多個(gè)工作線程輪流進(jìn)行事件監(jiān)聽(tīng)、事件分發(fā)、處理事件的模式。在Follower/Leader模式工作的任何一個(gè)時(shí)間點(diǎn),只有一個(gè)工作線程處理成為L(zhǎng)eader ,負(fù)責(zé)I/O事件監(jiān)聽(tīng),而其他線程都是Follower,并等待成為L(zhǎng)eader。
- 在I/O密集型的程序,采用并發(fā)方式可以提高CPU的使用率,可采用多進(jìn)程和多線程兩種方式實(shí)現(xiàn)并發(fā)。當(dāng)前有高效的兩種并發(fā)模式,半同步/半異步模式、Follower/Leader模式。
- Swoole異步網(wǎng)絡(luò)模型分析
- 事件處理模型:Reactor 和Proactor兩種事件處理模型
巨人的肩膀:
Linux網(wǎng)絡(luò)編程
B站OS課程各位老師
操作系統(tǒng)概論
https://xiaolincoding.com/
很好的一篇文章,既講了Java 中 IO 相關(guān)的理論知識(shí),并通過(guò)多個(gè)代碼案例加深了理解。很贊
https://learn.lianglianglee.com/%E6%96%87%E7%AB%A0/Java%20NIO%E6%B5%85%E6%9E%90.md
Javadoop
javaGuide
CS-Note
清華大學(xué)OS課
在 Windows 操作系統(tǒng)中,提供了一個(gè)叫做 I/O Completion Ports 的方案,通常簡(jiǎn)稱為 IOCP,操作系統(tǒng)負(fù)責(zé)管理線程池,其性能非常優(yōu)異,所以在 Windows 中 JDK 直接采用了 IOCP 的支持,使用系統(tǒng)支持,把更多的操作信息暴露給操作系統(tǒng),也使得操作系統(tǒng)能夠?qū)ξ覀兊?IO 進(jìn)行一定程度的優(yōu)化。
程序員田螺老師的零拷貝詳解
總結(jié)
以上是生活随笔為你收集整理的java基础巩固-宇宙第一AiYWM:为了维持生计,四大基础之OS_Part_2整起~IO们那些事【包括五种IO模型:(BIO、NIO、IO多路复用、信号驱动、AIO);零拷贝、事件处理及并发等模型】的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Python 编程笔记(本人出品,必属精
- 下一篇: halcon相机标定助手_使用Halco