基於tcpdump實例講解TCP/IP協議


前言

雖然網絡編程的socket大家很多都會操作,但是很多還是不熟悉socket編程中,底層TCP/IP協議的交互過程,本文會一個簡單的客戶端程序和服務端程序的交互過程,使用tcpdump抓包,實例講解客戶端和服務端的TCP/IP交互細節。

TCP/IP協議

IP頭和TCP頭格式如下:

Internet Header Format
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

TCP Header Format
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

單單看這些頭會比較枯燥,后面會根據一個簡單的客戶端和服務端的TCP/IP報文交互實例講解這些報文頭的格式和含義


簡單的客戶端和服務端

客戶端和服務端是我們寫的一個簡單客戶端程序,運行在Linux上。

客戶端和服務端的功能如下:
客戶端從標准輸入讀入一行,發送到服務端
服務端從網絡讀取一行,然后輸出到客戶端
客戶端收到服務端的響應,輸出這一行到標准輸出

服務端代碼如下:

#include  <unistd.h>
#include <sys/types.h> /* basic system data types */
#include <sys/socket.h> /* basic socket definitions */
#include <netinet/in.h> /* sockaddr_in{} and other Internet defns */
#include <arpa/inet.h> /* inet(3) functions */

#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

#define MAXLINE 1024
//typedef struct sockaddr SA;
void handle(int connfd);

int main(int argc, char **argv)
{
int listenfd, connfd;
int serverPort = 6888;
int listenq = 1024;
pid_t childpid;
char buf[MAXLINE];
socklen_t socklen;

struct sockaddr_in cliaddr, servaddr;
socklen = sizeof(cliaddr);

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(serverPort);

listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
perror("socket error");
return -1;
}
if (bind(listenfd, (struct sockaddr *) &servaddr, socklen) < 0) {
perror("bind error");
return -1;
}
if (listen(listenfd, listenq) < 0) {
perror("listen error");
return -1;
}
printf("echo server startup,listen on port:%d\n", serverPort);
for ( ; ; ) {
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &socklen);
if (connfd < 0) {
perror("accept error");
continue;
}

sprintf(buf, "accept form %s:%d\n", inet_ntoa(cliaddr.sin_addr), cliaddr.sin_port);
printf(buf,"");
childpid = fork();
if (childpid == 0) { /* child process */
close(listenfd); /* close listening socket */
handle(connfd); /* process the request */
exit (0);
} else if (childpid > 0) {
close(connfd); /* parent closes connected socket */
} else {
perror("fork error");
}
}
}


void handle(int connfd)
{
size_t n;
char buf[MAXLINE];

for(;;) {
n = read(connfd, buf, MAXLINE);
if (n < 0) {
if(errno != EINTR) {
perror("read error");
break;
}
}
if (n == 0) {
//connfd is closed by client
close(connfd);
printf("client exit\n");
break;
}
//client exit
if (strncmp("exit", buf, 4) == 0) {
close(connfd);
printf("client exit\n");
break;
}
write(connfd, buf, n); //write maybe fail,here don't process failed error
}
}

 

客戶端代碼如下:

#include  <unistd.h>
#include <sys/types.h> /* basic system data types */
#include <sys/socket.h> /* basic socket definitions */
#include <netinet/in.h> /* sockaddr_in{} and other Internet defns */
#include <arpa/inet.h> /* inet(3) functions */
#include <netdb.h> /*gethostbyname function */

#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

#define MAXLINE 1024

void handle(int connfd);

int main(int argc, char **argv)
{
char * servInetAddr = "127.0.0.1";
int servPort = 6888;
char buf[MAXLINE];
int connfd;
struct sockaddr_in servaddr;

if (argc == 2) {
servInetAddr = argv[1];
}
if (argc == 3) {
servInetAddr = argv[1];
servPort = atoi(argv[2]);
}
if (argc > 3) {
printf("usage: echoclient <IPaddress> <Port>\n");
return -1;
}

connfd = socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(servPort);
inet_pton(AF_INET, servInetAddr, &servaddr.sin_addr);

if (connect(connfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
perror("connect error");
return -1;
}
printf("welcome to echoclient\n");
handle(connfd); /* do it all */
close(connfd);
printf("exit\n");
exit(0);
}

void handle(int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
int n;
for (;;) {
if (fgets(sendline, MAXLINE, stdin) == NULL) {
break;//read eof
}
/*
//也可以不用標准庫的緩沖流,直接使用系統函數無緩存操作
if (read(STDIN_FILENO, sendline, MAXLINE) == 0) {
break;//read eof
}
*/

n = write(sockfd, sendline, strlen(sendline));
n = read(sockfd, recvline, MAXLINE);
if (n == 0) {
printf("echoclient: server terminated prematurely\n");
break;
}
write(STDOUT_FILENO, recvline, n);
//如果用標准庫的緩存流輸出有時會出現問題
//fputs(recvline, stdout);
}
}

下載地址

編譯服務器:

gcc echoserver.c -o echoserver

編譯客戶端

gcc echoclient.c -o echoclient

 

TCP/IP連接建立,交互,關閉

首先我們要啟用tcpdump監控客戶端和服務端的報文:

tcpdump -S -nn -vvv -i lo port 6888

-S 打印TCP 數據包的順序號時, 使用絕對的順序號, 而不是相對的順序號

 -nn 表示不進行端口到名稱的轉換

-vvv 表示產生盡可能詳細的協議輸出

-i lo表示只監控網卡lo設備,默認是監控第一個網絡設備。

port 6888表示只監控端口6888的相關監控數據,包括從6888端口接收和從6888端口發送的報文。

tcpdump詳情可以參考本博客的"Linux tcpdump命令詳解"的tcpdump的簡單選項介紹。

接着我們要啟動服務端:

./echoserver

再啟動客戶端:

./echoclient 

建立連接

客戶端程序一啟動,就會connect服務端,tcpdump對應的輸出如下:

13:27:45.927137 IP (tos 0x0, ttl  64, id 304, offset 0, flags [DF], proto: TCP (6), length: 60) 127.0.0.1.60534 > 127.0.0.1.6888: S, cksum 0x5f32 (correct), 2584692379:2584692379(0) win 32792 <mss 16396,sackOK,timestamp 10962859 0,nop,wscale 6>
13:27:45.927254 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto: TCP (6), length: 60) 127.0.0.1.6888 > 127.0.0.1.60534: S, cksum 0x3648 (correct), 2589673026:2589673026(0) ack 2584692380 win 32768 <mss 16396,sackOK,timestamp 10962860 10962859,nop,wscale 6>
13:27:45.927265 IP (tos 0x0, ttl 64, id 305, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.60534 > 127.0.0.1.6888: ., cksum 0x1d6a (correct), 2584692380:2584692380(0) ack 2589673027 win 513 <nop,nop,timestamp 10962860 10962860>

這里是TCP連接的三握手的報文交互,其中協議各個字段的含義如下:

tos表示服務類型,4bit的tos分別表示最小時延,最大吞吐量,最高可靠性,最小費用。這里都是0,表示一般服務,其余4bit廢用,置0.

TTL(time - to - live)生存時間字段設置了數據報可以經過的最多路由器數。它指定了數據報的生存時間。TTL的初始值由源主機設置(通常為32或64),一旦經過一個處理它的路由器,它的值就減去1。當該字段的值為0時,數據報就被丟棄,並發送ICMP報文通知源主機。
id 對應IP報文頭的Identification,用於IP分片重組。

offset 也用於IP分片重組,表示相對於原始未分片的報文的位置。

flags MF表示有更多分片,DF表示不分片,這里是DF,未使用分片,所以id和offset的值都可以忽略。

proto 表示協議,可以是TCP,UDP等,這里是TCP。

length 總長度字段,是指整個I P數據報的長度(至於首部長度這里沒有給出,首部長度給出首部中32 bit字的數目。需要這個值是因為任選字段的長度是可變的。這個字段占4 bit,因此TCP最多有60字節的首部。然而沒有任選字段,正常的長度是20字節)。

127.0.0.1.60534 > 127.0.0.1.6888表示數據是從IP為127.0.0.1端口為60534發送到IP為127.0.0.1端口為6888。分別對應的IP報文頭的源地址和目的地址,以及TCP報文頭的源端口和目的端口。

S 當建立一個新的連接時,SYN標志變1。序號字段包含由這個主機選擇的該連接的初始序號ISN(Initial Sequence Number)。該主機要發送數據的第一個字節序號為這個ISN加1,因為SYN標志消耗了一個序號,這里客戶端的ISN是2584692379,服務端的ISN是2589673026。

chksum 16位檢驗和,這里有IP首部檢驗和和TCP報文段(包括TCP首部和數據)檢驗和,具體是哪個檢驗和不詳。

2584692379:2584692379(0)表示,第一個2584692379表示TCP報文段的序列號,(0)表示數據長度是0,即沒有數據,第二個2584692379是第一個2584692379+數據長度  計算出來的。TCP是可靠連接,三握手的最大目的是為了初始化雙方的ISN。假設客戶端連接服務端,發送數據,剛好網絡比較慢,在傳輸過程中,客戶端和服務端已經都重啟了,重新建立連接發送數據,發送過程中,服務端收到已經之前客戶端的數據,發現ISN非法,就會拋棄這個包,不會對現有的服務造成影響。這個只是ISN的一方面的作用。

win TCP窗口大小,通知對方,發送方最多還可以接收的數據量,用於TCP的擁塞控制。第一個報文表示客戶端通知服務端,客戶端可以接受的數據的緩存區最大是32792個字節。服務端通知客戶端,服務端最多可以接受的緩存區最大是32768,這個窗口大小在一方接受數據,卻沒有read的時候,窗口會逐漸減小,直至為0,最后對方不可以發送任何數據(如果要做該測試,需要發送的數據量大概接近65535,因為窗口的緩存區也會在剩余容量減小時,自動增加總共容量,直到總共容量接近65535,接下來就會看到win越來越小,直至0)。

ack TCP是可靠連接,所以收到發送方的數據,接受方就會發送ack確認,告訴發送方,接受方已經接收到數據,否則,發送方認為數據沒有發送成功,重復發送數據。第二個包有ack 2584692380,其中2584692380是第一個報文包的2584692379:2584692379(0)的第二個2584692379+1的值。

<mss 16396,sackOK,timestamp 10962859 0,nop,wscale 6>這里表示IP報文頭的可選字段,mss是最小最大分段大小,這里是16396,表示一個TCP報文段發送的數據最大可以是16396個字節,可能是lo設備的關系,這個mss很大,一般都是MTU 1500 個字節 - IP數據報文頭20個字節- TCP報文頭20個字節 = 1460個字節。wscale是TCP窗口擴大選項的窗口擴大因子,用於擴大TCP通告窗口,使TCP的窗口定義從16bit增加為32bit。這里的wscale是6,那么實際窗口是513左移6位,既513 X 64 = 32832,這個選項只在一個SYN報文中有意義。其他選項不詳,具體參考RFC。

交互

建立連接之后,我們通過客戶端分別發送a和123到服務端,客戶端后台顯示如下:

[root@localhost simpletcpip]# ./echoclient 
welcome to echoclient
a
a
123
123

tcpdump對應的輸出是:

13:27:48.248592 IP (tos 0x0, ttl  64, id 306, offset 0, flags [DF], proto: TCP (6), length: 54) 127.0.0.1.60534 > 127.0.0.1.6888: P, cksum 0xfe2a (incorrect (-> 0xb344), 2584692380:2584692382(2) ack 2589673027 win 513 <nop,nop,timestamp 10965181 10962860>
13:27:48.248739 IP (tos 0x0, ttl 64, id 495, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.6888 > 127.0.0.1.60534: ., cksum 0x0b47 (correct), 2589673027:2589673027(0) ack 2584692382 win 512 <nop,nop,timestamp 10965181 10965181>
13:27:48.249061 IP (tos 0x0, ttl 64, id 496, offset 0, flags [DF], proto: TCP (6), length: 54) 127.0.0.1.6888 > 127.0.0.1.60534: P, cksum 0xfe2a (incorrect (-> 0xaa32), 2589673027:2589673029(2) ack 2584692382 win 512 <nop,nop,timestamp 10965181 10965181>
13:27:48.249085 IP (tos 0x0, ttl 64, id 307, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.60534 > 127.0.0.1.6888: ., cksum 0x0b43 (correct), 2584692382:2584692382(0) ack 2589673029 win 513 <nop,nop,timestamp 10965182 10965181>
13:27:49.544830 IP (tos 0x0, ttl 64, id 308, offset 0, flags [DF], proto: TCP (6), length: 56) 127.0.0.1.60534 > 127.0.0.1.6888: P, cksum 0xfe2c (incorrect (-> 0xa1eb), 2584692382:2584692386(4) ack 2589673029 win 513 <nop,nop,timestamp 10966477 10965181>
13:27:49.544987 IP (tos 0x0, ttl 64, id 497, offset 0, flags [DF], proto: TCP (6), length: 56) 127.0.0.1.6888 > 127.0.0.1.60534: P, cksum 0xfe2c (incorrect (-> 0x9cd8), 2589673029:2589673033(4) ack 2584692386 win 512 <nop,nop,timestamp 10966477 10966477>
13:27:49.545010 IP (tos 0x0, ttl 64, id 309, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.60534 > 127.0.0.1.6888: ., cksum 0x011c (correct), 2584692386:2584692386(0) ack 2589673033 win 513 <nop,nop,timestamp 10966477 10966477>

第一個報文是客戶端給服務端發送了一個a數據,P表示push標志,發送方使用該標志通知接收方將所收到的數據全部提交給接收進程。這里的數據包括與PUSH一起傳送的數據以及接收方TCP已經為接收進程收到的其他數據。長度為54是因為a數據后面還有一個\n(輸入a然后回車導致的),所以是2個字節,比正常情況下沒有數據的52個字節多了2個字節。

第二個報文是服務端接受了a數據后,發送給客戶端的ack確認。

第三個報文和第四個報文分別是服務端發給客戶端的回顯a,以及客戶端的ack確認。

第五個報文是客戶端給服務端發送了123數據。

第六個報文,是服務端給客戶端發送回顯123,同時合並了對客戶端的ack確認。

第七個報文是客戶端對服務端的回顯123數據的ack確認。

關閉連接

直接在客戶端的控制終端執行Ctrl+C結束客戶端程序,就可以關閉TCP連接,tcpdump輸出如下:

13:38:10.081895 IP (tos 0x0, ttl  64, id 310, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.60534 > 127.0.0.1.6888: F, cksum 0x897d (correct), 2584692386:2584692386(0) ack 2589673033 win 513 <nop,nop,timestamp 11586913 10966477>
13:38:10.081987 IP (tos 0x0, ttl 64, id 498, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.6888 > 127.0.0.1.60534: F, cksum 0x11e0 (correct), 2589673033:2589673033(0) ack 2584692387 win 512 <nop,nop,timestamp 11586913 11586913>
13:38:10.081993 IP (tos 0x0, ttl 64, id 311, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.60534 > 127.0.0.1.6888: ., cksum 0x11df (correct), 2584692387:2584692387(0) ack 2589673034 win 513 <nop,nop,timestamp 11586913 11586913>

關閉的請求由客戶端發出,第一個報文是客戶端發給服務端,F表示Fin,發送關閉連接請求。這個關閉連接請求是由於客戶端退出,操作系統回收客戶端資源,自動發出的。當然,如果我們在客戶端輸入Ctrl+D,最后執行close(connfd),關閉連接請求的報文就會發送。

第二個報文是服務端發給客戶端的關閉連接請求,當客戶端關閉連接是,服務端處理客戶端請求的handle函數中

if (n == 0) {
//connfd is closed by client
close(connfd);
printf("client exit\n");
break;
}

執行close(connfd),對應的第二個報文就會發送。

第三個報文只是客戶端對服務端的關閉連接請求報文的確認。

總結

本文基於tcpdump和一個簡單的客戶端和服務端,實例講解了TCP/IP協議,不止有協議的含義,而且有TCP/IP連接的建立,交互,關閉的一些細節。由於篇幅和tcpdump的輸出問題,未能將IP協議和TCP協議的報文頭的每個字段含義都講解一次,如果大家希望可以進一步了解,可以參考RFC 791 - Internet Protocol和RFC 793 - Transmission Control Protocol,還有TCP/IP協議卷一的相關內容。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM