TCP数据的传输(7)

一 ■■■■基本模型: 案五

 

二 TCP可靠传输的工作原理

 

1 使用检验和校验首部及数据的正确性

 

2 确认机制与超时重传

        超时重传是指发送方在一定时间内没有收到确认就重发数据,这个时间是根据网络情况及RTT计算出来的时间.

 

3 确认丢失和确认迟到

        即是说接收方丢弃重复接收的数据并重传确认,发送方直接丢弃已收到的确认报文

 

4 滑动窗口协议:

        连续ARQ协议ARQ即自动重传请求协议,是滑动窗口协议一部分,如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送.连续ARQ协议,是指在滑动窗口内的数据可以连续发送,不需要等待对方确认,这样可以提高信道利用率,在按序收到接收方的确认后,窗口前移.接收方一般采用累积确认的方式,就是说,接收方不必对收到的分组逐个发送确认,而是可以在收到几个分组后,对按序到达的最后一个分组发送确认.

        累积确认方式的优点是:容易实现,即使中间的确认丢失接收方也不必重传确认.缺点是不能向发送方反映出接收方已经正确收到的所有分组的信息,因为接收方只能对按序到达的最后一个分组发送确认,所以容易造成GO-BACK-N,表示需要重发接收方已收到但没有确认的不按序到达的分组.

        还有一种选择确认的方式,可以确保不用重发不按序到达的数据,通过在TCP首部的选项中加上"允许SACK"选项来向发送方确认不按序到达的数据.

 

5 流量控制,数据传输效率及拥塞控制

通过协调发送窗口和接收窗口,实现数据的可靠传输,除前面的1,2,3点,还包括:

 

(1)流量控制:接收端只允许另一端发送接收端缓冲区所能接纳的数据,防止缓冲区溢出 ,即是通过接收方告诉发送方允许的发送窗口大小下进行流量的控制.

 

(2)数据传输效率:发送方可以通过Nagle算法与防止糊涂窗口综合症算法,防止网络上出现过多的小报文段导致网络性能降低

Nagle算法如下:若发送应用进程把要发送的数据逐个字节地送到TCP的发送缓存,则发送方就把第一个数据字节先发送出去,把后面到达的数据字节都换存起来。当发送方收到对第一个数据字符的确认后,再把发送缓存中的所有数据组装成一个报文段发送出去,同时继续对随后到达的数据进行缓存。只有在收到对前一个报文段的确认后才继续发送下一个报文段。Nagle算法还规定:当到达的数据已经达到发送窗口大小的一半或已经达到MSS,就立即发送一个报文段。

 

糊涂窗口综合症: 设想一种情况:TCP接收方的缓存已满,而交互式的应用进程一次只从接受缓存中读取一个字节(这样接收缓存空间就仅腾出一个字节),然后向发送方发送确认,并把窗口设置为一个字节(但发送的数据报是40字节长)。接着,发送方又发来一个字节的数据(此时发送方发送的IP数据报是41字节长)。接收方发回确认,如此进行下去,是网络效率很低。

要解决这个问题,可以让接收方等待一段时间,是的或者接受缓存已经有足够的空间容纳一个最长的报文段,或者等到接受缓存已有一半的空闲空间,只要出现这两种情况之一接收方就发出确认报文。此外发送方也不要发送太小的报文段,而是把数据积累成足够大的报文段,或达到接收方缓存空间一半大小。

 

        上述两种方法可配合使用,使用在发送方不发送很小的报文段的同时,接收方也不要在缓存刚刚有了一点小空间就急忙把这个很小的窗口大小信息通知给主送方

 

(3)拥塞控制: 使用慢开始算法和拥塞避免算法防止网络负载过大造成拥塞.(后来还增加了两个新的拥塞控制算法:快重传和快恢复,后面不对此作分析)

慢开始算法如下:

        发送方让自己的发送窗口等于拥塞窗口

在主机刚刚开始发送 报文段时可先将 拥塞窗口 cwnd 设置为一个最大报文段 MSS 的数值。
在每收到一个对新的 报文段的确认后,将 拥塞窗口增加至多一个 MSS 的数值。
用这样的方法逐步增大发送端的 拥塞窗口 cwnd,可以使分组注入到网络的速率更加合理。

 拥塞避免算法:

        为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量。慢开始门限ssthresh的用法如下:

    当 cwnd < ssthresh 时,使用上述的慢开始算法。

    当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。

    当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。

拥塞避免算法:让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1MSS,而不是翻倍。这样拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。

    无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送方窗口值的一半(但不能小于2MSS)。然后把拥塞窗口cwnd重新设置为1MSS,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。

 

拥塞控制总结:

发送方窗口的上限值 = Min [ rwnd, cwnd ]

    当rwnd < cwnd 时,是接收方的接收能力限制发送方窗口的最大值。

    当cwnd < rwnd 时,则是网络的拥塞限制发送方窗口的最大值。

 

三 性能相关:

 

1 tcp连接的内存使用

(1)SO_SNDBUF表示这个连接上的内核写缓存上限,受制于系统级的上下限

(2)SO_RCVBUF表示连接上的读缓存上限,受制于系统级的上下限

(3)读缓存是一个动态变化的、实际用到多少才分配多少的缓冲内存,当这个连接非常空闲时,且用户进程已经把连接上接收到的数据都消费了,那么读缓存使用内存就是0。写缓存也是同样道理。

(4)因此,只有当接收网络报文的速度大于应用程序读取报文的速度时,可能使读缓存达到了上限,这时这个缓存使用上限才会起作用。所起作用为:丢弃掉新收到的报文,防止这个TCP连接消耗太多的服务器资源。同样,当应用程序发送报文的速度大于接收对方确认ACK报文的速度时,写缓存可能达到上限,从而使send这样的方法失败,内核不为其分配内存。

 

2 BDP带宽时延积与性能调优

(0)问题: 如果接收缓存太小,那么不能很好地利用网络资源;如果窗口过大,对会加重网络的负载造成传输的性能下降;并且会很快耗尽服务器的内存资源.

 

(1)接收端接收窗口的大小某种程度上限制了发送端发送窗口的大小(流量控制),当然发送窗口的大小还取决于拥塞窗口(拥塞控制),所以配置好服务端接收缓存的上限对数据的传输效率有很大的影响

    所以发送窗口实际上就是TCP连接对方的接收窗口,所以发送缓存的上限也可以按接收缓存来推断

 

(2)linux有一个系统配置项tcp_adv_win_scale, 如tcp_adv_win_scale = 2 ,意味着,将要拿出1/(2^tcp_adv_win_scale)缓存出来做应用缓存。即,默认tcp_adv_win_scale配置为2时,就是拿出至少1/4的内存用于应用读缓存,那么,最大的接收滑动窗口的大小只能到达读缓存的3/4。

 

(3)当应用缓存所占的份额通过tcp_adv_win_scale配置确定后,读缓存的上限应当由最大的TCP接收窗口决定。初始窗口可能只有4个或者10个MSS,但在无丢包情形下随着报文的交互窗口就会增大,当窗口过大时,“过大”是什么意思呢?即,对于通讯的两台机器的内存而言不算大,但是对于整个网络负载来说过大了,就会对网络设备引发恶性循环,不断的因为繁忙的网络设备造成丢包。而窗口过小时,就无法充分的利用网络资源。所以,一般会以BDP来设置最大接收窗口(可计算出最大读缓存)。

 

(4)BDP(Bandwidth Delay Product,带宽时延积) = link_bandwidth * RTT(round-trip time)

BDP就表示了网络承载能力,最大接收窗口就表示了网络承载能力内发送端可以不经确认发出的报文,不经确认发出的报文就是会存在于接收窗口,应用读不了的报文!

 

例如若我们的带宽为2Gbps,时延为10ms,那么带宽时延积BDP则为2G/8*0.01=2.5MB,所以这样的网络中可以设最大接收窗口为2.5MB,这样最大读缓存可以设为4/3*2.5MB=3.3MB。

 

(5)linux的TCP缓存上限可以配置自动调整策略,我们希望的场景是,在并发连接比较少时,把缓存限制放大一些,让每一个TCP连接开足马力工作;当并发连接很多时,此时系统内存资源不足,那么就把缓存限制缩小一些,使每一个TCP连接的缓存尽量的小一些,以容纳更多的连接。

 

    但是要注意当我们在编程中对连接设置了SO_SNDBUF、SO_RCVBUF,将会使linux内核不再对这样的连接执行自动调整功能!

 

四 关于Java API 的 read 和 write

1 死锁风险

当在两个方向上传输大量数据时容易产生死锁:

        write(byte[])返回的条件是byte[]里面的内容全部被移出,假设sendq和recvq的大小分别为s和r,将一个大小为n的字节数组传递给write()方法用,其中n > s + r,write()方法返回的条件是接收程序从输入流中读取了至少n-(r + s)的字节.如果接收程序没有调用read()方法而是调用write方法,而要发出的数据同样n' > s' + r',那么两端的程序都将永远保持阻塞状态.

 

注意: 当用户程序调用write,如果缓冲区足够写,则将数据写入本地发送缓冲区后就返回,具体的发送操作交由内核处理.write()方法调用会阻塞等待,直到最后一个字节成功写入到TCP实现的本地缓存中,如果可用的缓存空间比要写入的数据小,在write()方法调用返回前.必须把一些数据成功传输到连接的另一端.因此,write()方法的阻塞时间最终还是取决于接收端的应用程序.不幸的是java现在还没有提供任何使write()超时或由其他线程将其打断的方法.

 

★死锁风险的解决方案: 

(1)客户端和服务端一个连接中,共有两对不同方向上的读和写,为了确保在任何情形一不会出现死锁,那么可以启用新线程进行服务端的读,这样就确保客户端的写不会死掉,..........

(2)使用nio

 

2 性能相关

(0)吞吐量: 指数据从发送端发送到接收程序的频率

(1)如果要传输n字节的数据, 使用大小为n的缓冲区调用一次write方法,通常要比使用大小为1字节的缓冲区调用n次效率高很多

(2)如果调用write()方法时使用了比sendq大很多的缓冲区,套接字底层实现先将sendq填满,【等待数据转移出去】,再重新填满sendq,反复进行,这就以系统消耗的形式(系统需要进行上下文切换)浪费了一些时间【这种系统耗费与重新调用一次write方法的情况相似】. 从inputstream读取数据也是一样的道理,即使提供给read()方法的缓冲区很大,数据还是会被复制成recvq大小的块,在块之间又会产生新的系统耗费.

(3)可以通过Socket的setSendBufferSize()和setReceiveBufferSize()方法来改变发送和接收缓冲区的大小.要记住一点,当程序要一次发送比缓冲区容量大很多的数据时才需要考虑这些情况,这是为了减少块复制的次数;同时还要注意,如果处理了一些从Socket的基本输入流继承而来的更高层次的流(如FilterOutputStream实例或PrintWriter实例),这些因素的效果就会略有不同,因为更高层次的流可能会执行它们字节的内部缓存或增加额外的系统开销.

 

 3 keep-alive机制

该机制在经过一段不活动时间后,将向另一个终端发送一个探测消息,如果另一个终端还处于活跃状态,它将回复一个确认消息,如果几次尝试依然无另一端的确认消息,则关闭套接字,并在下一次尝试IO操作时抛出一个异常,注意应用程序只有在探测信息失败时才能察觉到keep-alive机制的工作.可以通过调用setKeepAlive()方法将其设置为true来开启keep-alive机制

 

4 消除缓冲延迟

        tcp协议将数据缓存起来直到足够多时一次发送,以避免发送过小的数据包而浪费网络资源,可以通过Socket.setTcpNoDelay(true)禁用缓存功能(取消nagle算法)

 

5 紧急数据

        如果有需要紧急发送的数据发送到输出流并妄加在常规数据队列的后面,可以通过Socket.sendUrgentData(int data)发送紧急数据,接收者需要调用Socket.setOOBInline(true)以启用接收者对频道外数据的接收,该字节在接收者的输入流中被接收,但当前仍是排在输入流中的最后面,如果没有启用接收者接收频道处数据的功能,紧急字节将被丢弃

        注意:java中的紧急数据几乎没什么用,因为java接收者并不能区分其是否在接收紧急数据,紧急字节与常规字节按照传输顺序混在了一起.

 

 

五 在应用层面字节数据通信的一些知识

 

1 TCP/IP协议约束信息必须在块中发送和接收,而块的长度必须是8位的倍数,即最小的传输单位是字节

 

2 基本整形(传输时是需要位数补全的):

int: 4 byte

short: 2 byte

long: 8 byte

 

3 发送者和接收者需要达成的共识:

(1)对于任何多字节的整数,是使用从高位到低位的big-endian顺序还是使用从低位到高位的little-endian顺序(幸运的是字节中位的顺序在实现时是以标准的方式处理的,不需要发送者和接收者协商)

 

4 所传输的数值是有符号的还是无符号的,有符号整形的值以二进制补码的方式存储,与无符号整形的编码是不一样的.由于java并不支持无符号整形,如果要在java中编码和解码无符号数,则需要做一点额外的工作.

 

5 有符号数与无符号数的二进制表示

在计算机中,负数以其正值的补码形式表达。

原码:一个整数,按照绝对值大小转换成的二进制数,称为原码。 

反码:将二进制数按位取反,所得的新二进制数称为原二进制数的反码。

补码:反码加1称为补码。

 

6 thinking in java 按位操作符,移位运算

按位操作符: &, |,(按位操作符与逻辑操作符功能类似,但是它们不会中途"短路") ^(对于二进制的两个数值,相同则运算为0,不同则运算为1), ~(取反)

 

移位运算: 

只可用来处理整型;

左移位操作符<<: 向左移动指定位数,低位补0

有符号右移位操作符>>: 向右移动指定位数,高位补上符号位相等的值

无符号右移操作符>>>: 无论正负都补0

<<=或>>=或>>>=:操作符左边的值会移动由右边的值指定的位数,再将得到的结果赋给左边的变量

 

注: 对char,byte,short类型的数值进行移位处理,首先会被转换为int类型,并且得到的结果也是一个int类型的值,

 

7 布尔值的编码

待......................

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值