之前我们已经初步了解了端口号!
今天我们首先就要更加深入了解端口号!
在 TCP/IP 协议中 , 用 " 源 IP", " 源端口号 ", " 目的 IP", " 目的端口号 ", " 协议号 " 这样一个五元组来标识一个通信
其中端口号的划分
0-1023是知名端口号(一半不允许绑定,除非使用root,但是我们一般也不会用)
1024-65535操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.
比如
ssh 服务器 , 使用 22 端口
ftp 服务器 , 使用 21 端口
telnet 服务器 , 使用 23 端口
http 服务器 , 使用 80 端口
https 服务器 , 使用 443
我们自己写一个程序使用端口号时 , 要避开这些知名端口号 .
然后我们就可以开始学习udp协议了!
udp协议非常简单,它的报头是固定长度的,这样在传输过程中很容易就可以进行解包了!
16位UDP长度, 表示整个数据报(UDP首部+UDP数据)的最大长度;
如果校验和出错, 就会直接丢弃;
如果校验和出错, 就会直接丢弃;
我们接下来说一下UDP 的特点
UDP 传输的过程类似于寄信 .
无连接 : 知道对端的 IP 和端口号就直接进行传输 , 不需要建立连接 ;
不可靠 : 没有确认机制 , 没有重传机制 ; 如果因为网络故障该段无法发到对方 , UDP 协议层也不会给应用层 返回任何错误信息;
面向数据报 : 不能够灵活的控制读写数据的次数和数量
面向数据报
应用层交给 UDP 多长的报文 , UDP 原样发送 , 既不会拆分 , 也不会合并 ;
用 UDP 传输 100 个字节的数据 :
如果发送端调用一次 sendto, 发送 100 个字节 , 那么接收端也必须调用对应的一次 recvfrom, 接收 100 个
字节 ; 而不能循环调用 10 次 recvfrom, 每次接收 10 个字节 ;
UDP 的缓冲区
UDP 没有真正意义上的 发送缓冲区 . 调用 sendto 会直接交给内核 , 由内核将数据传给网络层协议进行后 续的传输动作;
UDP 具有接收缓冲区 . 但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致 ; 如果 缓冲区满了, 再到达的 UDP 数据就会被丢弃 ;
UDP 的 socket 既能读 , 也能写 , 这个概念叫做 全双工
我们注意到 , UDP 协议首部中有一个 16 位的最大长度 . 也就是说一个 UDP 能传输的数据最大长度是 64K( 包含 UDP 首
部 ).
然而 64K 在当今的互联网环境下 , 是一个非常小的数字 .
所有如果我们需要传输的数据超过64K,我们必须对数据进行分包发送!!!
基于 UDP 的应用层协议
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议 ( 用于无盘设备启动 )
DNS: 域名解析协议
当然 , 也包括你自己写 UDP 程序时自定义的应用层协议 ;
由于udp比较简单,而我们真正的重点其实是tcp,所以接下来我们开始学习tcp!
首先是标准报头有20个字节,然后可以带选项(我们目前暂时忽略),最后就是我们的数据!
所以报头仍然是定长报头,所以我们接收方很容易就知道哪些数据是报头!
然后我们的报头里面有4位首部长度,这个就可以得到报头+选项的长度,所以如果有选项,只要 首部长度-20字节就是选项的数据大小了!
但是大家可能疑惑4位长度最多只能表示15,这明显不够啊!这就是因为我们这个地方其实有基本长度的,4个字节!也即真正的大小是0-60个字节,这样就可以满足我们的需要了!
然后我们来学习 窗口大小是什么。
首先我们要知道接受方是有缓冲区大小的,如果我们完全不加限制的发送数据,就会很容易写满对方的缓冲区,而一旦溢出,这些数据就会被直接丢掉,肯定是不行的,所以我们必须对发送方进行控制!
其次就是每次发送数据都是要有应答,才能确认完成一次数据发送!
所以为了进行流量控制,每次应答的时候,接受方返回的应答都会把把剩余的缓冲区大小发送到发送方!也即所谓的窗口大小,这样发送方就可以对发送量进行调节了!
接下来我们学习 序号和 确认序号!
首先我们想一个问题,数据发送的顺序和应答的顺序一定是一致的吗?
答案就是:不是。因为在路途中可能发生各种情况,所以这个顺序不是一定的!
但是对接收方来说我们肯定要保证数据的前后顺序,否则将会出现很多意外情况!
那么我们怎么办呢?显然我们可以加一个标识去标明顺序!
所以这个序号就是数据的发送顺序是相关的!
那么每个被发送的字节的数据,天生就拥有序号也即是第多少个发送的顺序!
所以我们每一次发送的时候把第一个被发送的字节的序号填到序号里面就可以实现排序的功能!
至于这个序号也没有上限我们在滑动窗口的地方再细讲!
那么同理确认序号即对收到的报文序号+1再发送响应!
为什么要加1呢?这个地方我们说一个原因!
这个确认序号的含义是当前序号之前的序号已经收到了!
这样比如传输了0-1000,1000-2000,2000-3000,3000-4000
一旦返回了4001到发送方,发送方就知道0-4000全部收到了!
这样的意义是什么呢?
大家可以想如果1000-2000的应答丢失了,但是收到了4001,接收方也会知道对方已经收到了1000-2000的数据,这样我们就可以允许少量的应答已经丢失了!这样就可以提高效率!
顺便强调一下,有时候应答和数据发送会放在一起提高效率—— 捎带应答!
如上我们目前暂时就完成了序号和确认序号的学习,部分更深的内容我们后面再谈!
接下来我们继续了解 报头的类型!
所以我们的报头存在6个类型标志位!
ACK:即应答选项,携带应答消息的时候就启动ACK标志位!
SYN:请求建立连接!我们在三次握手,四次挥手的地方讲!
FIN:通知对方,本端要关闭了!同样之后再细讲!
PSH:催促对方取走数据,腾出缓冲区的空间!
RST:当我们开始连接的时候可能会出现一方认为连接成功,另外一方连接失败,带了这个标注位就是告诉对方要重新连接!(具体的连接后面再讲)
URG:代表报文中的数据有效,非常紧急,需要插队提前处理!一旦被标志,那么16位紧急指针就会有效,然后接收方就会根据紧急指针将数据提前到什么数据前处理!(这里要提醒的是在tcp中紧急数据往往只允许一个字节(大多数情况),所以紧急指针指哪里就会被提到哪里之前进行读取)
讲完上面的报头之后我们就要开始学习tcp的一些保障效率和可靠性的机制了了!
1. 超时重传机制
主机 A 发送数据给 B 之后 , 可能因为网络拥堵等原因 , 数据无法到达主机 B;
如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答 , 就会进行重发 ;
但是 , 主机 A 未收到 B 发来的确认应答 , 也可能是因为ACK丢失了!
因此主机 B 会收到很多重复数据 . 那么 TCP 协议需要能够识别出那些包是重复的包 , 并且把重复的丢弃掉 . , 就可以很容易做到去重的效果 . 这时候我们可以利用前面提到的序列号 那么, 如果超时的时间如何确定 ?
最理想的情况下 , 找到一个最小的时间 , 保证 " 确认应答一定能在这个时间内返回 ".
但是这个时间的长短 , 随着网络环境的不同 , 是有差异的 .
如果超时时间设的太长 , 会影响整体的重传效率 ;
如果超时时间设的太短 , 有可能会频繁发送重复的包 ;
TCP 为了保证无论在任何环境下都能比较高性能的通信 , 因此会动态计算这个最大超时时间 .
Linux 中 (BSD Unix 和 Windows 也是如此 ), 超时以 500ms 为一个单位进行控制 , 每次判定超时重发的超时
时间都是 500ms 的整数倍 .
如果重发一次之后 , 仍然得不到应答 , 等待 2*500ms 后再进行重传 .
如果仍然得不到应答 , 等待 4*500ms 进行重传 . 依次类推 , 以指数形式递增 .
累计到一定的重传次数 , TCP 认为网络或者对端主机出现异常 , 强制关闭连接 .
2.连接管理机制
之前我们讲的都是发送双方的上层操作,现在我们要讲开始和结束的操作!
我们简称3次握手,4次挥手!
三次握手
首先一方发送SYN请求建立连接!然后收到的一方发送确认ACK和请求连接的SYN(原本应该发两次的,但是这个应答一定是同时的,所以一般都会 捎带应答,压缩成了一次),然后开始发送的一方收到以后也发送ACK进行应答!只要双方收到了就完成了双方连接的建立!
然后我们就有疑问了,为什么要这样呢?如果只发送一次SYN然后收到SYN+ACK就可以建立不也可以吗?可是这样我们没有办法确定接收方能否发送数据!所以必须要保证双方都验证过接受和发送!!!于是要三次握手操作!
其次如果只有单次握手则更加无法验证了,并且如果有发送方疯狂发送SYN,每一次都要建立连接,很容易导致服务器连接资源被消耗完!如果是两次,同样也是服务器先建立连接,一样容易导致上述的问题!可是三次握手是客户端先建立连接,这样成本就会被转嫁到客户端上!可以大幅度提高这个攻击的成本!提高安全!
四次挥手
首先一方先发送FIN,标明要断开连接,开始进入FIN_WAIT_1状态!然后接受应答完成前两次挥手,进入FIN_WAIT_2状态,进入这个状态以后就不再发送数据!然后等待对方也将数据发送完以后,确定没有数据要发送了,也要断开连接,也发送FIN,同理左边也会发送应答!一旦右边接收到应答就直接关闭连接,左边还会等待一段时间,如果右边没有成功接受ACK,再次发送FIN,可以再进行补发,然后再关闭!(但是哪怕最后左边关闭了,右边仍没有收到ACK,超过一段时间以后也会自动关闭的(异常关闭))
3.流量控制
接收端处理数据的速度是有限的 . 如果发送端发的太快 , 导致接收端的缓冲区被打满 , 这个时候如果发送端继续发送 , 就会造成丢包, 继而引起丢包重传等等一系列连锁反应 .
因此 TCP 支持根据接收端的处理能力 , 来决定发送端的发送速度 . 这个机制就叫做 流量控制
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 " 窗口大小 " 字段 , 通过 ACK 端通知发送端 ;
窗口大小字段越大 , 说明网络的吞吐量越高 ;
接收端一旦发现自己的缓冲区快满了 , 就会将窗口大小设置成一个更小的值通知给发送端 ;
发送端接受到这个窗口之后 , 就会减慢自己的发送速度 ;
如果接收端缓冲区满了 , 就会将窗口置为 0; 这时发送方不再发送数据 , 但是需要定期发送一个窗口探测数 据段, 使接收端把窗口大小告诉发送端 .
接收端如何把窗口大小告诉发送端呢 ? 回忆我们的 TCP 首部中 , 有一个 16 位窗口字段 , 就是存放了窗口大小信息 ; 那么问题来了, 16 位数字最大表示 65535, 那么 TCP 窗口最大就是 65535 字节么 ?
实际上 , TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是 窗口字段的值左移 M 位 ;
4.滑动窗口
刚才我们讨论了确认应答策略 , 对每一个发送的数据段 , 都要给一个 ACK 确认应答 . 收到 ACK 后再发送下一个数据段 . 这样做有一个比较大的缺点, 就是性能较差 . 尤其是数据往返的时间较长的时候 .
既然这样一发一收的方式性能较低 , 那么我们一次发送多条数据 , 就可以大大的提高性能 ( 其实是将多个段的等待时 间重叠在一起了).
上图的窗口大小就是 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值
4000 个字节 ( 四个 段). 发送前四个段的时候, 不需要等待任何 ACK, 直接发送 ;
收到第一个 ACK 后 , 滑动窗口向后移动 , 继续发送第五个段的数据 ; 依次类推 ;
操作系统内核为了维护这个滑动窗口 , 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答 ; 只有确 认应答过的数据, 才能从缓冲区删掉 ; 窗口越大, 则网络的吞吐率就越高 ;
那么如果出现了丢包 , 如何进行重传 ? 这里分两种情况讨论 .
情况一 : 数据包已经抵达 , ACK 被丢了
这种情况下 , 部分 ACK 丢了并不要紧 , 因为可以通过后续的 ACK 进行确认 ;
情况二 : 数据包就直接丢了 .
当某一段报文段丢失之后 , 发送端会一直收到 10 发送端 " 我想要的是 1001" 一样;
如果发送端主机连续三次收到了同样一个 "1001" 这样的应答 , 就会将对应的数据 1001 - 2000 重新发送 ; 这个时候接收端收到了 1001 之后 , 再次返回的 ACK 就是 7001 了 ( 因为 2001 - 7000) 接收端其实之前就已 经收到了, 被放到了接收端操作系统内核的 接收缓冲区 中 ; 这种机制被称为 " 高速重发控制 "( 也叫 " 快重传 ")
5.拥塞控制
虽然 TCP 有了滑动窗口这个大杀器 , 能够高效可靠的发送大量的数据 . 但是如果在刚开始阶段就发送大量的数据 , 仍 然可能引发问题.
因为网络上有很多的计算机 , 可能当前的网络状态就已经比较拥堵 . 在不清楚当前网络状态下 , 贸然发送大量的数据 , 是很有可能引起雪上加霜的.
TCP 引入 慢启动 机制 , 先发少量的数据 , 探探路 , 摸清当前的网络拥堵状态 , 再决定按照多大的速度传输数据 ;
此处引入一个概念程为 拥塞窗口 发送开始的时候, 定义拥塞窗口大
每次收到一个 ACK 应答 , 拥塞窗口 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较 , 取较小的值作为实际发送的窗口;
像上面这样的拥塞窗口增长速度 , 是指数级别的 . " 慢启动 " 只是指初使时慢 , 但是增长速度非常快 .
为了不增长的那么快 , 因此不能使拥塞窗口单纯的加倍 .
此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候 , 不再按照指数方式增长 , 而是按照线性方式增长
当 TCP 开始启动的时候 , 慢启动阈值等于窗口最大值 ;
在每次超时重发的时候 , 慢启动阈值会变成原来的一半 , 同时拥塞窗口置回1。
少量的丢包 , 我们仅仅是触发超时重传 ; 大量的丢包 , 我们就认为网络拥塞 ;
当 TCP 通信开始后 , 网络吞吐量会逐渐上升 ; 随着网络发生拥堵 , 吞吐量会立刻下降 ;
拥塞控制 , 归根结底是 TCP 协议想尽可能快的把数据传输给对方 , 但是又要避免给网络造成太大压力的折中方案 .
6.延迟应答
如果接收数据的主机立刻返回 ACK 应答 , 这时候返回的窗口可能比较小 .
假设接收端缓冲区为 1M. 一次收到了 500K 的数据 ; 如果立刻应答 , 返回的窗口就是 500K;
但实际上可能处理端处理的速度很快 , 10ms 之内就把 500K 数据从缓冲区消费掉了 ;
在这种情况下 , 接收端处理还远没有达到自己的极限 , 即使窗口再放大一些 , 也能处理过来 ;
如果接收端稍微等一会再应答 , 比如等待 200ms 再应答 , 那么这个时候返回的窗口大小就是 1M;
一定要记得 , 窗口越大 , 网络吞吐量就越大 , 传输效率就越高 . 我们的目标是在保证网络不拥塞的情况下尽量提高传输
效率 ;
那么所有的包都可以延迟应答么 ? 肯定也不是 ;
数量限制 : 每隔 N 个包就应答一次 ;
时间限制 : 超过最大延迟时间就应答一次 ;
具体的数量和超时时间 , 依操作系统不同也有差异 ; 一般 N 取 2, 超时时间取 200ms;
上面基本就是一些tcp为了保证效率和可靠性所产生的策略!
接下来我们来了解一下tcp的特性—— 面向字节流
创建一个 TCP 的 socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区 ;
调用 write 时 , 数据会先写入发送缓冲区中 ;
如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出 去;
接收数据的时候 , 数据也是从网卡驱动程序到达内核的接收缓冲区 ;
然后应用程序可以调用 read 从接收缓冲区拿数据 ;
另一方面 , TCP 的一个连接 , 既有发送缓冲区 , 也有接收缓冲区 , 那么对于这一个连接 , 既可以读数据 , 也可 以写数据. 这个概念叫做 全双工 由于缓冲区的存在, TCP 程序的读和写不需要一一匹配 ,
例如 :
写 100 个字节数据时 , 可以调用一次 write 写 100 个字节 , 也可以调用 100 次 write, 每次写一个字节 ;
读 100 个字节数据时 , 也完全不需要考虑写的时候是怎么写的 , 既可以一次 read 100 个字节 , 也可以一次 read一个字节 , 重复 100 次 ;
然后我们再谈一下tcp的一些常见的问题!
粘包问题
首先要明确 , 粘包问题中的 " 包 " , 是指的应用层的数据包 .
在 TCP 的协议头中 , 没有如同 UDP 一样的 " 报文长度 " 这样的字段 , 但是有一个序号这样的字段 .
站在传输层的角度 , TCP 是一个一个报文过来的 . 按照序号排好序放在缓冲区中 .
站在应用层的角度 , 看到的只是一串连续的字节数据 .
那么应用程序看到了这么一连串的字节数据 , 就不知道从哪个部分开始到哪个部分 , 是一个完整的应用层 数据包. 那么如何避免粘包问题呢? 归根结底就是一句话 ,
明确两个包之间的边界 .
对于定长的包 , 保证每次都按固定大小读取即可 ; 例如上面的 Request 结构 , 是固定大小的 , 那么就从缓冲 区从头开始按sizeof(Request) 依次读取即可 ;
对于变长的包 , 可以在包头的位置 , 约定一个包总长度的字段 , 从而就知道了包的结束位置 ;
对于变长的包 , 还可以在包和包之间使用明确的分隔符 ( 应用层协议 , 是程序猿自己来定的 , 只要保证分隔 符不和正文冲突即可)
对于 UDP 协议来说 , 是否也存在 " 粘包问题 " 呢 ?
对于 UDP, 如果还没有上层交付数据 , UDP 的报文长度仍然在 . 同时 , UDP 是一个一个把数据交付给应用 层. 就有很明确的数据边界 .
站在应用层的站在应用层的角度 , 使用 UDP 的时候 , 要么收到完整的 UDP 报文 , 要么不收 . 不会出现 " 半 个" 的情况 .
TCP 异常情况
进程终止 : 进程终止会释放文件描述符 , 仍然可以发送 FIN. 和正常关闭没有什么区别 .
机器重启 : 和进程终止的情况相同 .
机器掉电 / 网线断开 : 接收端认为连接还在 , 一旦接收端有写入操作 , 接收端发现连接已经不在了 , 就会进行 reset. 即 使没有写入操作, TCP自己也内置了一个保活定时器 , 会定期询问对方是否还在
. 如果对方不在, 也会把连接释放.
另外 , 应用层的某些协议 , 也有一些这样的检测机制 . 例如 HTTP 长连接中 , 也会定期检测对方的状态 . 例如 QQ, 在 QQ 断线之后, 也会定期尝试重新连接 .
最后我们来总结一下tcp协议并且与udp比较一下!
TCP 小结
为什么 TCP 这么复杂 ? 因为要保证可靠性 , 同时又尽可能的提高性能 .
可靠性 :
校验和
序列号( 按序到达)
确认应答
超时重发
连接管理
流量控制
拥塞控制
提高性能 :
滑动窗口
快速重传
延迟应答
捎带应答
其他 :
定时器 ( 超时重传定时器 , 保活定时器 , TIME_WAIT 定时器等 )
基于 TCP 应用层协议
HTTP
HTTPS
SSH
Telnet
FTP
SMTP
当然 , 也包括你自己写 TCP 程序时自定义的应用层协议 ;
TCP/UDP 对比
我们说了 TCP 是可靠连接 , 那么是不是 TCP 一定就优于 UDP 呢 ? TCP 和 UDP 之间的优点和缺点 , 不能简单 , 绝对的进行 比较
TCP 用于可靠传输的情况 , 应用于文件传输 , 重要状态更新等场景 ;
UDP 用于对高速传输和实时性要求较高的通信领域 , 例如 , 早期的 QQ, 视频传输等 . 另外 UDP 可以用于广 播;
归根结底 , TCP 和 UDP 都是程序员的工具 , 什么时机用 , 具体怎么用 , 还是要根据具体的需求场景去判定 .
综上,我们的tcp和udp协议就学习到这里了,感谢观看!