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
n = 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); |
n = data_size; |
while (n > 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的事件處理,提升效率。
它會顯示例以下面的信息:
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引用下面一段話:
再引用網絡資源的一段話:
五、RST出現緣由
TCP異常終止的常見情形
咱們在實際的工做環境中,致使某一方發送reset報文的情形主要有如下幾種:
1,客戶端嘗試與服務器未對外提供服務的端口創建TCP鏈接,服務器將會直接向客戶端發送reset報文。
2,客戶端和服務器的某一方在交互的過程當中發生異常(如程序崩潰等),該方系統將向對端發送TCP reset報文,告之對方釋放相關的TCP鏈接,以下圖所示:
3,接收端收到TCP報文,可是發現該TCP的報文,並不在其已創建的TCP鏈接列表內(好比server機器直接宕機),則其直接向對端發送reset報文,以下圖所示:
TCP_NODelay
TCP_NODELAYTCP/IP協議中針對TCP默認開啓了 Nagle算法。Nagle算法經過減小須要傳輸的數據包,來優化網絡。關於Nagle算法,@ 郭無意 同窗的答案已經說了很多了。在內核實現中,數據包的發送和接受會先作緩存,分別對應於寫緩存和讀緩存。
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.
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
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本身在應用層來實現可靠性保證了。好像企鵝家就是這麼幹的。