【系統軟件工程師面試】2. 網絡部分

網絡部分

一、tcp/udp區別

二、tcp 三次握手/ connect/ accept 關係, read返回0

三、select/ epoll

ET/LThtml

在一個非阻塞的socket上調用read/write函數, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK)
從字面上看, 意思是:EAGAIN: 再試一次,EWOULDBLOCK: 若是這是一個阻塞socket, 操做將被block,perror輸出: Resource temporarily unavailablejava

總結:
這個錯誤表示資源暫時不夠,能read時,讀緩衝區沒有數據,或者write時,寫緩衝區滿了。遇到這種狀況,若是是阻塞socket,read/write就要阻塞掉。而若是是非阻塞socket,read/write當即返回-1, 同時errno設置爲EAGAIN。
因此,對於阻塞socket,read/write返回-1表明網絡出錯了。但對於非阻塞socket,read/write返回-1不必定網絡真的出錯了。多是Resource temporarily unavailable。這時你應該再試,直到Resource available。linux

綜上,對於non-blocking的socket,正確的讀寫操做爲:
讀:忽略掉errno = EAGAIN的錯誤,下次繼續讀
寫:忽略掉errno = EAGAIN的錯誤,下次繼續寫程序員

對於select和epoll的LT模式,這種讀寫方式是沒有問題的。但對於epoll的ET模式,這種方式還有漏洞。web

 

epoll的兩種模式LT和ET
兩者的差別在於level-trigger模式下只要某個socket處於readable/writable狀態,不管何時進行epoll_wait都會返回該socket;而edge-trigger模式下只有某個socket從unreadable變爲readable或從unwritable變爲writable時,epoll_wait纔會返回該socket。面試

因此,在epoll的ET模式下,正確的讀寫方式爲:
讀:只要可讀,就一直讀,直到返回0,或者 errno = EAGAIN
寫:只要可寫,就一直寫,直到數據發送完,或者 errno = EAGAIN算法

正確的讀apache

= 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {
    n += nread;
}
if (nread == -1 && errno != EAGAIN) {
    perror("read error");
}

 

正確的寫編程

int nwrite, data_size = strlen(buf);
= data_size;
while (> 0) {
    nwrite = write(fd, buf + data_size - n, n);
    if (nwrite < n) {
        if (nwrite == -1 && errno != EAGAIN) {
            perror("write error");
        }
        break;
    }
    n -= nwrite;
}

 

正確的accept,accept 要考慮 2 個問題
(1) 阻塞模式 accept 存在的問題
考慮這種狀況:TCP鏈接被客戶端夭折,即在服務器調用accept以前,客戶端主動發送RST終止鏈接,致使剛剛創建的鏈接從就緒隊列中移出,若是套接口被設置成阻塞模式,服務器就會一直阻塞在accept調用上,直到其餘某個客戶創建一個新的鏈接爲止。可是在此期間,服務器單純地阻塞在accept調用上,就緒隊列中的其餘描述符都得不處處理。緩存

解決辦法是把監聽套接口設置爲非阻塞,當客戶在服務器調用accept以前停止某個鏈接時,accept調用能夠當即返回-1,這時源自Berkeley的實現會在內核中處理該事件,並不會將該事件通知給epool,而其餘實現把errno設置爲ECONNABORTED或者EPROTO錯誤,咱們應該忽略這兩個錯誤。

(2)ET模式下accept存在的問題
考慮這種狀況:多個鏈接同時到達,服務器的TCP就緒隊列瞬間積累多個就緒鏈接,因爲是邊緣觸發模式,epoll只會通知一次,accept只處理一個鏈接,致使TCP就緒隊列中剩下的鏈接都得不處處理。

解決辦法是用while循環抱住accept調用,處理完TCP就緒隊列中的全部鏈接後再退出循環。如何知道是否處理完就緒隊列中的全部鏈接呢?accept返回-1而且errno設置爲EAGAIN就表示全部鏈接都處理完。

綜合以上兩種狀況,服務器應該使用非阻塞地accept,accept在ET模式下的正確使用方式爲:

while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) {
    handle_client(conn_sock);
}
if (conn_sock == -1) {
    if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)
    perror("accept");
}

 

一道騰訊後臺開發的面試題
使用Linuxepoll模型,水平觸發模式;當socket可寫時,會不停的觸發socket可寫的事件,如何處理?

第一種最廣泛的方式:
須要向socket寫數據的時候才把socket加入epoll,等待可寫事件。
接受到可寫事件後,調用write或者send發送數據。
當全部數據都寫完後,把socket移出epoll。

這種方式的缺點是,即便發送不多的數據,也要把socket加入epoll,寫完後在移出epoll,有必定操做代價。

一種改進的方式:
開始不把socket加入epoll,須要向socket寫數據的時候,直接調用write或者send發送數據。若是返回EAGAIN,把socket加入epoll,在epoll的驅動下寫數據,所有數據發送完畢後,再移出epoll。

這種方式的優勢是:數據很少的時候能夠避免epoll的事件處理,提升效率。

四、timeout wait過多, 2MSL

  1. netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'    

它會顯示例以下面的信息:

TIME_WAIT 814
CLOSE_WAIT 1
FIN_WAIT1 1
ESTABLISHED 634
SYN_RECV 2
LAST_ACK 1

經常使用的三個狀態是:ESTABLISHED 表示正在通訊,TIME_WAIT 表示主動關閉,CLOSE_WAIT 表示被動關閉。

若是服務器出了異常,百分之八九十都是下面兩種狀況:

1.服務器保持了大量TIME_WAIT狀態

2.服務器保持了大量CLOSE_WAIT狀態

由於linux分配給一個用戶的文件句柄是有限的(能夠參考:http://blog.csdn.net/shootyou/article/details/6579139),而TIME_WAIT和CLOSE_WAIT兩種狀態若是一直被保持,那麼意味着對應數目的通道就一直被佔着,並且是「佔着茅坑不使勁」,一旦達到句柄數上限,新的請求就沒法被處理了,接着就是大量Too Many Open Files異常,

 

1.服務器保持了大量TIME_WAIT狀態

這種狀況比較常見,一些爬蟲服務器或者WEB服務器(若是網管在安裝的時候沒有作內核參數優化的話)上常常會遇到這個問題,這個問題是怎麼產生的呢?

從 上面的示意圖能夠看得出來,TIME_WAIT是主動關閉鏈接的一方保持的狀態,對於爬蟲服務器來講他自己就是「客戶端」,在完成一個爬取任務以後,他就 會發起主動關閉鏈接,從而進入TIME_WAIT的狀態,而後在保持這個狀態2MSL(max segment lifetime)時間以後,完全關閉回收資源。爲何要這麼作?明明就已經主動關閉鏈接了爲啥還要保持資源一段時間呢?這個是TCP/IP的設計者規定 的,主要出於如下兩個方面的考慮:

1.防止上一次鏈接中的包,迷路後從新出現,影響新鏈接(通過2MSL,上一次鏈接中全部的重複包都會消失)
2. 可靠的關閉TCP鏈接。在主動關閉方發送的最後一個 ack(fin) ,有可能丟失,這時被動方會從新發fin, 若是這時主動方處於 CLOSED 狀態 ,就會響應 rst 而不是 ack。因此主動方要處於 TIME_WAIT 狀態,而不能是 CLOSED 。另外這麼設計TIME_WAIT 會定時的回收資源,並不會佔用很大資源的,除非短期內接受大量請求或者受到攻擊。

關於MSL引用下面一段話:

[plain]  view plain copy print ?
  1. MSL 為 一個 TCP Segment (某一塊 TCP 網路封包) 從來源送到目的之間可續存的時間 (也就是一個網路封包在網路上傳輸時能存活的時間),由 於 RFC 793 TCP 傳輸協定是在 1981 年定義的,當時的網路速度不像現在的網際網路那樣發達,你能夠想像你從瀏覽器輸入網址等到第一 個 byte 出現要等 4 分鐘嗎?在現在的網路環境下幾乎不可能有這種事情發生,所以我們大可將 TIME_WAIT 狀態的續存時間大幅調低,好 讓 連線埠 (Ports) 能更快空出來給其餘連線使用。  

再引用網絡資源的一段話:

[plain]  view plain copy print ?
  1. 值 得一說的是,對於基於TCP的HTTP協議,關閉TCP鏈接的是Server端,這樣,Server端會進入TIME_WAIT狀態,可 想而知,對於訪 問量大的Web Server,會存在大量的TIME_WAIT狀態,假如server一秒鐘接收1000個請求,那麼就會積壓 240*1000=240,000個 TIME_WAIT的記錄,維護這些狀態給Server帶來負擔。固然現代操做系統都會用快速的查找算法來管理這些 TIME_WAIT,因此對於新的 TCP鏈接請求,判斷是否hit中一個TIME_WAIT不會太費時間,可是有這麼多狀態要維護老是很差。  
  2. HTTP協議1.1版規定default行爲是Keep-Alive,也就是會重用TCP鏈接傳輸多個 request/response,一個主要緣由就是發現了這個問題。  

也就是說HTTP的交互跟上面畫的那個圖是不同的,關閉鏈接的不是客戶端,而是服務器,因此web服務器也是會出現大量的TIME_WAIT的狀況的。
 
如今來講如何來解決這個問題。
 
解決思路很簡單,就是讓服務器可以快速回收和重用那些TIME_WAIT的資源。
 
下面來看一下咱們網管對/etc/sysctl.conf文件的修改:
[plain]  view plain copy print ?
  1. #對於一個新建鏈接,內核要發送多少個 SYN 鏈接請求才決定放棄,不該該大於255,默認值是5,對應於180秒左右時間   
  2. net.ipv4.tcp_syn_retries=2  
  3. #net.ipv4.tcp_synack_retries=2  
  4. #表示當keepalive起用的時候,TCP發送keepalive消息的頻度。缺省是2小時,改成300秒  
  5. net.ipv4.tcp_keepalive_time=1200  
  6. net.ipv4.tcp_orphan_retries=3  
  7. #表示若是套接字由本端要求關閉,這個參數決定了它保持在FIN-WAIT-2狀態的時間  
  8. net.ipv4.tcp_fin_timeout=30    
  9. #表示SYN隊列的長度,默認爲1024,加大隊列長度爲8192,能夠容納更多等待鏈接的網絡鏈接數。  
  10. net.ipv4.tcp_max_syn_backlog = 4096  
  11. #表示開啓SYN Cookies。當出現SYN等待隊列溢出時,啓用cookies來處理,可防範少許SYN攻擊,默認爲0,表示關閉  
  12. net.ipv4.tcp_syncookies = 1  
  13.   
  14. #表示開啓重用。容許將TIME-WAIT sockets從新用於新的TCP鏈接,默認爲0,表示關閉  
  15. net.ipv4.tcp_tw_reuse = 1  
  16. #表示開啓TCP鏈接中TIME-WAIT sockets的快速回收,默認爲0,表示關閉  
  17. net.ipv4.tcp_tw_recycle = 1  
  18.   
  19. ##減小超時前的探測次數   
  20. net.ipv4.tcp_keepalive_probes=5   
  21. ##優化網絡設備接收隊列   
  22. net.core.netdev_max_backlog=3000   
[plain]  view plain copy print ?
  1.   
修改完以後執行/sbin/sysctl -p讓參數生效。
 
這裏頭主要注意到的是net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_recycle 
net.ipv4.tcp_fin_timeout 
net.ipv4.tcp_keepalive_*
這幾個參數。
 
net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle的開啓都是爲了回收處於TIME_WAIT狀態的資源。
net.ipv4.tcp_fin_timeout這個時間能夠減小在異常狀況下服務器從FIN-WAIT-2轉到TIME_WAIT的時間。
net.ipv4.tcp_keepalive_*一系列參數,是用來設置服務器檢測鏈接存活的相關配置。
 
2.服務器保持了大量CLOSE_WAIT狀態
休息一下,喘口氣,一開始只是打算說說TIME_WAIT和CLOSE_WAIT的區別,沒想到越挖越深,這也是寫博客總結的好處,總能夠有意外的收穫。
 
TIME_WAIT狀態能夠經過優化服務器參數獲得解決,由於發生TIME_WAIT的狀況是服務器本身可控的,要麼就是對方鏈接的異常,要麼就是本身沒有迅速回收資源,總之不是因爲本身程序錯誤致使的。
但 是CLOSE_WAIT就不同了,從上面的圖能夠看出來,若是一直保持在CLOSE_WAIT狀態,那麼只有一種狀況,就是在對方關閉鏈接以後服務器程 序本身沒有進一步發出ack信號。換句話說,就是在對方鏈接關閉以後,程序裏沒有檢測到,或者程序壓根就忘記了這個時候須要關閉鏈接,因而這個資源就一直 被程序佔着。我的以爲這種狀況,經過服務器內核參數也沒辦法解決,服務器對於程序搶佔的資源沒有主動回收的權利,除非終止程序運行。
 
若是你使用的是HttpClient而且你遇到了大量CLOSE_WAIT的狀況,那麼這篇日誌也許對你有用: http://blog.csdn.net/shootyou/article/details/6615051
在那邊日誌裏頭我舉了個場景,來講明CLOSE_WAIT和TIME_WAIT的區別,這裏從新描述一下:
服 務器A是一臺爬蟲服務器,它使用簡單的HttpClient去請求資源服務器B上面的apache獲取文件資源,正常狀況下,若是請求成功,那麼在抓取完 資源後,服務器A會主動發出關閉鏈接的請求,這個時候就是主動關閉鏈接,服務器A的鏈接狀態咱們能夠看到是TIME_WAIT。若是一旦發生異常呢?假設 請求的資源服務器B上並不存在,那麼這個時候就會由服務器B發出關閉鏈接的請求,服務器A就是被動的關閉了鏈接,若是服務器A被動關閉鏈接以後程序員忘了 讓HttpClient釋放鏈接,那就會形成CLOSE_WAIT的狀態了。
 
因此若是將大量CLOSE_WAIT的解決辦法總結爲一句話那就是:查代碼。由於問題出在服務器程序裏頭啊。

五、RST出現緣由

TCP異常終止的常見情形

咱們在實際的工做環境中,致使某一方發送reset報文的情形主要有如下幾種:

1,客戶端嘗試與服務器未對外提供服務的端口創建TCP鏈接,服務器將會直接向客戶端發送reset報文。

 

 

2,客戶端和服務器的某一方在交互的過程當中發生異常(如程序崩潰等),該方系統將向對端發送TCP reset報文,告之對方釋放相關的TCP鏈接,以下圖所示:

 

 

3,接收端收到TCP報文,可是發現該TCP的報文,並不在其已創建的TCP鏈接列表內(好比server機器直接宕機),則其直接向對端發送reset報文,以下圖所示:

 

TCP_NODelay

做者:Pengcheng Zeng
連接:https://www.zhihu.com/question/42308970/answer/123620051
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

參考 tcp(7): TCP protocol
TCP_NODELAY
If set, disable the Nagle algorithm. This means that segments are always sent as soon as possible, even if there is only a small amount of data. When not set, data is buffered until there is a sufficient amount to send out, thereby avoiding the frequent sending of small packets, which results in poor utilization of the network. This option is overridden by TCP_CORK; however, setting this option forces an explicit flush of pending output, even if TCP_CORK is currently set.
TCP/IP協議中針對TCP默認開啓了 Nagle算法。Nagle算法經過減小須要傳輸的數據包,來優化網絡。關於Nagle算法,@ 郭無意 同窗的答案已經說了很多了。在內核實現中,數據包的發送和接受會先作緩存,分別對應於寫緩存和讀緩存。
那麼針對題主的問題,咱們來分析一下。
啓動TCP_NODELAY,就意味着禁用了Nagle算法,容許小包的發送。對於延時敏感型,同時數據傳輸量比較小的應用,開啓TCP_NODELAY選項無疑是一個正確的選擇。好比,對於SSH會話,用戶在遠程敲擊鍵盤發出指令的速度相對於網絡帶寬能力來講,絕對不是在一個量級上的,因此數據傳輸很是少;而又要求用戶的輸入可以及時得到返回,有較低的延時。若是開啓了Nagle算法,就極可能出現頻繁的延時,致使用戶體驗極差。固然,你也能夠選擇在應用層進行buffer,好比使用java中的buffered stream,儘量地將大包寫入到內核的寫緩存進行發送;vectored I/O(writev接口)也是個不錯的選擇。
對於關閉TCP_NODELAY,則是應用了Nagle算法。數據只有在寫緩存中累積到必定量以後,纔會被髮送出去,這樣明顯提升了網絡利用率(實際傳輸數據payload與協議頭的比例大大提升)。可是這由不可避免地增長了延時;與TCP delayed ack這個特性結合,這個問題會更加顯著,延時基本在40ms左右。固然這個問題只有在連續進行兩次寫操做的時候,纔會暴露出來。
咱們看一下摘自Wikipedia的Nagle算法的僞碼實現:
if there is new data to send
  if the window size >= MSS and available data is >= MSS send complete MSS segment now else if there is unconfirmed data still in the pipe enqueue data in the buffer until an acknowledge is received else send data immediately end if end if end if
經過這段僞碼,很容易發現連續兩次寫操做出現問題的緣由。而對於讀-寫-讀-寫這種模式下的操做,關閉TCP_NODELAY並不會有太大問題。
The user-level solution is to avoid write-write-read sequences on sockets. write-read-write-read is fine. write-write-write is fine. But write-write-read is a killer. So, if you can, buffer up your little writes to TCP and send them all at once. Using the standard UNIX I/O package and flushing write before each read usually works.

連續進行屢次對小數據包的寫操做,而後進行讀操做,自己就不是一個好的網絡編程模式;在應用層就應該進行優化。對於既要求低延時,又有大量小數據傳輸,還同時想提升網絡利用率的應用,大概只能用UDP本身在應用層來實現可靠性保證了。好像企鵝家就是這麼幹的。

相關文章
相關標籤/搜索