十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【Linux】网络编程 —— 传输层协议 TCP

【Linux】网络编程 —— 传输层协议 TCP 欢迎来到Linux专栏 ~~ 网络编程博客主页张小姐的猫~江湖背景所属专栏Linux ~ 不破不立作者水平很有限如果发现错误可在评论区指正感谢网络编程欢迎来到Linux专栏 ~~ 网络编程✨TCP协议初认识序号 首部长度 窗口大小等6个常见标志位确认应答ACK机制超时重传机制连接管理机制✅理解TIME_WAIT状态流量控制️滑动窗口正常工作过程异常工作过程滑动窗口丢包情况写在最后✨TCP协议初认识TCP全称为传输控制协议( Transmission Control Protocol )。人如其名要对数据的传输进行⼀个详细的控制序号 首部长度 窗口大小等TCP协议段格式如下接下来我们理解几个相关问题1️⃣报头和有效载荷如何分离TCP报头长度由 TCP 首部中的♦️4位首部长度字段告诉接收方通过首部长度 - 20字节就能算出 有效载荷的大小进而进行分离TCP报文段 │ ├── 前20字节 ────── TCP首部 │ └──20字节以后 ─── 数据Payload♦️4位首部长度报头的长度是浮动的20 n选项这就引出了4位首部长度选项了4位按照二进制的认识来看[0000, 1111] —— [015]十进制很明显是有十六位不能表达出整个报头!4位首部长度还有一个长度基本单位的4字节所以4位首部长度的取值范围是[060]又因为报文是从20大小起步的最终是[2060]举个例子假设报头长度是40字节首部长度应该填几 n 40 / 4 102️⃣正确地认识可靠性协议对发出去的报文要有应答收到应答说明历史报文100%被对方收到了没有百分百可靠的协议因为最新的报文永远没有应答但是历史应答的可靠性是100%确定的。你没有收到报文我也要知道 —— 也是可靠性的一方面两个朝向上发送报文并都对报文进行应答能保证报文本身的可靠性——确认应答机制不就是全双工的可靠性吗ps主播让观众扣666应答本质就是想知道观众有没有在认真看直播内容报文可靠性如果A端没有收到的情况有两种报文丢失和发过来的应答丢失了A如果没有收到任何应答就会判定报文丢失了发送方A会设置一个Deadline过了这个Deadline时间就判定报文丢失了也就是丢包是规定出来的为什么是规定呢 因为发送方一直接收不到应答就有两个可能报文丢失和应答丢失只能规定在一定时间内收不到应答就人为判定报文丢了确认应答机制的核心是对于发送方对方有or没有收到报文重传发送方都有一个确定的结果而不是保证数据100%发送给对方网络情况那如果是应答丢失了呢也要进行重传 那不是发送了两份报文了吗 —— 引出了报文需要带序号去重 比较两个报文序号丢弃掉老报文3️⃣序号和确认序号TCP通信双方地位是对等的TCP协议通信角度是对称的。真实情况下发送端A一次性要给接收方B发送一大堆B对每个进行应答TCP底层传输过程中数据到达B端可能是乱序的但是B最终交付给应用层的数据一定是有序的如果通过网络发送因为互联网不是一条固定线路不同TCP段可能走不同路径出行方式不同为了解决这种乱序的不可靠现象才需要序号排序一下 —— 保证报文是按序到达的那么B如何发送应答才能让A知道是哪个报文丢失了吗 ——确认序号确认序号收到的序号值 1含义该序号之前的所有数据我收到了收到序号为1000应答序号1001 —— 说明1001之前的数据我全部收到了有效地减少报文重发的次数——下一次发送可以从1001开始了4️⃣传输的真实情况我们上面说了这么多发送报文和发回来什么应答其本质的报文我们都要把它看作是一个完整的TCP数据段 TCP报头 数据那么纯应答是不带数据的就是发送为什么序号和确认序号是两个要分开呢发送的序号是100最后应答我在序号上1,不也可以吗因为TCP是全双工通信A→B和B→A是两条完全独立的数据流所以必须各自维护自己的序号。5️⃣捎带应答本来应该单独发送一个ACK确认报文但是刚好自己也有数据要发送于是把ACK信息放到自己的数据报文里面一起发送。同时发确认 数据—— 确认序号 序号设计两个序号确认序号 序号的作用去重按序到达捎带应答超时重传…接下来我们来理解16位窗口大小—— 站在宏观角度上看待流量控制发送的数据大小要以对方的接受能力为主保证了 可靠性 效率1️⃣接受方如何衡量自己的接收能力呢——接收缓冲区中剩余空间的大小报文里包含了一个16位窗口大小选项 就是接收缓冲区的大小2️⃣发送方如何得知对方的接收能力呢——把自己的接收能力填写到应答报文里的16位窗口大小——发送给对方记住记住 报文里16位窗口大小填写的是自己的缓冲区剩余大小发送给人家就知道对方能接收多少了6个常见标志位报文是有类型的—— 应答报文、建立连接、断开连接等等那么TCP协议报头中就必须有对应的字段表面报文的类型1️⃣ACK确认号是否有效如果该报文是应答报文说明该标志位要置1因为真实情况下存在捎带应答99%的情况下ACK标志位要置12️⃣SYN请求建立连接我们把携带SYN标识的称为同步报文段3️⃣RST对方要求重新建立连接我们把携带RST标识的称为复位报文段三次握手完成 ——只要自己把报文发出去就算完成了A认为自己已经完成了三次握手建立好连接了就开始发送新数据了但服务器B 就要立马回一个报文RST置1告诉对方赶紧复位链接把刚刚的链接关了重新进行一次链接。4️⃣PSH提示接收端应用程序立刻从TCP缓冲区把数据读走发送端不断去询问服务器服务器就会不断返回一个不带数据的报文重点关注首部地址长度一旦发现剩余空间不为0就说明可以往里面写数据了PSH置 1 说明客户端的忍耐度到极限了要求接收端尽快把数据取走5️⃣URG16位紧急指针是否有效—— 开关为1——紧急指针有效16位紧急指针标识哪部分数据是紧急数据一般URG和紧急指针都是搭配使用紧急指针特定的偏移量中对应的一个字节指明哪一块是紧急数据如果TCP数据流中存在一部分“紧急数据”接收方应该优先处理当我们把应用层的数据拷贝到下面的缓冲区outbuffer时自然就会有一个数组下标 —— 对应序号。比如我要发送1000个字节不就是把序号设置为1000吗前一千个字节数据当做是我的数据——将来发送的数据都会有编号 ——数组下标本质就是偏移量也可以把紧急指针理解成应用层缓冲区中的某一个偏移量正常的话报文是按序的进行处理的那么有紧急情况的话怎么让报文进行一个插队呢接收方收到紧急指针就能定位出所指向的一个字节被优先读取6️⃣FIN通知对方本端要关闭了,我们称携带FIN标识的为结束报⽂段接下来感性理解建立连接和断开连接。很多个客户端都需要建立链接 那是不是要对链接进行管理啊 —— 先描述再组织tcpsock结构体对其管理structtcp_sock{/* 基础socket */structinet_connection_sockinet_conn;/* 发送相关 */// 当前发送序号u32 snd_nxt;// 已经发送但未确认的数据u32 snd_una;// 发送窗口u32 snd_wnd;// 发送缓存structsk_buff_headwrite_queue;/* 接收相关 */// 下一个期望收到的序号u32 rcv_nxt;// 接收窗口u32 rcv_wnd;// 接收缓存structsk_buff_headreceive_queue;/* 重传相关 */// 重传计时器structtimer_listretransmit_timer;// 重传次数u32 retransmits;/* 拥塞控制 */// 拥塞窗口u32 snd_cwnd;// 慢启动阈值u32 snd_ssthresh;/* 状态 */// TCP状态enumtcp_statestate;};三次握手通信之前验证网络状态是ok的工作下面来讲一下四次挥手四次挥手是以最小的成本的方式争得了双方的同意——关闭全双工通过捎带应答可以将四次挥手进行压缩成三次挥手重新理解三次握手三次握手的本质是四次握手双方主机建立通信共识—— 双方同意验证全双工验证网络是否通畅—— 双方父母也同意最常见的情况下建立链接 —— 三次握手断开链接 —— 四次挥手那为什么是三次握手呢求婚哈哈因为服务器收到客户端的连接必须无条件的答应—— 好啊ACK我也要和你建立连接SYN那为什么是四次挥手呢离婚客户端发起关闭链接的请求本质一对FIN ACK的意思是我发完了我再也不发数据了关闭的只是写端但不影响客户端能继续读东西啊双方都关闭close两对FIN ACK关闭全双工A我要和你断开链接 B好的 但我还有数据没给你发完。此时ACK和SYN不一定能合并在一起——A说我要离婚B说好的但是要等到孩子上大学才离生动形象确认应答ACK机制确认应答机制是。TCP将每个字节的数据都进行了编号即为序列号如果收到了1001说明下一次的起始序号是1001即从1001开始继续发1000个这批数据的最后一个就是2000确认号1001只标记下一段数据的起始位置每一个ACK都带有对应的确认序列号意思是告诉发送者我已经收到了哪些数据下⼀次你从哪里开始发超时重传机制情况1️⃣主机A发送数据给B之后可能因为网络拥堵等原因数据无法到达主机B如果主机A在一个特定时间间隔内没有收到B发来的确认应答就会进行重发情况2️⃣主机A未收到B发来的确认应答也可能是因为ACK丢失了;对于这两个情况我都规定出来一个特定的时间间隔。发送的数据在规定时间内没有得到确认 —— 判定为丢包于是重传该数据。主机A并不能直接判断是数据报文丢了还是ACK应答丢了 ——无脑重传因此主机B会收到很多重复数据。那么TCP协议需要能够识别出那些包是重复的包并且把重复的丢弃掉。—— 这时候我们可以利用前面提到的序列号就可以很容易做到去重的效果那么如果超时的时间如何确定?最理想的情况下找到一个最小的时间保证确认应答一定能在这个时间内返回但是这个时间的长短随着网络环境的不同是有差异的如果超时时间设的太长会影响整体的重传效率如果超时时间设的太短有可能会频繁发送重复的包TCP为了保证无论在任何环境下都能比较高性能的通信因此会动态计算这个最大超时时间连接管理机制在正常情况下TCP要经过三次握手建立连接四次挥手断开连接结论1️⃣三次握手每一次都是从 发出 开始算的—— 发送方 和 接收方都置考虑自己就行三次握手是双方的TCP层自主完成的也就是connect本身是不参与三次握手的他只是握手的发起者最后返回ESTABLISHED给connect那服务器端的accept有参与到握手过程吗 ❌️把一个建立好的链接获取到进程的上下文中返回一个文件描述符把这个链接个文件描述符关联起来所以accept获取已经建立好的链接再次理解为什么是三次握手呢首先阻碍正常通信的情况1.双方意愿、2.网络问题验证全双工三次握手的本质就是解决上述的两个问题1️⃣验证全双工客户端能发送和接收服务器也能发送应答和接收。 —— 以最小成本来验证网络通信问题2️⃣双方意愿问题客户端问服务器SYN服务器回答ACK同样服务器问客户端SYN客户端回答ACK—— 这是四次握手因为服务器收到客户端的连接必须无条件的答应—— 好啊ACK我也要和你建立连接SYN捎带应答所以三次握手能以最小成本来验证双方意愿问题那么为什么两次握手不行呢也是从两方面去验证网络全双工 双方意愿全双工只能验证一方 意愿同样也只能验证一方四次挥手的本质因为TCP是全双工一条连接其实是两条独立的单向信道A→B 和 B→A关闭连接 把这两条信道各自单独关掉每关掉一条信道都需要两件事一方说「我发完了」(FIN)另一方回「收到」(ACK)。再次理解四次挥手过程中的断开链接双方都可以作为断开链接的一端断开链接的本质一方不再给对端写数据了close不等于把链接结构体关闭fd释放了只是把链接结构体的状态修改成FIN_WAIT2后续还可以变成TIME_WAIT只不过是我不再写数据了但是代表我不发送报头和标志位了除了close外那样就会有局部关闭的系统调用#includesys/socket.hintshutdown(intsockfd,inthow);接下来我们验证1️⃣TCP建立链接时 和accept没关系也就是来了一个链接后我们什么都不管不accept、也不去进行处理voidLoop(){signal(SIGCHLD,SIG_IGN);while(true){// InetAddr clientaddr; // 无参构造// auto sockfd _listensock-Accepter(clientaddr); // 此处的sockfd是智能指针指向的是TcpServer对象// if (!sockfd)// continue;// LOG(LogLevel::DEBUG) get a new link,socket: clientaddr.ToString() sockfd: sockfd-Sockfd();// if (fork() 0)// {// // child// service(sockfd, clientaddr);// sockfd-Close();// exit(0);// }sleep(1);}}跑起来之后是变成了listen状态了tcp000.0.0.0:80800.0.0.0:*LISTEN4100400/./httpserve接着客户端进行连接试试看发现是创建出一个链接了 ——说明服务端没有获取链接链接照样是能建立好的不需要accept之前我们使用listen函数intlisten(intsockfd,intbacklog);sockfd已 bind 的 socket 文件描述符backlog 1内核已完成连接全连接队列的最大长度 —— 一般设置 16 or 32返回值成功 0失败 -1之前我们没有详细讲这个backlog默认把它设置成 16 了现在我把他改成 1说明在listen状态下进行链接的数量是有上限的没有accept链接能够建立成功的接收端维持的暂时不用accept到应用层的链接个数是有上限的backlog 1—— 全连接队列全连接队列类似餐厅门口外的等待位置但全连接队列不宜过长✅理解TIME_WAIT状态close_wait和time_wait究竟是什么状态主动断开链接的一方会进入到TIME_WAIT如果客户端要和我断开链接但我还没有和客户端断开链接 CLOSE_WAIT服务器端ACCEPT完一直不对它做CLOSE关闭即可voidLoop(){signal(SIGCHLD,SIG_IGN);while(true){InetAddr clientaddr;// 无参构造autosockfd_listensock-Accepter(clientaddr);// 此处的sockfd是智能指针指向的是TcpServer对象if(!sockfd)continue;LOG(LogLevel::DEBUG)get a new link,socket: clientaddr.ToString()sockfd: sockfd-Sockfd();// if (fork() 0)// {// // child// service(sockfd, clientaddr);// sockfd-Close();// exit(0);// }sleep(1);}}因为客户端一直没收到客户端发的FIN就不能由FIN_WAIT2➡️TIME_WAIT服务端也在等close不然就一直卡在CLOSE_WAIT结论四次挥手之后主动断开链接的一方进入TIME_WAIT状态被动断开一方立即释放链接链接和进程是没有关系的telnet进程退出了仍然可以查到这个链接OS负责的一个链接管理工作当客户端处于TIME_WAIT状态时但没有CLOSE进入CLOSE才算真正的消失TIME_WAIT是链接结构体中的一个状态设置不就说明链接还在吗 —— 对于客户端来说不算关闭此时链接的端口号还是占用的——地址复用setsockopt而被动断开一方已经完成挥手 ——算断开链接了1️⃣那么为什么要进入TIME_WAIT呢客户端要等待⼀个2MSL的时间才会进入CLOSED状态 ——只要客户端发送出来最后一个ACK就认为自己的四次挥手已完成了——为了防止最后的ack可能丢失所以干脆等上一会等2MSL确保对端的四次挥手也能顺利完成如果在2MSL时间里没有收到重传的FIN不就说明了我的最后一个ack是顺利发送到对面了吗MSLTCP报文在网络里最大生存时间2MSL万一因为你的ACK丢了而重传FIN ACK这一往返确保FIN能发送到客户端并且返回的ACK也能活到对端TIME_WAIT期间如果又收到对方重传的FINA 会重发 ACK并且把2MSL定时器重置回满档存在TIME_WAIT的理由如下确保双方四次挥手都尽可能正确完成让网络里残留的幽灵报文彻底消散同一个四元组源IP、源端口、目的IP、目的端口的新连接随时可能被建起来。万一上一次连接的某个延迟报文因为路由绕路、重排还在路上如果不等它就建新连接这个幽灵报文可能被新连接误收造成数据错乱。————————————所以必须等够时间2MSL让旧连接的所有报文从网络里消失。在ubuntu系统下一个MSL是60scat/proc/sys/net/ipv4/tcp_fin_timeout60流量控制接收端处理数据的速度是有限的。如果发送端发的太快导致接收端的缓冲区被打满这个时候如果发送端继续发送就会造成丢包继而引起丢包重传等等一系列连锁反应。因此TCP支持根据接收端的处理能力来决定发送端的发送速度。这个机制就叫做流量控制(FlowControl)接收端将自己可以接收的缓冲区剩余空间大小放入TCP首部中的窗口大小字段通过ACK端通知发送端将剩余空间大小告诉对端窗口大小字段越大说明网络的吞吐量越高接收端一旦发现自己的缓冲区快满了就会将窗口大小设置成⼀个更小的值通知给发送端发送端接受到这个窗口之后就会减慢的发送速度如果接收端缓冲区满了就会将窗口置为0这时发送方不再发送数据但是需要定期发送⼀个窗口探测数据段使接收端把窗口大小告诉发送端接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中有⼀个16位窗口字段,就是存放了窗口大小信息那么问题来了,16位数字最大表示65535那么TCP窗口最大就是65535字节么?实际上TCP首部40字节选项中还包含了一个窗口扩大因子M实际窗口大小是窗口字段的值左移M位;完成三次握手后第一次发送数据时应该发多大的数据三次握手不就已经交换了双方的缓冲区大小报头了吗 —— 能知道对端的剩余空间大小了呀️滑动窗口在讲滑动窗口之前要重谈一下应答机制如果一个报文把它发送出去了在收到这个应答之前这个报文应该被发送方OS丢弃吗 ———— 不能如果报文丢失了还需要重传所以要把这些报文给保存起来那保存在哪呢 ——— 引出滑动窗口之前我们知道对每一个发送的数据段都要给一个ACK确认应答。收到ACK后再发送下一个数据段。这样做有一个比较大的缺点就是性能较差。尤其是数据往返的时间较长的时候.既然这样一发一收的方式性能较低那么我们一次发送多条数据就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)先解决第一个问题滑动窗口在哪——是发送缓冲区的一部分我们把发送和接收缓冲区当做成数组来看char in/outbuffer[N]滑动窗口凡是在start_win和end_win空间范围里面对应的数据可以暂时不收到应答就可以直接发送的区域可直接发送的数据直接发送的数据大小要以对方的接受能力为主窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000个字节(四个段)发送前四个段的时候不需要等待任何ACK直接发送;收到第⼀个ACK后滑动窗口向后移动继续发送第五个段的数据依次类推操作系统内核为了维护这个滑动窗口需要开辟发送缓冲区来记录当前还有哪些数据没有应答只有确认应答过的数据才能从缓冲区删掉窗口越大则网络的吞吐率就越高正常工作过程刚开始的时候滑动窗口的大小由谁来定 ——滑动窗口 minACK - win真实发送的数据量ACK-win对方缓冲区的接收能力今天我们不考虑真是发送的数据量那么滑动窗口start_win 0end_win ACK_WIN主机A收到了确认应答确认序号是2001 ——不就说明2001之前的数据已经收到了吗对应的滑动窗口就要右滑确认序号是2001说明下次数据要从2001开始发2001之前的数据全收到2001左边数据变成已确认数据不就是start_win的下标变成2001那么end_win会怎么动呢——start_winACK_WINack报文里都能拿得到这两数据这样不就是一种流量控制吗 是的细节问题1️⃣滑动窗口会变大吗 什么时候变大的情况因为start_win是不断在增大的所以要滑动窗口继续变大 ——end_win突然变大比如上层突然把B的剩余空间多腾出来了空间缓冲区大小突然变大滑动窗口左侧是确认序号右侧就是增大一大截2️⃣滑动窗口会变小吗 什么时候以B收到序号2001为例win 4000 来说start_win 2001end_win start_win 4000只要剩余空间大小win不变的前提下如果收到的序号越来越大 滑动窗口的左端在往右移动————滑动窗口在变小整体在向右滑动所以滑动窗口会变小对方的接收缓冲区越来越小上层应用层接收数据太慢导致滑动窗口自然变小 —— 流量控制start_win就是确认应答时的序号因为不断收数据序号必然会增大end_win是start_win 当前所剩余的空间大小3️⃣滑动窗口会不变吗那如果上层是很及时地拿数据接收一个上层就拿一个 ——— 导致剩余空间大小一致不变那么滑动窗口的start_win无论是什么end_winstart_win 剩余空间大小导致滑动窗口大小是不变的那么滑动窗口会向左滑动吗———首先看左边的start_win接收序号只能是在不断变大或者不动接着看右边的end_win有可能会往左移动吗❌️初始的end_winstart_win 剩余空间大小win无论剩余空间大小win是变大还是不动或者是变小空间大小win是变大 —— 窗口右移、win不动的话 —— 也是右移 、win减少的情况左侧右移滑动窗口右移异常工作过程那么如果出现了丢包如何进行重传这里分两种情况讨论情况一数据包已经抵达ACK被丢了这种情况下部分ACK丢了并不要紧因为可以通过后续的ACK进行确认因为收到了6001报文就说明前面1~6000的数据全部收到了情况二数据包就直接丢了下面介绍一种快重传机制也叫高速重发控制当某⼀段报文段丢失之后发送端会一直收到1001这样的ACK就像是在提醒发送端我想要的是1001一样如果发送端主机连续三次收到了同样⼀个1001这样的应答就会将对应的数据1001-2000重新发送这个时候接收端收到了1001之后再次返回的ACK就是7001了(因为2001-7000)接收端其实之前就已经收到了被放到了接收端操作系统内核OS的接收缓冲区中快重传是又快又能重传为什么还要超时重传呢超时重传是对快重传的一种兜底策略无论应答丢了还是报文丢了此时接收方没能接收到三个重复的确认应答 ——超时重传就出手了自动补发但是情况远远不止上面的两种情况还有可能是两种的组合具体问题具体分析滑动窗口丢包情况1️⃣最左侧丢失确认序号的定义为什么要定义成xxx序号之前的报文已经全部收到了为了支持滑动窗口左侧下标不会跳过任何没有经过确认的报文所以如果一个报文我们把它发送出去了在没有收到应答之前这个报文应该被发送方OS放在哪里了滑动窗口内部在没有收到确认之前就不会被跳过收到应答后才会执行丢弃所以丢弃的本质是滑动窗口向右滑动数据归属于滑动窗口的左侧区域 —— 对报文数据做删除。2️⃣中间丢失中间丢失问题直到一个报文丢失左侧就会卡住 ——不就转化成左侧丢失的问题吗3️⃣最右侧丢失同样和上面中间丢失是一样的滑动窗口左侧也是会移动到卡住的位置 —— 转化成左侧丢失的例子所有的丢包问题 —— 都是左侧丢包问题卡住动不了无非就是快重传和超时重传罢了滑动窗口会滑越界吗有可能的 所以我们就要把它看作是一个环形数组那如果我有数据要发送但对方的接收缓冲区为为0此时左侧和右侧都是指向0的写在最后接下来登场的是 拥塞控制
返回列表