赞
踩
协议三要素:
Windows 上使用 ipconfig 命令,Linux 上使用 ifconfig 命令,Linux 上还可以使用 ip addr 命令
这种方式打破了原来设计的几类地址的做法,将 32 位的 IP 地址一分为二,前面是 网络号,后面是 主机号
将子网掩码和 IP 地址按位计算 AND,就可以得到网络号
MAC 地址,是一个网卡的物理地址,用十六进制,6 byte 表示。
MAC 地址号称全局唯一,不会有两个网卡有相同的 MAC 地址。MAC 地址像是身份证号,是一个唯一的标识,而 IP 地址更像是现住址的门牌号,具有定位功能
可以使用命令行自己配置一个地址,可以使用 ifconfig ,也可以使用 ip addr,设置好之后,用这两个命令,将网卡 up 一下,就可以开始工作了,这样配置自由度太高,可能自己会配置一个不能使用的 IP。
真正现实中可能是去管理员那里申请一段正确的 IP 地址,然后放在一个配置文件里面。不同系统的配置文件格式不同,大概包括 CIDR、子网掩码、广播地址和网关地址。
将 IP 写入配置文件配置服务端机器还可以,不需要变化,但如果是客户端呢,每天人来来往往、走来走去,如果在使用写入配置文件的方式分配 IP 地址,效率太低
所以便有了 动态配置协议(DHCP),网络管理员只需要配置一段共享的 IP 地址,每一台新接入的机器都通过 DHCP 协议,来这个共享的 IP 地址里申请,然后自动配置好就可以了。等人走了,或者用完了,还回去,这样其他机器也能用。
所以说,如果是数据中心里面的机器,IP 一旦配置好,基本不会变,这就相当于买房自己装修。DHCP 的方式就相当于租房,你不用装修,都是帮你配置好的,你暂时用一下,用完退租就可以了。
集线器有多个口,可以将多台电脑连接起来,但是,和交换机不同,集线器没有大脑,它完全在物理层工作,它会将自己收到的每一个字节,都复制到其他端口上去。这是第一层物理层联通的方案。
因为 Hub 采用的是广播的模式,如果每一台电脑发出的包,其他电脑都能收到,那就麻烦了,会出现以下问题:
这几个问题,就是数据链路层要解决的问题,就是 Medium Access Control,即 媒体访问控制,其实就是控制在往媒体上发数据的时候,谁先发、谁后发的问题,防止发生混乱。这解决的是第二个问题。这个问题中的规则,学名叫做 多路访问 。
第二个问题解决了,再看第一个问题,解决第一个问题就牵扯到第二层的 网络包格式 。对于以太网,第二层的最开始,就是目标的 MAC 地址和源的 MAC 地址。接下来就是 类型,大部分的类型是 IP 数据包,然后里面包含 TCP、UDP等层层封装的数据。而有了最开始的 MAC 地址,第一个问题也就解决了。
对于以太网,第二层数据包的最后面是 CRC,也就是 循环冗余检测。通过 XOR 异或的算法,来计算整个包在发送的过程中是否出现了错误,主要解决了第三个问题。
这里还有一个没有解决的问题,当源机器知道目标机器的时候,可以将目标地址放入包中,但是如果不知道呢?一个广播的网络里面接入了 N 台机器,我怎么知道每个的 MAC 地址是谁呢?这就是 ARP 协议,也就是已知 IP 地址,求 MAC 地址的协议
在一个局域网里面,如果知道了 IP 地址,可以通过广播的模式来向局域网内所有机器发出信号 “哪位电脑的 IP 地址是 XXX.XXX.XXX.XXX,你的 MAC 地址是啥?” ,广而告之,发送一个广播包,谁是这个 IP 谁来回答。
机器本地会进行 ARP 缓存,机器也会不断上线下线,所以 IP 地址是可能会变的,所以 ARP 的 MAC 地址缓存有过期时间。
使用以上方法,用 Hub 连接起来,便可以组成一个局域网了,但是一旦机器数量增多,问题就出现了,因为 Hub 是广播的,不管某个接口是否需要,所有的 Bit 都会被发送出去,然后主机来判断是不是需要,一旦流量变大,产生冲突的概率就提高了,所以需要智能点儿的设备。因为每个口都只连接一台电脑,这台电脑又不怎么切换 IP 和 MAC 地址,只要记住这台电脑的 MAC 地址,如果目标 MAC 地址不是这台电脑的,这个口就不用转发了。
交换机就具有这种功能,交换机可以将数据包中的 MAC 头拿下来,检查一下目标 MAC 地址,然后根据策略进行转发。
那交换机是如何知道每个口的电脑的 MAC 地址呢?这需要交换机会学习
电脑 A 要将一个包发送给电脑 B,当这个包到达交换机的时候,一开始交换机也不知道电脑 B 在哪个接口,只能将包转发给除自己外所有的接口,这个时候,交换机会记住,电脑 A 是来自于一个明确的接口的。以后当有包的目的地址是电脑 A 的,直接发送到这个口就可以了。当然,每个机器的 IP 地址会变,所在的接口也可能会变,因而交换机上的学习成果,我们称为 转发表,是有一个过期时间的。
当多台交换机连接在一起,就形成了一个拓扑结构,随着拓扑结构越来越复杂,这么多网线,绕过来绕过去,不可避免的会出现一些意料不到的情况。其中最常见的问题就是环路问题
数据包会在里面转来转去,每台机器都会发广播包,交换机转发也会复制广播包,当广播包越来越多的时候,网络就会越来越堵塞,这就需要使用 STP 协议,通过生成最小生成树的算法,将有环路的图变成没有环路的树,从而解决环路问题。
当局域网内机器越来越多,交换机也随之增多,就算交换机比 Hub 智能一些,但是难免有广播的问题,广播消息一大推,性能就下来了。
一个公司有不同的部门,有的部门需要保密,由于都在同一个广播域内,很多包都会在一个局域网里面飘啊飘,碰到会抓包的有心之人,就能抓到这些包,如果没有加密,就能看到这些敏感信息了。
基于以上两种情况,需要考虑划分局域网。
有两种划分方法,一种是 物理隔离,每个部分有单独的交换机,配置单独的子网,这样部门与部门之间的沟通就需要路由器了。但是这样划分无法充分满足部门人数变化的情况,一个部分人可能越来越多,也可能越来越少,交换机口多了浪费,少了又不够用。
另一种方式是 虚拟隔离,就是 VLAN,或者叫 虚拟局域网,我们需要在原来第二层的头上加一个 TAG,里面有一个 VLAN ID,一共 12 位。如果我们买的交换机是支持 VLAN 的,当这个交换机把第二层的头取下来的时候,就能够识别这个 VLAN ID。这样只有相同 VLAN 的包,才会互相转发,不同 VLAN 的包,是看不到的。这样广播问题和安全问题就都能够解决了。
对于支持 VLAN 的交换机,有一种口叫作 Trunk 口,它可以转发属于任何 VLAN 的口,交换机之间可以通过这种口相互连接。
ping 是基于 ICMP 协议工作的。ICMP 全称 Internet Control Message Protocol ,就是 互联网控制报文协议 。
网络包在异常复杂的网络环境中传输时,常常会遇到各种各样的问题。当遇到问题时,要传出消息来,报告情况,这样才可以调整传输策略。
ICMP 报文是封装在 IP 包里面的,因为传输指令的时候,需要源地址和目标地址
主动去查询网络情况怎么样,对应 ICMP 的查询报文类型。ping 就是查询报文,是一种主动请求,并且获得主动应答的 ICMP 协议。所以 ping 发的包也是符合 ICMP 协议格式的,只不过它在后面增加了自己的格式。
对 ping 的主动请求,进行网络抓包,称为 ICMP ECHO REQUEST,同理主动请求的回复,称为 ICMP ECHO REPLY。比起原生的 ICMP ,多了两个字段,一个是 标识符,另一个是 序号,在选项数据中,ping 还会存放发送请求的时间值,来计算往返时间,说明路程长短。
当我们正常发数据包时,目标地址出现问题,回复给我们的报文,对应的就是 ICMP 的差错报文类型。
局域网怎么与外网联系 – 路由器
使用路由器,然后配置我们的网卡(DHCP 是可以默认配置的),除了 IP 地址,我们还需要配置 网关。
在任何一台机器上,当要访问另一个 IP 地址的时候,都会先判断,这个目标 IP 地址,和当前机器的 IP 地址,是否在同一个网段(CIDR 和 子网掩码)。
如果是同一个网段 ,例如,访问局域网内旁边的电脑,那就没有网关什么事,直接将源地址和目标地址放入 IP 头,然后通过 ARP 获得 MAC 地址,将源 MAC 和 目的 MAC 放入 MAC 头中,发出去就可以了。
如果不是同一个网段 ,例如,要发往局域网之外的网络,这就需要发往默认网关 Gateway。Gateway 的地址一定是和源 IP 地址是一个网段的。往往不是第一个,就是第二个。例如 192.168.1.0/24 这个网段,Gateway 往往会是 192.168.1.1/24 或者 192.168.1.2/24。
网关往往是一个路由器,是一个三层转发设备,就是把 MAC 头和 IP 头都取下来,然后根据里面的内容,看看接下来把包往哪里转发的设备。
路由器就是一台网络设备,它有多张网卡,当一个入口的网络包送到路由器时,它会根据一个本地的转发信息库,来决定如何正确地转发流量,这个转发信息库通常被称为 路由表 。
一张路由表中会有多条路由规则,每一条规则至少包含这三项信息
可以通过命令来进行配置,核心思想是:根据目的 IP 地址来配置路由。
也可以根据多个参数来配置路由,这称作为 策略路由 ,这些都是静态路由。
如果网络环境很复杂并且多变,静态路由配置起来就很麻烦,因此需要动态路由算法。
动态路由算法的本质意义就是 如何在图中找到最短路径的问题,常用的有两种,一种是 Bellman-Ford 算法,另一种就是 Dijkstra 算法。
在计算机网络中,距离矢量路由算法 便是基于 Bellman-Ford 算法的;链路状态路由算法 是基于 Dijkstra 算法的
TCP 是 面向连接的 ,UDP 是 面向无连接的
在互通之前,面向连接的协议会先建立连接,例如,TCP 会三次握手,而 UDP 不会,所谓建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性 。
TCP 提供 可靠交付,通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达;而 UDP 继承了 IP 包的特性,不保证不丢失,不保证顺序到达 。
TCP 是 面向字节流的 ,发送的时候发的是一个流,没头没尾;UDP 是 基于数据报的,一个一个地发,一个一个地收。
TCP 是可以拥有 拥塞控制 的,会根据网络情况调整自己的行为;UDP 则不会,上边的应用让它发,它就发,不管网络情况怎样。
因此,TCP 其实是一个有状态的服务,它可以记着发送了没有,接收到没有,发送到哪个了,应该接收哪个了,错一点儿都不行。而 UDP 则是无状态服务,数据报发出去就发出去了,不够结果如何。
流量控制是照顾通信对象
拥塞控制是照顾通信环境
假设网络通路是不可靠的,A 想要与 B 建立连接。当 A 发出一个请求的时候,这个请求杳无音信,这时候有很多种可能,可能是这个包丢了,也可能没有丢,只是绕了弯路,也可能是 B 不想要与 A 建立连接。
A 并不能确认是什么情况,它只有重复发送请求包,这时候 B 这边收到了 A 的请求包,它可能不愿意与 A 建立连接,则 A 会重试一段时间然后放弃,它也可能愿意与 A 建立连接,那 B 就会向 A 发送一个确认包,如果只有两次握手的话,这时候已经结束了, A 应该与 B 成功建立连接了,但是,B 发送的这个确认包也可能会丢,或者这时候恰好 A 掉线了,B 也不能知道这个包的状态,只能保持着连接的状态。这是两次握手不能确认建立连接的原因之一。
或者再看另外一种极端情况,A 与 B 原来建立了连接,并且进行了简单的通信,结束了连接,但是 A 之前发出请求建立连接的时候,由于网络状态不好,重复发了好几次请求,这时候其中一个请求包珊珊来迟,B 自然会认为这是一个正常的请求,于是建立了连接,这样的话,连接就会一直保持着。因此两次握手肯定不行。
由于网络状态不好,B 也可能会发送多次应答,但只要有一个应答包到达 A,A 就认为连接已经建立了,因为对于 A 来说,它的消息是有来有回的。然后 A 会对 B 的应答包发送应答包,表示 A 已经收到了 B 的应答,而 B 也在等这个消息,只有等到了 A 的应答包,才能确认连接的建立,这样对于 B 来讲,它的消息才算有来有回的。
当然 A 发给 B 的应答包也可能会丢,也可能绕路,甚至 B 挂了,按这样来说,B 要是收到 A 的应答包之后,还应再发一个应答包再发给 A,但是这样下去没底了,所以四次握手是可以的,四十次握手也是可以的,关键是再多次的握手也不能保证就真的可靠了。只要双方的消息都有去有回,就基本可以了。
三次握手除了双方建立连接外,主要还是为了沟通 TCP 包的序号问题 。
A 要告诉 B ,它这边发起的包的序号是从哪个号开始的,B 也同样要告诉 A,它这边发起的包的序号是从哪个号开始的。
为什么不都从 1 开始呢,因为这样往往会出现冲突
例如,A 和 B 建立了连接,A 向 B 发送了 1、2、3 三个包,但是发送 3 这个包的时候,中间丢了,或者绕路了,然后重发 3,这时候 A 掉线了,重连上 B 之后,打算向 B 发送 1、2 这两个包,但是上次绕路的那个 3 又回来了,发给了 B ,B 自然认为这就是下一个包,于是发生了错误。
因而,每个连接都要有不同的序号,这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每 4 微秒加一。计算一下,如果到重复,需要 4 个多小时,那个绕路的包早就失效了。
一开始,客户端和服务端都处于 CLOSED 状态,服务端主动监听某一端口号,处于 LISTEN 状态。然后客户端主动发起连接 SYN,之后处于 SYN-SEND 状态。服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK ,之后处于 ESTABLISHED 状态,因为它一收一发成功了。服务端收到 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为它也一收一发成功了。
断开的时候,A 发送 FIN ,进入FIN_WAIT_1 状态,B 收到 A 的消息后,发送 FIN 的 ACK,进入 CLOSED_WAIT 状态。A 收到 B 的 ACK 进入 FIN_WAIT_2 状态,当 B 处理完所有数据之后,发送 FIN + ACK ,进入 LAST_ACK 状态。A 在收到 B 的 FIN + ACK 之后,向 B 发送 ACK 包确认收到信息,此时,A 进入 TIME_WAIT 状态,等待 2MSL 时间后,关闭连接,B 在收到 A 的 ACK 之后关闭连接。
当 A 收到 B 发送的 FIN + ACK 之后,知道 B 处理完数据了,要断开连接了,所以发送了一个 ACK 给 B,按理说,A 这时候已经可以断开连接了,可是如果这个 ACK 包丢了怎么办,如果 A 已经断开连接了,B 就再也没办法收到 ACK 了,B 也就没办法知道 A 到底有没有收到自己要关闭连接的请求包。所以,TCP 协议要求 A 最后等待一段时间,这个时间要足够长,长到如果 B 没有收到 A 的 ACK 包,B 会重发 FIN + ACK ,然后 A 会重新发送一个 ACK 并且给与足够时间到达 B。
等待的时间是 2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间的报文将被丢弃。
因为 TCP 报文是基于 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理它的路由器,值就减一,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒、1 分钟和两分钟等。
为了保证顺序性,每一个包都有一个 ID,在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个个来的,而是会应答某个之前的 ID,表示都收到了,这种模型称为 累计确认 或者 累计应答 。
为了记录所有发送的包和接收的包,TCP 也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的 ID 一个个排列,根据处理的情况分成四个部分。
在 TCP 里,接收端会给发送端报一个窗口的大小,叫做 滑动窗口 。这个窗口的大小应该等于第二部分加上第三部分。超过这个窗口的,接收端就处理不过来,就不能发送了。
接收端的缓存里记录的内容要简单一些
即对每一个发送了,但是没有 ACK 的包,都设有一个定时器,超过了一定的时间,就重新尝试。这个时间不能过短,时间必须大于往返时间 RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。
估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状态在不断地变化。除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为 自适应重传算法。
对于多次的超时重传,TCP 的策略是超时时间间隔加倍,每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
超时重传存在的问题是,超时周期可能相对较长
有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK ,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段。
例如,接收方发送 6 送到了,8 也送到了,但是 7 还没来,那肯定就是丢了,于是就发送 6 的 ACK ,要求下一个是 7 。接下来,收到后续的包,仍然发送 6 的 ACK,要求下一个是 7。当客户端收到 3 个重复 ACK,就会发现 7 的确丢了,不等超时,马上重发。
另一种方式称为 Selective Acknowledegment(SACK),这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了。
在接收端发送对于包的确认中,同时会携带一个窗口的大小。
发送端会根据接收端发送的 ACK 包中携带的窗口大小来调整自己的滑动窗口的大小,以此来实现流量控制。
当滑动窗口为 0 了之后,发送端不会再给接收端发送数据包,而此时接收端也没有可以让自己 ACK 的数据包了,这样两边就不清楚对方的情况了。发送端不清楚接收端那边的数据处理完了没有,接收端也没有办法告诉发送端我处理到哪里了(因为接收端没有可以 ACK 的包了)。这样就陷入僵局了,为了打破这个僵局,当滑动窗口为 0 了之后,发送方会定时发送窗口探测包,询问接收端是否有机会调整窗口的大小。(接收方也得矜持一点,不能有了一个空位,就马上告诉发送方你可以传数据包了,当发送端传了之后,发送端又没窗口了,发送端又要发送窗口探测数据包,这样窗口一会儿没,一会儿有,即使有,窗口的大小也是1、2啥的。这种探测窗口的数据包发的就挺多的,这些包丢没丢还得需要机制去保证,效率就下来了)。一般都是当接收端窗口大小到达一定的大小,或者窗口大小是缓冲区的一半了,才给发送端一个信息,可以更新窗口了,更新窗口的大小为 XXX。
拥塞控制也是通过窗口的大小来控制的,前面的滑动窗口 rwnd 是怕发送方把接收方缓存塞满,而拥塞窗口 cwnd 是怕把网络塞满。
这里有一个公式 发送的未确认的包的数量 <= min(cwnd, rwnd)
,是拥塞窗口和滑动窗口共同控制发送的速度。
TCP 的拥塞控制目的是在不堵塞、不丢包的情况下,尽量发挥带宽。通道容量 = 带宽 * 往返延迟。如果我们设置发送窗口,使得发送但未确认的包为通道的容量,就能够撑满整个管道。
TCP 的拥塞控制主要来避免两种现象,包丢失 和 超时重传。一旦出现这些现象,就说明发送速度太快了,要慢一点。
但是一开始如何知道速度多快呢?怎么知道应该把窗口调整到多大呢?TCP 对应的策略就是 慢启动 。
一条 TCP 连接开始,cwnd 设置为一个报文段,一次只能发送一个;当收到这一个确认的时候,cwnd 加一,于是一次能够发送两个;当这两个的确认到来的时候,每个确认 cwnd 加一,两个确认 cwnd 加二,于是一次能够发送四个;当这四个的确认到来的时候,每个确认 cwnd 加一,四个确认 cwnd 加四,于是一次能够发送八个。可以看出这是 指数性的增长 。
直到 cwnd 增长到超过 ssthresh(65535个字节),就要慢下来了,改变成每收到一个确认后,cwnd 增加 1/cwnd ,也就是每次增加一个,这样就变成了 线性增长 。
但是线性增长还是增长,还是越来越多,直到有一天,水满则溢,出现了拥塞。拥塞的一种表现形式是丢包,需要超时重传,这个时候,将 sshresh 设为 cwnd/2 ,将 cwnd 设为1,重新开始慢启动。但是这种方式太激进了,将一个高速的传输速度一下子停下来,会造成网络卡顿。
之前说的 快速重传算法,当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK ,于是发送端就会快速的重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2,然后 sshresh = cwnd,当三个包返回的时候,cwnd = sshresh + 3,也就是还在比较高的值,呈线性增长。
正是这种知进退,使得时延很重要的情况下,反而降低了速度。但是仔细想一下,TCP 的拥塞控制主要来避免的两个现象都是有问题的
为了优化这两个问题,后来有了 TCP BBR 拥塞算法。它企图找到一个平衡点,就是通过不断地加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。
总结:
- 顺序问题、丢包问题、流量控制都是通过滑动窗口来解决的
- 拥塞控制是通过拥塞窗口来解决的
注意:监听的 Socket 和真正用来传数据的 Socket 是两个,一个叫作 监听 Socket,一个叫作 已连接 Socket 。
对于 UDP 来讲,它是没有连接的,所以不需要三次握手,也就不需要调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 和端口号,因而也需要 bind。UDP 是没有维护连接状态的,因而不需要每对连接建立一组 Socket,而是只要有一个 Socket ,就能够和多个客户端通信。也正是因为没有连接状态,每次通信的时候,都调用 sendto 和 recvfrom ,都可以传入 IP 地址和端口。
相当于自己是一个代理,在那里监听来的请求。一旦建立了一个连接,就会有一个已连接 Socket ,这时候就可以创建一个子进程,然后将基于已连接 Socket 的交互交给这个新的子进程来做。
Linux 中可以使用 fork 函数来创建子进程,是在父进程的基础上完全拷贝一个子进程。在 Linux 内核中,会复制文件描述符的列表,也会复制内存空间,还会复制一条记录当前执行到了哪一行程序的进程。这两个进程刚复制完的时候,几乎一模一样,如果 fork 的返回值是 0 ,则是子进程;如果返回值是其他的整数(子进程的 ID),就是父进程。
接下来,子进程就可以通过这个已连接 Socket 和和客户端进行互通了,当通信完毕,就可以退出进程。父进程可以通过 fork 函数返回的子进程的 ID 来查看子进程是否完成项目,是否需要退出。
相比于进程来讲,线程要轻量的多。
Linux 中可以通过 pthread_create 创建一个线程,也是调用 do_fork。不同的是,虽然新的线程在 task 列表会新创建一项,但是很多资源,例如文件描述符列表、进程空间,还是共享的,只不过多了一个引用而已。
新的线程也可以通过已连接 Socket 处理请求,从而达到并发处理的目的。
以上基于进程或线程模型的,会有一个 C10K问题 ,新到来一个 TCP 连接,就需要分配一个进程或线程,一台机器无法创建很多进程或线程。C10K 问题就是,一台机器要维护一万个连接,就要创建一万个进程或者线程,操作系统是无法承受的。
由于 Socket 是文件描述符,因而某个线程盯的所有的 Socket ,都放在一个文件描述符集合 fd_set 中,然后调用 select 函数来监听文件描述符集合是否有变化。一旦有变化,就会依次查看每个文件描述符。那些发生变化的文件描述符在 fd_set 对应的位都设为 1,表示 Socket 可读或者可写,从而可以进行读写操作,然后再调用 select ,接着盯着下一轮的变化。
select 函数使用轮询的方式来监听所有 Socket,并且使用 select ,能够同时监听的 Socket 数量由 FD_SETSIZE 限制。
epoll 是通过注册 callback 函数的方式,当某个文件描述符发生变化的时候,就会主动通知。
select 实现多路复用的方式是,将已连接的 Socket 都放到一个 文件描述符集合 ,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式就是通过 遍历 文件描述符集合,当检查到有事件产生后,将此 Socket 标记为可读或可写,接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过 遍历 的方式找到可读或可写的 Socket ,然后再对其进行处理。
select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制,默认最大值为 1024。
poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
但是 poll 和 select 并没有太大的本质区别,都是使用 线性结构 存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket ,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,性能的损耗会呈指数级增长。
epoll 通过两个方面,很好解决了 select / poll 的问题。
第一点,epoll 在内核里使用 红黑树来跟踪进程所有待检测的文字描述符,把需要监控的 socket 通过 epoll_ctl()
函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket ,减少了内核和用户空间大量的数据拷贝和内存分配。
第二点,epoll 使用事件驱动的机制,内核里 维护了一个链表来记录就绪事件 ,当某个 socket 有事件发生时,通过回调函数,内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait()
函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
epoll 支持两种事件触发模式,分别是 边缘触发(edge-triggered ET) 和 水平触发(level-triggered LT)
select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少系统调用的次数,边缘触发模式一般和非阻塞 IO 搭配使用
epoll 是解决 C10K 问题的利器
来源于:小林 coding《图解系统》
方法类型:
在使用过程中,POST 往往是用来创建一个资源的,而 PUT 往往是用来修改一个资源的
是以 key-value 的形式保存的
例如,Accept-Charset,表示客户端可以接受的字符集,防止传过来的是另外的字符集,从而导致出现乱码。
Content-Type 是指正文的格式,比如 JSON、image、text。
Cache-control,是用来控制缓存的,当客户端发送的请求中包含 max-age 指令时,如果判定缓存层中,资源的缓存时间数值比指定时间的数值小,那么客户端可以接受缓存的资源;当指定 max-age 值为 0,那么缓存层通常需要将请求转发给应用集群。
If-Modified-Since 也是关于缓存的,如果服务器的资源在某个时间之后更新了,那么客户端就应该下载最新的资源;如果没有更新,服务端会返回 “304 Not Modified” 的响应,那客户端就不用下载了,也会节省带宽。
首部字段也是以 key-value 的形式保存的
Retry-After 是告诉客户端应该在多长时间以后再次尝试一下。主要用于配合状态码 503 service Unavailable 响应一起发送的时候,表示服务预计不可用的时间,当与重定向响应一起发送的时候,比如 301(Moved Permanently,永久迁移),表示用户代理在发送重定向请求之前需要等待的最短时间。
Content-Type,表示返回的类型,HTML、JSON
原文:白话http队头阻塞
并且使用 HTTP 管道化还有一些限制:
现代浏览器默认都关闭了管道化,并且大部分服务器也是默认不支持管道化的
针对每个域名而言,浏览器允许客户端使用 并发长连接,比如 Chrome 是 6 个,也就是页面中如果针对同一个域名有多个 HTTP 请求,Chrome 会针对这个域名建立 6 个 TCP 长连接,在每个长连接里面再去处理 HTTP 请求
例如下面这种情况
以下主要来源于小林Coding《图解网络》
HTTP 2.0 使用 HPACK 算法,客户端和服务器两端都会维护 “字典”,用长度较小的索引号表示重复的字符串,再用哈夫曼编码压缩数据。
HPACK 算法主要包含三个组成部分:
静态表包含 61 组高频出现在头部的字符串和字段,它是写入 HTTP 2.0 框架的,不会变化的,表中包含 index(索引)、Header Name(字段名)、Header Value(索引对应的值)。
其中表中有部分 index 没有对应的 Header Value,因为这些 Value 并不是固定的,而是变化的,这些 Value 都会经过哈夫曼编码后,发送出去。
静态表中只包含了 61 种高频出现在头部的字符串,不在静态表范围内的头部字符串就要自行构建动态表,它的 index 从 62 起步,会在编码解码时随时更新。
比如,第一次发送时头部中的 user-agent
字段数据有上百个字节,经过哈夫曼编码发送出去后,客户端和服务器双方都会更新自己的动态表,添加一个新的 index 号 62,那么在下一次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 index 号就好了,因为双方都可以根据自己的动态表获取到字段的数据。所以,使得动态表生效有一个前提:必须同一个连接上,重复传输完全相同的 HTTP 头部,理论上随着同一 HTTP 2.0 连接上发送的报文越来越多,客户端与服务端双方字典积累的越来越多,最终每个头部字段都会变成 1 个字节的 index,这样便避免了大量的冗余数据的传输,大大节约了带宽。
但是动态表越大,占用的内存也就越大,如果占用了太多内存,是会影响服务器性能的,因此 Web 服务器都会提供类似 http2_max_requests
的配置,用于限制一个连接上能够传输的请求数量,避免动态表无限增大,请求数量到达上限后,就会关闭 HTTP 2.0 连接来释放内存。
HTTP 2.0 厉害的地方在于将 HTTP 1 的文本格式改成二进制格式传输数据,极大提高了 HTTP 传输效率,而且二进制数据使用位运算能高效解析
HTTP 2.0 将所有的传输信息分割为更小的消息和帧(Frame),常见的帧有 Header 帧,用于传输 Header 内容,再就是 Data 帧,用来传输正文实体,采用二进制编码。
HTTP 2.0 通过 Stream 这个设计,多个 Stream 复用一条 TCP 连接,达到并发的效果,解决了 HTTP 1.1 队头阻塞的问题,提高了 HTTP 传输的吞吐量
在 HTTP 2.0 的连接上,不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream ),因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,而 同一 Stream 内部的帧必须是严格有序的。
客户端和服务器双方都可以建立 Stream ,客户端建立的 Stream ID 必须是奇数号,而服务器建立的 Stream ID 必须是偶数号。
HTTP 2.0 对于每个 Stream 还可以设置不同优先级,比如可以设置服务器先传递 HTML/CSS,再传递图片,以此来提高用户体验。
HTTP 1.1 都是客户端向服务器发起请求后,才能获取到服务器响应的资源。
HTTP 2.0 中服务器可以对一个客户端请求发送多个响应,服务器向客户端推送资源无需客户端明确的请求,省去了客户端重复请求的步骤。
客户端发起的请求,必须使用的是奇数号的 Stream ,服务器主动的推送,使用的是偶数号 Stream 。服务器在推送资源时,会通过 PUSH_PROMISE
帧传输 HTTP 头部,并通过帧中的 Promised Stream ID
字段来告知客户端,接下来会在哪个偶数号 Stream 中发送包体。客户端解析 Frame 时,发现它是一个 PUSH_PROMISE
类型,便会准备接收服务端要推送的流。
现在压力都集中在底层一个 TCP 连接上,TCP 层是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当 “前一个字节数据” 没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这一个字节数据到达时,HTTP 应用层才能从内核中拿到数据,这就是 HTTP 2.0 队头阻塞的问题。
一条 TCP 连接是由四元组标识的,分别是源 IP、源端口、目的 IP、目的端口。一旦一个元素发生变化时,就需要断开重连。比如说,在移动设备上,从移动网络切换到 WIFI 时,都会导致重连,而建立连接的过程包括 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。
QUIC 是以一个 64 位的随机数作为 ID 来标识,而且 UDP 是无连接的,所以当 IP 或者端口变化时,只要 ID 不变,就不需要重新建立连接
在 TCP 中,为了保证可靠性,使用 序号 和 应答 机制,来解决顺序问题和丢包问题,通过 自适应重传算法,采样往返时间 RTT,不断调整超时时间,一旦某个包在超时时间内未得到应答,就重新发送。
但是其中存在着采样不准确的问题。例如,发送一个包,序号为 100,发现没有返回,于是再发送一个 100,过一阵返回一个 ACK 101,但是这个包往返时间按哪个算呢,按第一个发送的时间算,时间可能算长了,按第二个发送的时间算,时间了算短了。
QUIC 也有个序列号,是递增的。任何一个序列号的包只发送一次,下次就要加一了。例如,发送一个包,序号是 100,发现没有返回;再次发送的时候,序号就是 101 了;如果返回的是 ACK 100,就是对第一个包的响应,如果返回 ACK 101,就是对第二个包的响应,RTT 计算相对准确。
QUIC 定义了一个 offset 的概念来确保两个包发送的是同样的内容,QUIC 是面向连接的,也就像 TCP 一样,是一个数据流,发送的数据在这个数据流里面有个偏移量 offset,可以通过 offset 查看数据发送到了哪里,这样只要这个 offset 的包没有来,就要重发;如果来了,按照 offset 拼接,还是能够拼成一个流。
假如某一个流丢了一个 UDP 包,即使该流的其他数据包到达了,数据也无法被 HTTP/3 读取,知道 QUIC 重传丢失的报文,数据才会交给 HTTP/3。但是其他流的数据报文只要被完整接收,HTTP/3 就可以读取到数据。
所以,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,某个流发生丢包了,只会影响该流,其他流不受影响。
TCP 的流量控制是通过 滑动窗口协议。QUIC 的流量控制也是通过 window_update ,来告诉对端它可以接受的字节数。但是 QUIC 的窗口是适应自己的多路复用机制的,不但在一个连接上控制窗口,还在一个连接中的每个 stream 控制窗口。
QUIC 的 ACK 是基于 offset 的,每个 offset 的包来了,进行缓存,就可以应答,应答后就不会重发,中间的空档会等待到来或者重发即可,而窗口的起始位置为当前收到的最大 offset ,从这个 offset 到当前的 stream 所能容纳的最大缓存,是真正的窗口大小。
另外,还有整个连接的窗口,需要对于所有的 stream 的窗口做一个统计。
对于一个连接上的控制窗口来说:接收窗口 = stream1 接收窗口 + stream2 接收窗口 + ··· + streamN 接收窗口 。
非对称加密的算法都是公开的,所有人都可以自己生成一对公钥私钥。
当服务端向客户端返回公钥 A1 的时候,中间人将其 替换 成自己的公钥 B1 传送给浏览器。
而浏览器此时一无所知,使用公钥 B1 加密了密钥 K 发送出去,又被 中间人截获,中间人利用自己的私钥 B2 解密,得到密钥 K ,再使用服务端的公钥 A1 加密传送给服务端,完成了通信链路,而服务端和客户端毫无感知。
出现这一问题的核心原因是 客户端无法确认收到的公钥是不是真的是服务端发来的,为了解决这个问题,互联网引入了一个公信机构,这就是 CA。
服务端在使用 HTTPS 前,去经过认证的 CA 机构申请颁发一份 数字证书,数字证书里包含有证书持有者、证书有效期、公钥等信息,服务端将证书发送给客户端,客户端校验证书身份和要访问的网站身份确实一致后再进行后续的加密操作。
但是,如果中间人也聪明一点,只改动证书中的公钥部分,客户端依然不能确认证书 是否被篡改,这时就需要一些防伪技术了
使用 私钥 来对信息进行加密作为 数字签名:
在 HTTPS 连接建立的过程中,明文数据和数字签名组成证书,传递给客户端
因为签名是由 CA 机构的私钥生成的,中间人篡改信息后无法拿到 CA 机构的私钥对其再次加密,这样就保证了证书可信
问题1:如何保证公钥不被篡改?
问题2:公钥加密计算量太大,如何减少耗用的时间?
首先,客户端先向服务器发出发出加密通信的请求,叫作 ClientHello 请求
主要提供以下信息:
服务器收到客户端请求后,向客户端发出回应,这叫作 ServerHello。
包含以下内容:
客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。
如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息:
服务器在收到客户端的第三个随机数 pre-master key 之后,计算生成本次会话所用的 “会话密钥”。然后,向客户端最后发送下面信息:
至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用 “会话密钥” 加密内容。
来源于:SSL/TLS协议运行机制的概述
例如,一个应用要访问数据库,在这个应用里面应该配置这个数据库的域名,而不是 IP 地址,因为一旦这个数据库,因为某种原因,换到了另外一台机器上,而如果有很多个应用都配置了这台数据库的话,一换 IP 地址,就需要将这些应用全部修改一遍。但是如果配置了域名,则只要在 DNS 服务器里,将域名映射为新的 IP 地址,这个工作就完成了,大大简化了运维。
另一方面,我们可以通过配置一些策略,在域名解析的时候,解析出不同服务器的 IP,这次返回第一个,下次返回第二个,这样轮询或带权重的返回 IP 地址,实现负载均衡。
为了保证我们的高可用,往往会部署多个机房,每个地方都会有自己的 IP 地址。当用户访问某个域名的时候,这个 IP 地址可以轮询访问多个数据中心。如果一个数据中心因为某些原因挂掉了,只要在 DNS 服务器中,将这个数据中心对应的 IP 地址删除,就可以实现一定的 高可用。
另一方面,可以通过引入 全局负载均衡器 来实现用户访问属于相同运营商的距离较近的数据中心。大概流程就是,在 DNS 解析的时候,权威 DNS 服务器返回的并不是用户请求的域名的 IP 地址,而是返回该域名对应的全局负载均衡器的 IP 地址,然后,在负载均衡器上实现一定的策略,来达到负载均衡。
地 DNS 向其中一台顶级域 DNS 服务器发送查询报文
5. 该顶级域服务器注意到 b.com 的前缀,向本地 DNS 服务器返回负责该域名的权威 DNS 服务器的地址
6. 本地 DNS 服务器向权威 DNS 服务器发送查询报文,权威 DNS 服务器查询后将对应的 IP 地址返回给本地 DNS 服务器
7. 本地 DNS 在将 IP 地址返回给客户端,客户端和目标建立连接
例如,一个应用要访问数据库,在这个应用里面应该配置这个数据库的域名,而不是 IP 地址,因为一旦这个数据库,因为某种原因,换到了另外一台机器上,而如果有很多个应用都配置了这台数据库的话,一换 IP 地址,就需要将这些应用全部修改一遍。但是如果配置了域名,则只要在 DNS 服务器里,将域名映射为新的 IP 地址,这个工作就完成了,大大简化了运维。
另一方面,我们可以通过配置一些策略,在域名解析的时候,解析出不同服务器的 IP,这次返回第一个,下次返回第二个,这样轮询或带权重的返回 IP 地址,实现负载均衡。
为了保证我们的高可用,往往会部署多个机房,每个地方都会有自己的 IP 地址。当用户访问某个域名的时候,这个 IP 地址可以轮询访问多个数据中心。如果一个数据中心因为某些原因挂掉了,只要在 DNS 服务器中,将这个数据中心对应的 IP 地址删除,就可以实现一定的 高可用。
另一方面,可以通过引入 全局负载均衡器 来实现用户访问属于相同运营商的距离较近的数据中心。大概流程就是,在 DNS 解析的时候,权威 DNS 服务器返回的并不是用户请求的域名的 IP 地址,而是返回该域名对应的全局负载均衡器的 IP 地址,然后,在负载均衡器上实现一定的策略,来达到负载均衡。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。