## 0.TCP連接的建立和終止
### 1) 三次握手
?? 建立一個TCP連接時會發生下述情形:
(1)服務器必須準備好接受外來的連接。這通常通過調用socket,bind和listen這三個函數來完成,我們稱之為被動打開。
(2)客戶通過調用connect發起主動打開。這導致客戶TCP發送一個SYN(同步)分節,它告訴服務器客戶將在(待建立的)連接中發送的數據的初始序列號。通常SYN分節不攜帶數據,其所在IP數據報只含有一個IP首部,一個TCP首部及可能有的TCP選項。
(2-1)TCP選項之MSS選項:
發送SYN的TCP一端使用本選項通告對端它的最大分節大小(maximum segment size)即MSS,也就是它在本連接的每個TCP分節中愿意接受的最大數據量。發送端TCP使用接收端的MSS值作為所發送分節的最大大小。
(3)服務器必須確認(ACK)客戶的SYN,同時自己也得發送一個SYN分節,它含有服務器將在同一連接中發送的數據的初始序列號。服務器在單個分節中發送SYN和對客戶SYN的ACK。
(4)客戶必須確認服務器的SYN。
?? 這種交換至少需要3個分組,因此稱之為TCP的三路握手。(這里SYN為1字節,所以ACK時候只要在K上簡單加1即可)

### 2) TCP連接終止
?? TCP終止一個連接則需4個分節:
(1)某個應用進程首先調用close,我們稱該端執行主動關閉。該端的TCP于是發送一個FIN分節,表示數據發送完畢。
(2)接收到這個FIN的對端執行被動關閉。這個FIN由TCP確認。它的接收也作為一個文件結束符(EOF)傳遞給接收端應用進程(放在已排隊等候該應用進程接收的任何其他數據之后),因為FIN的接收意味著接收端應用進程在相應連接上再無額外數據可接收。
(3)一段時間后,接收到這個文件結束符的應用進程將調用close關閉它的套接字。這導致它的TCP也發送一個FIN。
(4)接收這個最終FIN的原發送端TCP(即執行主動關閉的那一端)確認這個FIN。
備注:從執行被動關閉到執行主動關閉(步驟2和步驟3之間)一端流動數據是可能的,這稱為半關閉,畢竟當時接收端的套接字并未close掉。(FIN和SYN一樣為1字節,所以ACK也是簡單的N+1)

### 3) TCP狀態轉換圖

### 4) 觀察分組

## 1. socket函數
?? 為了執行網絡I/O,一個進程必須做的第一件事就是調用socket函數,指定期望的通信協議類型。
~~~
#include <sys/socket.h>
int socket( int family, int type, int protocol );
返回:若成功則為非負描述符,若出錯則為-1
~~~
### 1) socket函數的family常值
| family | 說明 |
|-----|-----|
| AF_INET | IPv4協議 |
| AF_INET6 | IPv6協議 |
| AF_LOCAL | unix域協議 |
| AF_ROUTE | 路由套接字 |
| AF_KEY | 密鑰套接字 |
### 2) socket函數的type常值
| type | 說明 |
|-----|-----|
| SOCK_STREAM | 字節流套接字 |
| SOCK_DGRAM | 數據報套接字 |
| SOCK_SEQPACKET | 有序分組套接字 |
| SOCK_RAW | 原始套接字 |
### 3) socket函數AF_INET或AF_INET6的protocol常值
| protocol | 說明 |
|-----|-----|
| IPPROTO_TCP | TCP傳輸協議 |
| IPPROTO_UDP | UDP傳輸協議 |
| IPPROTO_SCTP | SCTP傳輸協議 |
?? socket函數在成功時返回一個小的非負整數值,它與文件描述符類似,我們把它稱為套接字描述符,簡稱sockfd.
?? 對于unix一切皆文件,則套接字描述符為網絡通信中的文件描述符。程序可以通過套接字描述符進行通信。
## 2. connect函數
?? TCP客戶用connect函數來建立與TCP服務器的連接
~~~
#include <sys/socket.h>
int connect( int sockfd, const struct sockaddr *servaddr, socklen_t addrlen );
返回:若成功則為0,若出錯則為-1
~~~
?? sockfd是由socket函數返回的套接字描述符,第二個,第三個參數分別是一個指向套接字地址結構的指針和該結構的大小。
?? 客戶在調用函數connect前不必非得調用bind函數,因為如果需要的話,內核會確定源IP地址,并選擇一個臨時端口作為源端口。
?? 如果是TCP套接字,調用connect函數將激發TCP的三路握手過程,而且僅在連接建立成功或出錯時才返回,其中錯誤返回可能有以下幾種情況:
1)若TCP客戶沒有收到SYN分節的響應,則返回ETIMEDOUT錯誤(超時)
2) 若對客戶的SYN的響應是RST(表示復位),則表明該服務器主機在我們指定的端口上沒有進程在等待與之連接(例如服務器進程也許沒在運行,畢竟端口用于標識一個進程)。這是一種硬件錯誤(hard error),客戶一接收到RST就馬上返回ECONNREFUSED錯誤。
?? RST是TCP在發生錯誤時發送的一種TCP分節。產生RST的三個條件是:目的地為某端口的SYN到達,然而該端口上沒有正在監聽的服務器;TCP想取消一個已有連接;TCP接收到一個根本不存在的連接上的分節。
3)若客戶發出的SYN在中間的某個路由器上引發了一個“destination unreadchable”ICMP錯誤,則認為是一種軟件錯誤(soft error)。客戶主機內核保存該消息,并繼續發送SYN。若超時,則將ICMP錯誤作為EHOSTUNREACH或ENETUNREACH錯誤返回給進程。
?? 若connect失敗則該套接字不再可用,必須關閉,我們不能對這樣的套接字再次調用connect函數。當循環調用函數connect為給定主機嘗試各個IP地址直到有一個成功時,在每次connect失敗后,都必須close當前的套接字描述符并重新調用socket。
## 3. bind函數
?? bind函數把一個本地協議地址賦予一個套接字。
~~~
#include <sys/socket.h>
int bind( int sockfd, const struct sockaddr *myaddr, socklen_t addrlen );
返回:若成功則為0,若出錯則為-1
~~~
?? 第二個參數是一個指向特定與協議的地址結構的指針,第三個參數是該地址結構的長度。
1) 服務器在啟動時捆綁它們的總所周知端口(端口用于標識一個進程,如果端口為0,則由內核選擇端口,而且必須使用getsockname來返回協議地址來得到內核所選擇的這個端口號)
2) 進程可以把一個特定的IP地址捆綁到它的套接字上(一般都是通配地址,用常量值INADDR_ANY來指定,如htonl( INADDR_ANY))
## 4. listen函數
?? listen函數僅由TCP服務器調用,它做兩件事情:
1) 當socket函數創建一個套接字時,它被假設為一個主動套接字,也就是說,它是一個將調用connect發起連接的客戶套接字。listen函數把一個未連接的套接字轉換成一個被動套接字,指示內核應接受指向該套接字的連接請求。
2) 第二個參數規定了內核應該為相應套接字排隊的最大連接個數。
~~~
#include <sys/socket.h>
int listen( int sockfd, int backlog );
返回:若成功則為0,若出錯則為-1
~~~
### (1) 理解backlog
內核為任何一個給定的監聽套接字維護兩個隊列:
未完成連接隊列:每個這個的SYN分節對應其中一項:已由某個客戶發出并到達服務器,而服務器正在等待完成相應的TCP三路握手過程。這些套接字處于SYN_RCVD狀態。
已完成連接隊列:每個已完成TCP三路握手過程的客戶對應其中一項。這些套接字處于ESTABLISHED狀態。

?? 每當在未完成連接隊列中創建一項時,來自監聽套接字的參數就復制到即將建立的連接中。連接的創建機制是完全自動的,無需服務器進程握手:

## 5. accept函數
?? accept函數由TCP服務器調用,用于從已完成連接隊列隊頭返回下一個已完成連接。如果已完成連接隊列為空,那么進程被投入睡眠(假定套接字為默認的阻塞方式)
~~~
#include <sys/socket.h>
int accept( int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen );
返回:若成功則為非負描述符,若出錯則為-1
~~~
?? 參數cliaddr和addrlen用來返回已連接的對端進程(客戶)的協議地址。addrlen是值-結果參數:調用前,我們將由*addrlen所引用的整數值置為由cliaddr所指的套接字地址結構的長度,返回時,該整數值即為由內核存放在該套接字地址結構內的確切字節數。
?? 我們在srv.c中增加以下代碼,就可以看到客戶端的IP和端口了:
~~~
socklen_t len;
struct sockaddr_in servaddr, cliaddr;
len = sizeof(cliaddr);
connfd = accept( listenfd, ( SA * )&cliaddr, &len );
inet_ntop( AF_INET, &cliaddr.sin_addr, buff, sizeof(buff));
printf("connection from %s,port %d\n", buff , ntohs(cliaddr.sin_port));
~~~
而服務端則會輸出:
~~~
leichaojian@ThinkPad-T430i:~$ ./daytimetcpsrv1
connection from 127.0.0.1,port 57452
~~~
## 6. close函數
?? close函數也用來關閉套接字,并終止TCP連接。
~~~
#include <unistd.h>
int close( int sockfd );
~~~
## 7. getsockname和getpeername函數
?? 這兩個函數或者返回與某個套接字關聯的本地協議地址(getsockname),或者返回與某個套接字關聯的外地協議地址(getpeername)
### 1)getsockname的測試函數如下:
服務端:
~~~
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <fcntl.h>
#include <netinet/in.h>
#define MAXLINE 1024
#define SA struct sockaddr
int main(int argc, char **argv)
{
int listenfd, connfd;
int buff[MAXLINE];
pid_t pid;
time_t ticks;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
socklen_t cliLen;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9877);
bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
listen(listenfd, 5);
for ( ; ; ){
cliLen = sizeof(cliaddr);
connfd = accept(listenfd, (SA *)&cliaddr, &cliLen);
if ((pid = fork()) == 0){
close(listenfd);
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
write(connfd, buff, strlen(buff));
_exit(0);
}
if (waitpid(pid, NULL, 0) != pid){
printf("waitpid error\n");
exit(1);
}
close(connfd);
}
return 0;
}
~~~
客戶端:
~~~
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <fcntl.h>
#define MAXLINE 1024
#define SA struct sockaddr
int main(int argc, char **argv)
{
int sockfd, n;
struct sockaddr_in servaddr;
char buff[MAXLINE + 1];
struct sockaddr_in cliaddr;
socklen_t cliLen;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9877);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
connect(sockfd, (SA *)&servaddr, sizeof(servaddr));
cliLen = sizeof(cliaddr);
getsockname(sockfd, (SA *)&cliaddr, &cliLen);
while ((n = read(sockfd, buff, MAXLINE)) > 0){
buff[n] = '\0';
fputs(buff, stdout);
}
return 0;
}
~~~
?? 當我們分別用gdb調試的時候,發現服務端和客戶端的cliaddr的內容是一致的:
~~~
(gdb) p cliaddr
$2 = {sin_family = 2, sin_port = 49635, sin_addr = {s_addr = 16777343},
sin_zero = "\000\000\000\000\000\000\000"}
~~~