標籤:socket tcp-ip
TCP/IP深入思考
這是我讀Effective TCP/IP Programming的 讀書筆記和思考,以及做的一些實驗。強烈建議後端工程師有空讀一讀這本書,有些細節的確是我們平時沒有注意的,讀了最好自己動手做一些實驗加深理解。
fin的含義
對端發送fin會導致read()返回,但send()照樣可以發送,也就是單純的fin其實相當於shutdown(SHUT_WR)。
當對端應用程式崩潰後(未close),本段一直調用send(),對端會返回RST,此時read()會返回Connection reset by peer。
當對端應用程式崩潰後或close串連,本段調用read()返回0,
調用send()會導致EPIPE(Broken pipe),第二次send()才會出錯。
對端主機崩潰
由於主機崩潰,或不結束應用程式關機,或直接關閉modern,導致對方接受不到fin,rst等,recv()會永久阻塞。解決辦法:
1.開啟keepalive機制,預設情況下(2+9*75/60)小時後逾時
2.recv()逾時機制
3.心跳,其實就是發送send(),一段時間會檢測到串連斷了
SO_LINGER深入理解
如果應用程式執行主動關閉,最後會在TIME_WAIT狀態保持2MSL時間,而SO_LINGER選項能迫使串連立刻關閉,不走正常4步關閉流程。
server
from socket import *s = socket(AF_INET,SOCK_STREAM)s.bind(("localhost",9123))s.listen(10)cc = s.accept()print cc #查看零時連接埠為47128
client
from socket import *c = socket(AF_INET,SOCK_STREAM)c.connect(("localhost",9123))c.close()
由於client主動關閉,這時client應該處於FIN_WAIT2,server對應處於CLOSE_WAIT
[[email protected]10-9-22-239 ~]# netstat -an | grep 47128tcp 1 0 127.0.0.1:9123 127.0.0.1:47128 CLOSE_WAIT tcp 0 0 127.0.0.1:47128 127.0.0.1:9123 FIN_WAIT2
注意netstat要加n參數,不然會根據/etc/services解析成服務名。
當server端再調用cc[0].close()時,此時clinet將處於TIME_WAIT狀態。我們將client的socket加個SO_LINGER選項。
from struct import *c.setsockopt(SOL_SOCKET,SO_LINGER,pack("ii",1,0))
此時我們查看發現沒有對應狀態,也就是未新戶型FIN_WAIT2的狀態,直接關閉,不走正常四步關閉串連流程。
當然強壯的應用伺服器不應該幹擾TIME_WAIT狀態,這是TCP可靠機制的重要組成部分。
SO_LINGER選項還有其它作用,通常當close()一條串連時,即使發送緩衝區仍然有資料要發送,close()也會立即返回。當然TCP還是會嘗試發送未發送出去的資料,但應用程式並不知道是否發送成功,SO_LINGER就是為解決這個問題的。
struct linger{ int l_onoff; // on/off選項 int l_linger; //逗留時間};
1.l_onoff為0,linger不啟用,行為和預設close()相同
2.l_onoff非0,如果l_linger非0,close()在等待資料發送完畢或逗留時間逾時,如果逗留時間到,仍然有未發送資料,close()返回EWOULDBLOCK;如果linger為0,就將丟棄串連,也就是上面我們展示的那樣。
Nagle演算法和延遲ACK
Nagle演算法有助於預防網路中的小報文泛洪,但與延遲ACK互動可能會導致200ms延遲。BSD實現ACK延遲200ms,RFC規定不能大於500ms,我在Linux上做的實驗發現都是40ms延遲ACK。
關閉Nagle演算法
const int on = 1;setsocket(s, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));
//from tornadotry: self.socket.setsockopt(socket.IPPROTO_TCP,socket.TCP_NODELAY, 1)except socket.error as e: # Sometimes setsockopt will fail if the socket is closed # at the wrong time. This can happen with HTTPServer # resetting the value to false between requests. if e.errno not in (errno.EINVAL, errno.ECONNRESET): raise
應對方法:
1.將寫操作合并,可使用writev()
2.多用buff,如go中bufio.Reader,bufio.Writer
非阻塞connect
非阻塞connect()會立即返回EINPROGRESS,如果串連成功通訊端會立刻可寫,那如何檢查是否串連成功呢?
status = connect( sock, ......); // 返回0,成功串連if ( status != 0 && errno != EINPROGRESS) { close(sock);}
可以使用getsockopt檢查是否出錯,當然之前必須等待該socket狀態變化
int error;socklen_t len = sizeof(error); int code = getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len); if (code < 0 || error) { //傳回值小於0或error不等於0說明串連錯誤 close(s); return SOCKET_ERROR;}
用python類比如下:
import socketimport selectimport errnos=socket.socket(socket.AF_INET,socket.SOCK_STREAM)s.setblocking(False)try: s.connect(("www.google.com",80))except socket.error as e: if e.errno == errno.EINPROGRESS: print("INprocess") else: raiser,w,x = select.select([s],[s],[s])err = s.getsockopt(socket.SOL_SOCKET,socket.SO_ERROR)if err != 0: print("error",errno.errorcode[err]) s.close()else: print("connect ok")
很明顯,不翻牆www.google.com暫時串連不了,出錯資訊
INprocess(‘error‘, ‘ETIMEDOUT‘)
理解udp connect
使用udp connect好處:
1.使用connect()對效能有較大提高,BSD實現中sendto()是connect的特列,核心暫時讓通訊端與目的地址串連,發送資料包,然後解除串連。
2.可以接受非同步錯誤,由於udp無狀態,資料發送出去,系統就會忘記,所以即使返回icmp報文,也不知道是哪個應用程式發送的資料包,使用connect會記錄對應資訊。
s=socket(AF_INET,SOCK_DGRAM)s.connect(("localhost",9999))s.send("1111")s.recv() //error: [Errno 111] Connection refused
send()之後,icmp報文報告不可達,再次recv或recvfrom會報錯。
緩衝區大小
發送緩衝區設定為MSS的3倍大,可有效減少Nagle演算法與延遲ACK導致的互動問題。
Linux預設發送緩衝區為4K,乙太網路mss是1460,還是很合理的。
發送緩衝區大小大於等於對方的接受緩衝區,因為發送方只能收到ACK才會釋放已發送資料,所以發送緩衝區容易滿,不發送新的資料,就會產生40ms延遲ACK。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
Effective TCP/IP Programming讀書筆記