loop_in_codes

低调做技术__欢迎移步我的独立博客 codemaro.com 微博 kevinlynx

tcp要点学习-建立连接

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的函数时,正常情况下就会开始三次握手。
随便在网上找张三次握手的图:

connection

如前文所述,三次握手也就是产生了三个数据包。客户端主动连接,发送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报文却被忽略。

posted on 2008-05-11 01:03 Kevin Lynx 阅读(3462) 评论(10)  编辑 收藏 引用 所属分类: game developnetwork

评论

# re: tcp要点学习-建立连接 2008-05-11 02:40 Fox

看来,这一块的东西,我又可以偷偷懒,直接请教你了;)  回复  更多评论   

# re: tcp要点学习-建立连接 2008-05-15 17:34 买书网

TCP协议中的要点写的很好  回复  更多评论   

# re: tcp要点学习-建立连接 2008-05-16 09:25

写的很通俗易懂。
  回复  更多评论   

# re: tcp要点学习-建立连接[未登录] 2008-09-03 22:21 thinkinnight

文章写得很好,看了之后我就自己想试了一下。结果发现自己编写的服务器、客户端在三次握手后,由客户端又向服务器发送了一个RST ACK,不知道是为什么。

服务器端并没有使用while循环,而只是一次的accept。我想原因可能在这里,但是具体的还是不清楚如何出的问题。能交流一下吗?谢谢  回复  更多评论   

# re: tcp要点学习-建立连接 2008-09-04 13:54 Kevin Lynx

@thinkinnight
发送RST通常都是因为异常退出导致的。可能你没有正常关闭。  回复  更多评论   

# re: tcp要点学习-建立连接[未登录] 2008-09-04 16:01 thinkinnight

Kevin Lynx你好, 关键是我不知道问题是出在哪里.程序是最简单的client和server, 为winsock, 代码的主要部分如下(去掉wsastartup和wsacleanup,其余socket相关代码均在下方,没有写close):

[server]

sockaddr_in server;
SOCKET s = socket(AF_INET, SOCK_STREAM, NULL);

server.sin_family = AF_INET;
server.sin_port = htons(5001);
server.sin_addr.S_un.S_addr = INADDR_ANY;

int _err = bind(s,(sockaddr *)&server, sizeof(server));
_err = listen(s,5);
SOCKET forclient = accept(s, (sockaddr *)NULL, NULL);


[client]

sockaddr_in client;
memset(&client,0,sizeof(client));

client.sin_family = AF_INET;
client.sin_port = htons(5001);
client.sin_addr.S_un.S_addr = inet_addr("192.168.0.2");

SOCKET s = socket(AF_INET,SOCK_STREAM,NULL);
int _err=connect(s, (sockaddr *)&client, sizeof(sockaddr));

windump结果(处理了一下,将ip隐藏了)
windump: listening on \Device\xxx_{xxxxxxx-xxxx-xxxx-xxxxx-xxxxxxxxxx}
15:53:20.162579 IP client.36519 > server.5001: S 36061147:360611
47(0) win 65535 <mss 1460,nop,nop,sackOK>
15:53:20.163065 IP server.5001 > client.36519: S 808783039:80878
3039(0) ack 36061148 win 65535 <mss 1460,nop,nop,sackOK>
15:53:20.163110 IP client.36519 > server.5001: . ack 1 win 65535

15:53:20.163243 IP client.36519 > server.5001: R 1:1(0) ack 1 wi
n 0

4 packets captured
26 packets received by filter
0 packets dropped by kernel

就是最后会有一个RST, ACK, 对这个不明白是为什么.
也不应该是半开端口吧.  回复  更多评论   

# re: tcp要点学习-建立连接[未登录] 2008-09-04 16:41 thinkinnight

windump是只监听到server 5001端口的通信,其他都被过滤掉了,有用的信息就这四条.  回复  更多评论   

# re: tcp要点学习-建立连接 2008-09-05 09:30 Kevin Lynx

加上正常关闭closesocket之类,在程序未退出前不要ctrl+c强制退出。你试下这些。我做实验也是在WIN平台下。  回复  更多评论   

# re: tcp要点学习-建立连接[未登录] 2008-09-06 21:34 thinkinnight

我试了一下,加上closesocket还是一样的结果,而且关键我觉得不是得到正确的序列,而是内部到底发生了什么造成这种结果。
刚看了TCP/IP卷一中对RST的描述,一共有三种情况:
1. 到不存在的端口的连接请求
2. 异常终止一个连接
3. 半打开连接

第一种情况下,应该是服务器向客户端发送RST。告知该端口不存在。
第二种情况,按照书上面的说法,需要置SO_LINGER,这样使得连接关闭时进行复位而不是正常的FIN,但是程序中并没有这样。可是现象倒是很符合。
第三种情况,书上也是接受方以RST作为应答,那现在也没有数据交互,而是直接出现RST,也不像。

所以只是想研究一下这种情况的原理、过程。
至于正常的情况,能做出来肯定是很好,不然反正书上那些内容也知道了,关键还是使用嘛,所以就是想对真实的现象有一些了解,我都怀疑是不是漏包了。。。但是看来看去,也只有这些
  回复  更多评论   

# re: tcp要点学习-建立连接 2010-02-11 10:03 tcpcoder

so good  回复  更多评论   


只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理