首页 > 代码库 > 网络--TCP

网络--TCP

1. 与UDP不同的是,TCP提供了一种面向连接的、可靠的字节流服务。面向连接比较好理解,就是连接双方在通信前需要预先建立一条连接,这犹如实际生活中的打电话。助于可靠性,TCP协议中涉及了诸多规则来保障通信链路的可靠性,总结起来,主要有以下几点:
(1)应用数据分割成TCP认为最适合发送的数据块。这部分是通过“MSS”(最大数据包长度)选项来控制的,通常这种机制也被称为一种协商机制,MSS规定了TCP传往另一端的最大数据块的长度。值得注意的是,MSS只能出现在SYN报文段中,若一方不接收来自另一方的MSS值,则MSS就定为536字节。一般来讲,在不出现分段的情况下,MSS值还是越大越好,这样可以提高网络的利用率。
(2)重传机制。设置定时器,等待确认包。
(3)对首部和数据进行校验。
(4)TCP对收到的数据进行排序,然后交给应用层。
(5)TCP的接收端丢弃重复的数据。
(6)TCP还提供流量控制。(通过每一端声明的窗口大小来提供的)
2. TCP包的首部:

技术分享

(1) 若不计选项字段,TCP的首部占20个字节。
(2) 源端口号以及目的端口号用于寻找发端和接收端的进程,一般来讲,通过端口号和IP地址,可以唯一确定一个TCP连接,在网络编程中,通常被称为一个socket接口。
(3) 序号是用来标识从TCP发端向TCP接收端发送的数据字节流。
(4) 确认序号包含发送确认的一端所期望收到的下一个序号,因此,确认序号应该是上次已经成功收到数据字节序号加1.
(5) 首部长度指出了TCP首部的长度值,若不存在选项,则这个值为20字节。
(6) 标志位(flag):
  URG: 紧急指针有效
  ACK:确认序号有效
  PSH:接收方应尽快将这个报文段交给应用层
  RST:重建连接
  SYN:同步序号用来发起一个连接
  FIN: 发端完成发送任务(主动关闭)

【解释】
◆ TCP提供解决方式是为了使一端告诉另外一端某些“紧急数据”已经放置在普通的数据流中,让接收端对紧急数据做特别处理。此时,URG位被置为1,并且16位的紧急数据被置为一个正的偏移量,通过此偏移量与TCP首部中的序号字段相加,可以得出紧急数据的最后一个字节的序号,常见的应用有传输中断键(在通过telnet连接过程中)。
◆ RST: 复位字段被用于当一个报文发送到某个socket接口而出现错误时,TCP则会发出复位报文段。常见出现的情况有以下几种:
  发送到不存在的端口的连接请求:此时对方的目的端口并没有侦听,对于UDP,将会发出ICMP不可达的 错误信息,而对于TCP,将会发出设置RST复位标志位的数据报。

  异常终止一个连接:正常情况下,通过发送FIN去正常关闭一个TCP连接,但也有可能通过发送一个复位报文段去中途释放掉一个连接。在socketAPI中通过设置socket选 项SO_LINGER去关闭这种异常关闭的情况。

3. TCP的连接与终止过程:

技术分享

(1) 三次握手:
  建立一个TCP连接,必须经历三次握手过程,其中发送第一个SYN的一端将执行主动打开,接收这个SYN并发回下一个SYN的另一端执行被动打开。
(2) 四次释放:
  要释放一个TCP连接,需要通过四次握手过程,这是由TCP的半关闭特性造成的,因为TCP连接时全双工的,因此,需要TCP两端要单独执行关闭。值得注意的是,主动关闭的一端在发送FIN之后,依然还能正常接收对方的数据,只是通知对方它已经没有数据需要发送了,同理,被动关闭的一端在收到FIN之后,仍然可以发送数据,直到它自身同样发出FIN之后,才停止发送数据。

(3) TCP连接的超时问题:
  完成一个TCP连接,中间涉及到一个超时的问题,大多数伯克利系统的超时时限为75s,Solaris9的超时时限为240s,因此,一般认为是在75-240之间。
【引申】在具体的实现中,如何由用户自己去完成设置socket连接超时时间?
【解决方法】目前实现socket超时连接主要是通过select来完成的。具体步骤如下:
  ◆ 建立socket
  ◆ 将socket设置为非阻塞模式(若是阻塞模式,那么时间设置就毫无意义)
  ◆ 调用connect去进行连接
  ◆ 使用select检查socket是否可写,并同时判断其结果(为什么是可写?因为需要检测socket是否收到ACK。)
  ◆ 将socket转化为阻塞模式

(4)TCP的半关闭
  所谓“半关闭”,是指连接的一端在结束它的发送之后还能接收到对方发过来的数据的能力。具体表现在,当完成三次握手的双方,其中有一端发出FIN,此时它将进入半关闭状态,此时它关闭了自身的发送功能,但是它依然可以接收到对方的数据,如对方发过来的ACK消息。那么在实际开发中,是怎么实现的呢?
这牵涉到系统中shutdown和close函数的区别问题。
int shutdown(int s, int how) <sys/socket.h>
shutdown是用来终止参数s所指定的socket接口,参数how主要有以下几种情况:
  how = 0 终止读取操作
  how = 1 终止写入操作
  how = 2 终止读取和写入操作
返回的errorcode可能有:
  EBADF /* Bad file descriptor */
  ENOTSOCK /* Socket operation on non-socket */
  ENOTCONN /* Socket is not connected */

4. TCP的状态变迁图:

 技术分享

CLOSED: 这个没什么好说的了,表示初始状态。
LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。
SYN_RCVD: 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本 上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态 时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。
SYN_SENT: 这个状态与SYN_RCVD遥想呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状 态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
ESTABLISHED:这个容易理解了,表示连接已经建立了。
FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别 是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即 进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马 上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什 么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报 文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。

 

5. TCP中流量控制机制——滑动窗口
受到诸多因素的影响,如硬件(双方网卡吞吐量差异)、网络环境,网络极易出现各种各样的拥塞,目前所采取的措施主要有以下两种:改进拥塞算法以及控制发送端和接收端的流量。本节主要讲述如何在接收端和发送端进行流量控制。
(1)滑动窗口——接收端
  在讲解滑动窗口协议之前,可以回顾下最初人们为了在接收端实施流量控制所提出了的经典算法——停止等待算法,其核心思想是:接收端在收到一个数据报之后,停止接收新的数据报,直到发出ACK(对收到的数据报的确认)之后进行恢复。算法思想以及实现非常简单,但是却遇到一个效率的问题,特别是随着网络设备的数据处理能力得到了很大的提高,效率低下显得尤其明显,后来人们也尝试了多种改进措施,如本节的滑动窗口就是其中之一。
其基本原理是:在接收端存在一个接收缓存区,用来接收来自于发送方的数据,只有当应用进程从接收缓存区中取出数据(可能只是部分)并发出其ACK后,才算作这部分数据已经接收,然后调节此时的滑动窗口大小。发送方根据返回的窗口大小,计算出所能发送的数据大小。因此,可以这么理解,滑动窗口算法是接收端作为主动方根据自身的缓存以及处理能力主动去调节对方的发送流量的一种调节算法。
下面通过一个滑动窗口模型图了解下发送方是如何处理所收到的滑动窗口的?

技术分享

在发送方依然存在一个缓存区(发送缓存区),其发送的数据可以是下面几种状态:
◆ 发送并确认 (1-3)
◆ 发送但未确认 (4-6)
◆ 可以发送 (7-9)
◆ 不能够发送 (10以后)
值得注意的是,滑动窗口是基于所收到的确认序列号的。当发送方根据所收到的确认序列号以及窗口大小,不断向后移动,并相应更新数据状态。

(2)滑动窗口——发送端(拥塞窗口)
网络拥塞出现的原因是多方面的,除了收发两段的硬件差异之外,还与网络通信链路相关,如通信链路中转发路由器的缓存,试想这样一种情况,若发送端与接收端的处理能力以及吞吐能力很强,若它仅仅通过接收端所返回的滑动窗口大小,难以阻止数据报在被路由器转发过程中因为阻塞而被丢弃的情形,因为与发送端相连的路由器因为自身的缓冲空间的限制,难以存储并转发这么多数据而出现丢包现象。那么这种情况如何避免呢?最好的机制是中间的路由器也要参与给发送端反馈窗口大小的事情,也正是本节所说的拥塞窗口。
综合上面的阐述,发送端会收到两个窗口大小,分别是来自于接收端和中间路由器,注意前者在每次的数据报中都会出现,而后者只是在网络中出现拥塞时由中间路由器发送。那么,发送方此时取接收端的窗口大小与拥塞窗口中的最小值作为发送上限值


socket INADDR_ANY 监听0.0.0.0地址 socket只绑定端口让路由表决定传到哪个ip
比如你的机器有三个ip
192.168.1.1
202.202.202.202
61.1.2.3
如果你serv.sin_addr.s_addr=inet_addr("192.168.1.1"); 然后监听100端口
这时其他机器只有connect 192.168.1.1:100才能成功。
connect 202.202.202.202:100和connect 61.1.2.3:100都会失败。
如果serv.sin_addr.s_addr=htonl(INADDR_ANY); 的话,无论连接哪个ip都可以连上的
 
服务器只监听一个端口,并不会发生冲突
  1.远端端口不同,连接状态处于ESTALISHED的,只能接受数据不能接受SYN
  2.连接处于LISTEN,只能处理SYN不能接受数据

 

网络--TCP