网络是如何连接的》第 1 章和第 2 章注释
网络是怎样连接的 笔记
第一章 浏览器生成消息
URL
协议类型
http:
https:
ftp:
file:
mailto:
HTTP协议
Request:Method + URI Response: Status Code + Header + Body + .......
客户端向Web服务器发送数据时,会先发送头字段
收到请求消息后,服务器会对内容进行解析,通过URI和方法来进行处理,然后将结果放在响应消息中,响应消息开头有一个状态码,后面就是头字段和数据。
响应消息会被发送回客户端,客户端收到之后,浏览器会从消息读出所需的数据并显示在屏幕上
格式
// 请求方法 + 请求URI + HTTP版本
[Method] [URI] HTTP1.1
// 消息头 存放例如 cookie,日期,支持语音,压缩格式等
[ReqHeader]
// 消息体
// 完全没有内容的空行
[Block line]
// 需要发送的数据
[ReqBody]
状态码
通过DNS查询IP地址
生成HTTP消息后,下一步就是根据域名查询IP地址
互联网是基于TCP/IP设计的,由一些小的子网,通过路由器连接起来组成一个大的网络。
IP
IP地址是一串32位的数字,按照8位一组,分为4组,用十进制表示,再用 .
隔开
在IP地址的规则中,网络号和主机号连起来总共是32位,但这两部分的具体结构是不固定的,
因此我们还需另外的附加信息来表示IP地址的内部结构,即 子网掩码
。
子网掩码的格式如下图 ② 所示,是一串32位数字,其左侧为1,右侧为0 其中,为1的部分表示网络号,为0的部分表示主机号 也可把1的部分用十进制表示并写在IP右侧。 主机号全0代表整个子网 主机号全1代表向子网的全部设备发包,即广播 (例:UDP广播)
web_p28.jpg
域名和IP地址并用的理由
使用IP地址只需要处理4字节的数字,而域名需要处理几十个到255字节的字符,增加了路由器的负担,也增加了延迟。
Socket库 查询IP
注:Socket库是用于调用网络功能的程序组件集合
对于DNS服务器,我们计算机上一定有相应的DNS客户端,而相对于DNS客户端的部分称为DNS解析器,通过DNS查询IP地址的操作称为域名解析。 解析器是一段程序,包含在Socket库中
根据域名查询IP时,浏览器会调用解析器,解析器会向DNS服务器发送查询消息,然后DNS服务器 会返回响应消息,其中包含查询到IP地址,解析器会将IP地址写入到浏览器指定的内存地址中。
解析器 原理
当浏览器调用解析器时,程序的控制流程就会转移到解析器的内部。 当控制流程转移后,解析器会生成要发送给DNS服务器的查询消息,与生成HTTP消息的过程类似,并将它发送给DNS服务器,发送这一操作是委托给操作系统的协议栈执行的。当解析器调用协议栈后,控制流程再次转移,协议栈会执行发送消息的操作,然后通过网卡将消息发送给DNS服务器。
当DNS服务器收到消息后,它会根据消息中的查询内容进行查询,如果要访问的Web服务器已在DNS服务器上注册,那么这条记录就能被找到,然后其IP地址会被写入响应消息并返回给客户端。消息经过网络到达客户端,再经协议栈传递给解析器,然后解析器读取出IP地址,并将IP地址写入指定的内存地址。
注:向DNS服务器发送消息是,我们需要知道DNS服务器的IP地址,只不过这个IP地址作为 TCP/IP 的一个设置项目事先准备好了,不需要再进行查询。
DNS服务器 原理
客户端发送的查询消息包含以下3种信息:
(a) 域名
// 在最早设计DNS时,DNS在互联网以外的其他网络中的应用也被考虑到了,Class就是用来识别网络的信息。不过如今除了互联网并没有其他网络,因此Class的值永远是 IN .
(b) Class
// 对于不同记录的类型,返回的信息也会不同
// 例:A IP地址
// 例:MX 邮件服务器
(c) 记录类型
域名的层次结构
域名与IP地址的对照信息存放在多台DNS服务器中,这些DNS服务器相互接力配合,从而查找出要查询的信息。
DNS服务器中的所有信息都是按照域名以分层次的结构来保存的。
DNS中的域名都是用 .
进行分割的看,例如 blog.mashiro.ski
在域名中,越靠右的位置表示其层级越高。
这种具有层次结构的域名信息会注册到DNS服务器中,而每个域都是作为一个整体来处理的 即,一个域的信息是作为一个整体存放在DNS服务器中的,不能将一个与拆开存放在多台DNS服务器中,但一台DNS服务器可以存放多个域的信息。
寻找相应的 DNS服务器 并获取 IP地址
首先,将负责管理下级域的DNS服务器的IP地址注册到它们的上级DNS服务器中去,然后上级DNS服务器的IP再向上注册,以此类推。
在 com, net, org
等*域的上面还有一级域名,被称作 根域
根域
不像*域一样拥有自己的名字,因此书写时经常被忽略,如果要明确表示根域,应该像 cloudflare.com.
一样,在最后加一个 .
,就代表根域
不过一般都不写,因此根域的存在经常被忽略
根域的DNS服务器中保管着*域的DNS服务器信息,由于上级DNS服务器保管着所有下级DNS服务器的信息,所以可以自顶而下,找到任意一个域的DNS服务器。
因此,只要客户端能找到任意一台DNS服务器,就可以通过它找到根域的DNS服务器,然后自顶而下,找到存有所需信息的DNS服务器。
分配给根域的DNS服务器的IP地址在全世界仅有13个,而这些地址几乎不发生变化,因此将这些地址保存在所有的DNS服务器中也并不是一件难事 (根域的DNS服务器信息已经保存在DNS服务器程序的配置文件中了)
注:根域DNS服务器在运营商使用多台服务器来对应一个IP地址,所以尽管IP数量只有13个,但实际的服务器数量还是很多的。
web_p42.jpg
web_p43.jpg
DNS缓存
DNS服务器有一个缓存功能,可以记住之前查询过的域名,如果要查询的域名和相关信息已经在缓存中,那么就可以直接从缓存中得到所需的信息。
但原本的信息可能会发生改变,此时缓存的信息就变得不正确了,因此缓存有一个有效期,当缓存的信息超过有效期后,这些信息就会从缓存中删除
委托协议栈发送信息
同样需要调用Socket库,但需要按指定顺序调用多个程序组件
我们可以将数据通道想象成一条管道,将数据从一段送入管道,数据就会到达管道的另一端然后被取出,数据的流动是双向的,这些通道的出入口称为 套接字
。
web_p46.jpg
管道的生命周期是这样的: 1.服务器创建套接字,等待客户端向该套接字连接管道 (创建套接字阶段) 2.客户端创建一个套接字,连接到服务器的套接字上 (连接阶段) 3.收发数据 (通信阶段) 4.断开管道并删除套接字 (断开阶段)
管道在连接时是有客户端发起的,但在断开时可以由客户端或服务器任意一方发起 当管道断开后,套接字就会被删除
注:以上四个操作都是由协议栈来执行的,浏览器等应用程序并不会自己去做,而是委托给协议栈代劳
创建套接字阶段
应用程序调用Socket库中的socket程序组件 控制流程会转移到socket内部并执行创建套接字的操作,然后控制流程会回到应用程序
套接字创建完成后,协议栈会返回一个 描述符
,应用程序会将其存放在内存中
在同一台计算机上可能同时存在多个套接字,我们需要一种方法来识别出某个特定的套接字,也就是描述符
的作用
当创建套接字后,我们就可以使用这个套接字来执行收发数据的操作 这时我们只要出示描述符,协议栈就能够判断出我们希望用哪一个套接字来连接或收发数据
连接阶段
应用程序调用Socket库的connect的程序组件
需要传入 描述符、IP地址、端口号
三个参数
服务端端口号是根据应用的种类实现规定好或设定好的 例如:80, 443, 25, 22是规定的 而一些程序可以在配置文件内修改端口
客户端在创建套接字时,协议栈会为这个套接字随便分配一个端口号 当协议栈进行连接操作时,会将这个端口号通知给服务器
连接操作的对象是某个具体的套接字,因此必须要识别到具体的套接字才行,端口号就是这样一种识别方式。当同时指定IP地址和端口号时,就可以明确识别出某台具体的计算机上的某个具体的套接字。
描述符是和委托创建套接字的应用程序交互时使用的,并不是用来告诉网络连接的另一方的 如果说描述符是用来在一台计算机内部识别套接字的机制,那么端口号就是用来让通信的另一方能识别出套接字的机制
通信阶段
发送
应用程序调用Socket库的write程序组件 需要传入 描述符、发送数据
接收
应用程序调用Socket库的read程序组件 调用read时需要指定用于存放接收到的响应消息的内存地址,这一内存地址称为接收缓冲区 当消息被存放到内存缓冲区时,就相当于已经转交给了应用程序
断开阶段
调用Socke库的close程序组件 连接在套接字之间的管道会被断开,套接字本身也会被删除
Web使用的HTTP协议规定,当Web服务器发送完响应消息后,应该主动执行断开操作 因此服务器会首先调用close来断开连接,断开操作传达到客户端之后,客户端的套接字也会进入断开阶段 接下来,浏览器调用read执行接受数据操作时,read会告知浏览器收发数据操作已结束,连接已断开,浏览器得知后,也会调用close进入断开阶段
第二章 用电信号传递 TCP/IP 数据
学习 网络控制软件(协议栈)和网络硬件(网卡)是如何发送数据的
创建套接字
协议栈的内部结构
web_p61.jpg
协议栈的上半部分有两块,分别是负责用TCP协议收发数据的部分和用UDP协议收发数据的部分,它们会接受应用程序的委托执行收发数据的操作
下面一半是用IP协议控制网络包收发操作的部分 在互联网上传送数据时,数据会被切分成一个一个网络包,而将网络包发送给通信对象的操作就是由IP来负责的。 还包括ICMP协议和ARP协议 ICMP用于告知网络包传送过程中产生的错误以及各种控制信息 ARP用于根据IP地址查询的 MAC地址
套接字的实体就是通信控制信息
在协议栈内部有一块用于存放控制信息的内存空间,记录了用于控制通信操作的控制信息 协议栈是根据套接字中记录的控制信息来工作的
调用socket时的操作
创建套接字阶段
协议栈首先会分配用于存放一个套接字所需的内存空间 写入表示初始状态的控制信息 将表示这个套接字的描述符告知应用程序
连接服务器
连接实际上是通信双方交换控制信息 连接操作中所交换的控制信息是根据通信规则来确定的
当执行收发数据操作时,需要一块用来临时存放要收发的数据的内存空间,这块内存空间称为缓冲区
负责保存控制信息的头部
控制信息可以分为两类 第一类是客户端和服务器相互联络时交换的控制信息 这些内容在TCP协议的规格中进行了定义 这些字段是固定的,每次客户端与服务器进行通信时,都需要提供
web_p71.jpg
这些信息被添加在客户端与服务器传递的网络包的开头,因此被称为头部 为避免各种不同的头部发成混淆,一般记作 TCP头部、MAC头部、IP头部
第二类是保存在套接字中,用来控制协议栈操作的信息 应用程序传递的信息、从通信对象接收到的信息以及收发数据操作的状态等信息会保存在这里 协议栈会根据这里的信息来执行每一步操作
套接字的控制信息和协议栈的程序本身其实是一体的,因此协议栈需要的信息会因为协议栈本身的实现方式不同而不同
连接操作的实际过程
从应用程序调用connect
开始,传入了 描述符、IP地址、端口号
其中IP地址、端口号被传递给协议栈中的TCP模块
TCP头部
客户端先创建一个包含很多开始数据收发操作的控制信息的头部
通过头部中的发送方和接收方端口可以找到需要连接的套接字
然后将头部中的控制位的 SYN位
设置为1,它表示连接。还需要设置适当的序号和窗口大小
TCP握手
TCP模块会将信息传递给IP模块并委托它发送 IP模块执行网络报发送操作后,网络包会通过网络到达服务器,然后服务器上的IP模块将收到的数据传递给TCP模块,TCP模块根据TCP头部中的信息找到端口号对应的套接字 找到套接字后,套接字中会写入相应的信息,并将状态改为正在连接
上述操作完成后,服务器TCP模块会返回响应,这个过程和客户端一样,需要在TCP头部中设置发送方和接收方以及SYN位 (如果拒绝连接,则设置RST位为1),此外还需要将控制位的ACK (Acknowledge character) 位设为1,表明已接收到相应的网络包 之后,服务器上的TCP模块会将TCP头部传递给IP模块,并委托IP模块向客户端返回响应
网络包通过网络回到客户端,通过IP模块到达TCP模块,并通过TCP头部的信息确认连接服务器的操作是否成功 (SYN位为1),这时会向套接字中写入服务器的IP地址、端口号等信息,同时还会将状态修改为连接完毕
最后,客户端也需要将控制位的ACK位设置为1并发回服务器,表明刚刚的响应包已收到
收发数据
从应用程序调用write
将要发送的数据交给协议栈开始
应用程序在调用write
时会指定发送数据的长度
协议栈在接收到数据后会将数据存放在内部的发送缓冲区中,并等待下一段数据
这样做的原因是: 应用程序交给协议栈发送的数据长度是由应用程序本身来决定的,不同的应用程序会在实现上有所不同 一次将多少数据交给协议栈是由应用程序决定的,协议栈并不能控制,因此如果收到就马上发送,可能会发送大量的小包,到网络效率下降 因此需要在数据积累到一定量再发送出去,至于要积累多少,不同种类和版本的操作系统会有所不同,但都是根据以下因素判断的
第一个要素是MSS
- MTU MTU (Maximum Transmission Unit) 最大传输单元,表示一个网络包的最大长度 在以太网中一般是1500字节 (在ADSL等网络中,会小于1500),包含头部长度
- MSS MSS (Maximum Segment Size) 最大分段大小,由MTU减去头部长度得到 TCP和IP头部加起来一般是40字节,因此MSS的大小一般是1460字节
web_p77.jpg
另一个要素是时间 当应用程序发送数据的频率不高时,如果每次都要等到长度接近MSS再发送,可能会因为等待时间过长导致发送延迟 为此,协议栈内部有一个计时器,当经过一定时间 (毫秒级) 之后,就会把网络包发送出去
但是上面两个要素是互相矛盾的 如果长度有限,那么网络效率会提高,但是延迟会增大 如果时间有限,那么延迟会减小,但是网络效率会降低
因此发送时需要综合考虑两个要素以达到平衡,但TCP协议规格中没有规定如何平衡 因此不同种类和版本的操作系统在相关操作上也就存在差异
数据拆分
发送缓冲区中的数据超过MSS的长度时,数据就会被以MSS长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络包中 当需要发送这些数据时,就在每一块数据的前面加上TCP头部,然后交给IP模块执行发送操作
web_p78.jpg
使用ACK位确认网络包已收到
TCP具备确认对方是否成功收到网络包,以及对方没收到时重发的功能,因此在发送网络包之后,接下来还需要进行确认操作
这一机制非常强大,无论网络中发生任何错误,我们都可以发现并重新发包 因此,网卡、集线器、路由器都没有错误补偿机制,一旦检测到错误就直接丢弃相应的包
如果发生网络中断等问题,在怎么重新发包都没有用的情况下,TCP会在尝试几次重传无效后强制结束通信,并向应用程序报错
原理
首先,TCP模块在拆分数据时,会先算好每一块数据相当于从头开始的第几个字节,在发送数据时,将算好的字节数写在TCP头部中,“序号” 字段就是用来填写这个数据的 然后,发送数据的长度也需要告知接收方,不过是通过整个网络包长度减去头部长度得到的 有了上面两个数值,我们就可以知道发送的数据是从第几个字节开始的,长度是多少了
通过这些信息,接收方可以检查收到的网络包没有遗漏 例如: 上次接收到1460字节,那么下一个包的序号应为1461,如果收到了序号为2921的包,那么就说明中间的包有遗漏
如果确认没有遗漏,接收方会将到目前为止接收到的数据长度加起来,计算出一共已经收到了多少个字节,然后将这个数值写入TCP头部的ACK号中发送给发送方 (同时需要将控制位的ACK位设置为1) 这个返回的ACK号的操作被称为确认响应,通过这样的方式,发送方法就能够确认对方到底收到了多少数据 注:收到一个包就返回一个ACK号
在实际的通信中,序号并不是从1开始,而是需要用随机数计算出一个初始值,这是因为如果序号都从1开始,整个通信过程会非常容易预测,从而导致攻击的发生 但如果是随机的,对方就不清楚了,因此需要在开始收发数据之前将初始值告知通信对象
在之前的操作中,有一步将 SYN控制位 设为1的操作,就是在这一步将序号的初始值告知对方的 在设置SYN控制位为1的同时,还需要设置序号字段的值为序号的初始值
注:SYN (Synchronize),意思是通过告知初始序号使通信双方保持步调一致,以便完成后续的数据收发检查
web_p80.jpg
双向传输
上文只考虑了单相的数据传输,但TCP数据的收发是双向的,在客户端向服务器发送数据的同时,服务器也会向客户端发送数据 因此只需增加一种左右相反的情形就可以了
首先,客户端先随机出一个序号,然后将序号和数据一起发送给服务器,服务器收到之后会计算ACK并返回给客户端 相反的,服务器也先随机出一个序号,然后将序号和数据一起发送给客户端,客户端收到后计算ACK号返回给访问
客户端和服务器双方都需要各自计算序号,因此双方需要在连接过程中胡先告知自己计算出的序号初始值
web_p82.jpg
TCP采用这样的方式确认对方是否收到了数据,才对方确认之前,数据会被保存在发送缓冲区中 如果对方没有返回某些包对应的ACK号,那么就重新发送这些包
根据网络包平均往返时间调整ACK号等待时间
返回ACK号的等待时间 (超时时间) 当ACK号的返回变慢,这是我们就必须将等待时间设置得稍微长一点,否则可能会发生已经重传了包之后,上一次的ACK号才刚刚收到的情况 在局域网中,ACK号几毫秒就可以返回;而互联网拥堵时,可能需要几百毫秒才能返回ACK号
正因为波动如此之大,所以将等待时间设置为一个固定值并不是一个好办法 因此,TCP采用了动态调整等待时间的方法 即,在发送数据的过程中持续测量ACK号的返回时间 如果ACK号返回变慢,则相应延长等待时间 如果ACK号马上返回,则相应缩短等待时间
注:由于计算机的时间测量精度较低,ACK返回时间过短时无法被正确测量,因此等待时间有一个最小值,这个值在每个操作系统上不一样,基本上是0.5s~1s之间
使用窗口有效管理ACK号
如果发送一个包就等待一个ACK号,那么中间的等待时间就会浪费 因此TCP采用滑动窗口来管理数据发送和ACK号的操作 滑动窗口就是 在发送一个包之后,不等待ACK号返回,直接发送后续一系列包
web_p85.jpg
使用滑动窗口可能会出现发送包的频率超过接收方处理能力的情况 具体解释就是: 当接收方接收到TCP收到包之后,会先将数据存放到接收缓冲区中,然后接收方计算ACK号,将数据块组装起来还原成原本的数据并传递给应用程序 如果这些操作还没完成,之后又有很多包到达,数据还是会存放在接收缓冲区里,但如果缓冲区溢出,之后的数据就进不来了,因此接收方就接收不到后面的包了
因此,接收方需要告诉发送方自己最多能接收多少数据,然后发送方根据这个值对数据发送操作进行控制,来避免上述情况的发生
具体工作方式
接收方将数据暂存到接收缓冲区中并执行接收操作
当接收完成后,接收缓冲区中的空间会被释放出来,也就可以接收更多的数据了
这时接收方会通过TCP头部中的 窗口字段
将自己能接收的数据量告知发送方
这样发送方就不会发送过多的数据,导致接收缓存区溢出
和序号、ACK号一样,发送操作也是双向进行的
前面提到的能接收的最大数据量称为窗口大小 (一般与接收缓冲区大小一致),它是TCP调优参数中非常有名的一个
web_p87.jpg
ACK 与 窗口 的合并
每收到一个包,就向发送方分别发送ACK号和窗口更新这两个单独的包 接收方给发送方发送的包太多了,会导致网络效率下降
因此接收方在发送ACK号和窗口更新时,并不会马上把包发送出去,而是会等待一段时间 在这个过程中成功很可能会出现其他的通知操作,这样就可以把两种通知合并在一个包里发送了
当需要连续发送多个ACK号时,也可以减少包的数量,这是因为ACK号表示的是已接收到的数据量 也就是说,它是告诉发送方目前已经接收的数据的最后为在哪里,因此需要连续发送ACK号时,只要发送最后一个ACK号就可以了,中间的可以全部省略
当需要连续发送多个窗口更新时也可以减少包的数量,因为连续发生窗口更新说明应用程序连续请求了数据,接收缓冲区的剩余空间连续增加了,这时也只需要发送中间的结果就可以了
接收 HTTP 响应消息
发送HTTP请求消息后,需要等待Web服务器返回响应消息,浏览器需要对其进行接收,这一操作需要协议栈的参与
浏览器在委托协议栈发送请求消息之后,会调用 read 程序,和发送数据一样,接收数据需要将数据暂存到接收缓冲区
首先,协议栈尝试从接收缓冲区取出数据传递给应用程序,但这时候数据刚发出去,响应消息可能还没返回,这时,协议栈会将应用程序的委托,也就是从缓冲区取出数据并传递给应用程序的工作暂时挂起,等服务器返回的响应消息到达之后再继续执行接收操作
首先,协议栈会检查收到的数据块和TCP头部的内容,判断是否有数据丢失,如果没有问题则返回ACK号 然后,协议栈将数据块暂存到数据缓冲区中,并将数据块按顺序连接起来还原出原始的数据,最后将数据交给应用程序,之后,协议栈要找到合适的时机向发送方发送窗口更新
从服务器断开并删除套接字
数据发送完毕后断开连接
收发数据的时间点应该是应用程序判断所有数据都已经发送完毕的时候 数据发送完毕的一方会发起断开过程,不同的应用程序会选择不同的断开时机 协议栈允许任意一方发起断开过程
以服务器发起断开过程为例 服务器 首先服务器的应用程序调用Socket库的close程序 然后服务器的协议栈生成包含断开信息的TCP头部,即 将控制位的FIN位设为1 并委托IP模块向客户端发送数据
客户端 首先,收到服务器发来的FIN位为1的TCP头部时,客户端的协议栈会将自己的套接字标记为进入断开操作状态 然后,未告知服务器已收到FIN位为1的包,客户端会向服务器返回一个ACK号 注:客户端的应用程序可能在收到FIN包之前就来读取数据,这时读取操作会被挂起,直到FIN包到达 之后应用程序来读取数据,如果接收缓冲区中还有数据,那么这些数据将会传递给应用程序,协议栈会告知应用程序来自服务器的数据已经全部收到了 接着客户端应用程序会调用close来结束数据收发操作,客户端也会像服务器一样发送一个FIN位为1的包,然后服务器会返回一个ACK号给客户端 之后,服务器和客户端的通信就全部结束了
web_p91.jpg
删除套接字
和服务器的通信结束之后,套接字并不会立即被删除,而是会等待一段时间之后再被删除 等待的这段时间是为了防止误操作
例如:客户端返回的ACK号丢失了,服务器没有收到,可能会重发一次FIN包,如果这时客户端的套接字已经删除,端口被释放,而恰巧又有新的套接字使用了这个端口,收到了服务器重发的FIN包,新的套接字开始执行断开操作,错误就发生了
至于具体的等待时间,协议没有明确规定,这和包的重传方式有关,通常持续几分钟 一般来说等待几分钟之后再删除套接字
数据收发小结
web_p91.jpg
IP与MAC的包收发操作
网络包的基本知识
包是由头部和数据两部分构成的 头部包含目的地址等控制信息 头部后面就是委托方发送给对方的数据
首先,发送方的网络设备会负责创建包,创建包的过程就是生成含有正确控制信息的头部,然后附上要发送的数据 接着,包会被发往最近的网络转发设备,转发设备会根据头部中的信息判断接下来应该发往哪里,这个过程需要用到一张表 (路由表),这张表里记录了每一个地址对应的发送方向,也就是按照头部里记录的目的地址在表里进行查询,并根据查询到的信息判断接下来应该发往哪个方向
经过多个转发设备的接力之后,包最终就会到达接收方的网络设备
发送方和接收方是相对的,因此我们不需要明确区分,在这里将发送方和接收方统称为 终端节点
,相应的,转发设备被称为 转发节点
或 中间节点
路由器是按照IP规则传输包的设备 集线器是按照以太网协议传输包的设备
即: IP协议根据目标地址判断下一个IP转发设备的位置 子网中的以太网协议将包传输到下一个转发设备
web_p96.jpg
如图 2.14(b) 所示,TCP/IP包有两个头部,分别是MAC头部和IP头部 这两个头部有不同的作用 首先,发送方将包的目的地,即服务器IP写入IP头部中,IP协议根据这一地址查找包的传输方向,从而找到下一个路由器的地址,并委托以太网协议将包传输过去。这时IP协议会查找下一个路由器的MAC地址,并将这个地址写入MAC头部,这样一来,以太网协议就知道要将这个包发到哪个路由器上了
集线器是根据以太网协议工作的设备。为了判断包接下来应该向哪里传输,集线器内有一张表(用于以太网协议的表)可以根据以太网头部中记录目的信息查出相应的传输方向 当存在多个集线器时,网络包会按顺序逐一通过这些集线器进行传输
包会到达下一个路由器,路由器中有一张用于IP协议的表,可根据这张表和IP头部中记录的目的地信息查出接下来要发往那个路由器 为了将包发到下一个路由器,我们还需要查处下一个路由器的MAC地址,并覆盖记录到MAC头部中,可以理解为改写了MAC头部 注:收到包的时候MAC头部会被舍弃,而当再次发送的时候又会加上包含新的MAC地址的新MAC头部
web_p99.jpg
上文讲了IP和以太网的分工,其中以太网的部分也可以替换成其他的东西,例如无线局域网、ASDL、FTTH等,它们都可以替代以太网的觉得帮助IP协议来传输网络包 因此,将IP和负责传输的网络分开,可以更好地根据需要使用各种通信技术。像互联网这样庞大复杂的网络,在架构上需要保证灵活性 注:使用除以太网之外的其他网络进行传输时,MAC头部也会被替换为适合所选通信规格的其他头部
包收发操作概览
实际上将包从发送方传输到接收方的工作是由集线器、路由器等网络设备完成的,因此IP模块仅仅是整个包传输过程的入口而已
IP模块的工作
包收发操作的起点是TCP模块委托IP模块发送包的操作,这个委托的过程就是TCP模块在数据块前面加上TCP头部,然后传递给IP模块。与此同时,TCP模块还需要制定通信对象的IP地址
收到委托后,IP模块会将包的内容当做一整块数据,在前面加上包含块控制信息的头部 IP模块会添加IP头部和MAC头部这两种头部 IP头部中包含IP协议规定的、根据IP地址将包发往目的地所需的控制信息 MAC头部包含通过以太网的局域网将包传输至最近的路由器所需的控制信息
加上这两个头部之后,包就封装好了,这就是IP模块负责的工作 注:凡是局域网所使用的头部豆角MAC头部,但其内容根据局域网的类型有所不同
封装好的包会被交给网络硬件,后文将它们统称为“网卡” 传递给网卡的网络包是由一连串的 0 和 1 组成的数字信息,网卡会将这些数字信息转换为电信号或光信号,并通过网线(或光纤)发送初期,然后这些信号就会到达集线器、路由器等转发设备,再由转发设备一步一步地送达接收方
接受过程和发送过程是相反的,信息先以电信号的形式传进来,然后网卡将其转换为数字信息并传递给IP模块。接下来,IP模块会将MAC头部和IP头部后面的内容,也就是TCP头部加上数据块,传递给TCP模块
因为IP模块并不关心数据的内容,也不关心TCP阶段的操作 因此,接下来的关于IP的工作方式,可以使用于任何TCP委派的收发数据
生成包含接收方IP地址的IP头部
web_p103.jpg
IP模块会生成IP头部附加在TCP头部前面,其中最重要的内容就是IP地址,这个地址是TCP模块告知的
IP不会自行判断包的目的地,而是将包发往应用程序指定的接收方,即便有应用程序指定了错误的IP地址,IP模块也只能照做
IP头部中还需要填写发送方的IP地址,可以认为是发送方计算机的IP地址,但这种说法并不准确 更准确的说是发送这个包的网卡的IP地址
在填写发送方IP地址时就需要判断到底应该填写哪个地址,这个判断相当于在多块网卡中判断应该使用哪一块网卡来发送这个包,也就相当于判断应该把包发往哪个路由器 因此只要确定了目标路由器,也就确定了应该使用哪块网卡,也就确定了发送方的IP地址
web_p105.jpg
上文提到路由器中有一张IP协议的表,这张表叫做路由表
首先,对套接字中记录目的地IP地址与路由器左侧的 Network Destination 栏进行比较
例如,目标地址为 192.168.1.21,那么就对应第6行,因为它和 192.168.1 的部分匹配
目标地址为 10.10.1.166,那么就和 10.10.1 的部分匹配,所以对应第3行
我们只需要找到与IP地址左边部分相匹配的条目,找到相应的条目之后,接下来看从右数第2列和第3列的内容
右起第 2 列 Interface ,表示网卡等网络接口, 右起第 3 列 Gateway ,表示下一个路由器的地址,将包发送给这个IP地址,该地址对应的路由器就会将包转发到目标地址 注:如果Gateway和Interface列的IP地址相同,就表示不需要路由器进行转发,可以直接将包发给接收方的IP地址
路由表的第一行中,目标地址和子网掩码都是 0.0.0.0
,这表示默认网关,如果其它所有条目都无法匹配,就会自动匹配这一行
注:子网掩码 用来判断IP地址中网络号与主机号分界线的值
这样我们就可以判断用哪块网卡来发包了,然后在IP头部的发送方IP地址中填上这块网卡对应的IP地址
接下来还需要填写协议号,它表示包的内容是来自哪个模块的 如果是TCP模块委托的内容,则设置为 06 (十六进制),如果是UDP模块委托的内容,则设置为 17 (十六进制) 浏览器中,HTTP请求消息都是通过TCP来传输的,因此这里就会填写表示TCP的 06 (十六进制)
生成以太网用的MAC头部
IP模块还需要在IP头部前面加上MAC头部
以太网在判断网络包目的地时和TCP/IP的方式不同,因此必须采用相匹配的方式才能在以太网中将包发往目的地,而MAC头部就是干这个用的
MAC头部包含了接收方和发送方的MAC地址等信息
web_p107.jpg
MAC头部的开头是接收方和发送方的MAC地址,可以认为它们和IP头部中的接收方和发送方IP地址的功能差不多,只不过IP地址的长度为 32位,而MAC地址的长度为 48位 此外,IP地址是层次化结构,而MAC地址的 48位 可看做一个整体 但从表示接收方和发送方的意义上来说,它们之间是没有区别的
web_p110.jpg
在IP中,协议号表示IP头部后面的包内容的类型 而在以太网中,我们可以认为以太网类型后面就是以太网包的内容,而以太类型就表示后面内容的类型 以太网包的内容可以是IP、ARP等协议的包,他们都有对应的值,这也是根据规则来确定的
在生成MAC头部时,只要设置上表中的三个字段就可以了
以太类型
,填写表示IP协议的值 0800
(十六进制)
发送方MAC地址,填写网卡本身的MAC地址
接收方MAC地址,对方的MAC地址
网卡的MAC地址是出厂时写入ROM的,只需要经这个值读出来写入即可 注:实际上只有在操作系统启动过程中对网卡进行初始化时才会读取MAC地址,读取出来之后会存放在内存中 读取MAC地址的操作是由网卡驱动程序来完成的,因此网卡驱动也可以不从网卡ROM中读取地址,而是将配置文件中设定的MAC地址拿出来放到内存中并用于设定MAC头部,或者也可以通过命令输入MAC地址
通过ARP查询目标路由器的MAC地址
在以太网中,有一种叫做广播的方法(前文提过),可以将包发给连接在同一以太网中的所有设备 ARP就是利用广播对所有设备提问:“XX这个IP地址是谁的”,然后就会有人回答:“XX这个IP是我的,我的MAC地址是XXXX” 如果对方和自己处在同一子网中,那么通过上面的操作就可以得到对方的MAC地址 然后,我们将这个MAC地址写入MAC头部,MAC头部就完成了
注:不是这个IP地址的设备会忽略广播,什么都不回答 如果路由表设置的正确,双方应该在同一子网,否则对方无法作出ARP响应,这是只能认为对方不存在,包的发送操作就会失败
如果每次发包都要这样查询一次,网络中就会增加很多ARP包,因此我们会将查询结果放到一块叫作ARP缓存的内存空间中 在发送包的时候,先查询一下ARP缓存没如果其中已经保存了对方的MAC地址,就不需要发送ARP查询,直接使用ARP缓存中的地址,而当ARP缓存中不存在时,发送ARP查询
ARP缓存中的值会在几分钟左右被删除,这个策略能够在几分钟后消除缓存和现实的差异,但IP地址刚刚发生改变的时候,ARP缓存中依然会保留老的地址,这时就会发生通信异常
以太网的基本知识
以太网是一种为多台计算机能够彼此*和廉价的相互通信而设计的通信技术
web_p112.jpg
这种网络的本质其实就是一根网线,如图(a)所示,图上还有种叫做收发器的小设备,它的功能只是将不同网线之间的信号连接起来而已 因此,当一台计算机发送信号时,信号就会通过网线流过整个网络,最终到达所有的设备 为了控制这一操作,就需要MAC头部,通过头部中接收方的MAc地址,就能够知道包是发送给谁的通过发送方MAC地址,就能知道包是谁发出的 注:多台设备同时发送信号会造成碰撞,当然也有相应的解决方案 随着交换机的普及,信号已经不会发生碰撞了
这个原型后来变成了图(b)中的结构 这个结构是将主干网线替换成了一个中继式集线器(集线器),收发器网线替换成了双绞线 虽然网络的结构有所变化,但信号会发送给所有设备这一基本性质并没有改变
后来,图(c)这样的使用交换式集线器(交换机)的结构普及开来,现在我们说的以太网指的都是这样的结构 这个结构看上去和(b)很像,但其实里面有一个重要的变化,即信号会发送给所有设备 这一性质变了,现在信号只会流到根据MAC地址指定的设备,而不会到达其它设备了 根据MAC地址传输包这一点并没有变,因此MAC头部的设计也得以保留
尽管以太网经历了多次变迁,但其基本的3个性质至今仍未改变,即 1.将包发送到MAC头部的接收方MAC地址代表的目的地 2.用发送方MAC地址识别发送方 3.用以太类型识别包的内容 因此可以认为具备这三个性质的网络就是以太网
以太网和IP一样,并不关心网络包的实际内容,因此以太网的收发操作也和TCP的工作阶段无关,都是共同的
将IP包转换成电信号或光信号发送出去
IP生成的网络包只是存放在内存中的一串数字信息,没办法直接发送给对方 我们需要将数字信息转换为电信号或光信号,才能在网线或光纤上传输,这才是真正的数据发送过程
负责执行这一操作的是网卡,但网卡也无法单独工作,要控制网卡还需要网卡驱动程序 如下图所示,这是一张网卡主要构成要素与的概念图,并不代表硬件的实际结构
web_p115.jpg
网卡并不是通上电之后就可以马上开始工作的,而是和其他硬件一样都需要进行初始化 也就是打开计算机启动操作系统的时候,网卡驱动程序会对硬件进行初始化操作,然后硬件才进入可以使用的状态,操作包括硬件错误检查、初始设置等步骤,这些步骤对于很多其他硬件也是共通的 但也有一些操作是以太网特有的,那就是在控制以太网收发操作的MAC模块中设置MAC地址 注:MAC Media Access Control 网卡的ROM中保存着全世界唯一的MAC地址,将这个值读出之后就可以对MAC模块进行设置 旁注:之前在某个群里见过两块MAC地址相同的网卡,运气也确实不错
也有一些特殊的方法,比如从命令或配置文件中读取MAC地址并分给MAC模块,这种情况下,网卡会忽略ROM中的MAC地址 注:设置MAC地址时,必须注意不能喝网络中其它设备的MAC地址重复,否则网络将无法正常工作
给网络包再加 3 个控制数据
网卡驱动从IP模块获取包之后,会将其复制到网卡内的缓冲区中,然后向MAC模块发送发送包的命令
首先,MAC模块会将包从缓冲区中取出,并在开头加上报头和起始帧分界符,在末尾加上用于检测错误的FCS(帧校验序列) 注:IEEE处于历史原因使用了“帧”而不是“包”,因此在以太网数据中都说是“帧”,可以认为包和帧是一回事
web_p117_1.jpg
报头是一串像 10101010...
这样1和0交替出现的比特序列,长度为 56
位
它的作用是确定读取包的时机
当这些 1010 的比特序列被转换为电信号后,会形成下图这样的波形 接收方在收到信号时,遇到这样的波形就可以判断读取数据的时机
web_p117_2.jpg
用电信号来表达数字信息时,我们需要让 0 和 1 两种比特分别对应特定的电压和电流 通过电信号来读取数据的过程就是将这种对应关系颠倒过来 也就是说,通过测量信号中的电压和电流变化,还原出 0 和 1 两种比特的值 实际的信号并不像下图那样有分割每个比特的辅助线,因此在测量电压和电流是必须先判断出每个比特的界限在哪里 但是像下图(a)右边这种连续的信号,由于电压和电流没有变化,我们就没办法判断出其中每个比特到底该从哪里去切分
web_p118.jpg
要解决这个问题,最简单的方法就是在数据信号之外再发送一组用来区分比特间隔的时钟信号
如上图(b)所示,当时钟信号从下往上变化时,读取电压和电流的值,然后和 0 或 1 进行对应就可以了
但这种方法也存在问题,当距离较远,网线较长时,两条线路的长度会发生差异,数据信号和时钟信号的传输会产生时间差,时钟就会发生偏移
要解决这个问题,可以采用将数据信号和时钟信号叠加在一起的方法,如上图(c)所示 由于时钟信号是像上图(b)那样按固定频率进行变化的,只要能够找到这个变化的周期,就可以从接收到的信号(c)中提取出时钟信号(b),进而计算出数据信号(a),这和发送方将数据信号和时钟信号叠加的过程正好相反 然后,只要根据时钟信号(b)的变化周期,我们就可以从数据信号(a)中读取相应的电压和电流值,并将其还原为 0 或 1 的比特
重点在于如何判断时钟信号的变化周期,时钟信号是以 10Mbit/s 或 100 Mbit/s 这种固定频率进行变化的,只要对信号进行一段时间的观察,就可以找到其变化的周期 因此我们不能一开始就发送包的数据,而是要在前面加上一段用来测量时钟信号的特殊信号,这就是报头的作用 注:如果在包信号结束之后,继续传输时钟信号,就可以保持时钟同步的状态,下一个包就无需重新进行同步。有些通信方式采用了这样的设计,但以太网的包结束之后时钟信号也跟着结束了,没有通过这种方式来保持时钟同步,因此需要在每个包的前面加上报头,用来进行时钟同步
以太网根据速率和网线类型的不同分为许多派生方式,每种方式的信号形态也有差异,并不都是像本例中讲的这样,单纯通过电压和电流来表达 0 和 1 的。 因此,101010……这样的报头数字信息在转换成电信号后,其波形也不一定都是图2.25的样子,而是根据方式的不同而不同,但报头的作用和基本思路是一致的
起始帧分界符的末尾比特排列有少许变化,接收方以这一变化作为标记,从这里开始提取网络包数据
末尾的帧校验序列(FCS)用来检查包传输过程中
上一篇: 宽带入户后如何连接路由器?详细图解教程
下一篇: 网络是如何连接的?网络发展简介(IV)
推荐阅读
-
[M5Stack 物联网开发] 第 1 章 物联网 - 1.1 物联网中的网络连接
-
epoll简介及触发模式(accept、read、send)-epoll的简单介绍 epoll在LT和ET模式下的读写方式 一、epoll的接口非常简单,一共就三个函数:1. int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件注册函数,它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create的返回值,第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};events可以是以下几个宏的集合:EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLIN事件:EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。 EPOLLOUT:表示对应的文件描述符可以写; EPOLLOUT事件:EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那要先准备好下面条件:1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!其实,如果真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。1. 缓冲区由满变空.2.同时注册EPOLLIN | EPOLLOUT事件,也会触发一次EPOLLOUT事件这个两个也会触发EPOLLOUT事件 EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的产生,类似于select调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。-------------------------------------------------------------------------------------------- 从man手册中,得到ET和LT的具体描述如下EPOLL事件有两种模型:Edge Triggered (ET)Level Triggered (LT)假如有这样一个例子:1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符2. 这个时候从管道的另一端被写入了2KB的数据3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作4. 然后我们读取了1KB的数据5. 调用epoll_wait(2)......Edge Triggered 工作模式:如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。Level Triggered 工作模式相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。然后详细解释ET, LT:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取: 这里只是说明思路(参考《UNIX网络编程》) while(rs) {buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break; else return; }else if(buflen == 0) { // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf) rs = 1; // 需要再次读取 else rs = 0; } 还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send内部,当写缓冲已满(send返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send内部,但暂没有更好的办法. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total = buflen; const char *p = buffer; while(1) { tmp = send(sockfd, p, total, 0); if(tmp < 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno == EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno == EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp == total) return buflen; total -= tmp; p += tmp; } return tmp; } 二、epoll在LT和ET模式下的读写方式 在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK) 从字面上看, 意思是: * EAGAIN: 再试一次 * EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block * perror输出: Resource temporarily unavailable 总结: 这个错误表示资源暂时不够, 可能read时, 读缓冲区没有数据, 或者, write时,写缓冲区满了 。 遇到这种情况, 如果是阻塞socket, read/write就要阻塞掉。 而如果是非阻塞socket, read/write立即返回-1, 同 时errno设置为EAGAIN. 所以, 对于阻塞socket, read/write返回-1代表网络出错了. 但对于非阻塞socket, read/write返回-1不一定网络真的出错了. 可能是Resource temporarily unavailable. 这时你应该再试, 直到Resource available. 综上, 对于non-blocking的socket, 正确的读写操作为: 读: 忽略掉errno = EAGAIN的错误, 下次继续读 写: 忽略掉errno = EAGAIN的错误, 下次继续写 对于select和epoll的LT模式, 这种读写方式是没有问题的. 但对于epoll的ET模式, 这种方式还有漏洞. epoll的两种模式 LT 和 ET
-
第 1 章 C 语言基础知识 第一节 C 语言的基本认识 1.用 C 语言编写的程序称为源程序,也称为编译单元。2、C语言的书写格式是*的,每行可以写多条语句,可以写多行。3、一个 C 语言程序有且只有一个 ma
-
紧急模式问题处理 - 图 1 紧急模式 根本原因分析 应急模式提供了尽可能小的环境,即使无法进入应急模式,也可以在其中修复系统。在应急模式下,系统只安装根文件系统供读取,不尝试安装任何其他本地文件系统,不激活网络接口,只启动一些基本服务。 进入应急模式的原因通常是 /etc/fstab 文件中存在错误,导致文件系统挂载失败。 文件系统中存在错误,导致。 约束和限制 本节适用于 Linux 操作系统紧急模式。程序涉及修复文件系统。修复文件系统有丢失数据的风险,因此请先备份数据,然后再执行修复操作。 处理方法 输入根密码,然后进入修复模式。 在应急模式下,根分区以只读模式挂载。要修改根目录中的文件,需要执行以下命令以读写模式重新挂载根分区。# mount -o rw,remount / 请执行以下命令首先检查 fstab 文件是否有误,然后尝试挂载所有未挂载的文件系统。# mount -a 如果挂载点不存在,请创建一个挂载点。 如果不存在此类设备,请注释或删除挂载行。 如果指定了不正确的挂载选项,请将挂载参数更改为正确的参数。 如果没有发生错误,但出现 UNEXPECTED INCONSISTENCY;RUN fsck MANUALLY 消息(通常是由文件系统错误引起的),请跳至第 7 步。 执行以下命令打开 /etc/fstab 以修改相应的错误。# vi /etc/fstab /etc/fstab 文件包含以下字段,以空格分隔:[文件系统] [dir] [type] [options] [dump] [fsck] 表 1 /etc/fstab 参数 说明 参数 说明 [文件系统] 要挂载的分区或存储设备。 文件系统]列建议以 UUID 的形式写入。执行 blkid 命令可查询设备文件系统 UUID。 参考格式如下: # <device> <dir> <type> <options> <dump> <fsck>; UUID=b411dc99-f0a0-4c87-9e05-184977be8539 /home ext4 defaults 0 2 使用 UUID 的好处是,它们与磁盘顺序无关。如果你在 BIOS 中更改了存储设备的顺序,或重新插入了存储设备,或者因为某些 BIOS 可能会随机更改存储设备的顺序,那么使用 UUID 会更有效率。 [文件系统] 文件系统]的挂载位置。 类型 挂载设备或分区的文件系统类型,支持多种不同的文件系统:ext2、ext3、ext4、reiserfs、xfs、jfs、smbfs、iso9660、vfat、ntfs、swap 和 auto。 设置为自动类型后,挂载命令会猜测所使用的文件系统类型,这对 CDROM 和 DVD 等移动设备非常有用。 选项 挂载时要使用的参数,有些参数是特定文件系统特有的。例如,默认值参数使用文件系统的默认挂载参数,ext4 的默认参数为:rw、suid、dev、exec、auto、nouser、async。 有关更多参数,请执行以下命令查看 man 手册:# man mount
-
网络是如何连接的》第 1 章和第 2 章注释
-
科比鹰郡事件始末最强解析(全文)-插曲:OK两人最初的相识。OK两人在齐聚湖人之前就曾相遇。1992年,14岁的科比通过父亲的关系,进入了奥兰多更衣室向便士哈达威索要签名,但遭到置之不理,反倒是新秀时期的奥尼尔对科比鼓励有加,不仅给他签名,还和科比聊了很多,并且希望日后能在NBA看见科比,没想到一语成真,后者真的成为了他的队友,这也是日后奥尼尔对于科比和自己的矛盾产生仇恨心里的原因,因为奥尼尔始终认为最初对科比的善意没有换来以德报德。 多年以后,鹰郡事件女主角凯特琳澄清事实。据她自述,当时她是为了给母亲凑医药费,设法和科比发生关系,目的就是为了钱,科比支付的赔偿金拯救了她的母亲。最终事情真相浮出水面,这是有预谋的“仙人跳”讹诈,但是好事依然不出门。 不得不说,鹰郡事件是科比犯下的错。自此,除了篮球以外,科比将更多的时间回归家庭。然而鹰郡事件最大的受害者是科比本人和他的妻子瓦妮莎。那么他们都有哪些损失呢? 在道德上:科比饱受批评,引发不少妇女团体对他不满,甚至受人唾弃。 image 在家庭上:科比与瓦妮莎产生了信任危机,尤其是瓦妮莎的流产导致这个家庭失去了第二个孩子,而这个孩子或许是科比夫妇唯一的儿子。因此这件事让科比一直很后悔。 在金钱上:科比的律师费(至少1200万美元)、赔偿费(不少于500万美元)、再加上往返奔波费、诉讼费等估计超过2000万美金。此外商业上严重受阻,商业价值跌落谷底,赞助商几乎抛弃了他,阿迪达斯也在这个时候和科比解约。其经济损失不可估量。 在事业上:球迷的质疑和谩骂声音不断;湖人队内部发生微妙变化,队友和他有了一定距离;禅师菲尔杰克逊在得知科比“强奸”女性后更是对他区别对待,因为禅师的女儿曾经经历了强奸,他对科比十分厌恶,间接地导致禅师离开了湖人队;OK这对王炸组合解散;大卫斯特恩放弃了科比的造神计划,迅速将资源推向了詹姆斯;赛场上,虽然科比有着赶场法则的传奇,但就整个03—04赛季而言,科比表现起起伏伏,整体状态下滑明显。 image 不可否认,鹰郡事件导致科比处在生涯最低谷的时刻,那个阶段也许只有麦迪经常陪在科比身边给他带来一些安慰。但接下来科比以他顽强的意志和超乎常人的努力为自己进行救赎:三节打卡62分、单场81分神迹、连续4场50+、更改球衣号码变成黑曼巴、连续双一阵、MVP、奥运会冠军、两连冠+FMVP…… 现在回想起来,连当时受伤害最大的瓦妮莎都选择原谅了科比,但键盘侠们依然指指点点。 2018年3月5日,科比凭借其亲自参与制作并配音的退役动画短片《亲爱的篮球》获得了第90届奥斯卡“最佳动画短片奖”,而“小金人”的巨大荣誉,让奥尼尔都自嘲“兄弟,你真的让我羡慕!真没法追逐你了”,同时他也因此被选为了Animation Is电影节的评委。 image 随后科黑以“鹰郡事件”为理由向奥斯卡请愿,要收回科比的小金人,虽然小金人未收回,但10月18日,科比被取消了电影节的评委资格。 时间回到2020年。 1月26日科比因直升机坠毁意外身亡。斯人已逝,可喷子们还是没有放过他。有些人甚至借科比死亡这一消息来蹭热度! 就在科比去世的第二天,美国女演员埃文·蕾切尔·伍德(Evan Rachel Wood)在推特上称呼科比为“强奸犯”,遭到美国网友们的强烈反对。 一周后,据2月3日《每日邮报》消息,迪斯尼的女继承人阿比盖尔·迪斯尼在社交网络上连续发了24个帖子,直指2003年科比的性侵事件,认为科比犯强奸罪,强调科比不该被神化,他不是上帝。 但也有很多人相信科比没有强奸,据2月4日《每日邮报》报道,WNBA传奇女篮运动员丽莎·莱斯利为科比辩护,认为科比不是用武力侵犯女性的人,科比的妻子也一直在维护自己逝去的丈夫…… image 然而,不管是怎样的事实,即使法律宣判了科比无罪,即使是被敲诈勒索,成为受害人,可事到如今,科比依然被很多人戴上“强奸犯”的帽子,对于一个已经逝世的人来说,这是他最大的损失。 插曲:中国官方媒体对科比的态度,只列举一下几个主要媒体。