少点代码, 多点头发
本文已经收录至我的 GitHub, 欢迎大家踊跃 star 和 issues.
https://github.com/midou-tech/articles
三次握手建立链接, 四次挥手断开链接. 这个问题算非常经典的问题, 也是面试官非常喜欢问的问题.
不夸张的说, 龙叔在校招面试的时候每一家公司都问到过关于三次握手和四次挥手相关的问题, 相信大家也都差不多被面试官各种怼.
这个问题的重要性, 已经意识到. 不说废话了, 接下来就是听龙叔给你安排的明明白白.
先画个图, 看下 TCP 的建立连接 和 断开连接的整体过程.
tcp 三次握手四次挥手
看完这个图相信聪明的你在整体对三次握手和四次挥手有了一些基本把控. 但是, 里面的细节肯定是会有些生疏或者模糊的, 接下来就一个一个问题的揭露本质.
在解释之前先看点基础知识做做铺垫.
TCP 状态转移解释
状态 | 描述 |
---|---|
CLOSED | 阻塞或关闭状态,表示主机当前没有正在传输或者建立的链接 |
LISTEN | 监听状态,表示服务器做好准备,等待建立传输链接 |
SYN RECV | 收到第一次的传输请求,还未进行确认 |
SYN SENT | 发送完第一个 SYN 报文,等待收到确认 |
ESTABLISHED | 链接正常建立之后进入数据传输阶段 |
FIN WAIT1 | 主动发送第一个 FIN 报文之后进入该状态 |
FIN WAIT2 | 已经收到第一个 FIN 的确认信号,等待对方发送关闭请求 |
TIMED WAIT | 完成双向链接关闭,等待分组消失 |
CLOSING | 双方同时关闭请求,等待对方确认时 |
CLOSE WAIT | 收到对方的关闭请求并进行确认进入该状态 |
LAST ACK | 等待最后一次确认关闭的报文 |
再看下 TCP 的报文格式
TCP 报文格式
首部有 20 字节的固定长度, 含义如下:
源端口和目的端口
各占 2 字节, 就是存储源端口号和目的端口的
序号 seq
占 4 字节, 表示的范围就是整形的范围[0~2^32]. 序号使用在给数据部分每个字节进行编号的, 编号方式是 mod 2^32 .
确认号 ack
占 4 字节, 范围也是无符号整数的范围. 使用在对端传输给我的数据最后一个字节序号, 例如 A 传输给 B 101-500, 此时 B 返回的确认号一定是小于等于 501 的. 当 B 段正确接收数据之后才会返回确认号, 换句话说确认号之前的数据已经全部接收.
数据偏移
占 4bit, 数据偏移很多人很容易想到是不是表示数据的长度, 那就错了. 偏移嘛, 指的是 TCP 起始位置到数据部分的起始位置的偏移, 也就是 TCP 首部的长度.
保留
占 6bit, 保留字段顾名思义, 就是为今后使用, 默认置为 0.
紧急 URG 控制位
占用 1bit,URG=1, 表示紧急指针有效, 此时 tcp 数据优先传输. 相当于生活中的紧急通道, 特殊情况时使用.
在网络中也会有特殊情况, 例如, 发送一个很长的程序在远程服务器上运行, 此时发现程序有 bug, 需要中断运行, 因此我们从键盘输入 Ctrl c, 假如不使用紧急数据, 需要在缓冲区里排队, 都知道是 bug 了, 还要排队, 这怕是要出锅啊.
此时使用紧急数据传输, 不需要排队, 直接中断程序是不是更符合我们的预期.
需要注意一点是, 即使窗口为 0 时, 也可以发送紧急数据.
如何使用紧急 URG 控制位, 在 socket 编程中 send 函数 flag 参数
send(int socket, const void *buffer, size_t length, int flags);
flags 参数传 MSG_OOB 宏时, 表示此时有紧急数据. MSG_OOB 是个宏,
确认 ACK
占 1bit, 当 ACK=1 时生效. TCP 有条硬性规定, 当建立链接成功后所有传输的数据报文都必须把 ACK 置为 1.
推送 PSH
占 1bit, 发送方把 PSH 置为 1 时 会立即发送该数据包, 接收方收到 PSH=1 的报文会立即处理交付给应用层处理. 是不是感觉和 URG 很像, 其实还是有些区别的.
两者相同点:
URG 与 PSH 两者都使用于紧急处理的情况, 用来快速传输紧急数据.
两者不同点
URG 置为 1 时, 对于发送发,"带外数据" 与正常情况下应该发送的消息数据一起, 封装成数据报发送, 省去了在队列中等待的时间. 在接收方, 解析报文后, 获取数据之后还是要放在缓存区中, 等待满了之后在向上往应用层交付.
PSH 置为 1 时, 对于发送方, 表明这些数据不需要等向下发送的缓存区满, 立刻封装成报文, 发送, 省去了等待发送缓存区到达满的状态的时间. 在接收方, 也不需要等接受缓存区满, 直接向上交付给应用层.
复位 RST
占 1bit, 当 RST=1 时, TCP 会主动释放链接, 两种情况会用上.
TCP 出现严重差错时, 会主动释放连接, 重建链接, 传输数据.
遇到非法报文或者拒绝连接时会把 RST 置为 1.
同步 SYN
占 1bit, 同步控制位, 用来在传输连接建立时同步传输连接序号.
SYN=1 时, 表示这是一个连接请求或连接确认报文.
SYN=1,ACK=0, 表明这是一个连接请求数据段, 如果对方同意建立连接, 则对方会返回一个 SYN=1,ACK=1 的确认.
FIN 控制位
占 1bit, 用于释放一个传输连接.
FIN=1 时, 表示数据已全部传输完成, 发送端没有数据要传输了, 要求释放当前连接, 但是接收端仍然可以继续接收还没有接收完的数据.
FIN=0, 正常传输数据.
窗口大小
占 16bit,2byte, 用于表示发送方可以接受的最大数据大小.
该窗口是动态变化的, 用作流量控制时使用.
检验和
占 16bit,2byte, 用于对 TCP 头部, 伪头部, 数据三个部分进行校验.
紧急指针
占 16bit,2byte, 用于记录紧急数据的末尾在数据段中的位置.
当 URG=1 时, 该指针才生效.
可选项
可选项最长可达 40byte, 是可选的, 可以没有. 当可选项不存在时, TCP 头部长度为 20byte.
可选项可以包括窗口缩放选项 (Windows ScaleOption,WSopt),MSS(最大数据段大小) 选项, SACK(选择性确认)选项, 时间戳 (Timestamp) 选项等.
数据
TCP 数据部分, 由应用层应用程序提交的数据.
TCP 头部是基础知识, 必须了解才能更好的理解 TCP 数据如何封装和传输, 以及在建立链接和断开链接时都在操作那些地方.
三次握手建立连接
三次握手如何建立连接?
三次握手建立链接
从图中可以清楚的看到, 三次握手的过程, 我在在把过程清楚的解释一遍, 顺便说下每个过程容易被问到的知识点.
采用 C/S 模式解释, 假设 C 端发起传输请求.
在发送建立链接请求之前, C 端是保持 CLOSED 状态, S 端最开始也是处于 CLOSED 状态, 当执行 listen 函数套接字进入被动监听状态.
所谓被动监听, 是指当没有客户端请求时, 套接字处于 "睡眠" 状态, 只有当接收到客户端请求时, 套接字才会被 "唤醒" 来响应请求.
第一次: C 端发送 SYN=1 的请求报文, 此时 C 端进入 SYN SENT 状态, 等待服务器确认.
此时如果报文丢失发送不到对端会如何?
C 端发送报文之后会启动一个定时器, 在超时之后未收到 S 端的确认, 会再次发送 SYN 请求, 每次尝试的时间会是第一次的二倍, 如果总的总尝试时间为 75 秒, 此次建立链接失败.
第二次: S 端收到 C 端发送的 SYN 报文 (建立链接请求) 后, S 端必须返回确认号并且同时发送一条 SYN 报文, 此时进入 SYN RCVD 状态.
为啥要连带发送 SYN 报文?
TCP 是全双工通信, 协议规定当收到建立链接请求后必须返回序列号, 同时建立本端到对端的通信链接. 这也叫做捎带应答机制.
如果第二次报文丢失怎么办?
在发送完 ACK+SYN 报文后会启动一个定时器, 超时没有收到 ACK 确认, 会再次发送, 会进行多次重试. 超时时间依旧每次翻倍, 重试次数可设置.
修改 /proc/sys.NET/ipv4/tcp_synack_retries 的值
第三次: C 端收到 S 端发的 ACK+SYN 报文, 需要返回一个应答 ACK 的报文, 此时该连接会进入半连接状态的队列, 当 S 端收到 ACK 后, 一条完整的全双工 TCP 链接建立完成, 双方进入 ESTABLISHED 状态.
这里有个常用攻击手段, 攻击者伪造一个 SYN 请求发送给服务端, 服务端响应之后, 会收不到 C 端的 ACK 确认, 服务端会不断的重试, 默认会重试五次.
此时服务端会维持这个链接的所有资源, 如果有大量这样的请求, 服务端的资源会被耗完.
这就是 DOS 攻击.
如果第三次报文丢失怎么办?
S 端在发出 ACK+SYN 报文后会启动一个定时器, 在超时触发还没收到 ACK 就确认是丢失了, 会重试一次发送.
这里面的每个状态都必须搞明白, 面试官也超级爱问上面的状态转移.
龙叔还遇到过一个面试官问我用过 socket 编程么? 问我用过哪些 socket 函数?
C 端 socket 编程代码
- //C 端
- #define PORT 8080
- #define BUFFER_SIZE 1024
- int main(int argc, char **argv)
- {
- // 定义 IPV4 的 TCP 连接的套接字描述符
- int sock_cli = socket(AF_INET,SOCK_STREAM, 0);
- // 定义 sockaddr_in
- struct sockaddr_in servaddr;
- memset(&servaddr, 0, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_addr.s_addr = inet_addr(argv[1]);
- servaddr.sin_port = htons(PORT);
- // 连接服务器, 成功返回 0, 错误返回 - 1
- int ret = connect(sock_cli, (struct sockaddr *)&servaddr, sizeof(servaddr));
- // 客户端将控制台输入的信息发送给服务器端, 服务器原样返回信息, 阻塞
- while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
- {
- ret=send(sock_cli, sendbuf, strlen(sendbuf),0); /// 发送
- recv(sock_cli, recvbuf, sizeof(recvbuf),0); /// 接收
- fputs(recvbuf, stdout);
- }
- close(sock_cli); // 关闭连接
- return 0;
- }
S 端 socket 编程代码
- int main(int argc, char **argv)
- {
- // 定义 IPV4 的 TCP 连接的套接字描述符
- int server_sockfd = socket(AF_INET,SOCK_STREAM, 0);
- // 定义 sockaddr_in
- struct sockaddr_in server_sockaddr;
- server_sockaddr.sin_family = AF_INET;
- server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- server_sockaddr.sin_port = htons(PORT);
- //bind 成功返回 0, 出错返回 - 1
- if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1)
- //listen 成功返回 0, 出错返回 - 1, 允许同时监听的连接数为 QUEUE_SIZE
- if(listen(server_sockfd,QUEUE_SIZE) == -1)
- for(;;)
- {
- struct sockaddr_in client_addr;
- socklen_t length = sizeof(client_addr);
- // 进程阻塞在 accept 上, 成功返回非负描述字, 出错返回 - 1
- int conn = accept(server_sockfd, (struct sockaddr*)&client_addr,&length);
- // 处理数据部分
- ...
- }
- close(server_sockfd);
- return 0;
- }
为什么需要三次握手建立链接, 2 次可以么, 4 次行不行?
这问题问的, 面试官是咋了? 在这明知故问的, 整些有的没的. 肯定是不行啊, RFC 标准就是这样写的啊.
可不敢这样回答啊, 标准是说的三次握手建立链接, 可没说四次不行啊. 要是这样答, 妥妥的会收到, 同学我们今天的面试到此基本结束了, 你回家等消息...
龙叔来说说这个问题, 为什么不能两次?
如果第二次不发送 SYN+ACK, 只是发送确认应答消息 ACK, 会造成只能建立单向通信, 而且不能应答. 而 TCP 是全双工通信的, 而且必须保证可靠性.
如果第二发送 SYN+ACK, 不用应答. 此时会出现三种情况
一, 二次握手失败, C 端会重复发送 SYN 报文, 等待对端发送确认报文, S 端会保存 tcp 连接的所有资源, 大量的这种情况会导致 S 资源耗尽.
二, 二次握手成功, S 收不到 ACK 会重复发送 SYN+ACK 报文.
三, 二次握手完以后, 双方以为连接建立成功, 即可开始通信. 假如此时连接并没有真的建立成功, S 端开始发送消息, 会造成网络拥堵发生.
为什么不能是四次?
四次其实原则上来说是可以的, 就是把第二次的 ACK 和 SYN 分两次发送. 在理论上是完全可以行得通的, 但是 TCP 本着节约网络网络资源的前提.
还有一种是不拆开二次握手的捎带应答, 三次握手之后 C 端继续发送 SYN 报文, 其时这是徒劳的. 第三次完成以后链接已经建立, 后面无论多少次都是徒劳.
如果双方同时建立连接, 会发生什么情况?
TCP 同时建立链接
这就是双方同时建立链接的情况, 情况还不错, 反正能建立成功, 这点是肯定的. 但是要注意两点
第一, 此时只会建立一条全双工的 TCP 链接, 不是两条.
第二, 双方没有 CS 之分, 两端都是同时承担两个角色, 客户端和服务器.
四次挥手断开链接
先整个图看下四次挥手的整个过程和状态转移. 状态转移会考看仔细点.
四次挥手断开链接
依旧采用 C/S 模式解释此过程.
第一次: 当 C 端的应用程序结束数据传输是, 会向 S 端发送一个带有 FIN 附加标记的报文段(FIN 表示英文 finish), 此时 C 端进入 FIN_WAIT1 状态, C 端不能在发送数据到 S 端.
第二次: S 端收到 FIN 报文会响应一个 ACK 报文, S 端进入 CLOSE_WAIT 状态. 进入此状态后 S 端把剩余未发送的数据发送到 C 端, C 端收到 S 端的 ACK 之后, 进入 FIN_WAIT2 状态.
同时继续接受 S 端传输的其他数据包.
第三次: S 端处理完自己待发送的数据之后, 也会发送 FIN 断开链接的请求, S 端进入 LAST_ACK 状态.
第四次: C 端收到 S 端的断开链接请求后会启动一个定时器, 该定时器时长是 2MSL(最大段报文生存时间), 同时发送最后一次 ACK 报文.
为什么要四次挥手?
TCP 是全双工的通信机制, 每个方向必须单独进行关闭.
TCP 传输连接关闭的原则如下:
当一端完成它的数据发送任务后就可以发送一个 FIN 字段置 1 的数据段来终止这个方向的数据发送; 当另一端收到这个 FIN 数据段后, 必须通知它的应用层 对端已经终止了那个方向的数据传送.
为什么不能用三次握手中捎带应答机制减少一次握手?
这点到是很迷惑人, 但是掌握了 TCP 传输的一些细节就会发现并不难.
TCP 是全双工通信的, S 收到断开链接请求后只是表示 C 端不会传输数据到 S 端了, 但是并不表示 S 端不传输数据到 C 端.
如果采用捎带应答, S 端将无法把剩余的数据传输到 C 端.
为何最后一次 ACK 之后需要等待 2MSL 的时间?
网络是不可靠的, TCP 是可靠协议, 必须保证最后一次报文送达之后才能断开链接, 否则会再次收到 S 端的 FIN 报文信息.
而等待 2MSL 时间就是为了保证最后最后一次报文丢失时还能重新发送.
为何是 2MSL 的时间?
2MSL 是报文一个往返的最长时间, 假设小于这个时间会发生, ACK 丢了, 但是还没接收到对方重传的 FIN 我方就重新发送了 ACK.
如果已经建立了连接, 但是客户端突然出现故障了怎么办?
这个不难 TCP 自己做了保证, TCP 默认有个定时器, 每次收到客户端的请求后会把定时器设置好, 通常设置两小时, 超过两小时还没收到数据.
服务端会发送一个探测报文, 以后每隔 75 秒钟发送一次. 若一连发送 10 个探测报文仍然没反应, 服务器就认为客户端出了故障, 接着就关闭连接.
总结
三次握手和四次挥手的知识基本告一段落了, 就讲到这里了, 如果有什么不明白的地方可以加我微信探讨.
后面还会出一篇网络编程常用的 Linux 命令行工具, 比如 ping,tcpdump,netstat,nc 等等, 在出一篇计算机网络的总结文章. 计算机网络这部分基本完结了, 如果有不懂得可以看看公号里面前面的文章.
来源: https://www.cnblogs.com/zhonglongbo/p/12701607.html