什么是socket?什么是文件描述符?非科班程序員告訴你!
絮叨
隨着互聯網行業的蓬勃發展,市場對於程序員的需求激增,尤其是java程序員,而非科班出身的java程序員也不占少數,我本人也是其中之一。由於對計算機底層了解不深,導致有很多框架底層相關的實現不理解。但是作為一個優秀的java程序員,怎么能容忍這樣的事情發生,有不理解的就要千方百計的搞懂。
這兩天我剛開始學習redis,正當我在了解redis為什么如此快的時候,一個陌生的名詞出現在我眼前,“io多路復用”,於是乎我和大部分人一樣,有問題上就百度一下,可是我發現,要理解io多路復用,就必須理解linux的io模型,理解socket的連接,理解文件描述符等等我從來沒接觸過的知識。到了這一步,有些人可能會想,我只要會用就行了,這么復雜的東西工作也用不到。可是我想說,只是會用代表你只是一個合格的程序員,不代表你是個優秀的程序員。因此我翻遍了各大博客論壇,終於對這些陌生的概念有了一些理解,下面我會從一個非科班程序員的角度(大神請自覺繞道)帶你們了解socket和文件描述符,同時學習一下linux的其他io模型。
一、 什么是socket
在理解什么是socket之前,我們舉一個生活中常見的例子,打電話。我想要給女朋友打電話,首先我要撥通女朋友的電話號碼,經過無線電傳輸,對方手機收到到我打的電話信號並發出電話鈴聲,最后女朋友點擊接聽鍵,一次電話連線就完成了。連線成功后,你可以對女朋友說話,女朋友也可以對你說話。這個過程就和socket連接非常像
如果有計算機A想要和計算機B通過網絡進行通信,那么計算機A中必須要有一個socket,計算機B也要有一個socket,這兩個socket一旦進行連接,計算機A就能向計算機B發送接收數據,計算機B也能向計算機A發送接收數據了。計算機A向計算機B發送數據,就會用到SocketA的OutputStream,計算機A接收計算機B的數據,就會用到Socket的InputStream
二、socket建立連接的過程
首先服務器(server)上有一個socket 綁定了80端口(80端口是為HTTP超文本傳輸協議開放的端口),服務器會一直等待,直到有客戶端(client)向服務端發送了連接請求
client會把自己的ip和端口信息告訴server,這樣server就會在本地開啟一個與client同端口號的端口,並創建一個新的socket [socket也是個文件],保證80端口的socket能夠繼續監聽其他的連接
這樣一對socket就建立完成了,客戶端與服務端就能通過socket進行數據的發送和讀取了
socket中TCP的三次握手建立連接詳解
我們知道tcp建立連接要進行“三次握手”,即交換三個分組。大致流程如下:
- 客戶端向服務器發送一個SYN J
- 服務器向客戶端響應一個SYN K,並對SYN J進行確認ACK J+1
- 客戶端再想服務器發一個確認ACK K+1
只有就完了三次握手,但是這個三次握手發生在socket的那幾個函數中呢?請看下圖:
圖1、socket中發送的TCP三次握手【3次握手是在套接字中發生的】
從圖中可以看出,當客戶端調用connect時,觸發了連接請求,向服務器發送了SYN J包,這時connect進入阻塞狀態;服務器監聽到連接請求,即收到SYN J包,調用accept函數接收請求 向 客戶端發送SYN K ,ACK J+1,這時accept進入阻塞狀態;客戶端收到服務器的SYN K ,ACK J+1之后,這時connect返回,並對SYN K進行確認;服務器收到ACK K+1時,accept返回,至此三次握手完畢,連接建立。
上圖右邊服務器端套接字經歷closed、 listen、 syn-received 和established狀態
Socket 中 TCP 的四次握手釋放連接
上面介紹了 socket 中 TCP 的三次握手建立過程,及其涉及的 socket 函數。現在我們介紹 socket 中的四次握手釋放連接的過程,請看下圖
圖示過程如下:
某個應用進程首先調用 close 主動關閉連接,這時 TCP 發送一個 FIN M;
另一端接收到 FIN M 之后,執行被動關閉,對這個 FIN 進行確認,所以發送一個M+1。它的接收也作為文件結束符傳遞給應用進程,因為 FIN 的接收意味着應用進程在相應的連接上再也接收不到額外數據;
一段時間之后,接收到文件結束符的應用進程調用 close 關閉它的 socket。這導致它的 TCP 也發送一個 FIN N;
接收到這個 FIN 的源發送端 TCP 對它進行確認。
這樣每個方向上都有一個 FIN 和 ACK。
三、TCP/IP和Socket的關系
TCP/IP也叫做傳輸控制協議/網絡協議,它是一套用來連接互聯網上網絡設備的協議。那么什么是協議呢?通俗的來說,協議就好比是交通規則,它規划公路上的汽車司機怎么走,它是一套規范。TCP/IP協議就是一套互聯網間數據傳輸的規范。
說了這么多,那socket在哪里呢?
從圖中可以看出,Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口(API)。我們所說的TCP/IP協議棧是在操作系統內核實現的,而Socket就是操作系統內核提供給應用層的一系列接口,Socket封裝了TCP/IP,要使用TCP/IP來發送數據,就調用Socket的OutputStream,要使用TCP/IP接收數據,就調用Socket的InputStream,現在大家應該對TCP/IP與Socket的關系有所了解了吧
四、Socket的讀寫緩存區
現在計算機A與計算機B建立了Socket連接,這時候計算機A要發送數據給計算機B,是不是直接就發送過去了呢,答案是NO,Socket發送數據首先需要經過Socket的讀寫緩沖區,我們現在來了解一下Socket的讀寫緩沖區
首先我們必須要搞清楚數據發送的流程,用戶態的數據,要想發送到互聯網上,必須先把數據拷貝到內核態,由內核態幫我們把數據發送出去。 因此,計算機每創建一個socket,cpu就會在內存中為它分配一對讀寫緩沖區,讀寫緩沖區在內核態,它的大小不隨數據大小而改變。
計算機A想發數據到計算機B,首先計算機A把用戶態的數據拷貝到內核態的輸出緩沖區,再由把輸出緩沖區的數據通過互聯網發送到計算機B的輸入緩沖區,計算機B把輸入緩沖區的數據拷貝到用戶態,就完成了一次數據的發送和接收。
由於數據緩沖區的大小有限,如果數據緩沖區里有數據沒有發送出去,用戶態這時候又有其他數據要發送,數據緩沖區的空間就不夠用了,就會造成一系列問題。如果計算機B要接收數據,而一直沒有收到計算機A發送過來的數據,導致輸入緩沖區一直為空,也會造成問題。
對於上面這些存在的問題,linux有5中解決方案,這就是linux的5大IO模型。
在了解IO模型之前,我們還需要知道什么是文件描述符
五、什么是文件描述符
文件描述符(file descriptor)是操作系統內核為了高效管理已被打開的文件所創建的索引,用於指代被打開的文件。
在linux操作系統中,每一個進程中都有一個文件描述符表,它是一個指針數組,系統默認初始化了數組的前3位。第0位指向標准的輸入流(一般是鍵盤),第1位指向標准的輸出流(一般是顯示器),第2位指向標准的錯誤流(一般是也顯示器)。
現在如果有一個進程中只打開了一個 hello.txt 文件,那么這個進程的文件描述符表的第3位就是指向這個 hello.txt 的指針。之后如果該進程創建了一個socket,那么這個文件描述符表的第4位就是指向這個socket的指針,因為在linux中一切皆文件,socket也是一個文件。我們所說的文件描述符就是進程中這個數組的下標,因此他也可以說是一個索引。
這是自己總結的:通過上面可以知道,我們的80端口創建一個socket,socket也是文件,那么這個進程的文件描述符表中又加1,那么一直加到1024,應為我們的系統限制一個進程最多只能打開1024個文件。
IO復用
IO復用的歷史和多進程一樣長,Linux很早就提供了select
系統調用,可以在一個進程內維護1024個連接,后來加入poll
系統調用,poll
做了一系列改進后解決了1024個連接的限制問題,可以維持任意數量的連接。但是select
和poll
存在一個問題是,它們需要循環檢測連接是否有事件。這樣問題就來了,如果服務器有100w個連接,在某一時間只有一個連接是向服務器發送了數據,select/poll
就需要做100w次循環,而其中只會有1次命中,剩下99w9999次都是無效的,白白浪費CPU時間片資源。
直到Linux2.6內核開始提供新的epoll
系統調用,可以維持無限數量的連接,而且無需輪詢,這才真正解決了C10K問題。現在各種高並發異步IO的服務器程序都是基於epoll
實現的,如Nginx、Node.js、Erlang、Golnag。像Node.js這樣單進程單線程的程序,都可以維持超過100wTCP連接,這全部都要歸功於epoll
技術。
應為我們的80端口主進程最多只能生成1024個socket[socket其實就是連接],所以使用select模型,不管你生成多少子進程,再多的連接主進程都不能創建socket了,它只會把這1024 socket連接分配給子進程。
還有就是在select模型下,這些socket是在select調用前就有的,然后調用select監控socket,也就是select(socket),此時我們的進程是處於阻塞的,但是select一直在遍歷,當發現某個socket有數據了,此時就通知進程,然后進程就發送系統調用,此時該進程就發送read系統調用來讀數據。這個read也要遍歷這些所有socket才能取得想要數據。
poll本質上和select沒有區別,與select的區別的是他沒有最大連接數的限制,原因是他是基於鏈表來存儲的。如果它一個進程內維護100w個連接,那poll遍歷也需要很多時間。然后它再通知進程有一個socket有消息了,那么這個進程也遍歷100w個socket,此時也需要不少時間。
epoll和poll類似,不限制連接數,但是epoll模型不需要遍歷,當某個socket有數據的時候,直接通知進程,然后進程直接去那個socket中取。這時進程也不用遍歷了。
如果把上面的都理解為單進程單線程,那么其實就是這個線程處理上面的socket內容,每個socket其實就是一個請求,所以io復用能處理多請求
1、Blocking IO
在linux中,默認情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣:
第一步通常涉及等待數據從網絡中到達。當所有等待數據到達時,它被復制到內核中的某個緩沖區。
第二步就是把數據從內核緩沖區復制到應用程序緩沖區。
當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:准備數據。對於network io來說,很多時候數據在一開始還沒有到達(比如,還沒有收到一個完整的UDP包),這個時候kernel就要等待足夠的數據到來。而在用戶進程這邊,整 個進程會被阻塞。當kernel一直等到數據准備好了,它就會將數據從kernel中拷貝到用戶內存,然后kernel返回結果,用戶進程才解除 block的狀態,重新運行起來。
所以,blocking IO的特點就是在IO執行的兩個階段都被block了。
2、非阻塞式I/O
linux下,可以通過設置socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:
從圖中可以看出,當用戶進程發出read操作時,如果kernel中的數據還沒有准備好,那么它並不會block用戶進程,而是立刻返回一個error。 從用戶進程角度講 ,它發起一個read操作后,並不需要等待,而是馬上就得到了一個結果。用戶進程判斷結果是一個error時,它就知道數據還沒有准備好,於是它可以再次 發送read操作。一旦kernel中的數據准備好了,並且又再次收到了用戶進程的system call,那么它馬上就將數據拷貝到了用戶內存,然后返回。
所以,用戶進程第一個階段不是阻塞的,需要不斷的主動詢問kernel數據好了沒有;第二個階段依然總是阻塞的。
IO多路復用
IO multiplexing這個詞可能有點陌生,但是如果我說select,epoll,大概就都能明白了。有些地方也稱這種IO方式為event driven IO。我們都知道,select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。
IO復用同非阻塞IO本質一樣,不過利用了新的select系統調用,由內核來負責本來是請求進程該做的輪詢操作。看似比非阻塞IO還多了一個系統調用開銷,不過因為可以支持多路IO,才算提高了效率。
它的基本原理就是select /epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:
當用戶進程調用了select
,那么整個進程會被block,而同時,kernel會“監視”所有select負責的socket,當任何一個 socket中的數據准備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。
這個圖和blocking IO的圖其實並沒有太大的不同,事實上,還更差一些。因為這里需要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。(多說一句。所以,如果處理的連接數不是很高的話,使用 select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。
select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。比如epoll可以處理無限個連接
在IO multiplexing Model中,實際中,對於每一個socket,一般都設置成為non-blocking,但是,如上圖所示,整個用戶的process其實是一直被 block的。只不過process是被select這個函數block,而不是被socket IO給block。
講下我的理解。
多路復用
這個詞多出現在網絡編程,首先理解多路
.
多路
:有多個客戶端連接,一路就是一個連接
復用
:一個進程或線程處理上面所有的連接。如果不復用又需要同時服務多個客戶端,需要多線程或多進程,這個就不是復用了。