现在的位置: 首页 > 综合 > 正文

理解TCP/IP网络通信原理

2017年12月14日 ⁄ 综合 ⁄ 共 24050字 ⁄ 字号 评论关闭

为什么会有TCP/IP协议

在世界上各地,各种各样的电脑运行着各自不同的操作系统为大家服务,这些电脑在表达同一种信息的时候所使用的方法是千差万别。就好像圣经中上帝打乱了各地人的口音,让他们无法合作一样。计算机使用者意识到,计算机只是单兵作战并不会发挥太大的作用。只有把它们联合起来,电脑才会发挥出它最大的潜力。于是人们就想方设法的用电线把电脑连接到了一起。

但是简单的连到一起是远远不够的,就好像语言不同的两个人互相见了面,完全不能交流信息。因而他们需要定义一些共通的东西来进行交流,TCP/IP就是为此而生。TCP/IP不是一个协议,而是一个协议族的统称。里面包括了IP协议,IMCP协议,TCP协议,以及我们更加熟悉的http、ftp、pop3协议等等。电脑有了这些,就好像学会了外语一样,就可以和其他的计算机终端做自由的交流了。

TCP/IP协议分层

提到协议分层,我们很容易联想到ISO-OSI的七层协议经典架构,但是TCP/IP协议族的结构则稍有不同。如图所示

TCP/IP协议族按照层次由上到下,层层包装。最上面的就是应用层了,这里面有http,ftp,等等我们熟悉的协议。而第二层则是传输层,著名的TCP和UDP协议就在这个层次(不要告诉我你没用过udp玩星际)。第三层是网络层,IP协议就在这里,它负责对数据加上IP地址和其他的数据(后面会讲到)以确定传输的目标。第四层是叫数据链路层,这个层次为待传送的数据加入一个以太网协议头,并进行CRC编码,为最后的数据传输做准备。再往下则是硬件层次了,负责网络的传输,这个层次的定义包括网线的制式,网卡的定义等等(这些我们就不用关心了,我们也不做网卡),所以有些书并不把这个层次放在tcp/ip协议族里面,因为它几乎和tcp/ip协议的编写者没有任何的关系。发送协议的主机从上自下将数据按照协议封装,而接收数据的主机则按照协议从得到的数据包解开,最后拿到需要的数据。这种结构非常有栈的味道,所以某些文章也把tcp/ip协议族称为tcp/ip协议栈。

一些基本的常识

在学习协议之前,我们应该具备一些基本知识。

  • 互联网地址(ip地址)

    网络上每一个节点都必须有一个独立的Internet地址(也叫做IP地址)。现在,通常使用的IP地址是一个32bit的数字,也就是我们常说的IPv4标准,这32bit的数字分成四组,也就是常见的255.255.255.255的样式。IPv4标准上,地址被分为五类,我们常用的是B类地址。具体的分类请参考其他文档。需要注意的是IP地址是网络号+主机号的组合,这非常重要。

  • 域名系统

    域名系统是一个分布的数据库,它提供将主机名(就是网址啦)转换成IP地址的服务。

  • RFC

    RFC是什么?RFC就是tcp/ip协议的标准文档,在这里我们可以看到RFC那长长的定义列表,现在它一共有4000多个协议的定义,当然,我们所要学习的,也就是那么十几个协议而已。

  • 端口号(port)

    注意,这个号码是用在TCP,UDP上的一个逻辑号码,并不是一个硬件端口,我们平时说把某某端口封掉了,也只是在IP层次把带有这个号码的IP包给过滤掉了而已。

  • 应用编程接口

    现在常用的编程接口有socket和TLI。而前面的有时候也叫做“Berkeley socket”,可见Berkeley对于网络的发展有多大的贡献。

 

 

 

理解TCP/IP网络通信原理

一、 OSI模型

其中最下面两层:物理层与数据链路层可以不需要了解,他们主要是系统负责驱动的设备与网络硬件。

一般我们常用的网络协议是TCPUDP,但在TCP/UDP与数据链路层之间有一个网络层,我们可以直接绕过TCPUDP,直接使用IPv4或者IPv6,我们称为原始套节字(RAW Socket)。

上面三层统称应用层,主要有http,ftp,telnet等协议支持。

二、 网络工具

1、 Netstat

a) 提供接口信息

netstat –i

说明:其中eth0是以太网接口。lo是本地回旋(loopback)接口

b) 路由表

netstat  -r

c) 坎坎坷

2、 ifconfig

a) 查找所有接口信息

ifconfig  -a

b) 查找指定接口的详细信息

ifconfig   接口名

3、 ping

a) 查找多个ip,争对广播IP使用ping指令

ping  广播ip地址

三、 常用协议关系

(一) UDP

UDP是简单的传输层协议,UDP的主要问题是数据缺乏可靠性。如果要确保数据到达目的地,应用程序必须建立一组特性来保障。

每个数据报文都有一定长度,一般一个数据报就是一条记录。

UDP是无连接的。

TCP 是全双工的。

(二) TCP

TCP是有连接的。

TCP提供数据可靠传输。

TCP发送的数据是有序无界的。一般称之为流。

T CP提供数据缓冲流量控制。

TCP 是全双工的,在发送数据的同时,也可以接收数据。

(三) 协议头的关系

四、 TCP的连接关闭过程

1、 服务器准备好接收外来连接,称为被动打开(passive open)。该过程由socketbindlisten等函数完成。

2、 客户主动打开连接(active open)。该过程由connect函数完成。该过程中客户一般SYN分节同步,告诉服务器在将要的连接中发送的数据的初始序列号。SYN分节不携带数据,只有IP头部,TCP头部和可能的TCP选项。

备注:常用TCP选项如下

MSS选项:SYN的最大分节大小(maximum segment size)。

接收窗口大小选项:最大数据接收空间大小。

上面两个选项可以使用socketopt设置,分别对应TCP_MAXSEGSO_RCVBUF

3、 服务器确认客户的SYN分节,同时也发送一个SYN分节给客户,也包含在将要连接中发送数据的初始序列号。同时服务器还发送一个对SYNACK

4、 客户确认服务器的SYN

上面过程需要交换三个分组,一般称为TCP的三路握手(three-way  hand-shake)。

其中JK是数据的初始序列号。ACK确认的是下一个将要接收的数据的期望序列号。

TCP的连接需要三个分节,TCP的关闭需要4个分节。

TCP的状态图:

五、 IP头布局

结构体定义:

struct ip

  {

#if __BYTE_ORDER == __LITTLE_ENDIAN

    unsigned int ip_hl:4; //header length

    unsigned int ip_v:4; //version

#endif

#if __BYTE_ORDER == __BIG_ENDIAN

    unsigned int ip_v:4; //version

    unsigned int ip_hl:4; //header length

#endif

    u_int8_t ip_tos; // type of service

    u_short ip_len; // total length

    u_short ip_id; // identification

    u_short ip_off; // fragment offset field

#define IP_RF 0x8000 // reserved fragment flag

#define IP_DF 0x4000 // dont fragment flag

#define IP_MF 0x2000 // more fragments flag

#define IP_OFFMASK 0x1fff // mask for fragmenting bits

    u_int8_t ip_ttl; // time to live

    u_int8_t ip_p; // protocol 

    u_short ip_sum; // checksum 

    struct in_addr ip_src, ip_dst; // source and dest address 

  };

下面是每个字段的说明。

字段

字段说明

unsigned int ip_v:4

版本。常见版本:

4 IPv4

5 Stream IP Datagram mode (experimental IP)

6 IPv6

7 TP/IX (the "next" Internet Protocol)

8 The "P" Internet Protocol

9 TUBA

其中IPv4的版本使用宏定义:

#define IPVERSION 4               /* IP version number */

unsigned int ip_hl:4

IP头长度。单位是word,每个word4字节。

比如5word,就是20个字节。

u_int8_t ip_tos

服务类型。指数据包的管理方式。设置为0表示按正常方式管理。

Tos有四种选项可以设置:Minimum Delay, Maximum Throughput, Maximum Reliability, and Minimum Cost。这四个选项可以使用setsockopt设置。

u_short ip_len

u_short ip_id

数据包ID。每个数据包唯一的ID

u_short ip_off

数据包是否分节表示。

#define IP_RF 0x8000 /* reserved fragment flag */

#define IP_DF 0x4000 /* dont fragment flag */

#define IP_MF 0x2000 /* more fragments flag */

#define IP_OFFMASK 0x1fff /* mask for fragmenting bits */

u_int8_t ip_ttl

u_int8_t ip_p

u_short ip_sum

struct in_addr ip_dst

struct in_addr ip_src

六、 UDP头布局

七、 TCP头布局

 
TCPTCP/IP协议族中非常复杂的一个协议。它具有以下特点:

    1:面向连接的运输层协议。在使用TCP协议之前,首先需要建立TCP连接。传送数据完毕后,必须释放已经建立的TCP连接。

    2:一条TCP连接有两个端点,连接是点对点的。

    3:提供可靠交付的服务。通过TCP连接传送的数据,不会出现差错不会丢失并且按序到达。

    4:提供全双工通信。TCP允许通信双方的应用程序进程在任何时候都能发送数据。TCP连接的两端都设有缓存,分为发送缓存和接收缓存,用来临时存放双向通信的数据。发送时,应用程序把数据传给TCP的缓存后,就可以做自己的事情了。TCP会在合适的时候把数据发送出去。接收时,TCP把接收到的数据放入缓存,供上层的应用程序读取。

    5:面向字节流。虽然应用程序和TCP的交互是一次一个数据块,但TCP把应用程序交下来的数据看成一串无结构的字节流。TCP对应用进程一次把多长的报文发送到TCP缓存中是不关心的。TCP根据对方给出的窗口值和当前网络的拥塞程度决定一个报文段包含多少字节。如果应用程序传到TCP缓存的数据块太长,TCP就会把它划分短些再传送。如果应用程序发来的数据太少时,TCP将会等待积累足够多的字节后再构成报文段发送出去。


      TCP连接的端点叫做套接字。它是由IP和端口号构成。每一条TCP连接唯一的被通信两端的两个端点所确定。同一个IP地址可以有多个不同的TCP连接,而同一个端口号也可以出现在多个不同的TCP连接中。

     TCP发送的报文段是交付给IP层传送的,但IP层只提供尽最大努力的交付。也就是说TCP下面的网络所提供的是不可靠的服务。可靠传输必须依靠TCP来实现。停止等待协议就是一种方式。


     停止等待就是每发送完一个分组就停止发送,等待接收方的确认,在收到确认后再传送下一个分组。如果发送方在一段时间后仍然没有收到确认,就认为刚才发送的分组丢失了,因而重传前面发送过的分组,这被称为超时重传。要实现超时重传,就要在每发送完一个分组后设置一个超时计时器。如果在超时计时器到期之前收到了对方的确认,就撤销已设置的超时计时器。因此发送方在发送完一个分组之后,必须暂时保存已发送的分组的副本。只有在收到响应的确认后才能清除暂时保留的分组的副本。分组和确认分组都必须进行编号,这样才能知道哪一个发送过的数据已被确认,那些没有收到确认。超时计时器设置的重传时间应该比数据在分组传输的平均往返时间更长一些。超时重传时间的设定是非常复杂的,因为已发送的分组到底经过那些网络,以及这些网络会产生多大的延迟都是不确定的。


        如果发送方发送了数据后,在超时时间内没有收到接收方的确认,它会重传数据。如果此时接受方再次接收到了此数据,它会将此数据丢弃,并向发送方发送确认。发送方没有收到确认,可能是因为接受方的确认出错或丢失。发送方还可能收到重复的确认,对待重复的确认只需要丢弃即可。上述可靠的传输协议被称为自动重传请求ARQAutomatic Repeat Request)。采用停止等待协议可能会导致信道利用率非常低,为了提高传输效率,需要使用流水线传输。也就是说发送方可以连续发送多个分组,不必没发完一个分组就停下来等待对方的确认。这样可以使信道上一直有数据不间断的在传送。这被称为连续ARQ协议或滑动窗口协议。它比较复杂但却是TCP协议的精髓。


       所谓滑动窗口就是说位于此窗口内的分组可以被连续的发送出去,而不需要等待对方的确认。发送方每收到一个确认,就会把发送窗口向前滑动一个分组的位置。假设此时1-5个分组位于发送窗口内,这5个分组就会被连续的发送出去。当发送方收到第一个分组的确认后,就会向后移动一个分组的位置,此时就可以发送第六个分组了。接收方一般都采用累积确认的方式。也就是说,接收方不必对收到的每个分组逐个发送确认,而是可以收到几个分组后,对按序到达的最后一个分组发送确认。这样就表示到这个分组为止的所有分组都被正确接收。如果此时发送方发送了前5个分组,而第三个分组丢失了,这是接收方只能对前两个分组发送确认。发送方不知道后面三个分组的下落。实际上仅仅第三个分组没有收到,但是发送方仍然会发送后三个分组。


     TCP是面向字节流的,但是TCP传送的数据却是报文段。一个TCP报文段分为首部和数据两部分。只有真正弄清TCP的首部各字段的作用才能掌握TCP的原理。

     TCP的前20个字节是固定的。后面的40字节是根据需要增加的。因此TCP首部的最小长度时20

     1:源端口和目的端口:各占两个字节。

     2:序号:四个字节。共232次方个序号。TCP连接中传送的字节流中的每一个字节都按顺序编号。此处的序号字段指的是本报文段所发送的数据第一个字节的序号。

     3:确认号:4个字节。表示期望收到对方下一个报文段的第一个数据字节的序号。如确认号是N,则表明序号N-1之前的数据都正确收到。确认号也是4个字节,可以对4GB数据进行编号。

     4:数据偏移,4位。它指出TCP报文段的数据起始处距离TCP报文段起始处有多远。这个字段实际上指出了TCP的首部。由于首部中有长度不确定的选项字段,因此此数据偏移是必要的。数据偏移只有4位,但是它的一个单位代表4字节。因此数据偏移的最大值是6015*4)。即TCP首部的最大长度。

     5:保留 6位。保留为以后使用。都是0.

     6:紧急URG 当此处为1时,表明紧急指针字段有效。它告诉系统此报文段有紧急数据,需要尽快传送。系统会把紧急数据插入TCP缓存的最前面。它和紧急指针字段配合使用。

     7:确认ACK:ACK=1时,确认号字段才有效。当ACK=0时确认号无效。当连接建立前ACK=0,建立之后ACK就一直是1了。

     8:推送PSH。当PSH=1时,系统会立即将此报文段发送出去,而不再等待整个缓存都被填满后才向上交付。

     9:复位RST。当其为1,时,表明TCP出现严重错误,必须释放连接然后再重新建立连。

     10:同步SYN:在连接建立时用来同步序号。当其为1ACK=0时,表明这是一个(同步)连接请求报文段。当对方同意后,应在响应报文段中使用SYN=1ACK=1。因此SYN=1,要么表示连接请求要么表示连接接受报文。

     11:终止FIN:用来释放连接。当其为1时,表明此报文段的发送方的数据已经发送完毕,要求释放连接。

     12:窗口:2字节。窗口指发送方的接收窗口。它告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量。之所以有此限制是因为接收方的数据缓存空间是有限的。它指出现在允许对方发送数据量。由于接收缓存不断变化,因此窗口值也不断变化。

     13:检验和:2字节。检验和检验 的是首部和数据这两部分。在计算时还应加上12字节的伪首部。

     14:紧急指针:2字节,此字段只在紧急URG=1时才有意义。它指出本报文段中紧急数据的字节数。由于紧急数据放在了缓冲区最前方,紧急指针指出的是紧急数据的末尾在报文段的位置。当所有的紧急数据都处理完后,TCP 就告诉应用程序恢复正常操作。

     15:选项:长度可变,最长可达40字节。当没有选项时,首部长度时20.


     MSS是选项的一种。它被称为最大报文段长度,是每个TCP报文段中数据字段的最大长度。它加上TCP首部才是整个TCP报文段。再加上20字节的IP首部才能组装成一个IP数据报。当MSS非常小时,网络的利用率就低。如果发送只含一个字节的数据时,在IP层传输至少需要40字节的开销(IP20字节,TCP头至少20)。到链路层还需要开销。如果MSS非常大,在IP层传输时就可能要分片,到终点后再将各个分片组装成原来的TCP报文段,这也会使开销增大。因此,MSS可以尽量大,只要在IP层传输时不分片就行了。MSS的默认长度时536

    窗口扩大选项是为了扩大窗口,窗口字段长度是16位,因此最大的窗口大小是64字节。窗口扩大选项占3个字节。每一个字节表示移位值。新的窗口值等于原来的16+窗口扩大选项的值。这相当于把窗口值向左移动移位值位。

    时间戳选项占10字节,最主要的两个地段是:时间戳值和时间戳回送回答字段。

时间戳选项具有以下两个功能

    1:计算往返时间。

    2:用于处理TCP序号超过232次方的情况。这又被称为防止序号绕回。在报文段中加入时间戳就可以区分新的报文段和迟到很久的报文段。


    socket的两端分别有两个窗口,发送窗口和接收窗口。滑动窗口的单位是字节,假设A收到了B的确认报文段,其中窗口时20,而确认号是31,这表明B期望接受的下一个序号是31,到序号30为止的数据已经收到了。根据这两个数据,A就构造自己的发送窗口。它的起始值为31,而末尾值为50。发送窗口表示在没有收到B的确认前,A可以连续把窗口内的数据都发送出去。


     凡是已经发送过得数据,在未收到确认之前都必须暂时保留,以便在在超时重传。发送窗口内的序号表示允许发送的序号。

     发送窗口与缓存的关系。

     发送窗口存储应用程序将要发送的数据,以及发送但却为收到确认的数据。 发送窗口只是发送缓存的一部分,已经被确认过的数据应当从发送缓存中删除。因此发送缓存和发送窗口的后沿是重合的。

接收缓存用来暂时存放按需到达的,但还未被应用程序读取的数据以及未按序到达的数据。 如果收到的分组有差错,就要丢弃。如果接收应用程序来不及读取,接收缓存就会被填满,就会减少接受窗口,直到减为0。反之就可以增大。

     虽然发送方的发送窗口是根据接收方方接收窗口设置但是,但同一时刻,发送窗口并不一定与接收窗口相同。这是因为通过网络传送窗口值需要经历一定的延迟。

     TCP要求接收方必须有累计确认的功能,这样可以减小传输开销。接收方可以在合适的时候发送确认,也可以在自己有数据发送时把确认信息捎上。但应该注意的是接收方不应过分推迟发送确认,否则会导致发送方不必要的重传,这反而会浪费网络资源。

     TCP利用滑动窗口来实现流量控制。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接受。在连接建立时,发送方告诉接收方我的接受窗口是x,接收方收到后会设置自己的发送窗口。发送方的发送窗口不能超过接收方给出的接收窗口的数值。TCP窗口的单位是字节,而不是报文段。在每次发送数据是报文段内会将此时接受窗口告诉发送者。发送者会根据收到的报文段接收窗口的值调整自己的发送窗口。当接收方接收缓存满时就发送给发送方零窗口,告诉发送方停止发送。假设过一段时间接收方调整接收窗口为100,而此报文段在传送过程中丢失,这就导致发送方等待接收方的非零窗口通知,而接收方在等待发送方的数据。这样就导致了死锁。为了防止这种情况,TCP为每个连接设置一个持续计时器。TCP连接的一方收到零窗口通知后,就启动计时器,设置的时间到期后它会发送一个探测报文段。如果此时返回的仍然是零窗口,则重新设定计时器。如果窗口不是零,那么死锁的僵局就可以被打破了。


      TCP是面向连接的协议。分为三个阶段:

      一:连接建立

      二:数据传送

      三:连接释放。


    在TCP连接建立的过程中需要解决一下三个问题:

    1:要使每一方能够通知对方的存在。

    2:允许双方协商一些参数。如窗口最大值、是否使用窗口扩大选项和时间戳选项。

    3:能够对运输实体资源,如缓存进行分配。


    TCP连接的建立采取客户服务器方式。主动发起连接的叫做客户。被动等待连接建立的应用程序叫做服务器。


    TCP的建立连接需要客户服务器通信三次。这就是常说的三次握手。

    首先服务器创建socket,绑定端口并进入监听模式等待客户端的请求。客户端B向服务器发送连接请求报文段。此时同部位SYN=1,初始序号seq=xACK=0(连接未建立,确认字段无效)。此时客户端进入同步--已发送状态。

    服务器收到连接请求,如同意建立连接,则会发送确认。此时SYN=1ACK=1,确认号ack=x+1。设置自己的初始序号seq=y。此时服务器进入同步--收到状态。

    客户端收到确认后,还要给服务器进行确认,此时ACK=1ack=y+1.seq=x+1。此时的报文段已经可以携带数据了。此时客户端进入已经建立状态。

    服务器收到后也进入已建立状态。


      很多人会很疑问会什么客户端还需要发送一次确认,这主要是防止已失效的连接请求报文段突然又传送到服务器。比如客户端发出连接请求,但此请求丢失,没有收到服务器发来的额确认。于是一段时间后客户端继续请求,此请求被服务器接收,客户端收到了服务器的请求。数据传输完毕后,连接被释放。如果此时客户端第一次发送的报文段在网络中滞留一段时间后到达服务器,服务器收到这个失效的请求,却误以为是客户端发出的新的连接请求。于是向客户端发送同一连接请求。由于客户端没有发出连接请求,当然会将此报文丢弃。而服务器却一直等待客户端的回复。

      TCP连接的释放。

      数据传输结束后任何一方都可以申请释放连接。AB发送连接释放报文段,并停止发送数据。主动关闭TCP连接。此时
A
的连接释放报文段首部FIN应置为1,序号seq=x,等于前面已传送过得数据的最后一个序号加一。此时A进入终止--等待(FIN-WAIT-1)状态1。等待B的确认。TCP规定FIN报文段即使不携带数据也要消耗一个序号。

B收到连接释放报文段后即发出确认,确认号ackx+1seq=y。然后B就进入了关闭--等待(CLOSE_WAIT)状态。此时
AB这个方向的连接已经释放了,这是TCP处于半关闭状态。即A不会在发送数据,但B要发送数据A
仍要接收。
A收到B的确认后就进入了终止--等待(FIN-WAIT-2)状态2,等待B发出的连接释放报文段。若B没有
数据要发送给
A,它发出连接释放报文段将FIN置为1ACK=1seq=wack=u+1.此时B处于最后确认状态。
等待
A的确认。A在收到B的连接释放报文段后,必须对此进行确认。将ACK置为1,确认号ack=w+1,
seq=u+1
。然后进入时间--等待(TIME-WAIT)状态,此时TCP连接还没有被释放,还必须经过时间计时器设置的2MSL
A
才进入关闭状态。MSLMaximum segment Lifetime。即最长报文段寿命。


       之所以要设置这个最大报文段寿命,有两个原因:

       1:为保证A发送的最后一个ACK报文能够到达B。因为它有可能丢失,此时便导致B收不到对方对FIN+ACK报文段的确认。此后B会超时重传。由于A设置了2MSL计时器,使得A有机会收到B的报文。如果A不设置2MSL,而是发送完ACK报文段后立即释放,就无法收到B重传的FIN+ACK报文段。B也就无法按正常步骤进入CLOSED状态。

       2:防止已失效的连接请求报文段出现。A发送完最后一个ACK报文段后,等待2MSL就可使本此连接所产生的所有报文段都从网络中消失,不会对下一次的连接造成影响。

       除等待计时器外,TCP还设置一个保活计时器。如果客户端主动与服务器建立起连接,而此后客户端突然出现故障,此后服务器不能收到从客户端发来的数据,此时应采取一定的措施,是服务器不白白等待。服务器每收到一次客户端的数据就重置保活计时器,时间通常是2小时。若两小时没有收到客户端的数据,服务器就发送探测报文,以后每隔75分钟发送一次,若一连发送10探测报文段后仍无客户端的响应,服务器就认为客户端出现故障,接着就关闭了连接。

       在客户--服务器模型中,通常务器分配的一个socket对某一端口进行监听。一旦监听到有连接请求。就调用acceptaccept将会产生新的socket。然后服务器创建新的线程,新的socket将会作为新线程的参数。系统会为这个新的socket分配一个服务器端的自由端口号,这个套接字专用于在连接建立后与客户端交换数据。它一般被称为响应套接字。

  看一个例子:

  1. SADATA       wsd;  
  2.     int err;  
  3.     err=WSAStartup(MAKEWORD(2,2), &wsd);  
  4.     if(err!=0)//网络有问题  
  5.     {  
  6.         printf("发生未知网络错误,程序将退出");  
  7.   
  8.         return 0;  
  9.     }  
  10.     SOCKET ListenSocket=socket(AF_INET,SOCK_STREAM,IPPROTO_IP);;  
  11.     struct sockaddr_in local,client;  
  12.     int     iAddrSize;  
  13.     local.sin_addr.s_addr = htonl(INADDR_ANY);  
  14.     local.sin_family = AF_INET;  
  15.     local.sin_port = htons(80);  
  16.     if(bind(ListenSocket, (struct sockaddr *)&local,sizeof(local))==SOCKET_ERROR)//绑定到80端口。  
  17.     {  
  18.         printf("bind error\r\n");  
  19.         return 0;  
  20.     }  
  21.     if(listen(ListenSocket, SOMAXCONN)==SOCKET_ERROR)//监听失败  
  22.     {  
  23.         printf("Listen error!\n");  
  24.     }  
  25.     printf("Listen on 80!\n");  
  26.     int num=1;  
  27.     while(1)  
  28.     {  
  29.         iAddrSize = sizeof(client);  
  30.         SOCKET sServerSocket = accept(ListenSocket,(struct sockaddr *)&client,&iAddrSize);  
  31.   
  32.         if(sServerSocket!=INVALID_SOCKET)  
  33.         {  
  34.   
  35.             printf("IP: %s 连接\r\n",inet_ntoa(client.sin_addr));  
  36.             DWORD TID;  
  37.             HANDLE hHandle= CreateThread(NULL,NULL,RecvThreadDirect,(LPVOID)sServerSocket,0,&TID);//创建线程处理请求,参数为请求连接的socket。  
  38.             printf("创建第%d个线程",num);  
  39.             num++;  
  40.             //WaitForSingleObject(hHandle,INFINITE);  
  41.             CloseHandle(hHandle);  
  42.             Sleep(100);  
  43.   
  44.         }  
  45.         else  
  46.             break;  
  47.     }  

 

 

 

连接建立和终止

先看一下telnet连接服务器80端口的抓包:

tcp

 

上图由wireshark抓取,并显示了TCP状态图(注意:由于网络阻塞,发生了丢包现象,4是对2的重发,而5是对4的响应(同3相同))。

根据上图可以看到建立一个TCP连接的过程为(三次握手的过程):

  1. 客户端向服务器端发送一个SYN请求,同时传送一个初始序列号(ISN);
  2. 服务器发回包含客户端初始序列号的SYN报文段作为应答,同时将ACK序号设置为ISN+1;
  3. 客户端向服务器发送一个ACK确认,ACK序号为ISN+1.

终止一个TCP连接需要4次握手,这是由于TCP的半关闭(当一方调用shutdown关闭连接后,另一端还是可以发送数据,典型的例子为rsh)导致的:TCP连接是全双工的,连接的每一端在关闭连接时都向对方发送一个FIN来终止连接,同时对方会对其进行确认(回复ACK)。通常,都是一方完成主动关闭,另一方来完成被动关闭:

  1. 以上面的抓包为例,客户端向服务器发送了一个FIN(NO. 6);
  2. 服务器端对上面的FIN进行确认(NO. 7),同时向客户端发送一个FIN(这儿其实是两个动作,一个是对上面FIN的ACK,另一个是发送一个FIN,但由于TCP的捎带ACK机制,两者放在一个包里发送了);
  3. 客户端对服务器端的FIN进行确认(NO. 8)。

连接超时

很多情况下,连接可能不会那么顺利,比如发生了丢包就需要重传(NO. 4)。当连接无法建立的时候,会是什么情况呢,看下抓包情况:

tcp_timeout_dump

可以看到,当连接失败时会进行几次重试,但在31s后放弃连接。重试的时间间隔很有规律:1,2, 4, 8, 16,即每次的重试间隔都是上次间隔的2倍。

MSS

最大报文长度(MSS)表示TCP传往另一端的最大块数据的长度。MSS在连接建立时传送给对方,只会出现在SYN报文段中。

MSS让主机限制另一端发送数据报的长度。

TCP状态变迁图

1

TIME_WAIT状态

  1. TIME_WAIT状态也称2MSL等待状态,它是任何报文段被丢弃前在网络内的最长时间。当TCP执行一个主动关闭,并发回一个最后ACK,该连接必须处于TIME_WAIT状态,并停留2MSL时间,这可以让TCP再次发送最后的ACK以防这个ACK丢失。
  2. 在该2MSL期间,该连接占用的端口不可用,任何迟到的报文也会丢弃。通常,为了服务器的快速重启(需要重用端口),可以设置SO_REUSEADDR选项。
  3. 而对于一个被动关闭的TCP连接来说,不会进入TIME_WAIT状态。

FIN_WAIT_2状态表示我们已经发出了FIN,并且对方已对其进行了确认,但对方还未发回FIN以关闭对方连接。只有当对方完成这个关闭,本端才会由FIN_WAIT_2状态进入TIME_WAIT状态。

复位报文段

  1. 当连接到一个不在监听的端口时,客户端回收到一个RST响应(UDP连接到一个不存在的端口时会产生一个ICMP端口不可达的差错)。
  2. 在连接终止时,也可以通过发送一个复位报文段而不是FIN来终止连接,可通过设置SO_LINGER来这么做。
  3. 可通过TCP的SO_KEEPALIVE选项来检测半打开连接,当检测到这种连接时会发送一个RST报文。关于该选项更多的内容参见http://www.tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO/

SO_LINGER 选项

此选项指定函数close对面向连接的协议如何操作(如TCP)。内核缺省close操作是立即返回,如果有数据残留在套接口缓冲区中则系统将试着将这些数据发送给对方。

SO_LINGER选项用来改变此缺省设置。使用如下结构:

struct linger {

int l_onoff; /* 0 = off, nozero = on */

int l_linger; /* linger time */

};

有下列三种情况:

1、设置 l_onoff为0,则该选项关闭,l_linger的值被忽略,等于内核缺省情况,close调用会立即返回给调用者,如果可能将会传输任何未发送的数据;

2、设置 l_onoff为非0,l_linger为0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态;

3、设置 l_onoff 为非0,l_linger为非0,当套接口关闭时内核将拖延一段时间(由l_linger决定)。如果套接口缓冲区中仍残留数据,进程将处于睡眠状态,直 到(a)所有数据发送完且被对方确认,之后进行正常的终止序列(描述字访问计数为0)或(b)延迟时间到。此种情况下,应用程序检查close的返回值是非常重要的,如果在数据发送完并被确认前时间到,close将返回EWOULDBLOCK错误且套接口发送缓冲区中的任何数据都丢失。close的成功返回仅告诉我们发送的数据(和FIN)已由对方TCP确认,它并不能告诉我们对方应用进程是否已读了数据。如果套接口设为非阻塞的,它将不等待close完成。

注释:l_linger的单位依赖于实现: 4.4BSD假设其单位是时钟滴答(百分之一秒),但Posix.1g规定单位为秒。

 

同时打开

两个应用程序同时执行主动打开的情况是可能的,虽然发生的可能性较低。每一端都发送一个SYN,并传递给对方,且每一端都使用对端所知的端口作为本地端口。例如:

主机a中一应用程序使用7777作为本地端口,并连接到主机b 8888端口做主动打开。

主机b中一应用程序使用8888作为本地端口,并连接到主机a 7777端口做主动打开。

tcp协议在遇到这种情况时,只会打开一条连接。

这个连接的建立过程需要4次数据交换,而一个典型的连接建立只需要3次交换(即3次握手)

但多数伯克利版的tcp/ip实现并不支持同时打开。

2

如果应用程序同时发送FIN,则在发送后会首先进入FIN_WAIT_1状态。在收到对端的FIN后,回复一个ACK,会进入CLOSING状态。在收到对端的ACK后,进入TIME_WAIT状态。这种情况称为同时关闭。

同时关闭也需要有4次报文交换,与典型的关闭相同。

4

 

 

 

TCP交互数据流,成块数据流

目前建立在TCP协议上的网络协议特别多,有telnet,ssh,有ftp,有http等等。这些协议又可以根据数据吞吐量来大致分成两大类:(1)交互数据类型,例如telnet,ssh,这种类型的协议在大多数情况下只是做小流量的数据交换,比如说按一下键盘,回显一些文字等等。(2)数据成块类型,例如ftp,这种类型的协议要求TCP能尽量的运载数据,把数据的吞吐量做到最大,并尽可能的提高效率。针对这两种情况,TCP给出了两种不同的策略来进行数据传输。

1.TCP的交互数据流

对于交互性要求比较高的应用,TCP给出两个策略来提高发送效率和减低网络负担:(1)捎带ACK。(2)Nagle算法(一次尽量多的发数据)。通常,在网络速度很快的情况下,比如用lo接口进行telnet通信,当按下字母键并要求回显的时候,客户端和服务器将经历发送按键数据->服务器发送按键数据的ack -> 服务器端发送回显数据->客户端发送回显数据的ACK的过程,而其中的数据流量将是40bit + 41bit+41bit+40bit = 162bit,如果在广域网里面,这种小分组的TCP流量将会造成很大的网络负担。

1.1.捎带ACK的发送方式

这个策略是说,当主机收到远程主机的TCP数据报之后,通常不马上发送ACK数据报,而是等上一个短暂的时间,如果这段时间里面主机还有发送到远程主机的TCP数据报,那么就把这个ACK数据报“捎带”着发送出去,把本来两个TCP数据报整合成一个发送。一般的,这个时间是200ms。可以明显地看到这个策略可以把TCP数据报的利用率提高很多。

1.2.Nagle算法

上过bbs的人应该都会有感受,就是在网络慢的时候发贴,有时键入一串字符串以后,经过一段时间,客户端“发疯”一样突然回显出很多内容,就好像数据一下子传过来了一样,这就是Nagle算法的作用。

Nagle算法是说,当主机A给主机B发送了一个TCP数据报并进入等待主机B的ACK数据报的状态时,TCP的输出缓冲区里面只能有一个TCP数据报,并且,这个数据报不断地收集后来的数据,整合成一个大的数据报,等到B主机的ACK包一到,就把这些数据“一股脑”的发送出去。虽然这样的描述有些不准确,但还算形象和易于理解,我们同样可以体会到这个策略对于低减网络负担的好处。

在编写插口程序的时候,可以通过TCP_NODELAY来关闭这个算法。并且,使用这个算法看情况的,比如基于TCP的X窗口协议,如果处理鼠标事件时还是用这个算法,那么“延迟”可就非常大了。

2.TCP的成块数据流

对于FTP这样对于数据吞吐量有较高要求的要求,将总是希望每次尽量多的发送数据到对方主机,就算是有点“延迟”也无所谓。TCP也提供了一整套的策略来支持这样的需求。TCP协议中有16个bit表示“窗口”的大小,这是这些策略的核心。

2.1.传输数据时ACK的问题

在解释滑动窗口前,需要看看ACK的应答策略,一般来说,发送端发送一个TCP数据报,那么接收端就应该发送一个ACK数据报。但是事实上却不是这样,发送端将会连续发送数据尽量填满接受方的缓冲区,而接受方对这些数据只要发送一个ACK报文来回应就可以了,这就是ACK的累积特性,这个特性大大减少了发送端和接收端的负担。

2.2.滑动窗口

滑动窗口本质上是描述接受方的TCP数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据。如果发送方收到接受方的窗口大小为0的TCP数据报,那么发送方将停止发送数据,等到接受方发送窗口大小不为0的数据报的到来。书中的P211和P212很好的解释了这一点。

关于滑动窗口协议,书上还介绍了三个术语,分别是:

  1. 窗口合拢:当窗口从左边向右边靠近的时候,这种现象发生在数据被发送和确认的时候。
  2. 窗口张开:当窗口的右边沿向右边移动的时候,这种现象发生在接受端处理了数据以后。
  3. 窗口收缩:当窗口的右边沿向左边移动的时候,这种现象不常发生。

TCP就是用这个窗口,慢慢的从数据的左边移动到右边,把处于窗口范围内的数据发送出去(但不用发送所有,只是处于窗口内的数据可以发送。)。这就是窗口的意义。图20-6解释了这一点。窗口的大小是可以通过socket来制定的,4096并不是最理想的窗口大小,而16384则可以使吞吐量大大的增加。

2.3.数据拥塞

上面的策略用于局域网内传输还可以,但是用在广域网中就可能会出现问题,最大的问题就是当传输时出现了瓶颈(比如说一定要经过一个slip低速链路)所产生的大量数据堵塞问题(拥塞),为了解决这个问题,TCP发送方需要确认连接双方的线路的数据最大吞吐量是多少。这,就是所谓的拥塞窗口。

拥塞窗口的原理很简单,TCP发送方首先发送一个数据报,然后等待对方的回应,得到回应后就把这个窗口的大小加倍,然后连续发送两个数据报,等到对方回应以后,再把这个窗口加倍(先是2的指数倍,到一定程度后就变成现行增长,这就是所谓的慢启动),发送更多的数据报,直到出现超时错误,这样,发送端就了解到了通信双方的线路承载能力,也就确定了拥塞窗口的大小,发送方就用这个拥塞窗口的大小发送数据。要观察这个现象是非常容易的,我们一般在下载数据的时候,速度都是慢慢“冲起来的”

以上就是TCP数据传输的大致流程,虽然并不细致,但是足以描述TCP的工作原理,重点是TCP的流量控制原理,滑动窗口,拥塞窗口,ACK累计确认等知识点。

 

超时重传是TCP协议保证数据可靠性的另一个重要机制,其原理是在发送某一个数据以后就开启一个计时器,在一定时间内如果没有得到发送的数据报的ACK报文,那么就重新发送数据,直到发送成功为止。

1.超时

超时时间的计算是超时的核心部分,TCP要求这个算法能大致估计出当前的网络状况,虽然这确实很困难。要求精确的原因有两个:(1)定时长久会造成网络利用率不高。(2)定时太短会造成多次重传,使得网络阻塞。所以,书中给出了一套经验公式,和其他的保证计时器准确的措施。

1.1.递推公式概说

最早的TCP曾经用了一个非常简单的公式来估计当前网络的状况,如下

R<-aR+(1-a)M

RTP=Rb

其中a是一个经验系数为0.1,b通常为2。注意,这是经验,没有推导过程,这个数值是可以被修改的。这个公式是说用旧的RTT(R)和新的RTT(M)综合到一起来考虑新的RTT(R)的大小。但是,我们又看到,这种估计在网络变化很大的情况下完全不能做出“灵敏的反应”(Jacoboson说的,不是偶说的,呵呵),于是就有下面的修正公式:

Err=M-A

A<-A+gErr

D<-D+h(|Err|-D)

RTO=A+4D

具体的解释请看书的228页,这个递推公式甚至把方差这种统计概念也使用了进来,使得偏差更加的小。而且,必须要指出的是,这两组公式更新,都是在数据成功传输的情况下才进行,在发生数据重新传输的情况下,并不使用上面的公式进行网络估计,理由很简单,因为程序已经不在正常状态下了,估计出来的数据也是没有意义的。

1.2.RTO的初始化

RTO的初始化是由公式决定的,例如最初的公式,初始的值应该是1。而修正公式,初始RTO应该是A+4D。

1.3.RTO的更新

当数据正常传输的情况下,我们就会用上面的公式来更新各个数据,并重开定时器,来保证下一个数据被顺利传输。要注意的是:重传的情况下,RTO不用上面的公式计算,而采用一种叫做“指数退避”的方式。例如:当RTO为1S的情况下,发生了数据重传,我们就用RTO=2S的定时器来重新传输数据,下一次用4S。一直增加到64S为止。

1.4.估计器的初始化

在这里,SYN用的估计器初始化似乎和传输用的估计器不一样(我也没有把握)造我的理解,在修正公式中,SYN的情况下,A初始化为0,D初始化为3S。

而在得到传输第一个数据的ACK的时候,应该按照下面的公式进行初始化:

A=M+0.5

D=A/2

1.5.估计器的更新

和上面的讨论差不多,就是在正常情况下,用上面的公式计算,在重传的情况下,不更新估计器的各种参数。原因还是因为估计不准确。

1.6.Karn算法

这不算是一个算法,这应该是一个策略,说的就是更新RTO和估计器的值的时机选择问题,1.3.和1.5.所说得更新时机就是Karn算法。

1.7.计时器的使用

两句话:

  1. 一个连接中,有且仅有一个测量定时器被使用。也就是说,如果TCP连续发出3组数据,只有一组数据会被测量。
  2. ACK数据报不会被测量,原因很简单,没有ACK的ACK回应可以供结束定时器测量。

2.重传

有了超时就要有重传,但是就算是重传也是有策略的,而不是将数据简单的发送。

2.1.重传时发送数据的大小

前面曾经提到过,数据在传输的时候不能只使用一个窗口协议,我们还需要有一个拥塞窗口来控制数据的流量,使得数据不会一下子都跑到网路中引起“拥塞”。也曾经提到过,拥塞窗口最初使用指数增长的速度来增加自身的窗口,直到发生超时重传,再进行一次微调。但是没有提到,如何进行微调,拥塞避免算法和慢启动门限就是为此而生。

所谓的慢启动门限就是说,当拥塞窗口超过这个门限的时候,就使用拥塞避免算法,而在门限以内就采用慢启动算法。所以这个标准才叫做门限,通常,拥塞窗口记做cwnd,慢启动门限记做ssthresh。下面我们来看看拥塞避免和慢启动是怎么一起工作的

算法概要(直接从书中拷贝)

  1. 对一个给定的连接,初始化cwnd为1个报文段,ssthresh为65535个字节。
  2. TCP输出例程的输出不能超过cwnd和接收方通告窗口的大小。拥塞避免是发送方使用 的流量控制,而通告窗口则是接收方进行的流量控制。前者是发送方感受到的网络拥塞的估 计,而后者则与接收方在该连接上的可用缓存大小有关。
  3. 当拥塞发生时(超时或收到重复确认),ssthresh被设置为当前窗口大小的一半(cwnd 和接收方通告窗口大小的最小值,但最少为2个报文段)。此外,如果是超时引起了拥塞,则 cwnd被设置为1个报文段(这就是慢启动)。
  4. 当新的数据被对方确认时,就增加cwnd,但增加的方法依赖于我们是否正在进行慢启 动或拥塞避免。如果cwnd小于或等于ssthresh,则正在进行慢启动,否则正在进行拥塞避免。 慢启动一直持续到我们回到当拥塞发生时所处位置的半时候才停止(因为我们记录了在步骤2 中给我们制造麻烦的窗口大小的一半),然后转为执行拥塞避免。

补充上面的拥塞避免公式在P238页。这整个的流程让我联想到开车换档的过程。

2.2.快速重传和快速恢复算法

这是数据丢包的情况下给出的一种修补机制。一般来说,重传发生在超时之后,但是如果发送端接受到3个以上的重复ACK的情况下,就应该意识到,数据丢了,需要重新传递。这个机制是不需要等到重传定时器溢出的,所以叫做快速重传,而重新传递以后,因为走的不是慢启动而是拥塞避免算法,所以这又叫做快速恢复算法。流程如下:

  1. 当收到第3个重复的ACK时,将ssthresh设置为当前拥塞窗口cwnd的一半。重传丢失的 报文段。设置cwnd为ssthresh加上3倍的报文段大小。
  2. 每次收到另一个重复的ACK时, cwnd增加1个报文段大小并发送1个分组(如果新的 cwnd允许发送)。
  3. 当下一个确认新数据的ACK到达时,设置cwnd为ssthresh(在第1步中设置的值)。这个 ACK应该是在进行重传后的一个往返时间内对步骤1中重传的确认。另外,这个ACK也应该 是对丢失的分组和收到的第1个重复的ACK之间的所有中间报文段的确认。这一步采用的是拥 塞避免,因为当分组丢失时我们将当前的速率减半。

2.3.ICMP会引起重新传递么?

答案是:不会,TCP会坚持用自己的定时器,但是TCP会保留下ICMP的错误并且通知用户。

2.4.重新分组

TCP为了提高自己的效率,允许再重新传输的时候,只要传输包含重传数据报文的报文就可以,而不用只重传需要传输的报文。

 

TCP一共有四个主要的定时器,前面已经讲到了一个--超时定时器--是TCP里面最复杂的一个,另外的三个是:

  1. 坚持定时器
  2. 保活定时器
  3. 2MSL定时器

其中坚持定时器用于防止通告窗口为0以后双方互相等待死锁的情况;而保活定时器则用于处理半开放连接

1.坚持定时器

坚持定时器的原理是简单的,当TCP服务器收到了客户端的0滑动窗口报文的时候,就启动一个定时器来计时,并在定时器溢出的时候向向客户端查询窗口是否已经增大,如果得到非零的窗口就重新开始发送数据,如果得到0窗口就再开一个新的定时器准备下一次查询。通过观察可以得知,TCP的坚持定时器使用1,2,4,8,16……64秒这样的普通指数退避序列来作为每一次的溢出时间。

糊涂窗口综合症

TCP的窗口协议,会引起一种通常叫做糊涂窗口综合症的问题,具体表现为,当客户端通告一个小的非零窗口时,服务器立刻发送小数据给客户端并充满其缓冲区,一来二去就会让网络中充满小TCP数据报,从而影响网络利用率。对于发送方和接收端的这种糊涂行为。TCP给出了一些建议(或者是规定)。

  1. 接收方不通告小窗口。通常的算法是接收方不通告一个比当前窗口大的窗口(可以为0),
    除非窗口可以增加一个报文段大小(也就是将要接收的MSS)或者可以增加接收方缓存空间
    的一半,不论实际有多少。
  2. 发送方避免出现糊涂窗口综合症的措施是只有以下条件之一满足时才发送数据: ( a )可
    以发送一个满长度的报文段; ( b )可以发送至少是接收方通告窗口大小一半的报文段; ( c )可以
    发送任何数据并且不希望接收ACK(也就是说,我们没有还未被确认的数据)或者该连接上
    不能使用Nagle算法。

ok,现在我们回忆一下,可以发现TCP的很多规定都是为了在一次传送中发送尽量多的数据,例如捎带ACK数据报文的策略,Nagle算法,重传时发送包含原数据报文的策略,等等。

2.保活定时器

保活定时器更加的简单,还记得FTP或者Http服务器都有Sesstion Time机制么?因为TCP是面向连接的,所以就会出现只连接不传送数据的“半开放连接”,服务器当然要检测到这种连接并且在某些情况下释放这种连接,这就是保活定时器的作用。其时限根据服务器的实现不同而不通。另外要提到的是,当其中一端如果崩溃并重新启动的情况下,如果收到该端“前生”的保活探察,则要发送一个RST数据报文帮助另一端结束连接。

 

 

4.2 数据发送流程图

 

 

各层主要函数以及位置功能说明:

        1)sock_write:初始化msghdr{}结构 net/socket.c

        2)sock_sendmsg:net/socket.c

        3)inet_sendmsg:net/ipv4/af_net.c

        4)tcp_sendmsg:申请sk_buff{}结构的空间,把msghdr{}结构中的数据填入sk_buff空间。net/ipv4/tcp.c

        5)tcp_send_skb:net/ipv4/tcp_output.c

        6)tcp_transmit_skb:net/ipv4/tcp_output.c

        7)ip_queue_xmit:net/ipv4/ip_output.c

        8)ip_queue_xmit2:net/ipv4/ip_output.c

        9)ip_output:net/ipv4/ip_output.c

        10)ip_finish_output:net/ipv4/ip_output.c

        11)ip_finish_output2:net/ipv4/ip_output.c

        12)neigh_resolve_output:net/core/neighbour.c

        13)dev_queue_xmit:net/core/dev.c

        4.3 数据接收流程图

 

各层主要函数以及位置功能说明:

        1)sock_read:初始化msghdr{}的结构类型变量msg,并且将需要接收的数据存放的地址传给msg.msg_iov->iov_base.      net/socket.c

        2)sock_recvmsg: 调用函数指针sock->ops->recvmsg()完成在INET Socket层的数据接收过程.其中sock->ops被初始化为inet_stream_ops,其成员recvmsg对应的函数实现为inet_recvmsg()函数. net/socket.c

        3)sys_recv()/sys_recvfrom():分别对应着面向连接和面向无连接的协议两种情况. net/socket.c

        4)inet_recvmsg:调用sk->prot->recvmsg函数完成数据接收,这个函数对于tcp协议便是tcp_recvmsg net/ipv4/af_net.c

        5)tcp_recvmsg:从网络协议栈接收数据的动作,自上而下的触发动作一直到这个函数为止,出现了一次等待的过程.函数tcp_recvmsg可能会被动地等待在sk的接收数据队列上,也就是说,系统中肯定有其他地方会去修改这个队列使得tcp_recvmsg可以进行下去.入口参数sk是这个网络连接对应的sock{}指针,msg用于存放接收到的数据.接收数据的时候会去遍历接收队列中的数据,找到序列号合适的.

        但读取队列为空时tcp_recvmsg就会调用tcp_v4_do_rcv使用backlog队列填充接收队列.

        6)tcp_v4_rcv:tcp_v4_rcv被ip_local_deliver函数调用,是从IP层协议向INET Socket层提交的"数据到"请求,入口参数skb存放接收到的数据,len是接收的数据的长度,这个函数首先移动skb->data指针,让它指向tcp头,然后更新tcp层的一些数据统计,然后进行tcp的一些值的校验.再从INET Socket层中已经建立的sock{}结构变量中查找正在等待当前到达数据的哪一项.可能这个sock{}结构已经建立,或者还处于监听端口、等待数据连接的状态。返回的sock结构指针存放在sk中。然后根据其他进程对sk的操作情况,将skb发送到合适的位置.调用如下:

        TCP包接收器(tcp_v4_rcv)将TCP包投递到目的套接字进行接收处理. 当套接字正被用户锁定,TCP包将暂时排入该套接字的后备队列(sk_add_backlog).这时如果某一用户线程企图锁定该套接字(lock_sock),该线程被排入套接字的后备处理等待队列(sk->lock.wq).当用户释放上锁的套接字时(release_sock,在tcp_recvmsg中调用),后备队列中的TCP包被立即注入TCP包处理器(tcp_v4_do_rcv)进行处理,然后唤醒等待队列中最先的一个用户来获得其锁定权.
如果套接字未被上锁,当用户正在读取该套接字时, TCP包将被排入套接字的预备队列(tcp_prequeue),将其传递到该用户线程上下文中进行处理.如果添加到sk->prequeue不成功,便可以添加到 sk->receive_queue队列中(用户线程可以登记到预备队列,当预备队列中出现第一个包时就唤醒等待线程.)   /net/tcp_ipv4.c

        7)ip_rcv、ip_rcv_finish:从以太网接收数据,放到skb里,作ip层的一些数据及选项检查,调用ip_route_input()做路由处理,判断是进行ip转发还是将数据传递到高一层的协议.调用skb->dst->input函数指针,这个指针的实现可能有多种情况,如果路由得到的结果说明这个数据包应该转发到其他主机,这里的input便是ip_forward;如果数据包是给本机的,那么input指针初始化为ip_local_deliver函数./net/ipv4/ip_input.c

        8)ip_local_deliver、ip_local_deliver_finish:入口参数skb存放需要传送到上层协议的数据,从ip头中获取是否已经分拆的信息,如果已经分拆,则调用函数ip_defrag将数据包重组。然后通过调用ip_prot->handler指针调用tcp_v4_rcv(tcp)。ip_prot是inet_protocol结构指针,是用来ip层登记协议的,比如由udp,tcp,icmp等协议
/net/ipv4/ip_input.c

 

 

Linux通过同时对多种通信协议的支持来提供通用的底层基础服务。它的第一个网络模型的版本是4.3 BSD,也称为Net/1,今天的Linux已经使用Net/4 (Linux 2.2),其中大多数代码已经完全和BSD的版本不同,但是它依然支持UINX平台之间程序的移植。

Linux网络套接字实现的模式是UNIX下的普遍标准。同时,Net/4的网络层是完全另起炉灶重写的。首先,新的网络层尽可能地实行并行处理, 因此其伸缩性比起以前的版本,不可同日而语。其次,它包括了许多的优化,以便绕过不少流行操作系统网络实现中的不合理处(例如Windows)。到目前为止,Linux 是唯一与IPv4和IPv6协议标准完全保持兼容的操作系统,而Linux2.4的IPv4伸缩性又大有提高。

Linux支持的六种不同通信协议族:

1) TCP/IP (使用TCP/IP的Internet 协议族),本文讨论的重点。

2) UNIX域协议 (一种进程间通信的协议

3) X25协议

4) AX25协议 (业余无线X25)

5)IPX协议 (Novell IPX)

6) APPLETALK协议 (AppleTalk DDP)

1.1 内核源代码的组织

表1是本文要使用的Linux Net/4网络源代码的,其中大部分位于目录/usr/src/linux-2.2.x/net,列表如下,

插口层 
BSD Socket 
/net/socket.c 
/net/protocols.c 
INET Socket 
/ipv4/protocol.c 
/ipv4/af_inet.c 
/net/ipv4/core/sock.c 
协议层 
TCP/UDP 
/net/ipv4/udp.c 
/net/ipv4/datagram.c 
/net/ipv4/tcp_input.c 
/net/ipv4//tcp_output.c 
/net/ipv4/tcp.c 
/net/ipv4/tcp_minisocks.c 
/net/ipv4/tcp_timer.c etc... 
IP 
/net/ipv4/ip_forward.c 
/net/ipv4/ip_fragment.c 
/net/ipv4/ip_input.c 
/net/ipv4/ip_output.c 
接口层 
Ethernet 
......

1.2  Linux中TCP/IP网络层次结构与实现 
Linux通过一组相邻的软件层实现了TCP/IP模型,它由BSD Socket层、INET

Socket层、传输层、网络层,和链路层构成。应用程序使用系统调用向内核函数传递参数和数据从而进入内核空间,由内核中注册的内核函数对相应的数据结构进行处理。Linux的TCP/IP层次结构和实现方式如图
1 所示。

协议栈图片

抱歉!评论已关闭.