文章目录
- 计算机网络基础
- HTTP
- 相关问题
- UDP
- TCP
- 连接管理(三次握手/四次挥手)
- TCP可靠传输(确认答应)
- 超时重传
- 滑动窗口
- 流量控制
- 拥塞控制
- 延时应答
- 捎带应答
- 粘包问题
- 其他
- IP
- 数据链路层
- MUT
- 网卡接收数据流程
- 相关问题
- TCP会粘包、UDP永远不会粘包
- 学习博客
计算机网络基础
OSI模型定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),每一层实现各自的功能和协议,并完成与相邻层的接口通信。OSI模型各层的通信协议,大致举例如下表所示:
层次 | 常见协议 |
---|---|
应用层 | HTTP、SMTP、SNMP、FTP、Telnet、SIP、SSH、NFS、RTSP、XMPP、Whois、ENRP、等等 |
表示层 | XDR、ASN.1、SMB、AFP、NCP、等等 |
会话层 | ASAP、SSH、RPC、NetBIOS、ASP、Winsock、BSD Sockets、等等 |
传输层 | TCP、UDP、TLS、RTP、SCTP、SPX、ATP、IL、等等 |
网络层 | IP、ICMP、IGMP、IPX、BGP、OSPF、RIP、IGRP、EIGRP、ARP、RARP、X.25、等等 |
数据链路层 | 以太网、令牌环、HDLC、帧中继、ISDN、ATM、IEEE 802.11、FDDI、PPP、等等 |
物理层 | 例如铜缆、网线、光缆、无线电等等 |
基于TCP/IP的参考模型将协议分成四个层次,它们分别是链路层、网络层、传输层和应用层。下图表示TCP/IP模型与OSI模型各层的对照关系。
TCP/IP协议族按照层次由上到下,层层包装。最上面的是应用层,这里面有http,ftp,等等我们熟悉的协议。而第二层则是传输层,著名的TCP和UDP协议就在这个层次。第三层是网络层,IP协议就在这里,它负责对数据加上IP地址和其他的数据以确定传输的目标。第四层是数据链路层,这个层次为待传送的数据加入一个以太网协议头,并进行CRC编码,为最后的数据传输做准备。
入栈的过程,数据发送方每层不断地封装首部与尾部,添加一些传输的信息,确保能传输到目的地。出栈的过程,数据接收方每层不断地拆除首部与尾部,得到最终传输的数据。
全双工:一条链路,双向通信。
举个例子:间谍
通常抓到一个间谍,都会对其进行拷问。
说:你的上级是谁?平时是怎么联系的?
间谍:我和他认识,知道彼此身份,并且有相互联系的方式。
他是xxx,联系方式xxxxxx。所以别再打我,作用不大,因为我都会说。
半双工:一条链路,单向通信。
举个例子:间谍
通常抓到一个间谍,都会对其进行拷问。
说:你的上级是谁?平时是怎么联系的?
间谍:我和上级是单向通信的,他联系到我,我联系不到他。所以别再打我,作用不大。
HTTP
HTTP协议通常承载于TCP协议之上,有时也承载于TLS或SSL协议层之上,这个时候,就成了我们常说的HTTPS。如下图所示:
默认HTTP的端口号为80,HTTPS的端口号为443。
HTTP协议永远都是客户端发起请求,服务器回送响应。这样就限制了使用HTTP协议,无法实现在客户端没有发起请求的时候,服务器将消息推送给客户端。
HTTP协议是一个无状态的协议,同一个客户端的这次请求和上次请求是没有对应关系。
工作流程
一次HTTP操作称为一个事务,其工作过程可分为四步:
1)首先客户机与服务器需要建立连接。只要单击某个超级链接,HTTP的工作开始。
2)建立连接后,客户机发送一个请求给服务器,请求方式的格式为:统一资源标识符(URL)、协议版本号,后边是MIME信息包括请求修饰符、客户机信息和可能的内容。
3)服务器接到请求后,给予相应的响应信息,其格式为一个状态行,包括信息的协议版本号、一个成功或错误的代码,后边是MIME信息包括服务器信息、实体信息和可能的内容。
4)客户端接收服务器所返回的信息通过浏览器显示在用户的显示屏上,然后客户机与服务器断开连接。
如果在以上过程中的某一步出现错误,那么产生错误的信息将返回到客户端,有显示屏输出。对于用户来说,这些过程是由HTTP自己完成的,用户只要用鼠标点击,等待信息显示就可以了。
HTTP的基本工作流程如下:
(1)建立TCP连接:客户端与服务器建立TCP套接字连接。
(2)发送HTTP请求:客户端TCP套接字向服务器发送请求报文。
(3)服务器接受请求并返回HTTP响应:服务器解析请求,定位请求资源,并把资源数据写入TCP套接字,由客户端读取。
(4)释放连接TCP连接:若connection 模式为close,则服务器主动关闭TCP连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求。
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。
-
请求行:包括请求方法如GET、POST等,请求URI:资源的地址和协议版本。
-
请求头部:通知服务器客户端的请求信息。键值对形式出现,每行一对,关键字和值用英文冒号“:”分隔。
-
空行:分隔请求头部和请求数据的。
-
请求数据:GET方法中不使用,POST方法中使用。一般是存储post的参数和参数数据。
常用请求头部字段:
-
Accept:客户端可接受的信息类型,如text/html,application/xhtml+xml
-
Accept-Charset:客户端可接受的字符集,如gb2312
-
Accept-Encoding:客户端可接受的编码方式,如gzip
-
Accept-Language:客户端可接受的语言类型,如:zh-CN
-
Authorization:HTTP身份验证的凭证
-
Content-Length:设置请求体的字节长度,get请求可以没有,post请求必须包含这个
-
Host:设置服务器域名和TCP端口号,http协议,80端口就可以省略
-
Referer:客户端通过这个头告诉服务器,它是从哪个资源来访问服务器的,也就是防盗链
-
User-Agent:包含发出请求的客户端信息,浏览器类型/版本等,如:Mozilla/5.0 (Windows NT 10.0; Win64; x64)
-
Cookie:在客户端记录信息确定用户身份,设置服务器时使用set-cookie
-
Cache-Control:怎样处理缓存,例如:Cache-Control: no-cache
-
From:客户端的的email地址
-
Connection:告诉服务器这个客户端想要使用怎样的连接方式,值为keep-alive和close
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
示例:
常用响应头部字段:
-
Content-Encoding:服务器通过这个头告诉浏览器数据的压缩格式。
-
Content-Length:服务器通过这个头告诉浏览器回送数据的长度。
-
Content-Disposition:告诉浏览器以下载方式打开数据。
-
Content-Type:服务器通过这个头告诉浏览器回送数据的类型
-
Last-Modified:指定服务器上保存内容的最后修订时间。
-
Location:重定向的跳转的路径
-
Refresh :定时刷新/定时跳转
-
server:服务器信息
-
set-Cookie:cookie信息
响应状态码类别:
-
1XX:指定客户端进行某些动作
-
2XX:请求处理成功
-
3XX:重定向
-
4XX:客户端请求错误
-
5XX:服务端错误
常用状态码:
200 请求成功,服务器成功返回内容。注意:状态码为200,不代表返回的响应数据一定是对的(一定是我们想要请求的数据),这只代表服务器正常响应了客户端请求。
301 永久重定向
302 临时重定向
400 请求语法错误或参数错误
403 服务器拒绝执行请求
404 服务器找不到请求资源
500 服务器故障无法提供服务
503 服务器超负载或停机维护,一段时间后能提供服务
HTTP消息结构
相关问题
HTTP中Get和Post比较
- GET和POST本质上就是TCP链接,本质并无差别;
- GET产生一个TCP数据包;POST产生两个TCP数据包。但并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。
- GET把参数包含在URL中,POST通过request body传递参数。
- get安全性非常低,post安全性较高。 因为参数直接暴露在URL上,所以不建议使用get请求来传递敏感信息。
- GET在浏览器回退时是无害的,而POST会再次提交请求。
- GET请求只能进行url编码,而POST支持多种编码方式。
- GET请求会被浏览器主动缓存,而POST不会,除非手动设置。GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留
- GET请求在URL中传送的参数是有长度限制的,不能大于2KB。而POST没有。对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
Cookie和Session
HTTP简单概述
UDP
UDP协议端格式:
以上是为了排版好看,在真实传输中为以下样式:
源端口
16位,2个字节;表示发送方的端口目的端口号
16位,2个字节;表示接收方的端口包长度
指示了UDP数据报的整个长度,共16位,占两个字节,包括UDP首部和数据部分。
2个字节能表示的数据范围是0~65535,也就是能够表示的报文长度是65536字节(Byte),转换成KB,65536/1024 = 64 KB 这就是一个UDP报文所能表示的最大长度.校验和
用于校验报文的完整性,该字段称为校验和(checksum)。校验和字段用于验证UDP数据报在传输过程中是否发生了损坏或错误。
数据在传输的时候,本质上是0/1bit流,通过光信号或者电信号来表示,如果在传输的时候收到干扰,就可能会出现比特翻转现象.这个时候就需要校验和校验数据是否出错。若校验和出错,会直接丢弃
。
协议报头里面最多就给安排了2个字节表示端口号,因此最大带端口号取值区间换算十进制为0-65535,如果你设置一个特别大的数,它也放不下!
我们能不能把UDP这里的端口空间改成4个字节?答案:改不了。
原因:
- 这个代码是由操作系统内核上实现的,你想改?你都拿不到Windows的原码,你怎么改。
- 就算你把自己电脑的操作系统内核给改了,但是其他主机跟你电脑通信的时候,你也能把别人主机的UDP协议给改了吗?肯定是不能,充其量就是把自己的给改了。这样就会导致对方主机解析你的数据,或者解析错误,导致无法进行通信。
- 也没有必要去修改这个东西,两个字节,6万多个足够了。
UDP传输数据大于64k情况:
UDP报文长度是2个字节表示,范围是O-65535,进一步的讲范围就是0-64k。如果确实需要传一个大的数据。
可以在应用层,针对大的数据报,进行分包(拆成过个部分)。然后,再通过多个UDP数据报分别发送。
但是分别发送也有很大的问题: 顺序是不能保证的! 可能会出现“后发先制”。
这个时候接收方需要把收到的几个包重新拼接成完整的数据。这种解决方案,虽然能解决问题,但是属于下策。
原因:太麻烦了!分包和组包的代码,写起来非常复杂,要考虑很多情况(丢包,包的顺序错误等问题)。
直接上TCP即可。
UDP 协议首部中有一个 16 位的最大长度。也就是说一个 UDP 能传输的数据最大长度是 64K (包含 UDP 首部)。
关于校验和 :
常见基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
什么时候应该使用UDP?
当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用UDP。
TCP
TCP,即Transmission Control Protocol,传输控制协议。人如其名,要对数据的传输进行一个详细的控制。
TCP在传输链路中属于传输层,TCP报文如下图所示:
-
源端口号
(Source Port)
16位的源端口字段包含初始化通信的端口号。源端口和IP地址的作用是标识报文的返回地址。 -
目的端口号
(Destination Port)
16位的目的端口字段定义传输的目的。这个端口指明接收方计算机上的应用程序接口。 -
序列号
(Sequence Number)
该字段用来标识TCP源端设备向目的端设备发送的字节流,它表示在这个报文段中的第几个数据字节。序列号是一个32位的数。当SYN出现,序列码实际上是初始序列码(ISN),而第一个数据字节是ISN+1。用来标识从TCP源端向TCP目标端发送的数据字节流,它表示在这个报文段中的第一个数据字节。 -
确认号
(Acknowledge Number)
TCP使用32位的确认号字段标识期望收到的下一个段的第一个字节,并声明此前的所有数据已经正确无误地收到,因此,确认号应该是上次已成功收到的数据字节序列号加1。收到确认号的源计算机会知道特定的段已经被收到。确认号的字段只在ACK标志被设置时才有效。 -
首部长度
长度为4位,用于表示TCP报文首部的长度。用4位(bit)表示,十进制值就是[0,15],一个TCP报文前20个字节是必有的,后40个字节根据情况可能有可能没有。如果TCP报文首部是20个字节,则该位应是20/4=5。 -
保留位
(Reserved)
长度为6位,必须是0,它是为将来定义新用途保留的。 -
标志
(Code Bits)
长度为6位,在TCP报文中不管是握手还是挥手还是传数据等,这6位标志都很重要。6位从左到右依次为:- URG:紧急标志位,如果URG为1,表示这是一个携有紧急资料的封包。
- ACK:确认标志位,说明确认序号有效; 取1时表示应答字段有效,也即TCP应答号将包含在TCP段中,为0则反之。如果ACK为1,表示此封包属于一个要回应的封包。一般都会为1。
- PSH:推标志位,置位时表示接收方应立即请求将报文交给应用层;如果PSH为1,此封包所携带的数据会直接上传给上层应用程序而无需经过TCP处理。
- RST:复位标志,用于重建一个已经混乱的连接,用来复位产生错误的连接,也会用来拒绝错误和非法的数据包。如果RST为1,要求重传。表示要求重新设定封包再重新传递。
- SYN:同步标志,该标志仅在三次握手建立TCP连接时有效。如果SYN为1,表示要求双方进行同步沟通。
- FIN:结束标志,表示发送端已经发送到数据末尾,数据传送完成,发送FIN标志位的TCP段,连接将被断开。如果FIN为1,表示传送结束,然後双方发出结束回应进而正式终止一个TCP传送过程。
-
窗口大小
(Window Size)
长度为16位,TCP流量控制由连接的每一端通过声明的窗口大小来提供。 -
检验和
(Checksum)
长度为16位,该字段覆盖整个TCP报文端,是个强制性的字段,是由发送端计算和存储,到接收端后,由接收端进行验证。 -
紧急指针
(Urgent Pointer)
长度为16位,指向数据中优先部分的最后一个字节,通知接收方紧急数据的长度,该字段在URG标志置位时有效。 -
选项
(Options)
长度为0-40B(字节),必须以4B为单位变化,必要时可以填充0。通常包含:最长报文大小(MaximumSegment Size,MSS)、窗口扩大选项、时间戳选项、选择性确认(Selective ACKnowlegement,SACK)等。 -
数据
可选报文段数据部分。
报文字段解析
标志解析
连接管理(三次握手/四次挥手)
连接管理,也是 TCP 保证可靠性的一个机制。
连接管理,说具体点:
- 两个设备之间是如何建立连接的?
- 两个设备之间是如何断开连接的?
TCP 连接管理,是网络部分最高频的面试题,没有之一!!!
三次握手
-
第一次握手: 建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence
Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认; -
第二次握手: 服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
-
第三次握手: 客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
当SYN这一位为1,表示当前报文就是一个“同步报文段”。意思就是:主机A与主机B之间要建立连接。
当ACK"这一位为1的时候,它就是一个“确认报文段”。表示它是起到一个应答的效果,用来确认消息已经是送到了的。
这样的一个双向奔赴就构成一个建立连接的过程。
当双方都建立连接之后,客户端与服务器建立了连接,服务器和客户端建立了连接。
首先,客户端先给服务器发起了一个SYN同步请求。
此时我们就发现:不是说三次握手吗?这里怎么又四次了?
其实中间的两次,是一定合二为一的,再进行发送。
每次要传输的数据,都要经过一系列的封装和分用,才能完成传输。
如果分开发,ACK一条,SYN一条。这两条都是需要网络层、数据链路层的封装的,物理层再去转换,最后再发送给对方。对方接收到信息,再进行一顿分用解析操作。
道理很简单,你一次封装发送,就能解决问题,分二次发送,这不是浪费时间嘛!
因此 ACK和SYN可以合成一条发送:把下面的6个标志位中ACK和SYN分别设为1
最终变成:
为什么要三次握手?
为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
- 相当于“投石问路”,也就是检查一下当前这个网络的情况是否满足可靠传输的基本条件。如果你网络本身就很差,强行进行TCP传输,也会涉及到大量的丢包。这个时候,我们就直接不去进行可靠传输,让TCP就不工作了,省点力气。
- 让双方能够协商一些必要的信息。
总结:为了保证双方的发送和接收都没有问题。
四次挥手
简化 :
TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。
-
第一次挥手: 主机1(可以是客户端,也可以是服务器端),设置Sequence Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
-
第二次挥手: 主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
-
第三次挥手: 主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
-
第四次挥手: 主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
6个标记位中的 FIN为1,表示这是一个“结束报文段”。
三次握手和四次挥手最大的区别就是:
-
三次握手,一定是客户端主动发起的(主动发起的一方才叫客户端)
四次挥手,可能是客户端主动发起,也可能是服务器主动发起。【男女都能提出分手】 -
三次握手,中间的两次可以合并。
四次挥手,中间的两次,有时候是合并不了的。(有时候是能合并的)
如果中间的两次能合并,那就是三次挥手。如果合并不了,那就是4次挥手。
不能合并的原因在于B发送ACK和B发送FIN的时机是不同的,而像三次握手的过程,中间发送SYN和ACK是同一时机。
- 三次握手中,B 给A发送ACK和SYN都是操作系统内核处理的。
- 四次挥手中,B给A发送的 ACK,是操作系统内核处理的;
服务端B给客户端A发送的FIN,是用户代码负责的。
换个说法:B的代码中,调用了socket.close方法,才会触发FIN。
如果这两个都是操作系统内核负责处理的,内核就可以安排他们两个合并一下。但是,这里现在一个ACK是内核负责,一个FIN是用户代码(应用层)负责的。
- 内核负责:收到FIN,立即就由内核进行反馈(ACK)
- 用户代码负责:执行到用户代码中的close,才会触发FIN。
什么时候触发FIN,取决于用户代码是怎么写的。
如果代码的逻辑比较短,那么很快就能执行到close;也有可能,在执行close之前,要进行很多其他的工作,可能就需要过很久才能调用close。
最坏情况下,在执行 close之前,代码出现bug,导致无法执行到close方法,那么FIN就一直都触发不了。
所以,一个是用户代码触发的实际,一个是内核时机,这是两个不同的时机。
如果两个操作之间时差比较大,那就没有办法进行合并。
如果两个操作之间时差比较小,那就可能进行合并。
合并操作,又称延时应答
和捎带应答
。总之,这个4次挥手,可能是4次挥完,也可能是3次会完。关键看中间的ACK和FIN的执行时间的间隔有多长。但是,用户代码这个东西,有很多的不确定性,所以,我们一般认为是4次挥手。
4次挥手复杂的逻辑
TCP可靠传输(确认答应)
如果发送消息之后,能够知道对方 收到/没收到 数据,就是可靠的。
反之,发送消息之后,不知道对方 收到/没收到 数据,就是不可靠的。
TCP就是为了解决这个可靠性问题。解决方案:接收方 收到消息之后,就给发送方返回一个应答报文(ACK - acknowledge),表示自己已经收到了。
TCP每发送报文时候,针对每一个字节都进行编号,发送的时候将序号设置为当前字节的开始编号及结束编号:
发送时设置序号:
如图所示两次发送过程:
-
主机A 给主机B发送了一千个字节的数据,序号就是1-1000。
主机B给主机A返回的应答报文(ACK)就会带有一个确认序号,叫做1001;表示小于1001的数据都已经被主机B收到了,接下来,主机A应该从1001这个序号开始往后进行传递。 -
主机A给主机B发送了一千个字节的数据,序号就是1001 - 2000。
主机B给主机A返回的应答报文(ACK)就会带有一个确认序号,叫做2001;表示小于2001的数据都已经被主机B收到了,接下来,主机A应该从2001这个序号开始往后进行传递。
这里发送的数据是1 - 1000;意思是TCP报头中的序号是1,报文长度是1000通过这个信息,来明确范围。
超时重传
相当于对确认应答进行了补充 。确认应答是在网络一切正常的时候,通过 ACK 通知发送方,我收到了。如果出现了丢包的情况,超时重传机制就要起到效果了。
作为发送方我们是无法确认接收ACK失败的原因是哪一种。反过来说,我们能确认数据是否能到达,全依仗的是ACK。在我们的眼中有以下几种可能:
- 服务器没有收到我们的消息
- 服务器收到了,但是没有看到
- 服务器收到了,但是没回复
- 服务器收到了,并且也做出了回复,但是我们没有收到。
这时候,我们就直接往坏的想:就认为对方压根就没有收到,于是我们流重新再发一次。
但是,也不是立刻就进行重发。我们得等一会,给点反应时间,毕竟网络传输数据,需要时间对方处理信息,并且按下发送键,也是需要时间的。
如果我等了10分钟,还没有收到ACK,就重发一次。这个就叫作超时重传:超出了等待时间,就进行重传。
超时重传的问题:
在第一种情况下,因为服务器没有收到过信息,所以再次重发也不会有问题。
但第二种情况下,就有问题了:因为我们作为发送方式无法判断就收ACK失败的原因,所以,我们还会重发一次。这不就重复发送消息了嘛!
换句话来说:如果是ACK丢了,此时就会触发超时重传,就会导致接收方收到了重复的消息。其实这是一个非常严重的问题,如果换成转账操作,那这种操作就很危险了。
所以TCP内部就会一个去重操作:接收方收到的数据,会先放到操作系统内核的“接收缓冲区
”中。
接收缓冲区
:可以视为一个内存空间,并且也可以视为是一个阻塞队列。
收到新的数据,TCP就会根据序号,来检查这个数据是不是在接收缓冲区中已经存在了。如果不存在,就放进去。
如果存在,就直接丢弃。保证应用程序调用socket API拿到的这个数据一定是不重复的!
应用程序是感知不到超时重传的过程的,这是TCP内部,帮我们解决了一个很大的问题!
基于上述的两个机制,TCP 的可靠性,就得到了有效的保障。
一个是针对 顺利的情况下,它是怎么处理的,这就是确认应答
。
一个是 针对 通信丢包了,是怎么处理的,那就是超时重传
。
这两者相结合,其实我们就可以解决很多很多的问题。
但是呢!TCP 这样的协议,它是不满足于上述这两点的。它希望它能做得更好,更加极致。
滑动窗口
在没有滑动窗口机制下,没发送一个数据包,就需要等对方机器确认是否收到,再继续发送下一个包:
主机A给主机B发送数据之后((1-1000),B要给A返回一个ACK (1001) .
A拿到 ACK(1001)之后,A才能说,从哪个位置(1001〉开始继续传输数据(1001 - 2000)。
因为毕竟A要拿到ACK才能知道前面发送的数据(1-1000),对方是收到了的。
如果等待一段时间后,A并没有拿到B的 ACK。此时,就会触发超时重传,A重新发送数据(1-1001)
上述的操作:每次都发一条数据,然后等到收到ACK之后,才能发送下一条数据。这样的操作是非常低效的。此时,大量的时间都花在等待ACK上了。
因此引入“滑动窗口的机制
”。滑动窗口:本质就是在“批量的发送数据
”:
批量的发送数据:一次发一波数据(数条数据),然后一起等一波ACK。所以,这个时候就能近一步的缩小等待时间,从而提高传输的效率。
上例一次发送了四组数据:
1 ~ 1000
1001 ~ 2000
2001 ~ 3000
3001 ~ 4000
在发送这4组数据的过程中,不进行等待。发完这4组数据之后,再统一等待。这就意味一份等待时间,我们等待了多条ACK。可以这么去理解:把等待多条ACK的时间压缩成一份了。
我们提高传输效率的关键:缩短等待时间
。但是我们不能完全不等ACK,不等就肯定不是可靠传输了。就相当于抛弃了可靠传输,TCP也就失去了存在的意义了。
如果—次批量发送数据为N,统一等待—波。此时,这里的N就被称为“
窗口大小
”。
窗口大小:我们把一次发送多少数据,并且中间不用等。我们将这个数据的大小,称之为“窗口大小”。所以“窗口”,这个词也就出来了。
滑动的理解:并不用把N组数据的ACK都等到了,才继续往下执行/发送数据。而是收到一个ACK,就继续往下发送一组数据。
例如上图:1-1000字节的数据先到达主机B。主机B的操作系统内核,就立即反馈一条ACK(1001),表示数据(1001)收到了。也就是说:这个1001 (ACK)一到A,主机A立马就可以继续发送4001-5000 。
当前这个“窗口的大小”越大,就可以认为是传输速度就越快。
“窗口”越大,意味着在同一份等待时间里,发送的数据就越多.返回的ACK也越多。换个角度,每个ACK之间的时间间隔就越小,等到一个ACK的时间,也就越短。所以,窗口的大小越大,它执行效率就越高。
关于丢包:
丢包分成两种情况:
1、ACK丢了
例如在发送4001的数据之前,发送收到了一个2001,1001并没有收到。但是,其实仔细一想,不要紧。
因为 2001表示的意思就是:2001之前的数据都已经确认收到了。1001能否被收到,就无关紧要了。
由于ACK确认序号的含义,就保证了后一条ACK就能涵盖前一条。确认序号的含义:表示当前序号之前的数据都接收到了
。
2001:表示1-2000的数据已经被接收到了。
1001:表示1-1000的数据已经被接收到了。1-2000已经涵盖了1-1000的数据。既然如此,1001这条ACK丢就丢了,无所谓。发送方只要收到5001,意味着1-5000的数据就就接收到了。那么,即使3001, 4001这两个ACK丢包也毫无影响。
假设:先要等待4组ACK:1001,2001,3001, 4001
如果直接收到了4001,前面就都不用等了。直接再发4组数据。也就是说:滑动窗口,滑动的跨度是不是固定的,而是动态的。
最终结论:只要不是最后一个ACK丢包,就不用管。
如果最后一个ACK丢包了,就会回到原始的一个机制,超时重传。将最后一个ACK补齐。
2、数据丢了
首先,主机B收到1-1000,反馈一个ACK(1001)下面,主机A发送1001 -2000,但是丢包了。
接着主机A发送2001-3000,主机B仍然返回一个1001。包括后面的3001 -4000,4001-5000,5001-6000的数据到了,也还是返回1001。
由于1001-2000这个数据包丢了,所以主机B就向A反复索要1001-2000这个数据。即使A已经给了B后续的数据,B仍然是向A索要1001-2000的数据。
当A被B索要若干次之后,A就明白了【这个数据报,它给弄丢了】,于是就触发重传。而且,下一个ACK立马就变成7001 了。因为3001 - 7000的数据,都已经接收到了,所以下一个直接7001就可以了。
其实这个重传的过程,还是比较高效的。谁丢了就重传谁,而不是说把丢了的数据,从这往后全都重传一遍。因此,整体的过程,还是比较高效的,我们将其称为快重传。
流量控制
流量控制,是滑动窗口的延伸,目的是为了保证可靠性。在滑动窗口中,窗口越大(一次传输的数据量),传输速率也就越高。
那么,有些朋友就会这么去想:我把窗口弄得越大越好,这样的我们数据传输量就大幅度提升了。答案是不行:
把窗口弄大,不光要考虑发送方,还得考虑接收方。如果发送方发送的速度非常快,接收方完全就处理不过来,接收方就会把新接收到的包给丢了。那么,发送方是不是还得重传,这就不就在浪费时间和资源嘛!
接收方B每次从A接收到的新数据,都会先存入到一个接收缓冲区中。
不光是A在往B的接收缓冲区填充数据,还有B的应用程序在接收缓冲区中拿数据,所以B的应用程序,在调用read方法的时候,就是在从接收缓冲区中来取数据。
比如: read读取到一个字节的数据后,接收缓冲区中就少一个字节的数据。不管read 一次读取多少个字节,每read一次,就会取走接收缓冲区的一部分数据。
我们可以把这个接收缓冲区理解成是一个阻塞队列,这样的数据传输过程,就可以理解成生产者消费者模型A就是生产者,B的应用程序就是 消费者。接收缓冲区:交易场所/阻塞队列。
我们这里的接收缓冲区肯定是有一个总大小,它并不是无限大的。随着A发送数据,接收缓冲区里就会逐渐放入一些数据。因此,接收缓冲区的剩余空间就会逐渐减小。
- 如果剩余空间比较大,就认为B的处理能力是比较强的,就让A发的快点。
- 如果剩余空间比较小,就认为B的处理能力是比较弱的,就让A发的慢点。
所以,我们就使用这个剩余空间的大小,来去制约A 的发送速度。通过这样的方式,就可以让A 和B之间,进行一个更好的协调和适配。站在A的角度来说:
- 如果B这边处理的比较快,接收缓冲区里面的剩余空间就比较大,就可以让A发得快点。B也是能够处理的过来。
- 如果B这边处理的比较慢,接收缓冲区里面的剩余空间就比较小,就可以让A发得慢点。让B能够处理的过来。
而B怎么通知A自己的处理能力有多少呢?
通过ACK报文来告知。我们不是一给B发送数据,B就会返回一个ACK嘛。
那么,返回的 ACK里,就会带有一个接收缓冲区的剩余空间大小的信息。这个信息就存储在TCP报头里/TCP协议段格式。
通过这个16位窗口大小,来衡量当前接收方剩余空间的大小。发送方收到这个数据之后,就会灵活的调整发送速度(调整窗口大小)
当A一共给B发送了4k字节数据的时候,接收缓冲区还剩0字节的空间。当窗口大小为0的时,是否意味着A就不再发送数据了呢?
是的!A 确实是不发送了,但又不是完全不发送。
A需要定期发送一个探测报文,探测报文不传输实际的数据,它只是为了触发ACK。通过ACK来获取当前的窗口大小是多少。后面等到B处理完一定数据后,接收缓冲区还剩余2k字节。给A发送一个窗口更新通知(ACK),然后A得知后,就又开始传输数据了。
这就好比蓄水池满了,就不应该继续注水了。但是放水还一直在执行。(满只是一种暂时的状态,随着放水持续的工作,水位会逐渐降低)期间注水人员,会不断观察机械探测,返回的水位数据。等到水位降低到一定的程度,就又要开始进行放水了。
拥塞控制
拥塞控制,也是滑动窗口的延伸,也是用来限制 滑动窗口 发送的速率。衡量的是 发送方到接收方,这整个链路之间,拥堵情况(处理能力),根据这个情况,我们来去决定发送的速率是多少。
首先,要明白A和B之间,不是一根网线直连的,中间存在多个转发设备。(路由器/交换机/其他设备)
A能够发多快,不光取决于B的处理能力,也取决于中间链路的处理能力。只要这中间任意一环,它的处理速度变慢了,那么整体传输的再快也没用。即使B的处理速度再快,中间某一环节处理的很慢,那么A发的再快也没用。解决这个问题的方法就是拥塞控制。
拥塞控制,会根据中间这些设备的性能(是否能流畅的传输)来决定A的发送速率。但是,有个问题:A和B之间的中间节点/中间设备有多少个?这是不能确定的。所以,很难对这些设备一个个去衡量。
流量控制是专门针对B处理能力的一种机制,对于B来说,很好观察。直接查看接收缓冲区的剩余空间大小就知道了。
但是现在的情况就不一样了,中间有很多的设备,中间任意的设备处理能力弱,都会导致整体的处理效率降低。这个时候,我们就得想办法来衡量中间设备,但是中间设备又多又杂,这要怎么去处理呢?
拥塞控制的处理方案,就是通过“实验”的方式,逐渐调整发送速度。找到一个比较合适的值/范围,这就是我们实现拥塞控制的关键。由于中间的设备太多太杂,于是干脆将其视为一个整体。
A开始的时候,以一个比较小的窗口来发送数据,如果数据很流畅的就到达了B。这个时候,开始逐渐加大窗口的大小。
如果加大到一定的程度后,出现了丢包。【丢包就意味着通信链路出现了拥堵】这个时候,再减小窗口的的大小。
通过反复的增大/减小窗口大小的操作,来摸索出一个临界点(值/范围)。拥塞窗口就在这个范围中不断的变化,达到“动态平衡”。
指数规律增长:
现在我们有一个很矛盾的问题:既希望它的执行速度快,又希望不丢包。什么时候可以达到这一点?
窗口的大小无限接近丢包的极限,但是又没有跨过那道坎。所以没有真的丢包。这样形式,是我们最希望的。
就像前面说的:因为初始窗口太小,而我们实际上可能触发丢包的值是很大的。利用指数成长,就能快速摸到即将丢包的界限,更快的接近合适的值。
拥塞控制在最开始的时候,取的初始窗口大小,非常小。
纵轴是1个段位(一个点位具体是1字节,10字节…这个是不确定的,具体看操作系统的代码是怎么实现的)如果不丢包,下一轮变成2个单位,还不丢,下一轮4个单位,依次类推,只要不丢包,就成指数的增长。为什么一开始要指数增长呢?
这是因为我们给初始窗口太小,而我们实际上可能触发丢包的值是很大的。因此指数成长,就能帮助我们快速摸到即将丢包的界限。
线性增长
但是呢!又不能让它特别快速的增长,突破那个极限。等到指数增长到一定的程度时,就会进入线性增长。
使用线性增长的目的:主要是因为我们如果即将接近丢包的极限,你再来一个指数翻倍增长。
这一下,可能就直接上头了,挺不住了。
这好比一个运动负重15kg (30斤),他还能完全适应。但是,你一翻倍30kg (60斤),他直接就趴了。因此,应该是一点一点的增加,16,17,18… …这样的增加负重。这样运动员才能更好的适应,即使真的超重了,也不会立即就趴了,还能挣扎一下.这就是一个线性增长的过程。
乘法减小
但是线性增长也是增长啊,势必会达到一个拥堵的极限,这个时候就会产生丢包。一旦发生了丢包,就让拥塞窗口立即变小。回归到初始窗口的大小(最小的那个)继续重复刚才的操作(指数和线性增长)。
之所以没有一点一点的减少,而是直接回归初始大小是因为网络的情况是复杂,且又不稳定的。比如:上一次你传输一个20k数据都没有,下一次再传输20k就可能出现问题。
故:如果出现丢包,很可能你光把速度将下来一点,是不能解决问题的。
因为发生丢包的原因,都是网络上突发紧急情况,导致网络带宽一下降低很多,这时候你光将下一点没用!
如果要降低的太慢了,会导致出现持续性的丢包,对网络通信质量带来了很大的影响。一下把窗口变得很小,就是希望这次的传输,一定能成功。
如果下一次再失败,那影响就比较大了,因此每一次丢包都把窗口拉到最小。这样的话,我们就希望能提升这个最小窗口的传输成功率。如果成功了,我们子再来执行上述操作(指数+线性增长),重新去摸索那个界限。
**慢启动阈值(SSTHRESH) **
慢启动阈值是拥塞控制中用来限制慢启动阶段发送窗口大小的门限值,它是慢启动阶段与拥塞避免阶段的分界点。
换个说法慢启动阈值ssthresh,决定了什么时候从指数增长 -> 线性增长。
这个阈值也不是一直不变的:每次出现丢包,阈值就更新为当前出现丢包的窗口大小的一半。意思就是从这个位置,你再来一个指数翻倍,就会直接丢包了。也就意味着后续尝试(指数增长)操作的时候,达到这一半,就不会再尝试翻倍了。所以,这个时候我们用这个新的阈值来去衡量新的线性增长的过程。
问题来了:拥塞窗口和流量窗口都是来制约滑动窗口的大小的。那么,到底听谁的呢?以谁为主呢?
最终滑动窗口的大小== Math.min(拥塞窗口,流量控制窗口)
,取两者的最小值,为主要限制。
其实也很好理解,谁的处理能力低,就迁就谁。不能说它处理能力低,你就不管它的死活,强行带若它跑吧!
延时应答
延时应答 相当于是 流量控制的延伸。
流量控制 相当于是 踩了一下刹车,使发送方,发的不要太快。
延时应答,就想在这个基础上,能够尽量的再让窗口更大一些。
因为服务器一直在处理请求,所以服务器的容积一直在变化中。
具体就是在每次数据传输的时候,不立即返回当前服务器的可处理请求状态,而是延迟一会儿再返回。在这个延迟时间里面,服务器会取得更大的容积。这个操作,其实就是在有限的情况下,又尽可能的提高了一点传输速度。
延时应答,具体延时多少呢?
- 数量限制:每隔N个包就应答一次;
- 时间限制:超过最大延迟时间就应答一次
捎带应答
客户端和服务器是通过网络,进行通信。客户端和服务器之间的通信,有以下几种模型:
- 一问一答:客户端每发送一个请求,服务器就返回一个对应的响应。
- 多问一答:上传文件,可能会出现的情况
- 一问多答:下载文件,可能会出现的情况
- 多问多答:直播,串流。
平时最常用的一个模型:一问一答,比如用浏览器上网,打开网页,主要就是这样的模型。这也是我们在 web开发中最常使用的一个模型,一个请求对应着一个响应。客户端与服务器之间的交互,也是一问一答的模型。针对这种一问一答,我们的捎带应答就非常的有用,
假设:A给B发送一个 [ hello,how are you? ]
正常情况下,B收到A的请求之后,肯定是会返回一个ACK的,然后,紧接着B又要回复一个响应 [ l 'm fine,thanks。 and you? ’ ] ,A在收到B的响应之后,也会返回一个ACK
这个时候,我们需要明确一点:发送ACK的时机,是操作系统内核响应的(立即执行)。而B做出的响应,是应用程序返回的。【执行到相关的代码,才去发送的】
也就是说:这个B返回的 ACK和响应,两者触发的时间是不同的(不能进行合并),这个延时应答的作用就出来了,让B的 ACK不是马上返回,而是让它稍等一会。稍等一会,就可能和响应的时间无限接近,甚至是重合。
既然响应和ACK都重合/无限接近了,就可能会把ACK和响应进行合并。如果进行合并了,原本需要传输2次数据,现在只需要传输一次,效率自然就提升了。
捎带应答:就是让两个发送时机无限接近/重复的数据,进行合并。这就是捎带应答的效果:降低消耗,提升效率。
粘包问题
TCP粘包中的“粘”指的是:应用层数据包/报在TCP 接收缓冲区中,若干个应用层数据包/报是混在一起的,
粘包问题:分不出来这些数据包都是来自哪个程序的
粘包问题
其他
基于UDP 如何实现可靠传输?(看起来是在考UDP,其实是在考TCP)
答案:就是“抄作业”!!!!
本质上就是在应用层基于 UDP 复刻 TCP的 机制。
保证可靠性 最重要的机制:确认应答 和 超时重传,你可以实现一下。实现的同时,要确保这里面 引入 序号 和 确认序号。再进一步确保可靠性:引入连接管理(三次握手,四次挥手)
反正就是将上面的十个机制,都实现一下。用代码去实现,也就是在应用层上实现。 这样的 UDP 与 TCP的区别就在于:
- TCP 这十个机制都是操作系统内核代码实现好了的。
- UDP 是我们手动敲出来的 十个机制。
UDP 和TCP 的区别图示
- UDP具有较低的延迟和较小的网络开销,适用于对实时性要求较高的应用,如音频、视频流、实时游戏等。
- TCP在保证可靠性的基础上,会引入较高的延迟和额外的网络开销,适用于对数据完整性和顺序性要求较高的应用,如文件传输、Web请求等。
定时器
TCP 通过在发送时设置一个定时器来解决数据传输问题。如果当定时器溢出时还没收到确认,它就会重传该数据。关键在于超时和重传策略,即怎样决定超时的时间间隔和如何确定重传的频率。在TCP中,常见的定时器有四种:重传定时器、坚持定时器、保活定时器、2MSL定时器。这四个定时器都有各自的具体作用。
-
重传定时器
TCP是可靠的,因此,它对于发出去的信息,没有得到正常ACK反馈的,都会启动一个重传机制。这个重传机制使用一个重传定时器,当发现在规定时间内,没有收到ACK,那么,重新发送消息,如果还没有收到ACK,继续重新发送消息…当然,这每次继续重新发送消息的时间间隔是不一样的。一般默认,第一次重传是发现超时后1s,第二次重传是第一次重传后3s,第三次是6s… -
坚持定时器
坚持定时器是使用在一方滑动窗口为0之后,另外一方停止传输数据,进入坚持定时器的轮询,直到滑动窗口不再为0了。 -
保活定时器
这个就是我们经常说的tcp的keepalive了。实际使用场景是在应用层没有数据进行传输的时候,一定时间(tcp_keepalive_time,默认每2个小时)发送一次保持心跳的包,如果发送成功,则继续保持端口活跃,如果没有正常返回,则在指定次数内(tcp_keepalive_probes,默认是9次),指定间隔(tcp_keepalive_intvl,默认是17s)发送心跳包。如果最后都没有获得正常的ACK,那么才算连接失败。当然,tcp是否需要提供keepalive机制,是有争议的,我们可以为每个tcp连接设置是否启用keepalive和启用keepalive的各个指标设置。 -
2MSL定时器
这个定时器用在TCP四次挥手之后,主动发起TCP断开的一方需要保持2MSL的TIME_WAIT的状态。这个保持的一方就会开启2MSL定时器来计算TIME_WAIT的状态保持时间。
tcp第三次握手中如果客户端的ACK未送达服务器,会怎样?
-
Server端:
由于Server没有收到ACK确认,因此会重发之前的SYN+ACK(默认重发五次,之后自动关闭连接进入CLOSED状态),Client收到后会重新传ACK给Server。 -
Client端,两种情况:
在Server进行超时重发的过程中,如果Client向服务器发送数据,数据头部的ACK是为1的,所以服务器收到数据之后会读取 ACK number,进入 establish 状态
在Server进入CLOSED状态之后,如果Client向服务器发送数据,服务器会以RST包(异常终止报文)应答。
tcp如果已经建立了连接,但客户端出现了故障怎么办?
服务器每收到一次客户端的请求后都会重新复位一个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
初始序列号是什么?
TCP连接的一方A,随机选择一个32位的序列号(Sequence Number)作为发送数据的初始序列号(Initial Sequence Number,ISN),比如为1000,以该序列号为原点,对要传送的数据进行编号:1001、1002…三次握手时,把这个初始序列号传送给另一方B,以便在传输数据时,B可以确认什么样的数据编号是合法的;同时在进行数据传输时,A还可以确认B收到的每一个字节,如果A收到了B的确认编号(acknowledge number)是2001,就说明编号为1001-2000的数据已经被B成功接受。
什么时候应该使用TCP?
当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方,这往往用于一些要求可靠的应用,比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议。
IP
IP协议是TCP/IP协议的核心,所有的TCP,UDP,IMCP,IGMP的数据都以IP数据格式传输。要注意的是,IP不是可靠的协议,这是说,IP协议没有提供一种数据未传达以后的处理机制,这被认为是上层协议:TCP或UDP要做的事情。
在数据链路层中我们一般通过MAC地址来识别不同的节点,而在IP层我们也要有一个类似的地址标识,这就是IP地址。
以下位IPv4的报头:
名词解析:
-
4位版本
:
表示IP协议的版本号,当前只有2个取值4(0100)和6(0110)。又称lPv4和IPv6。我们当前主要讨论的是IPv4,至于IPv6,它还没有普及。 -
4位首部长度
表示当前IP协议的报头有多长。IP协议报头与TCP协议报头类似,长度都是可变的。
因为都带有选项,这个选项可有可无,有的话有几个?这些都是不确定的。所以我们通过4位首部长度来间接的描述出“选项”到底是多长?这一点和TCP相同。同时呢,4位值的是4个bit位,四个比特位的取值范围0-15.因此,这里的首部长度的单位也是4字节,比如:4个bit位 1111 == 15,实际表示的首部长就是60(4*15)字节。 -
8位服务类型(T0s)
TOS说是8位,其实只有4位是有效的,【3位优先权字段(已经弃用),4位Tos字段,和1位保留字段(必须置为0),】这4位TOS分别表示:最小延时,最大吞吐量,最高可靠性,最小成本
。这四个机制是相互冲突的,同一时刻,只能选择一个。【就是说这4个bit,同一时刻,只能有1位是1,其他位都是0】这里的 TOS相当于就是在切换形态。(类似于动漫中的变身效果) -
16位总长度(字节数)
表示一次传输数据最大长度,包括报头。 -
16位标识
数据报太大了,超过64k,IP协议会自动拆包,以16位标识区分,标识相同说明这是一个包 -
3位标志
最后的这个3位标志位,其实只有一位好使。其作用就是描述当前的包,是不是最后一个小包。
当这个标志位为0的时候,表示还有后续的小包。当这个标志位为1的时候,表示当前的这个数据包是最后一个包。也就是说:这3位标志起到的作用就是结束标记。 -
13位片偏移
13位片偏移区分在拆包后最终组装的时候区分每个包的排序,以拼凑成完整的包
通过这个13位片偏移,来描述多个包谁先谁后。片偏移的值越小,说明这个小包所在的位置越靠前。
因为我们知道:网络的环境是非常复杂的,可能存在先发后置的情况。虽然发送方、将一个大包拆成小包分别发送,但是这些小包到达的顺序是无法确定的。所以我们接受方接收的顺序,可能和发送方发送的顺序不一样。所以,通过这个片偏移量,我们就能确认正确的组包顺序。 -
八位生存时间,TTL字段
。这个字段规定该数据包在穿过多少个路由之后才会被抛弃。这里的单位不是s 或者ms,而是转发次数。IP数据报被发送的时候,会有一个初始的TTL(比如常见的取值:128或者64)IP数据报每次经过了一个路由器,该数据包的TTL数值就会减少1,当该数据包的TTL成为零,它就会被自动抛弃。 这个字段的最大值也就是255,也就是说一个协议包也就在路由器里面穿行255次就会被抛弃了,根据系统的不同,这个数字也不一样,一般是32或者是64。
有这样的机制,主要是因为有些包里面的IP地址,可能是永远也到不了的。【比如“翻墙”,IP地址不存在】像这样的包,不可能在网络上无休止的转发(占用硬件资源太多)。换个说法:正常情况下,IP的数据报都会在既定的 TTL内到达目的IP。 -
8位协议
表示当前传输层使用的协议是什么类型的。TCP或者是UDP都有不同的取值。通过这个取值来区分出当前我们要把这个数据交给传输层之后,是交给哪一个协议来处理。 -
16位检验和
这就跟前面的 UDP/TCP的校验和是一样的,都是用来校验数据是否正确。【数据量/内容】 -
32位源IP地址
源IP,表示:发件人地址。【发送方主机地址】 -
32位目的P地址
目的IP,表示:收件人地址。【接收方主机地址】
IP协议最重要的一个功能:进行转发。到底从哪里转发到哪里?
我们就可以通过IP地址来去描述。对于IPv4来说,一个IP地址本质上是32位的整数(整数==4byte == 32bit)通过会使用“点分十进制”这样的方式来表示这个IP地址。点分十进制:就是用三个小数点,把32bit位分成4部分,每个部分一个字节。每个部分的取值:0-255
8位服务类型T0S补充:
IP协议可以迪过T0S来进行切换形态。
- 最小延时:从主机A到主机B之间,取一条时间最短的路径
- 最大吞吐量:从主机A到主机B之间,取一条路径,它的传输带宽最高的。
- 最高可靠性:从主机A到主机B之间,取一条最不容易丢包的路径
- 最小成本:从主机A到主机B之间,取一条开销最小的路径
此时,我们就可以实际需要,来选择一条合适的路径IP协议能规划2点之间一条比较合适的路径,什么叫合适?
合适,其实就是上面的4位TOS(最小延时,最大吞吐量,最高可能性,最小成本),看你门选的哪一个!如果选择最小延时:认为花费时间最短的路径,就是合适的路径。如果选择最大吞吐量:认为传输带宽最高的路径,就是合适的路径。所以这个合适,是和TOS是密切相关的。虽然讲了这么多,但是在日常开发中,还是很少关注这个TOS内容,了解即可。
16位总长度(字节数)补充:
看到16位,我们就应该反应过来16位的最大长度为64k。那么,难道说我们的IP协议包最长也就只能表示64k嘛?
确实如此,因此,单个IP数据报最大长度确实不能超过64 k。那如果我们想要构造一个更长的数据报(搭载的载荷部分已经超过64k),怎么办?
处理方式:其实 IP协议自身实现了分包和组包这样的操作。前面在讲UDP的时候,也讲了UDP想要传输一个更大的数据,需要程序员在应用层代码上手动实现分包和组包。现在,如果是IP协议的话,其实它里面自己已经实现好了分包和组包。
IP报头解析
IP协议主要完成两方面工作:
- 地址管理
- 路由选择;路由选择,也就是规划路径。当两个设备之间,要找出一条通道,能够完成传输的过程。要想找出通道的前提是:要先认识路!
IP数据报中的目的IP地址,就表示了这个包要发到那里去。这个目的地址,如果当前路由器直接认识,就直接告诉你了。如果当前路由器不认识,就会告诉你一个大概的方向。让你走到下一个路由器的时候,再来问路。依次往后走,其实也是一个离目标越来越近的过程。近到一定的程度,总会遇到一个认识这个地址的路由器。于是就可以具体的转发过去了。
这就是路由选择的一个大致的过程。有的时候,不光遇到了一个认识这个地址的路由器。并且,它还认识多条通往这个IP地址的路。这个时候,就可以选择一个更合适的路了。
其实“路由选择”就是一个“问路”的过程。如果你接下来不知道往哪走,没关系!每到一个路由器,它都会告诉你一个大概的位置。根据这个位置去走,离目的IP就越近。走着走着,总会遇到一个认识目的IP的路由器。此时,就可以完成一个后续的转发。
什么叫做路由器“认识”这个IP地址?
其实在路由器内部维护了一个数据结构【路由表】,路由表里面就记录了一些网段信息(网络号),以及每个网络号对应的网络接口。目的IP,就在这些网络号中匹配。判断目的IP是不是就是当前网段里面的;如果匹配,你就根据它的网络号,走对应的网络接口。网络接口:其实就对应到路由器里面具体的端口,等于告诉你接下来,你就往这条路走就行了。
路由器有很多端口,有的WAN口,有的LAN口。(有的路由器甚至还有更多的接口)然后,这里面就会告诉你:当前这条路是沿着WAN口,还是LAN口走。告诉我们走那条路更合适。这里面关键就是:路由器里面有一个路由表的数据结构。
路由表本身,其实并不复杂。主要就是由网络号+网络接口组成的。
复杂的是:路由表是怎么来的?这个是有一系列专门的路由表生成算法。
就是可以自动生成一波,还可以手动配置【不做过多的深究,有兴趣的,自行了解】
学习原博客
数据链路层
物理层负责0、1比特流与物理设备电压高低、光的闪灭之间的互换。
数据链路层负责将0、1序列划分为数据帧从一个节点传输到临近的另一个节点,这些节点是通过MAC来唯一标识的(MAC,物理地址,一个主机会有一个MAC地址)。
数据链路层主要的协议,叫做“以太网”。像平时我们插的网线,就叫做“以太网线”。至于 以太网的协议,我们不做过多讨论。
以太网,这个协议不仅仅规定了数据链路层的内容。也规定了物理层的内容。以太网,是一个横跨两层的协议。
目的地址和源地址
:通过6个字节来表示。这个就要比 IPv4(4byte)的更长。大概长了6万多倍。这里的地址称为mac地址
,mac地址做到了每个设备/每个网卡都是唯一的。mac地址,是在网卡出厂的时候就被写死了。这个时候,我们就能通过mac地址来区分唯一的主机。
物理地址/mac地址,也不是不能改。有些网卡是支持手动配置的。
有一个问题:已经有IP地址,为什么还要mac/物理地址?这其实是一个美好的误会。
当年的网络层协议和数据链路层协议,是各自独立研发出来的。这就导致mac地址和IP地址,就有点重复了。
按理来说:一套地址就够了。但是,都研发出来了,就一起用呗。现在的现状,就是当前的mac地址和IP地址同时使用。但是表示不同的功能:
- IP用来表一次传输过程中的起点和终点。如果不考虑NAT的情况,一个IP数据报中的源IP和的IP是固定的。
- mac用来表示传输过程中,任意两个相邻节点之间的地址。一个以太网数据帧,在每次转发的过程中,源mac和目的mac都会改变。
类型
: 类型:描述后面的数据搭载的是什么样的数据。类型取不同的值,表示后面的数据搭载不同类型的数据。比如:类型取值为0800,那么后面的数据就是一个完整的IP数据报【一般情况】
如果是0806,那么后面的数据就是一个ARP请求/应答。如果是8035,那么后面的数据就是一个RARP请求/应答;这两个是属于特殊情况!
MUT
MTU
:一个以太网数据帧能够承载的数据范围,取值为46-1500之间。
其中的1500,我们将其称之为 MTU:一个以太网数据帧能够承载的数据范围。这个范围取决于硬件设备。因为以太网是和硬件是属于有密切关系的。换句话来说:以太网要求的 MTU的值就是1500。如果是其他的硬件设备,对应的数据链路层协议,可能又不一样。不同的数据链路层协议,它们的MTU值也是不一样的。
我们可以这样简单的去理解:其实数据链路层锁考虑的事情,就是两个相邻的节点如何进行传输。具体考虑到怎么传输,就需要考虑到“交通工具”。具体的的过程中,不同的“交通工具”,能够搭载的数据量是不相同的。【其实也好理解,一个骑小电驴的快递员和一个开货车的快递员,肯定是开货车的快递员,货物多啊。】
如果数据超过了MTU怎么办?注意!我们不是在IP层能够分包嘛?
其实,IP层的分包,其实不是给IP报头的64k准备的,更多的是为了适应数据链路层的MTU。尤其是像以太网这种MTU比较短的,是很容易触发分包的情况。
所以,MTU,其实是受限于硬件的。正因为受限于硬件,才对软件提出了更高的要求。所以,我们在IP层,才提了分包的操作。保证能对下层的这些限制,都能够进行一个处理和应用。
MTU对UDP协议的影响
一旦UDP携带的数据超过1472(1500 - 20 (IP首部)–8(UDP首部)),那么就会在网络层分成多个IP数据报。这多个IP数据报有任意一个丢失,都会引起接收端网络层重组失败。那么这就意味着,如果UDP数据报在网络层被分片,整个数据被丢失的概率就大大增加了。
主要的影响:分包之后,会导致UDP的丢包概率提升。并且UDP自身并没有可靠性的机制,所以多少都会对UDP产生影响
一旦 UDP 携带的数据 超过1472(1500-20(IP首部)-8(UDP首部)),那么 UDP 数据就会在网络层被分成多个 IP 数据报既:发送方 IP 层就需要将数据包分成若干片,而接收方 IP 层就需要进行数据报的重组。更严重的是,如果使用 UDP 协议,当 IP 层组包发生错误,那么包就会被丢弃。接收方无法重组数据报,将导致丢弃整个 IP 数据报。
MTU对于TCP协议的影响
TCP的一个数据报也不能无限大,还是受制于MTU。TCP的单个数据报的最大消息长度,称为MSS (Max Segment Size);
TCP在建立连接的过程中,通信双方会进行MSS协商。最理想的情况下,MSS的值正好是在IP不会被分片处理的最大长度(这个长度仍然是受制于数据链路层的MTU)。
双方在发送’SYN’的时候会在TCP头部写入自己能支持的MSS值。然后双方得知对方的MSS值之后,选择较小的作为最终MSS。MSS的值就是在TCP首部的40字节变长选项中(kind=2)。
MSS:TCP中,在IP不分包的前提下,最多搭载多少载荷/数据。MSS的值,取决于MTU,也取决于TCP 和IP的报头(报头都是可以变长的)
MTU 通过限制 MSS(单个数据报的最大消息长度) 的取值,来限制单个 TCP 包的长度
MTU 和 MSS的关系
MTU:最大传输单元,由不同的数据链路层对应物理层产生的(硬件规定),以太网的MTU=1500
MSS:最大分节大小,为 TCP 数据包每次传输的最大数据分段大小
MSS 的取值受限于 MTU
网卡接收数据流程
参考另一篇博客
网卡(Network Interface Card,简称NIC),也称网络适配器,是电脑与局域网相互连接的设备。
以往,从网卡的 I/O 区域,包括 I/O 寄存器或 I/O 内存中读取数据,这都要 CPU 亲自去读,然后把数据放到 RAM 中,也就占用了 CPU 的运算资源。直到出现了 DMA 技术,其基本思想是外设和 RAM 之间开辟直接的数据传输通路。一般情况下,总线所有的工作周期(总线周期)都用于 CPU 执行程序。DMA 控制就是当外设完成数据 I/O 的准备工作之后,会占用总线的一个工作周期,和 RAM 直接交换数据。这个周期之后,CPU 又继续控制总线执行原程序。如此反复的,直到整个数据块的数据全部传输完毕,从而解放了 CPU。
大致需要以下几个步骤:
- 网卡收到数据包。
- 将数据包从网卡硬件缓存转移到服务器内存中。
- 通知内核处理。
- 经过TCP/IP协议逐层处理。
- 应用程序通过read()从socket buffer读取数据。
首先,内核在 RAM 中为收发数据建立一个环形的缓冲队列,通常叫 DMA 环形缓冲区,又叫 BD(Buffer descriptor)表。
内核将这个缓冲区通过 DMA 映射,把这个队列交给网卡;
网卡收到数据,先把数据临时存放到 Rx FIFO 中,绕后通过 DMA 的方式把Rx FIFO 的数据包放到RAM 的环形缓冲区中。然后,网卡驱动向系统产生一个硬中断,内核收到这个硬中断后,启动软中断,在软中断中关闭硬中断,告诉 CPU 后续再由数据不用产生硬中断通知CPU,然后内核线程在软中断中从唤醒缓冲区取出数据,上传到协议栈中
硬中断 软中断
网卡接收数据流程原文
零拷贝
相关问题
一个 IP 报文经过路由器处理后,若 TTL 字段值变为 0,则路由器会进行的操作是(向IP报文的源地址发送一个出错信息,并丢弃该报文)
当 IP 报文从一个网络转发到另一个网络时MAC 地址会改变,但 IP 地址不变;IP报文从一个网络转发到另一个网络时,IP地址是不变的,而在链路层是一段链路一段链路转发的,MAC地址会改变
IPv4 的头部是变长的,IPv6 的头部是定长的;IPv6 中的 HOP Limit 字段作用类似于 IPv4 中的 TTL 字段;IPv6 中的 Traffic Class 字段作用类似于 IPv4 中的 Tos 字段
IP数据报首部中IHL (Internet首部长度)字段的最小值 5,最大值 15
UDP/TCP 包的大小限制
TCP会粘包、UDP永远不会粘包
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
粘包问题主要出现在用TCP协议传输中才会出现的问题,UDP不会出现,因为TCP传输中他会服务端会一次性把所有东西一并丢入缓存区,而读取的内容大小有时候没法准确的做到一一读取,所有会存在粘包。
而UDP他传输的时候是吧一个个内容丢过去,不管客户端能否完全接受到内容他都会接受他制定大小的内容,而内容大于他接受设定的大小时候多余的东西会被丢到
产生黏包的两种情况 :
- 发送端需要等待缓冲区满了才将数据发送出去, 如果发送数据的时间间隔很短, 数据很小, 就会合到一起, 产生黏包
- 接收方没有及时接收缓冲区的包, 造成多个包一起接收, 如果服务端一次只接收了一小部分, 当服务端下次想接收新的数据的时候, 拿到的还是上次缓冲区里剩余的内容
注意:粘包不一定发生,如果发生了:可能是在客户端已经粘了。客户端没有粘,可能是在服务端粘了。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
-
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
-
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
-
TCP是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,发送端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
解决粘包问题的方式
TCP/UDP对比
面向报文的传输方式是应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。因此,应用程序必须选择合适大小的报文。若报文太长,则IP层需要分片,降低效率。若太短,会是IP太小。
面向字节流的话,虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序看成是一连串的无结构的字节流。TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送。
学习博客
TCP/IP协议图解及演化过程
OSI网络七层模型
面试高频—TCP/IP十大问题