• TCP包頭html
ACK爲1時,確認序號有效,表示指望收到的下一個序號,是上次成功收到的字節序加1。算法
SYN, FIN都佔用一個序號。shell
• TCP鏈接的創建promise
client經過connect()來創建TCP鏈接,connect()會發送SYN報文;緩存
server經過bind()、listen()、accept()來接受一個TCP鏈接,listen()會處理三次握手。網絡
SYN報文中會指明滑動窗口的初始大小,滑動窗口是TCP接收方的流控,代表了當前時刻接受方能夠接收的數據大小。數據結構
SYN報文一般附帶TCP選項,其中有 MSS大小,發送方會用接受方的MSS來分割數據。併發
• TCP鏈接的停止dom
發送方經過close()發送FIN報文,接收方收到FIN以後會傳給應用程序,應用程序將其看做爲EOF,使得read()/recv()返回0。以後,一般接收方也會調用close(),因而也發送一個FIN報文。異步
被動收到FIN的一方在發送本身的FIN以前還能夠發送數據給先主動發送FIN的一方,這種成爲half-close,更多信息參考shutdown()。
應用程序顯明的調用close()會發送FIN報文(前提是文件描述符的引用參考已經遞減爲0)。另外,應用程序意外退出的時候(好比被kill掉),內核會關閉文件描述符,也會發送FIN報文。
• TCP鏈接的數據交互
TCP數據報文發送以後,啓動一個定時器,等待接收ACK報文,若是超時,則從新發送。TCP協議會動態計算RTT(往返時間),該值用於對超時的判斷。
收到對方發來的數據以後,接收方不會立刻恢復ACK報文,而是延後必定時間(通常200ms),若此時接收方也有數據要回復給對方,ACK報文就會和數據報文一塊兒發送,這種叫作捎帶延遲的ACK報文發送。
TCP創建在IP之上,因此到達的數據可能會失序。TCP會對收到的數據從新排序,再交給應用程序。
IPv4的主機和路由都有可能對數據包進行分片,IPv6中只有可能主機對數據包進行分片。分片發生在當須要發送的數據報文的長度超過了鏈路上的MTU(Maximum transmission unit)時。IPv4頭中的DF(don’t fragment)標誌位能夠用於阻止主機或者路由對數據包進行分片。MSS的值一般是MTU減去TCP頭再減去IP頭再減去以太網頭,一般對於IPv4來講就是1460,對於IPv6來講就是1440。MSS的目的就是防止TCP的分片,可是IPv4的中間路由有可能會形成分片的。
• TCP狀態機
解釋一下TIME_WAIT狀態,主動調用close()的一方在發送FIN報文以後進入FIN_WAIT_1狀態,若是收到了對方回覆的ACK報文而且也收到了對方發來的FIN報文以後就會進入TIME_WAIT狀態。
停留在TIME_WAIT狀態的時間爲2MSL,MSL是maximum segment lifetime,BSD實現中的數值爲30秒。IP頭裏面有TTL,MSL就是TTL爲255級時報文也不會超過的最大生存時間。
須要TIME_WAIT的緣由一是由於回覆給對方FIN的ACK報文可能會丟失,從而使得對方再一次發送FIN報文,如果TCP鏈接立刻退至CLOSED狀態,對於第二次到來的FIN就會發送RST報文。
第二個緣由是讓TCP連接expire掉,由於網絡上可能還有殘留的舊的TCP連接的數據,這些數據都要做廢,2個MSL是由於有兩個方向的數據做廢時間,在TIME_WAIT結束之前,舊的TCP佔用的端口號不能使用。
• TCP滑動窗口
接收窗口
接收方滑動窗口的左面是已確認的序號,窗口內部是可以接收的序號,右邊是不能接收的序號。
窗口合攏:接收方判斷某個序號之前的報文都已經收到,將該序號的報文移至接收緩衝區,並回復ACK報文,滑動窗口的左邊緣相右合攏。
窗口張開:當應用進程從接收緩衝區中取出數據,滑動窗口的右邊緣向右擴張。
若窗口的大小爲0,代表緩存已滿,當應用程序從緩存中取走數據後,接收方會宣告更大的窗口,發送方纔能發送數據。接收方宣告的滑動窗口的大小和接收方的接收緩衝區有關,能夠經過SO_RCVBUF來調節接收緩衝區的大小。
發送窗口
接收方宣告本身的接收窗口的大小,發送方以此做爲發送窗口的大小。
發送窗口左面是已發送已確認的報文,發送窗口內的左半部是已發送但未確認的報文,發送窗口內的有半部是能夠發送可是尚未發送的報文,發送窗口右面的不能夠發送。
發送方獲得接收方的ACK以後,發送窗口會右移。
發送方還有一個擁塞窗口的限制,用於避免網絡擁塞。實際可以發送的數據大小爲發送窗口和擁塞窗口二者的最小值。
接收緩衝區能夠經過/proc/sys/net/ipv4/tcp_rmem查看和修改
發送緩衝區能夠經過/proc/sys/net/ipv4/tcp_wmem查看和修改
• TCP超時重傳
超時的時間判斷並不固定,而是根據網絡情況時時跟新的,TCP會測量往返時間RTT,並經過均值方差等運算求出RTO(下一次超時時間)。
超時以後TCP會重傳,每一的RTO爲上一次的兩倍,超過必定重傳次數以後,再也不重發,認爲TCP連接已斷。
• TCP慢啓動與擁塞避免
TCP中有兩個參數cwnd(擁塞窗口大小)和ssthtresh(慢啓動門限)
慢啓動算法時,cwnd呈指數增長;擁塞避免算法時,cwnd呈線性增長。
發送方可以發送的數據上限爲發送窗口和cwnd的最小值。
擁塞窗口cwnd是發送方的流控,而發送窗口(由接收方的通告)是是接收方的流控。
慢啓動和擁塞避免在一塊兒實現,經過與慢啓動門限ssthresh比較判斷使用慢啓動仍是擁塞避免算法。
1.初始化 cwnd 爲 1 個報文段,ssthresh 爲 65535 個字節。
2.TCP 輸出例程的輸出不能超過 cwnd 和接收方通告窗口的大小。
3.當擁塞發生時(超時或收到重複ACK),ssthresh 被設置爲當前窗口大小的一半(cwnd 和接收方通告窗口大小的最小值,但最少爲 2 個報文段)。若是是超時引發了擁塞,則 cwnd 被設置爲 1 個報文段。
4.當新的數據被對方確認時,就增長 cwnd,但增長的方法取決於正在進行慢啓動或擁塞避免算法。若是 cwnd 小於或等於 ssthresh,則進行慢啓動,不然正在進行擁塞避免。慢啓動一直持續到咱們回到當擁塞發生時所處位置的一半時候才中止,而後轉爲執行擁塞避免。
總之,擁塞窗口比較小的時候啓用慢啓動算法,較大的時候啓用擁塞避免算法。
擁塞窗口是發送方的流量控制,而接收窗口則是接收方的流量控制。前者是發送方對網絡擁塞的估 計,後者則與接收方的緩存大小有關。
• TCP快速重傳與快速恢復
發送方收到三個重複的ACK報文以後認爲丟包,從而不等的超時而立刻重傳;只收到一個或兩個重複的ACK報文被認爲只是由於網絡傳輸的無序致使的。
因爲不需等到重傳定時器超時,因此叫作快速重傳,重傳之後擁塞窗口採用擁塞避免算法,這又叫作快速恢復算法。
• TCP Nagle算法
Nagle算法是爲了儘量發送大塊數據,避免網絡中充斥着許多小數據塊。
Nagle算法的基本定義是任意時刻,發送方最多隻能有一個未被確認的小段。小段是小於MSS的報文,如有其餘小段須要發送,則要等待ACK到來。Nagle算法帶來延遲,禁用可加上TCP_NODELAY選項。
• TCP定時器
堅持定時器
接收方發送0窗口通告則發送方不能發送數據直到接收方發送非0窗口,非0窗口一般在一個不含數據的ACK中,若是這個ACK掉了,則沒有確認和重傳機制。因此發送方有一個堅持定時器,週期性查詢接受方的窗口是否增大。
保活定時器
發送接收雙方長時間不傳輸數據,可是也要知道對方是否還存在,因此利用保活定時器來探尋。
創建鏈接定時器
Connect以後必定時間沒有對SYN的ACK報文,則中止嘗試。
重傳定時器
根據RTT的測量有關
延遲ACK定時器
ACK不立刻回覆,和數據發送。
FIN_WAIT_2定時器
在收到FIN的ACK以後由FIN_WAIT_1變爲FIN_WAIT_2,等待對方的FIN,假設不使用TCP半打開,必定時間後關閉鏈接
TIME_WAIT定時器
收到對方FIN以後發送了ACK,等待必定時間。這一是爲了防止對方的FIN發現超時並重發了,二是爲了使舊的TCP連接上的數據無效。定爲2MSL能夠保證數據無效,由於最大TTL也生存不了這麼長時間。
• TCP控制塊
每種TCP狀態都有一個控制塊pcb的鏈表,好比有處於監聽狀態的pcb鏈表、有處於穩定(established)狀態的pcb鏈表。每一個TCP連接用一個或多個pcb來描述,而且隨着狀態的變化,pcb可能掛在不一樣的鏈表上。內核每收到一個TCP報文,會判斷它是屬於哪一個pcb的,並作相應處理;如果不屬於任何pcb,則回覆RST報文。
根據滑動窗口,內核會維護幾種報文鏈表,好比unsend鏈表,unacked鏈表,ooseq鏈表(接受的無序鏈表)。在接收的時候,報文並不能確保必定是按順序到來,因此收到報文的序號並不必定等於接收方以前發送的ACK,那這個報文就掛在ooseq上,後面收到的TCP報文也按順序插在這個鏈表上。當等於接收方以前發送的ACK的那個序號來臨時,可能使ooseq上的報文變得有序,從而能夠交給上層(原先已經正確傳輸的包不用再傳,這是SACK?)。
注:摘自LWIP
• TCP優化
最小化報文傳輸的延時,禁用Nagle算法
最小化系統調用的負載,減小socket系統調用
調節 TCP 窗口,socketopt
throughput = window_size / RTT
window_size 最好等於或大於 BDP = link_bandwidth * RTT,可是過大會浪費內存
BDP是Bandwidth Delay Product,用來計算理論上最優的 TCP socket 緩衝區大小
動態優化協議棧(調整proc/sys/net下參數),可是這對整個系統產生影響
• socket應用程序
摘自UNIX Network Programming, Volume 1
• socket地址表示
IPv4地址表示
struct in_addr {
in_addr_t s_addr; /* 32-bit IPv4 address */
/* network byte ordered */
};
struct sockaddr_in {
uint8_t sin_len; /* length of structure (16) */
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 16-bit TCP or UDP port number */
/* network byte ordered */
struct in_addr sin_addr; /* 32-bit IPv4 address */
/* network byte ordered */
char sin_zero[8]; /* unused */
};
通用socket地址表示
struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family; /* address family: AF_xxx value */
char sa_data[14]; /* protocol-specific address */
};
在bind中就將其轉化爲通用的socket地址
bind(sockfd, (struct sockaddr *) &serv, sizeof(serv));
由於各個地址類型長度不一樣,傳遞時要指明不一樣地址類型的長度。
地址轉換
將地址字符串(eg:」192.168.1.2」)轉換成地址數據結構
int inet_pton(int family, const char *strptr, void *addrptr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
它們能夠同時支持IPv4和IPv6,替換了原先的inet_aton、inet_ntoa和inet_addr
int inet_aton(const char * cp,struct in_addr *inp);
char * inet_ntoa(struct in_addr in);
unsigned long int inet_addr(const char *cp);
• socket()函數
int socket(int domain,int type,int protocol);
AF_KEY是用於IPsec的
AF_ROUTE是和路由有關的
Linux支持PF_PACKET支持對數據鏈路層的直接訪問。
AF_XXX和PF_XXX沒有區別
socket()返回的是文件描述符
• bind()函數
int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
bind()指明瞭所用的地址和端口,不然的話可讓內核隨機指定地址和端口:
IPv4隨機指定地址
struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr = htonl (INADDR_ANY); /* wildcard */
IPv6隨機指定地址
struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any; /* wildcard */
隨機指定端口:serv.sin_port = 0;
若要得到隨機指定的地址和端口,能夠經過getsockname()進行。
• listen()函數
int listen(int sockfd,int backlog);
listen()使得內核能夠接收鏈接到這個socket(IP地址+端口)的TCP連接。它使得TCP狀態機從CLOSED轉到LISTEN。第二個參數代表內核能夠隊列緩存多少個輸入的連接。
內核會爲處於listening狀態的socket維護兩個隊列,一個是已經完成了三次握手的隊列(TCP連接處於TCP狀態機中的ESTABLISHED狀態),一個是尚未完成三次握手的隊列(TCP連接處於TCP狀態機中的SYN_RCVD狀態)。
當三次握手完成後,TCP連接就創建了,將這個成員從未完成隊列已到完成隊列(accept()是阻塞的,若已完成隊列有連接,則返回的已完成隊列的首個成員)。未完成隊列中的成員有75秒生存時間。listen()的第二個參數指的是這兩個隊列的成員總數。
如果隊列已滿,server對新進來的連接不予處理,client的connect()會從新嘗試連接。
有一種DOS(denial of service)攻擊叫SYN flooding,它是某個clinet瘋狂的發送SYN,嘗試與server創建連接,那server隊列滿了以後,正常的lient的連接請求就不能處理了。
• connect()函數
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
connect()由client調用,它發送SYN報文,嘗試進行TCP連接創建的三次握手。client不用bind(),內核會隨機指定一個端口和IP地址。若是一時沒有收到對方的回覆,connect()會繼續嘗試三次握手(即發送SYN報文),若是75秒後都沒有響應,則返回超時錯誤。
若是對方沒有開啓用於server的進程,也就是沒有listerning,那對方收到SYN以後會恢復RST報文。
幾種出錯的case:
子網中沒有192.168.1.100
solaris % daytimetcpcli 192.168.1.100
connect error: Connection timed out
server沒有開啓相應進程
solaris % daytimetcpcli 192.168.1.5
connect error: Connection refused
路由器找不到主機
solaris % daytimetcpcli 192.3.4.5
connect error: No route to host
Normal 0 7.8 磅 0 2 false false false EN-US ZH-CN X-NONE /* Style Definitions */ table.MsoNormalTable {mso-style-name:普通表格; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-priority:99; mso-style-parent:""; mso-padding-alt:0cm 5.4pt 0cm 5.4pt; mso-para-margin:0cm; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:10.0pt; font-family:"Times New Roman","serif";}
UDP若要收取ICMP的錯誤好比(「port unreachable」)的話,須要先connect()。
• accept()函數
int accept(int sockfd,struct sockaddr * addr,int * addrlen);
accept()從已完成隊列中取出首個成員並返回新的socket文件描述符,用於表示新的TCP連接。若是已完成隊列爲空,則accept()會掛起(即sleep)。
第一個參數是listen()用的socket文件描述符,accept()返回的是新的socket文件描述符。通常來講,server建立一個socket文件描述符,它的生命週期爲server的生命週期。accept()返回的socket文件描述符的生命週期在處理完client的連接以後就結束了(經過close())。
這種server處理完一個client的連接以後再從已完成隊列中取出下一個client的連接處理。因此,server只能同時處理一個client。若須要作到併發處理clients,則要用到fork()。
• fork()函數
pid_t fork(void);
fork()建立了一個新的進程,它是一次調用,兩次返回的。一次返回到parent進程,一次返回到child進程。返回給parent進程的fork()返回值是child進程的pid(process ID),返回給child進程的fork()返回值是0。child進程能夠經過getppid()得到parent進程的pid。child進程共享全部在parent進程中打開的文件描述符。
fork()另外一種應用是後跟exec(),shell上就是這種的典型應用。exec()有幾種變種,但做用都是加載新的程序,並執行新程序的main函數。但在socket中的應用一般爲了併發的處理client,這樣的server爲:
由於fork()有不一樣的返回值,parent進程由於返回值不爲0因此就直接運行到close(connfd),以後有循環到了accept處等待下一個連接。child進程會執行if條件內的語句。
這裏parent進程close了一次connfd,child進程close了listenfd的緣由是:文件是有參考計數的,即有多少個進程佔用了已打開的文件描述符。fork()返回以後,parent、child進程各自佔用了一個connfd和listenfd。一次close()只是減小文件的引用計數,直到引用計數爲止,纔會關閉文件。
若child進程退出了,它成爲zombie的狀態,並由內核發SIGCHLD信號給其parent進程,而後parent進程處理SIGCHLD信號並經過wait()或waitpid()將zombie狀態的child進程的資源釋放乾淨。之因此會有zombie狀態的目的是讓parent進程獲取已退出child進程的信息。若parent進程也退出了,則parent及其下child進程的zombie狀態由init進程來處理。
在parent進程中添加對SIGCHLD信號的處理函數:
Signal (SIGCHLD, sig_chld);
void sig_chld(int signo)
{
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d terminated\n", pid);
return;//will interrupt system calls
}
信號處理函數的返回會中斷系統調用,系統調用檢測到有中斷產生,就返回-1並設置errno爲EINTR。但是這個信號處理不該該影響咱們的socket系統調用,因此經常在socket程序能夠看到對errno的判斷:
for ( ; ; ) {
clilen = sizeof (cliaddr);
if ( (connfd = accept (listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for () */
else
err_sys ("accept error");
}
• close()函數
close(int sockfd);
close()函數將socket文件描述符的引用計數減一,當引用計數爲0時關閉socket文件並終止一條TCP連接(即發送FIN報文)。shutdown()也有相似的做用,但不遞減文件描述符的引用計數。TCP提供了鏈接的一端在結束它的發送後還能接收來自另外一端數據的能力,這就是TCP的半關閉(收到對方的FIN包只意味着對方不會再發送任何消息)。
若是不調用close(),系統會用盡文件描述符,更重要的是TCP連接得不到終止。
一般應用程序退出時,內核會幫助關閉還沒有關閉文件描述符(內核會幫助發送FIN報文)。
SO_LINGER改變TCP關閉時對 socket緩衝區的殘留數據操做的行爲。
• write()/read()函數
write()
在阻塞write()的狀況下,內核會將應用程序的數據拷貝到TCP發送緩衝區,當發送緩衝區的容量不夠時,應用程序進程被掛起,直到應用程序的數據所有拷貝完成以後write()才返回。write()的返回僅代表了應用程序能夠從新使用應用程序數據的內存空間。
在非阻塞write()的狀況下,若是發送緩衝區有空餘,就返回已寫入發送緩衝區的數據字節數(稱爲不足計數);若是發送發送緩衝區根本沒有任何空餘,則返回EWORLDBLOCK。對於UDP來講,它其實沒有真正的發送緩衝區,只是將應用程序拷貝到內核分配的空間,因此不會有緩衝區不夠的說法。
發送以後,數據會保存在TCP的發送緩衝區直到收到對端發來的ACK信號。數據鏈路層有一個發送隊列,當發送隊列已滿的話,錯誤會返回到協議棧,可是應用程序並不知曉。
read()
在阻塞read()的狀況下,若接收緩衝區中爲空,該進程將被掛起。對於UDP來講,到達一個UDP數據報後喚醒進程(SOCK_DGRAM);對於TCP來講,只要到達一些數據就會喚醒進程(SOCK_STREAM)。
非阻塞read()的狀況下,若接收緩衝區中爲空(TCP緩衝區無任何數據或者UDP緩衝區不存在一個UDP包),則返回EWOURLDBLOCK。
TCP包頭的push標誌指示接收端應儘快將數據提交給應用層。若是send函數提交的待發送數據量較小,例如小於MSS,那麼協議層會將該報文中的TCP頭部的push字段置爲1;若是待發送的數據量較大,須要拆成多個數據段發送時,協議層只會將最後一個分段報文的TCP頭部的push字段置1。收到帶有push標誌的TCP報文會促使read()返回。
參考:http://topic.csdn.net/u/20090428/13/4fd54186-d70a-4ff7-9b57-4af83f225e90.html
TCP異常
write()/read()可以反映TCP連接的異常狀況,但這一般是異步的!
在收到對方的FIN報文後,本方的read()就會返回0(0對TCP來講是EOF,0對UDP來講是收到一個0長度的報文)。本方也仍然能夠調用write(),由於TCP協議是支持半關閉的。但問題是對方發來的FIN多是應用程序主動調用close()來發的,也多是對方應用程序被kill掉由內核調用來發的,若是是後者,本方的發送就會使得對方回覆RST,本方就會有errno=ECONNRESET的錯誤。若是繼續對已經收到RST的socket調用write(),本方進程就會收到SIGPIPE,errno=EPIPE。
若是對方的機器掛了(連FIN報文都沒發送),本方的先調用write()而後阻塞在read()上,TCP發送了數據以後收不到ACK報文,再嘗試了重傳12次以後(大約9分鐘),read()返回錯誤,errno= ETIMEDOUT/EHOSTUNREACH/ENETUNREACH。沒有調用read()就不返回錯誤?即便返回了錯誤,離write()的調用也好久了,因此是異步的。
參考:http://www.cppblog.com/elva/archive/2008/09/10/61544.html
http://www.cnblogs.com/promise6522/archive/2012/03/03/2377935.html
封裝讀寫函數
read()、write()有可能過早返回,爲此,編寫封裝函數,每次讀寫n個字節。若讀寫函數返回負數,表示有錯誤或者被signal打斷,此時檢查errno的值判斷緣由,如果由於EINTR的話,則繼續進行。
v\:* {behavior:url(#default#VML);} o\:* {behavior:url(#default#VML);} w\:* {behavior:url(#default#VML);} .shape {behavior:url(#default#VML);} Normal 0 7.8 磅 0 2 false false false EN-US ZH-CN X-NONE /* Style Definitions */ table.MsoNormalTable {mso-style-name:普通表格; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-priority:99; mso-style-parent:""; mso-padding-alt:0cm 5.4pt 0cm 5.4pt; mso-para-margin:0cm; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:10.0pt; font-family:"Times New Roman","serif";}
不一樣的讀寫函數
UNIX Network Programming, Volume 1
Linux TCP IP 協議棧分析
LwIP協議棧源碼詳解
http://topic.csdn.net/u/20090428/13/4fd54186-d70a-4ff7-9b57-4af83f225e90.html
http://www.cppblog.com/elva/archive/2008/09/10/61544.html
http://www.cnblogs.com/promise6522/archive/2012/03/03/2377935.html