Author : Kevin Lynx
准备:
在这里本文将遵循上一篇文章的风格,只提TCP协议中的要点,这样我觉得可以更容易地掌握TCP。或者
根本谈不上掌握,对于这种纯理论的东西,即使你现在掌握了再多的细节,一段时间后也会淡忘。
在以后各种细节中,因为我们会涉及到分析一些TCP中的数据报,因此一个协议包截获工具必不可少。在
<TCP/IP详解>中一直使用tcpdump。这里因为我的系统是windows,所以只好使用windows平台的tcpdump,
也就是WinDump。在使用WinDump之前,你需要安装该程序使用的库WinpCap。
关于WinDump的具体用法你可以从网上其他地方获取,这里我只稍微提一下。要让WinDump开始监听数据,
首先需要确定让其监听哪一个网络设备(或者说是网络接口)。你可以:
windump -D
获取当前机器上的网络接口。然后使用:
windump -i 2
开始对网络接口2的数据监听。windump如同tcpdump(其实就是tcpdump)一样支持过滤表达式,windump
将会根据你提供的过滤表达式过滤不需要的网络数据包,例如:
windump -i 2 port 4000
那么windump只会显示端口号为4000的网络数据。
序号和确认号:
要讲解TCP的建立过程,也就是那个所谓的三次握手,就会涉及到序号和确认号这两个东西。翻书到TCP
的报文头,有两个很重要的域(都是32位)就是序号域和确认号域。可能有些同学会对TCP那个报文头有所
疑惑(能看懂我在讲什么的会产生这样的疑惑么?),这里我可以告诉你,你可以假想TCP的报文头就是个
C语言结构体(假想而已,去翻翻bsd对TCP的实现,肯定没这么简单),那么大致上,所谓的TCP报文头就是:
typedef struct _tcp_header
{
/**//// 16位源端口号
unsigned short src_port;
/**//// 16位目的端口号
unsigned short dst_port;
/**//// 32位序号
unsigned long seq_num;
/**//// 32位确认号
unsigned long ack_num;
/**//// 16位标志位[4位首部长度,保留6位,ACK、SYN之类的标志位]
unsigned short flag;
/**//// 16位窗口大小
unsigned short win_size;
/**//// 16位校验和
short crc_sum;
/**//// 16位紧急指针
short ptr;
/**//// 可选选项
/// how to implement this ?
} tcp_header;
那么,这个序号和确认号是什么?TCP报文为每一个字节都设置一个序号,觉得很奇怪?这里并不是为每一
字节附加一个序号(那会是多么可笑的编程手法?),而是为一个TCP报文附加一个序号,这个序号表示报文
中数据的第一个字节的序号,而其他数据则是根据离第一个数据的偏移来决定序号的,例如,现在有数据:
abcd。如果这段数据的序号为1200,那么a的序号就是1200,b的序号就是1201。而TCP发送的下一个数据包
的序号就会是上一个数据包最后一个字节的序号加一。例如efghi是abcd的下一个数据包,那么它的序号就
是1204。通过这种看似简单的方法,TCP就实现了为每一个字节设置序号的功能(终于明白为什么书上要告诉
我们‘为每一个字节设置一个序号’了吧?)。注意,设置序号是一种可以让TCP成为’可靠协议‘的手段。
TCP中各种乱七八糟的东西都是有目的的,大部分目的还是为了’可靠‘两个字。别把TCP看高深了,如果
让你来设计一个网络协议,目的需要告诉你是’可靠的‘,你就会明白为什么会产生那些乱七八糟的东西了。
接着看,确认号是什么?因为TCP会对接收到的数据包进行确认,发送确认数据包时,就会设置这个确认号,
确认号通常表示接收方希望接收到的下一段报文的序号。例如某一次接收方收到序号为1200的4字节数举报,
那么它发送确认报文给发送方时,就会设置确认号为1204。
大部分书上在讲确认号和序号时,都会说确认号是序号加一。这其实有点误解人,所以我才在这里废话了
半天(高手宽容下:D)。
开始三次握手:
如果你还不会简单的tcp socket编程,我建议你先去学学,这就好比你不会C++基本语法,就别去研究vtable
之类。
三次握手开始于客户端试图连接服务器端。当你调用诸如connect的函数时,正常情况下就会开始三次握手。
随便在网上找张三次握手的图:
如前文所述,三次握手也就是产生了三个数据包。客户端主动连接,发送SYN被设置了的报文(注意序号和
确认号,因为这里不包含用户数据,所以序号和确认号就是加一减一的关系)。服务器端收到该报文时,正
常情况下就发送SYN和ACK被设置了的报文作为确认,以及告诉客户端:我想打开我这边的连接(双工)。客户
端于是再对服务器端的SYN进行确认,于是再发送ACK报文。然后连接建立完毕。对于阻塞式socket而言,你
的connect可能就返回成功给你。
在进行了铺天盖地的罗利巴索的基础概念的讲解后,看看这个连接建立的过程,是不是简单得几近无聊?
我们来实际点,写个最简单的客户端代码:
sockaddr_in addr;
memset( &addr, 0, sizeof( addr ) );
addr.sin_family = AF_INET;
addr.sin_port = htons( 80 );
/**//// 220.181.37.55 addr.sin_addr.s_addr = inet_addr( "220.181.37.55" );
printf( "%s : connecting to server.\n", _str_time() );
int err = connect( s, (sockaddr*) &addr, sizeof( addr ) );
主要就是connect。运行程序前我们运行windump:
windump -i 2 host 220.181.37.55
00:38:22.979229 IP noname.domain.4397 > 220.181.37.55.80: S 2523219966:2523219966(0) win 65535 <mss 1460,nop,nop,sackOK>
00:38:23.024254 IP 220.181.37.55.80 > noname.domain.4397: S 1277008647:1277008647(0) ack 2523219967 win 2920 <mss 1440,nop,nop,sackOK>
00:38:23.024338 IP noname.domain.4397 > 220.181.37.55.80: . ack 1 win 65535
如何分析windump的结果,建议参看<tcp/ip详解>中对于tcpdump的描述。
建立连接的附加信息:
虽然SYN、ACK之类的报文没有用户数据,但是TCP还是附加了其他信息。最为重要的就是附加的MSS值。这个
可以被协商的MSS值基本上就只在建立连接时协商。如以上数据表示,MSS为1460字节。
连接的意外:
连接的意外我大致分为两种情况(也许还有更多情况):目的主机不可达、目的主机并没有在指定端口监听。
当目的主机不可达时,也就是说,SYN报文段根本无法到达对方(如果你的机器根本没插网线,你就不可达),
那么TCP收不到任何回复报文。这个时候,你会看到TCP中的定时器机制出现了。TCP对发出的SYN报文进行
计时,当在指定时间内没有得到回复报文时,TCP就会重传刚才的SYN报文。通常,各种不同的TCP实现对于
这个超时值都不同,但是据我观察,重传次数基本上都是3次。例如,我连接一个不可达的主机:
12:39:50.560690 IP cd-zhangmin.1573 > 220.181.37.55.1024: S 3117975575:3117975575(0) win 65535 <mss 1460,nop,nop,sackOK>
12:39:53.538734 IP cd-zhangmin.1573 > 220.181.37.55.1024: S 3117975575:3117975575(0) win 65535 <mss 1460,nop,nop,sackOK>
12:39:59.663726 IP cd-zhangmin.1573 > 220.181.37.55.1024: S 3117975575:3117975575(0) win 65535 <mss 1460,nop,nop,sackOK>
发出了三个序号一样的SYN报文,但是没有得到一个回复报文(废话)。每一个SYN报文之间的间隔时间都是
有规律的,在windows上是3秒6秒9秒12秒。上面的数据你看不到12秒这个数据,因为这是第三个报文发出的
时间和connect返回错误信息时的时间之差。另一方面,如果连接同一个网络,这个间隔时间又不同。例如
直接连局域网,间隔时间就差不多为500ms。
(我强烈建议你能运行windump去试验这里提到的每一个现象,如果你在ubuntu下使用tcpdump,记住sudo :D)
出现意外的第二种情况是如果主机数据包可达,但是试图连接的端口根本没有监听,那么发送SYN报文的这
方会收到RST被设置的报文(connect也会返回相应的信息给你),例如:
13:37:22.202532 IP cd-zhangmin.1658 > 7AURORA-CCTEST.7100: S 2417354281:2417354281(0) win 65535 <mss 1460,nop,nop,sackOK>
13:37:22.202627 IP 7AURORA-CCTEST.7100 > cd-zhangmin.1658: R 0:0(0) ack 2417354282 win 0
13:37:22.711415 IP cd-zhangmin.1658 > 7AURORA-CCTEST.7100: S 2417354281:2417354281(0) win 65535 <mss 1460,nop,nop,sackOK>
13:37:22.711498 IP 7AURORA-CCTEST.7100 > cd-zhangmin.1658: R 0:0(0) ack 1 win 0
13:37:23.367733 IP cd-zhangmin.1658 > 7AURORA-CCTEST.7100: S 2417354281:2417354281(0) win 65535 <mss 1460,nop,nop,sackOK>
13:37:23.367826 IP 7AURORA-CCTEST.7100 > cd-zhangmin.1658: R 0:0(0) ack 1 win 0
可以看出,7AURORA-CCTEST.7100返回了RST报文给我,但是我这边根本不在乎这个报文,继续发送SYN报文。
三次过后connect就返回了。(数据反映的事实是这样)
关于listen:
TCP服务器端会维护一个新连接的队列。当新连接上的客户端三次握手完成时,就会将其放入这个队列。这个队
列的大小是通过listen设置的。当这个队列满时,如果有新的客户端试图连接(发送SYN),服务器端丢弃报文,
同时不做任何回复。
总结:
TCP连接的建立的相关要点就是这些(or more?)。正常情况下就是三次握手,非正常情况下就是SYN三次超时,
以及收到RST报文却被忽略。