传输层之TCP与UDP
计算机网络体系为什么分层
层次化的体系结构,让系统有更好的稳定性,独立性,灵活性与可扩展性.
- 独立性: 各个层次互相独立,高层次的不需要知道底层的具体实现,只需要直接使用底层提供的接口就可以获取相应的服务.
- 灵活性: 各个层次使用的技术可以不断迭代更新,只要提供的功能与接口不变,无论如何实现对各个层次以及整个系统的工作都不会产生影响.
- 易于实现与维护: 整个系统分为相对独立的层次, 进行调试与维护时, 可以单独地对每一层进行调试,避免出现找不到问题等情况.
- 更加标准化
传输层
为应用进程之间提供端到端的逻辑通信
对收到的报文进行差错检测
提供面向连接和无连接服务
TCP
(一)特征
- 面向连接
- 面向字节流
- 提供可靠数据传输的服务
- 提供流量控制服务
- 提供拥塞控制服务
- 提供全双工通信
- 每一条连接只能有两个端点,是一对一的关系
疑问1:什么是面向字节流?
TCP把应用程序交下来的数据仅仅是看出一连串无结构的字节流, TCP不在字节流中插入记录标识符, 同时也不对字节流中的内容做任何的解析,没有边界.
在发送和接受的时候要自己额外设计定义好边界.
疑问2:既然是字节流传输, 就有字节序的问题
主机字节序: 有大端小端之分,由cpu决定
大端字节序: 数据的高字节存储在内存的低位地址上
小端字节序: 数据的低字节存储在内存的低位地址上
(低位---低位
高位----高位)
网络字节序: 采用大端模式
优缺点:
大端: 符号位在内存低位地址中,便于快速判断数据的正负
小端: cpu做数值运算时,从内存中一次从低到高获取数据进行运算,直到最后刷新最高位的符号位, 这样运算更高效.
疑问3: 为什么有了主机字节序还需要出现网络字节序?
主机字节序有两种, 不同的主机,其字节序不一定一致, 尤其是
两主机字节序不一致的情况下, 数据传输到对方时, 由于子节序不同, 解析出来的数据就会相反,无法正确地完成通信过程,所以需要统一成一个字节序.
疑问4: 为什么网络字节序采用大端模式
疑问5: 既然是面向字节流(无边界), 那么tcp本身如何识别数据传输结束?
1)应用一个回车和换行来标记每个应用记录的结束
2)服务器发送结束后直接断开连接, 但是如果还有其他业务需要通信这种方法不靠谱
3)在发送实际文件报文之前,先发送文件的字节长度给接收端,客户端通过这个这个大小决定何时结束.
DNS采用的是这种技术
3)自定义包格式, 结构化数据.这种方法可以在包首部附加额外信息,如包长度.HTTP协议采取的是这种方法.
方法3的大概实现如下:
//结构体的定义
//为了实现私聊,采用服务器转发的方式
struct send_info{
char _info_from[20];//发送者id
char _info_to[20];//接受者id
int _info_length;//发送消息主体的长度
char _info_content[1024];//消息主体
};
//发送端代码
struct send_info info_1;///定义结构体变量
///清空消息主体
memset(info_1._info_content, 0 ,sizeof(info_1._info_content));
///获取用户输入的数据或者文件流数据
info_1._info_length = read(STDIN_FILENO, info_1._info_content, 1024);
///清空发送缓存
char snd_buf[1024];
memset(snd_buf, 0, 1024);
///将结构体转换成字符串
memcpy(snd_buf, &info_1, sizeof(info_1));
//发送消息
send(connect_fd, snd_buf, sizeof(snd_buf), 0);
//接收端代码
struct send_info clt;//定义结构体变量
//清空接收缓存
memset(recv_buf, 'z', 1024);
///从接收缓存读取数据
recv(fd, recv_buf, 1024, 0);
//清空结构体
memset(&clt, 0, sizeof(clt));
//把接收到的消息转换为结构体
memcpy(&clt, recv_buf, sizeof(clt));
//消息内容结束加'\0'
clt._info_content[clt.info_length] = '\0';
//判断接收内容并输出
//上述代码中用到的memcpy(...)函数
///memcpy(void* dest, const void* src, size_t n_nums);
///memmove(void* dest, const void* src, size_t n_nums);
两者的联系:
1. 拷贝一定长度的内存内容
2. 对源不做任何的终止符检查, 总是拷贝设定长度的内存内容
区别:
1. 当内存发生局部重叠的时候, memmove(...)会进行处理,总是得到正确的拷贝结果; 而memcpy(...)不保证正确的的拷贝
2. 如下,有两种局部覆盖情况,其一,两个函数都可以正确拷贝,其二,memcpy(...)不可以正确拷贝
//memcpy(...)个人实现----在内存中逐字节拷贝
void* memcpy(void* dst, const void* src, size_t n_nums){
char* tmp = (char*)dest;
char* src = (char*)src;
for(int i = 0; i < n; i++){
*(tmp++) = *(src)++;
}
return dest;
}
//memmove(...)个人实现----两种方式
//方式一,新建一个缓冲区,先拷贝到缓冲区中,再逐一进行拷贝到目标中
void* memmove(void* dst, const void* src, size_t n_nums){
char* tmp = new char[n_nums];
char* d_dst = (char*)dst;
char* s_src = (char*)s_src;
for(int i = 0; i < n_nums; i++){
*(tmp++) = *(s_src++);
}
s_src = src;
for(int i = 0; i < n_nums, i++){
*(d_dst++) = *(tmp++);
}
delete tmp;
return dst;
}
///方式二, 从后往前拷贝
void* memmove(void* dst, const void* src, size_t n_nums){
char* d_dst = (char*)dst;
char* s_src = (char*)s_src;
if(d_dst > s_src && s_src + n > d_dst){
s_src = s_src + n - 1;
d_dst = d_dst + n - 1;
while(n_nums--)
*(d_dst--) = *(s_src--);
}
else{
while(n_nums--)
*(d_dst++) = *(s_src++);
}
return dst;
}
注意:
int main( void )
{
char str1[9] ="aabbccdd";
char str2[9] ="eeffgghh";
memcpy(str1 + 2, str1, 6);
printf("New string: %s\n", str1);
//strcpy(str1,"aabbccdd");//reset string
memmove(str2 + 2, str2, 6);
printf("New string: %s\n", str2);
}
输出为:
aaaabbcc
eeeeffgg
解析:
这里的内存局部覆盖是情况2, 然而两个函数得到的结果一致,与我们上述实现得到的结果不一致,原因如下:本来对第二种覆盖情况,memcpy(...)拷贝结果应该不正确的, 然而编译器对其进行了优化.我们可以反编译查看汇编代码,即可知道.

疑问6: tcp如何提供可靠数据传输服务?
1)将应用数据中的每个字节编号, 并且分割成最合适的发送数据块, 发送端会明确地知道自身已经发送了哪些字节.
2)当TCP收到发自TCP另一端的数据时, 将会发送一个确认.采取的方式不是立即确认, 而是累积确认.
3)TCP发出一个段后, 会启动一个超时计时器, 等待接收端接收到报文段.如果不能即使收到一个确认, 将重传这个报文段.
(2-3可简称为确认-重传机制)
4)TCP保持它首部和数据的检验和, 这是一个端到端的检验和, 目的是检测数据在传输的过程中的任何变化.只要检验和有差错, TCP就会丢弃这个报文段和不确认收到此报文段.(希望发送端超时并且重传)
5)IP协议不提供可靠数据传输, IP数据报到达时,可能会失序, 那么TCP报文段到达的时候也会失序.若有必要, TCP将对接收到的数据进行重新排序, 将收到的数据以正确的顺序交给应用层.
6)IP数据报会发生重复, TCP接收端必须丢弃重复的数据
7)TCP提供流量控制.TCP连接的双方都有固定的缓存空间.TCP接收端只允许发送接收端缓存区所能容纳的数据, 这能防止较快主机使较慢主机的缓冲区溢出.
疑问7: 为什么检验和不对, 都是丢弃处理
因为源IP地址,源端口号或者协议字段可能被破坏了
疑问8: 什么是全双工通信
全双工通信是指, 数据能在两个方向上独立地进行传输.因此,连接的每一端都必须保持每个方向上的传输数据序号.
<二>TCP报文段格式
组成: 首部 + 数据部分
- 首部组成(20固定字节 + 可选的40字节范围选项)
16位源端口号 16位目的端口号
32位序号
32位确认号
4位首部长度 保留(6位) 6位标记位 16位窗口大小
16为检验和 16位紧急指针
选项
-----------------------------------
序号: 表示在这个报文段中的第一个数据字节, 用来表示TCP发端向接收端发送的字节流.
确认号: 包含发送确认的一端所期望的下一个序号.只有ACK标志置1时,确认号才有效.
首部长度: 给出首部包含32bit的数目.包含32bit最大的数为1111,即首部最大包含15个32bit, 亦即,15 * 4byte = 60byte.所以首部最大长度为60bytes.
URG: 紧急标志位,当置1时, 发送端应尽快将该报文段发送出去.
ACK: 置1时, 确认序号有效.
PSH: 当置1时, 接收方应尽快将这个报文段交给应用层.
RST: 复位标记位.重建连接
SYN: 同步标记位, 用来发起一个连接
FIN: 结束标记位, 发送端完成发送任务.
16为窗口大小: 用来告诉发送端, 自身的接收缓存大小, 以字节为单位, 最大为65535bytes.
16位检验和: 由发送端进行计算, 接收端进行校验.(发送端未在计算时, 全为0)
16位紧急指针: 指出本报文段中紧急数据的字节数
选项: 常见的有MSS最长报文大小.
-------------------------------------------------------------------------------------
TCP发送端检验和计算:
伪首部:
32位源IP地址
32位目的IP地址
8位全0 8位TCP协议号 16位TCP总长度
步骤一: 将检验和添0
步骤二: 将TCP伪首部部分, TCP首部部分,数据部分以16位进行划分(不足的部分填充0)
步骤三: 将这些数逐个相加, 溢出部分加到最低位上
步骤四: 最后将得到的结果取反码, 并填入到首部检验和位置.
TCP接收端检验和校验:
步骤一: 将TCP伪首部部分, TCP首部部分,数据部分以16位进行划分(不足的部分填充0)
步骤二: 将这些数逐个相加, 溢出部分加到最低位上
步骤三: 最后将得到的结果取反码,若全为1,则无差错.
<三>TCP三次握手与四次挥手
- 三次握手描述(初始时,请求端处于closed状态,服务端处于listen状态)
第一次握手: 请求端发送一个SYN报文指明客户打算连接的服务器端口, 以及初始化序列号.(该序列号随机产生一个绝对序列号,为了便于表示, 使用相对序列号)此时, 请求端处于SYN_SEND状态
第二次握手: 服务端发送一个包含服务端初始序列号的SYN报文段作为应答, 并将ACK置位为1, 同时将确认号设置为客户端的序列号+1用以对客户端SYN报文段的确认.此时, 服务端处于SYN_RCVD状态
第三次握手: 客户端将ACK置位为1, 并将确认号设置为服务端序列号_1用以对服务端SYN报文段的确认.此时,客户端处于ESTABLISED状态, 服务端收到确认后也处于ESTABLISHED状态,连接建立完成.- 疑问1: 为什么要随机初始序列号
一方面, 避免在新的连接下, 接收端有可能会接受旧的连接中因在网络滞留时间过长而延迟到达的报文段, 因为这报文段的序列号很有可能恰好就在新连接所使用的序号范围之内; 另一方面是出于网络安全着想, 固定的序列号很容易被获取到初始序列号, 并被伪造序列号进行攻击. - 疑问2: 为什么要三次握手, 两次握手不可以吗
三次握手,能防止已失效的连接请求又传送到服务器,而产生错误.如果是两次握手,我们分以下情况进行讨论,正常情况下,两次握手都成功,连接自然是建立起来了.异常情况下, 即第一次握手失败, 服务端收不到连接请求报文, 自然就没有第二次握手.这时, 客户端重传第一次握手的报文,直到超时或者成功收到第二次握手的信息.如果客户端收不到第二次握手的报文, 也是会一直重传第一次握手的报文的,直到成功为止.这种情况也是没有问题的.然而, 如果客户端已经请求连接超时失败, 放弃连接了,这个时候, 因在网络中滞留过久而又到达了服务端, 服务端第二次握手,认为建立了连接,便会为其分配缓存与变量,因而导致服务端资源浪费. - 什么是半连接状态
服务端接收到客户端的SYN后, 会处于SYN_RCVD状态, 此时双方还没有完全建立连接, 服务器会把处于这种状态下的请求放在一个队列中, 称半连接队列. - SYN洪泛
攻击者不断地向服务端发送SYN报文, 而不理会服务端返回的ACK报文, 服务器便会重复地发送ACK报文, 这会消耗服务器的CPU, 同时服务器还会将这种处于半连接状态的请求放入请求队列中, 浪费服务器的内存资源.
解决方案:
SYN Cookie技术 - 连接建立时的超时
如,拔掉服务器主机的电缆,客户端发起连接请求,超时后又重新发起.当然不可能无限制的发起, 大多数伯克利系统将尽力一个新连接的最长时间限制为75秒(此话来自TCP/IP详解)
- 疑问1: 为什么要随机初始序列号
- 四次挥手描述(此时, 客户端与服务端都处于ESTABLISHED状态)
第一次挥手: 客户端发送一个FIN报文并停止发送数据,其中,FIN位置1, 序列号为前一次传送数据最后一个字节号 + 1, 此时,客户端处于FIN_WAIT_1状态
第二次挥手: 服务端收到客户端的FIN报文后,发送一个ACK报文, 将确认号 + 1, 序列号为前一次传送数据的最后一个字节号 + 1, 用以对客户端FIN报文的应答.此时, 服务端处于CLOSED_WAIT状态.(这个时候,服务器会向应用程序传送一个文件结束符)
第三次挥手: 服务端发送一个FIN报文并停止发送数据, 其中FIN置位为1, 序列号为前一次传送数据的最后一个字节 + 1, 确认号保持不变. 此时, 服务端处于LAST_ACK状态
第四次挥手: 客户端发送一个确认报文段, 其中将认号 + 1, 序列号不变, 再等到计时器设置为2MSL后, 连接彻底关闭.此时, 客户端处于CLOSED状态.当服务端接收到最后的确认报文时, 也处于CLOSED状态.- 疑问1: 为什么客户端发送了ACK报文段之后不直接关闭,而是等待一段时间?
因为要确保服务器能收到客户端的ACK报文, 如果ACK报文段丢失, 服务端没有收到, 便会重新发送FIN报文给客户端, 直到服务端收到FIN报文为止.MSL是指报文段在网络中存在的最长时间.2MSL即一来一回. - 需要注意的点
当连接处于TIME_WAIT状态时, 插口中使用的本地端口在默认情况下不能再被使用.即,假如是客户执行主动关闭进入TIME_WAIT, 在终止这个客户程序,又立即启动这个客户程序时, 不能重用相同的本地端口.而对于服务器来说, 服务器使用的是熟知端口,当我们终止服务器程序,又试图立即启动服务器程序时,服务器程序是不能使用这个熟知端口的, 必须等待几分钟, 才重启服务器程序. - 半关闭连接
连接的一端在结束发送后还能接收来自另一端数据的能力, 叫做半关闭状态.假如没有半关闭,便需要其他的一些技术让客户通知服务器,客户端已经完成了它的数据传送,但仍要接收来自服务器的数据.
- 疑问1: 为什么客户端发送了ACK报文段之后不直接关闭,而是等待一段时间?
<四>TCP报文以流水线式进行传输
发送方可以连续发送多个分组, 不必每发完一个分组就停顿下来等待对方的确认.
<五>流量控制与拥塞控制
流量控制: 指对给定的发送端和接收端之间的点对点通信量的控制, 也就是抑制发送端发送数据的速率,以便让接收端来得及接收.
拥塞控制: 由于对资源的需求总和 大于 可用资源,如带宽,路由器缓存等,而出现的拥塞现象.拥塞控制是一个全局性的过程,对整个网络的动态调控,涉及到所有的主机,所有的路由器等等.
实现的技术: 滑动窗口技术
- 描述:
TCP首部中有个字段叫Window, 即窗口大小, 它是接收端用来告诉发送端自己还有多少缓冲区可以接收数据.于是,发送端便可以根据这个接收端的处理能力来动态地调整自己的发送窗口的大小,而不会因为发送速率过快引起接收端的缓冲区溢出导致丢包.
对于发送端而言,发送缓冲区的数据有3类, 第一种为,已发送已确认, 第二种为, 已发送未确认, 第三种为, 未发送且可以发送(仍在接收端的接收范围,这部分也称为可用窗口), 第四种为未发送且接收端也不允许发送的(因为这些数据超出了接收端的接收范围),其中第二种与第三种,位于发送窗口中.
对于接收端而言, 接收缓冲区的数据也有3类, 第一种为, 数据属于已接收已回复确认但还没交付给上层应用程序的, 第二种为,已接收,但还没回复ACK报文的, 这些包可能属于失序到达的,属于延迟ACK范围, 第三种为,有空位,还没有被接收的数据. - 原理
当发送窗口中的数据发送且被确认时, 窗口合拢, 也就是窗口的左边沿向右边沿靠近
当接收端的接收进程已经读取确认的数据并释放了TCP的接收缓存的时候, 可用的接收缓存变大, 发送窗口张开, 即可用窗口变大, 窗口右边沿向右移动.
当右边沿向左移动的时候, 窗口收缩. - 原理模拟
- 零窗口问题
1)当接收缓存满,没有容量继续接收数据时, 出现的零窗口,即将窗口字段置0 发送端接收到窗口字段为0的报文时, 不能再发送数据
2)引起死锁情况发生
隔一段时间后,接收缓存又有空间继续接收数据, 接收端便会发送一个ACK报文段通告发送端表明当前的窗口字段不再为0, 即发送端可继续发送数据.然而, TCP不对ACK报文段进行确认, 只确认那些包含数据的ACK段确认.如果这个ACK报文通告丢失了, 则双方就有可能因为等待对方而使得连接终止, 即接收向发送端通告了非0窗口, 接收端进入等待接收数据状态, 而发送端在等待允许它继续发送数据的窗口更新.
3)解决方法
发送方使用一个坚持定时器来周期性地向接收方查询,以便发现窗口是否已经增大,即窗口探查.窗口探查包含一个字节的数据, 但是,接收端返回的窗口为0的ACK报文不会确认该字节, 所以这个字节会被持续地重传.直到窗口被打开或者应用进程的连接被终止. - 糊涂窗口综合征问题
1)随着接收方越来越忙, 接收方可以通告一个小的窗口,而发送也可以发送少量的数据发送的少量数据可能比IP首部+TCP首部的字节数还要少, 这样会导致开销大.
2)解决方案
由接收端引起的, 接收端可以不通告小窗口,当窗口值小于某个值时, 直接将窗口字段设置为0,关闭窗口, 直到窗口大小大于或等于MSS, 或者是等接收缓存有一半为空的时候再打开窗口让发送端可以发送数据.
由发送端引起的, 发送端可以攒数据,等到数据可以足以发送一个满长度的报文的时候再发.
<六>拥塞控制
拥塞窗口来控制
发送窗口 = min(cwnd, rwnd);
- 慢开始
刚刚加入网络的连接, 一点点的提速.
1)建立连接后初始化拥塞窗口为1, 表示可以传一个MSS大小的数据
2)每当收到一个ACK,拥塞窗口线性上升
3)每过一个RTT, 拥塞窗口呈指数上升
4)当到达阈值时, 进入拥塞避免算法 - 拥塞避免
1)每收到一个ACK, 拥塞窗口就增加1/cwnd
2)每过了一个RTT, 拥塞窗口线性上升
早早期的处理,出现超时丢包事件时, 又重新回到慢开始算法, 并且将阈值设置为前一个阈值的一半. - 快重传快恢复
当发送端收到3个重复的ACK时就立即重传, 而不是等到超时再重传,此时先拥塞窗口先被设置为当前的一般(cwnd = cwnd / 2),阈值再被设置为cwnd(这个是一半之后的cwnd, 有的地方是cwnd + 3 * MSS), 进入快恢复算法
快恢复算法是对于拥塞窗口而言的,相对于早期的直接将cwnd设置为1,再慢开始这种极端的反应方式, 快恢复算法是让cwnd减少为当前的一半.
1)发送端重传重复ACK中指定的数据包
2)如果发送端再收到重复的ACK,那么拥塞窗口 + 1;
3)如果发送端收到了新的ACK,那么cwnd = 阈值,进入拥塞避免算法.
这个算法也有缺点, 3个重复的ACK不代表只丢了一个数据包,很可能丢了好多包, 但这个算法只会重传一个, 而剩下的那些包智能等到超时.超时一个拥塞窗口就减半一下,多个超时会导致TCP的传输速度成指数下降, 也不会再出发快恢复算法了,其实,TCP选项中可以附带SACK选项,表明丢失后还有哪些数据收到的,发送端便可以先重传收到3个重复ACK的那个数据包,如果之丢失一个包,那么接收端返回的ACK是整个被发送端发送出去的数据, 否则,就是丢失了多个数据包, 那么发送端会继续重传滑动窗口中还没有被ACK的第一个包,直到完整地收到整个数据的ACK之后才结束快恢复过程.
浙公网安备 33010602011771号