打开抖音互联网会发生什么(八)|青训营笔记
打开抖音互联网会发生什么(八)|青训营笔记
这是我参与「第三届青训营 -后端场」笔记创作活动的的第八篇笔记。
里面有一些链接,建议都点开看看,能拓展很多知识。
本章预备知识:
- Linux因为本人之前买的腾讯云的服务器被人黑了然后挖矿,就被封禁(痛失300元),重新搞了个VirtualBox,再下个镜像安装。
- wireshark是一款网络抓包分析工具,非常强大。百度安装。
- Socket网络编程开发有关知识,Java版本。
本章主要内容:计算机网络相关知识。
课程收益:
课程目录:
刷抖音网络是怎么交互的?
网络接入
互联网
路由
同网段一般是通过一个交换机来实现,但这个交换机有可能不是物理上的交换机,也可以是UDP虚拟网络上的一个交换机(右下)。
SDN:软件定义网络(Software Defined Network,SDN)是网络虚拟化的一种实现方式。其核心技术OpenFlow通过将网络设备的控制面与数据面分离开来,从而实现了网络流量的灵活控制,使网络作为管道变得更加智能,为核心网络及应用的创新提供了良好的平台。
右上图:左侧想发到右侧需要经过路由的更改目标,左下的两个就是两个路由协议。
路由可以对称也可以不对称。
路由可以说工作在IP层也就是网络层,但动态路由协议这种是属于传输层。
目标IP地址应当是一直不变的,路由只是为了找到下一跳的路,然后通过ARP请求问下一跳你的Mac地址是多少,然后走到下一跳,循环走。
原Mac和目标Mac一直在变化,原IP和目标IP不变。
发包或者路由的一个伪代码流程(右下侧代码):函数名发一个包,先根据dst(一般是出口的IP)找到主机出口网关和下一跳,然后找到dst的mac(即要去的mac地址,就是下一跳的mac,这个过程也找到了出口的网卡),发包需要找到网卡的端口,发包的时候是以网卡为单位。
动态路由BGP/OSPF类似,不过是动态的配置。
ARP协议
找到下一跳的MAC就是通过ARP协议。
ARP是类似于广播,广播不能跨网段,寻找B(广播),B听到在找他就回应一下(应答单播),因此这种只能在同一网段(逻辑)的时候用,不同网段不能ARP。
免费ARP:当一个网段新增了一个机器,因为没有ARP缓存,所以大家访问他会比较慢,因此每次新增机器的时候搞一个免费ARP,让大家更新一下MAC表,这样下次有人访问新机器的话就很快。
IPV6也会有类似的做法,新增一个IP的时候,会免费的都问一遍,防止新增的IP跟别人重了(可能同一时间新增了同一IP)。
ARP代理:劫持一个ARP请求,伪造一下发到另一个地方。
ARP本质上是查找下一跳的MAC,不是请求目标地址。
IP协议
Mac地址不能代替IP地址吗?
不能,因为涉及到向下兼容问题,MAC协议是二层以太网协议,二层以太网协议还有很多种,很多类似于拨号可能用的不是MAC协议,这样就没法上网了。。。所以不能代替。所以美国军方想了个法子,把二层网络封装一下,弄成了IP协议。
IPv4不够用,一般怎么解决?
第一,IPv6。那不支持IPv6的设备咋办?用NAT。
NAT
NAT(Network Address Translation),是指网络地址转换,1994年提出的。当在专用网内部的一些主机本来已经分配到了本地IP地址(即仅在本专用网内使用的专用地址),但又想和因特网上的主机通信(并不需要加密)时,可使用NAT方法。
家里的路由器就是一个NAT。
NAT不只改了IP地址,还改了端口,否则外网的包回来的时候找不到对应的服务,因为IP是大IP,端口都一样就冲突了,路由不知道给谁了。
上面内容讲的是如何将网络打通,通过NAT,ARP,MAC,路由等等。
下面讲打通后如何传输。
网络传输
数据包
经典七层模型,不过实际工作中很少提到表示层和会话层,一般是直接用应用层替代。
用wireshark抓包。
需要按层去封装。
数据包发送
先请求DNS
域名系统(英文:Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用UDP端口53。
这个DNS的过程是逆解析的,流程如图中文字。
DNS的传输协议UDP
payload:有效载荷。
发包每次发多少?怎么避免分片?
每次发包要小于路径上每个路由器的MTU(什么是MTU,这里还介绍了分片的知识),这样可以避免分片。
怎么知道没丢包?
因为UDP不可靠,所以没法知道。
怎么权衡效率和质量?
没法搞,弄好了就是自己写一个TCPhh。
TCP三次握手
Tcpdump与Wireshark
这里简单尝试了一下,Tcpdump可以在Linux上实现抓包:tcpdump抓包与Wireshark分析,这里在虚拟机上抓了一下ping百度的
,然后保存到文件file1中,放入Wireshark中分析:
这里大概可以看到ICMP请求,emmm因为我不会发TCP,所以对TCP的抓包回头再弄吧。。。
Linux的虚拟机安装和图形化界面可以看我的另一篇文章:Linux笔记
TCP连接可以理解为一个状态,拔了网线,如果服务器没有探活机制(TCP_KEEPALIVE,这篇文章讲得很好:TCP的KeepAlive 与HTTP的 Keep-Alive),那这个链接还会一直持续。
这文章有一些基础概念:LVS是Linux Virtual Server的简写,意即Linux虚拟服务器,是一个虚拟的服务器集群系统。VIP 是LVS的接收IP -- virtual IP;DIP 是LVS的分发IP -- distribute IP;RIP 是真实目标服务器IP -- Real IP;不同的模型 他们的配置也不一样。
什么是MSS 这篇文章可以弄懂MSS是什么,是在三次握手的时候确定的(在TCP Option中,取双方MSS最小值,作为TCP的MSS),MSS是传输层的,配合网络层的MTU一起用,为了减少丢包和分片的。
SYN这次的第一位位置,ACK对方的下一次SYN是多少。
前三次不传数据,但是SYN会+1,ACK不会。更详细一点的可以见这片文章:小谈TCP协议中的Ack和Seq号。主要还是通过Tcpdump和Wireshark来抓包,分析seq和ack的变化。
滑动窗口和流量控制 复习的时候再看吧 内容太多啦
HTTP/HTTP1.1
为什么不直接用TCP通信?
因为TCP太麻烦了,所以为了方便封装了HTTP。
可以类比中文和军事专用语的区别,而且HTTP是长连接,不用每次都重新握手。
HTTPS
当明文被窃取的时候,可以通过HTTPS加密。
SSL/TLS握手
对称加密:如果交流加密算法也被窃听就寄了。
非对称加密:再对交流加密算法加密一层。
非对称加密的盒子怎么做呢?
通过CA进行第三方的授权。
CA是什么?
网络安全中ca是Certification Authority的缩写,一般是指CA证书,它是CA机构发行的一种电子文档,是一串能够表明网络用户身份信息的数字,它提供了一种在计算机网络上验证网络用户身份的方式。
总结
网络架构怎么给抖音提质
网络提速
HTTP2.0
多路复用,可以在TCP上同时跑多个stream。
怎么理解多路复用/stream?
如果TCP丢包怎么办?
对头阻塞,(或者通过ACK标记重传位置,但没解决本质问题)。
QUIC/HTTP3.0
对 对头阻塞的解决方式,谷歌的解决方式(2.0 3.0 go 云计算都是他做的)。这里有篇文章讲得很好:QUIC 和 HTTP/3 队头阻塞的细节,里面的彩蛋部分没看。
TCP因为Linux不可拔插,嵌入进去了,所以用UDP。
因为系统内核有很多种,所以坐在了用户态。
TLS/SSL握手优化,做0 RTT传输.
弱网优势:弱网情况下,3比2丢包情况优化很多。
路径优化算法:CDN。。
数据中心分布
POP接入:更接近核心机房
边缘机房:更接近用户 全国都有,不过越繁华的地段越多
同运营商访问
通过解析去做,解析发现客户端是电信就用电信的。。。
静态资源(图片视频)路径优化(CDN)
CDN主要是对静态资源做缓存,如果边缘机房里有这个视频的缓存,那就直接发过去了,不用往里面走了。
动态API(播放/评论接口)路径优化(DSA)
机房ABCDE都对其他的机房进行一次延测速,记录到表里,就很好计算最优路径。
网络稳定
容灾概念
一套流程下来就是容灾。
网络容灾的具体案例一
专线:没走外部的Internet,而是抖音自己内部的网线。
外网:走的外部的Internet。
网络容灾的具体案例二
本来域名是解析成两个的,1.1.1.1和2.2.2.2。如果一个崩了,改成域名只能解析成一个(这里需要做一次自动化的确认,确认B能够承载A的全部流量,通过CDM?)。如果B也崩了,就是雪崩。。。
网络容灾的具体案例三
云控:从客户端的SDK控制,不去访问已经崩了的A机房。
但是有可能用户从浏览器访问,就没法更改域名了。。
网络容灾的具体案例四
兜底:如果后面寄了,就用之前的缓存。
故障排查
- 故障明确
- 故障止损
- 分段排查
网络故障排查常用命令
网络故障排查案例一
中间节点的误判断,把正常的节点摘掉了。
网络故障排查案例二
用户防火墙把所有的ip都禁了。。单个的问题
网络故障排查案例三
网络故障排查案例四
没有办法的办法,线上去debug。Fast发包:默认是路由对称,目标mac和源mac默认掉转一下就完事了,但实际情况不一样,只能改代码了。
故障预防很重要
总结
课后作业1
Socket编程可以回顾一下之前的文章:Socket5代理服务器。
TCP的编程
目录结构如下:
TCP服务器
一个TCP服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理。
TCP服务端程序的处理流程:
- 监听端口
- 接收客户端请求建立链接
- 创建goroutine处理链接。
我们使用Go语言的net包实现的TCP服务端代码如下:
package main
import (
"bufio"
"fmt"
"net"
)
// tcp/server/main.go
// TCP server端
// 处理函数
func process(conn net.Conn) {
defer conn.Close() // 关闭连接
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:]) // 读取数据
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client端发来的数据:", recvStr)
conn.Write([]byte(recvStr+"asdasd")) // 发送数据 添加一点内容
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:20000")//通过这个去听客户端
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
for {
conn, err := listen.Accept() // 建立连接
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn) // 启动一个goroutine处理连接
}
}
TCP客户端
一个TCP客户端进行TCP通信的流程如下:
- 建立与服务端的链接
- 进行数据收发
- 关闭链接
使用Go语言的net包实现的TCP客户端代码如下:
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
// tcp/client/main.go
// 客户端
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:20000")//Dial相当于拨号
if err != nil {
fmt.Println("err :", err)
return
}
defer conn.Close() // 关闭连接
inputReader := bufio.NewReader(os.Stdin)
for {
input, _ := inputReader.ReadString('\n') // 读取用户输入
inputInfo := strings.Trim(input, "\r\n")
if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
return
}
_, err = conn.Write([]byte(inputInfo)) // 发送数据
if err != nil {
return
}
buf := [512]byte{}
n, err := conn.Read(buf[:])//接收数据
if err != nil {
fmt.Println("recv failed, err:", err)
return
}
fmt.Println(string(buf[:n]))//将接收到的打印
}
}
执行效果:
UDP的编程
UDP服务端
使用Go语言的net
包实现的UDP服务端代码如下:
package main
import (
"fmt"
"net"
)
// UDP/server/main.go
// UDP server端
func main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
var data [1024]byte
n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
if err != nil {
fmt.Println("read udp failed, err:", err)
continue
}
fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
_, err = listen.WriteToUDP(data[:n], addr) // 发送数据
if err != nil {
fmt.Println("write to udp failed, err:", err)
continue
}
}
}
UDP客户端
package main
import (
"fmt"
"net"
)
// UDP 客户端
func main() {
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("连接服务端失败,err:", err)
return
}
defer socket.Close()
sendData := []byte("Hello server This is Client!!!")
_, err = socket.Write(sendData) // 发送数据
if err != nil {
fmt.Println("发送数据失败,err:", err)
return
}
data := make([]byte, 4096)
n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
if err != nil {
fmt.Println("接收数据失败,err:", err)
return
}
fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}
目录结构同TCP,效果也类似,不过服务器常开,客户端只会运行一次。
作业1中的 对UDP的ACK的处理,感觉是从服务器端入手,得先打包吧应该,大致思路应该是先打包(TCP粘包解决,这里也有打包的思路),在首部添加长度字段4字节防止粘包,然后放入8字节分别控制seq和ack,当seq和ack对应不上的时候即丢包。重传效率,感觉是服务器告诉客户端我少了哪到哪的数据,让客户端在发送一遍。。。有点像http2.0?
第一个作业答案大致如上。。。但是动手能力残疾,下次一定搞。
作业二感觉有点太难了。。。socket编程实在是懂得太少 日后精进了一定回来搞。