网络与部署相关笔记
这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记
(一)概述
分为四个方面:
- 网络接入协议
- 网络传输协议
- 网络优化
- 网络稳定
(二)基础概念
1. 网络接入协议、网络传输协议
1.1 网络接入协议/概念
- MAC地址:网卡的物理地址
- 路由协议:路由器相关,怎么找目的 ip 的网络地址
- ARP协议:根据 ip 找 MAC 地址
- IP协议:ip 地址
- NAT
1.2 网络传输协议
- DNS:根据域名找ip地址
- UDP:用户数据报
- TCP:传输连接协议
- HTTP:超文本传输协议
- HTTPS:安全版 HTTP
- HTTP2.0:基于长连接的流水线传输,有队头堵塞
- QUIC:基于UDP的传输协议
2. 课程涉及软件/环境安装搭建
抓包软件
建议在Linux安装tcpdump软件(apt/yum命令安装,参考相关博客。如无法安装,可以下载源码安装www.tcpdump.org/)
安装wireshark(根据你的主机选择安装版本www.wireshark.org/,如果是Linux主机…
3. 思考题
- 按照 TCP/IP 的模型示意图,能否画出数据包的拆包/封包?
- TCP 的拥塞算法有哪些?课件建议熟练掌握。
- 建议熟练掌握 socket 编程。
- 建议阅读 golang/java 等高级编程下的 net 相关库的源码。
- 了解 Linux kernel 的网络包从收到包到用户态,从用户态发包到网卡整个流程?
(三)笔记详情
3.1 抖音网络是怎么交互的?
思考:为了让抖音工作,网络需要哪些交互?
- 网络接入
- 网络传输
3.2 网络接入
3.2.1 网络拓扑的整体认知
- 移动设备 ----> 路由器、4G、5G ----> 运营商网络 ----> 具体服务的服务器机房
3.2.2 路由发包原理
(1)同网段
配置网段即可默认添加静态路由。获取对端MAC直接发包。
(2)跨网段
配置网关路由。获取网关MAC地址发包。
(3)动态路由:BGP/OSPF 等,路由表在动态变化
(4)路由是网状的,不一定是对称的
(5)路由工作在 IP 层,但其内部涉及的一些协议与传输层有关
(6)路由是改的IP地址吗?
不是,是改下一跳的MAC地址。传输过程中源IP和目标IP始终不变,只有源MAC和目标MAC在改。
(7)发包代码
3.2.3 ARP协议(找下一跳MAC)
- 逻辑同网段才能发送ARP
- ARP广播/应答:ARP请求广播,ARP响应单播
- 免费ARP:主动广播告知MAC地址,新开了一个主机时(判断主机冲突)
- ARP代理:虚拟网络/伪造MAC地址
3.2.4 IP协议
- 唯一标识,互联网通用
- Mac地址不能代替IP地址吗?Mac协议是二层的协议,但是二层有很多协议,无法进行统一,因此在二层之上的三层使用ip协议进行统一;Mac地址难记,会重复,一个网卡一个,同一主机可能会有多个
- IPv4: 互联网终端节点的唯一标识
- IPv6: 不仅仅是IP地址长度的增加
3.2.5 NAT(当IPv4不够用时)
- NAT上网:家用路由器,将同网段下的设备的ip地址和端口同时改变,对外实现统一,对内实现区分。
- NAT出网:机房内网,主机上外网
- NAT原理:注意不仅仅是源地址变换,源端口/校验和/SEQ等都会变化
3.3 网络传输
3.3.1 数据包
本质上是一段内存,里面存储的内存是有序的,一般是按照TCP/IP的多层协议去封装。拆包/封包都是按照协议去写内存/读内存。
3.3.2 DNS递归迭代
3.3.3 UDP(常用作DNS的传输协议)
-
协议简单,想发什么包,就分配一个UDP的头,把payload 里面塞数据发出去就好。
-
需要考虑可靠性的场景,使用复杂,UDP用好很难
- 发包每次发多少?怎么避免分片?(MTU最大传输单元,有限制,需要将数据进行切分)
- 怎么知道没丢包?
- 怎么权衡传输效率和质量?
-
怎么保证协议可靠?
3.3.4 TCP
(1)三次握手
确认传输的起始序列号/MSS(最大分段大小,最大报文长度)/Option字段,建立连接
(2)TCP连接:是一个虚拟的概念,本质上两倍维持一段内存,记录连接状态,就是session
(3)TCP传输:理解sequence number(Seq)/acknowledge number(Ack)
- Timewait: 两倍的(MSL)最大报文存活时间/报文段寿命,保证连接正常关闭,防止前一次的应答报文(ACK)丢失,允许客户端响应报文有一次丢失机会;避免当前连接中的旧报文对相同四元组连接造成影响,确保当前连接存活的报文都消失。
(4)丢包重传:丢包怎么感知并重传?快速重传发生在什么时候?
快速重传,超时重传,ACK 机制保证可靠性
(5)滑动窗口
(6)流量控制
3.3.5 HTTP
-
HTTP 比 TCP 好在哪里:方便,HTTP 只是多加了一层规矩,HTTP 依然是 TCP ,只是这个规矩让用户更清晰、更简洁。
-
HTTP 1.1 的优化:长连接是重点
-
HTTPS
- HTTPS的产生背景:加密/可靠/防劫持
- SSL/TLS握手:非对称加密/对称加密、
3.4 网络提速
3.4.1 HTTP2.0
多路复用:可以在一个TCP 连接上跑多个 HTTP请求,但依然有队头阻塞
虽然说是可以在同一个 TCP 连接通道里同时进行多个 HTTP传输,但在内部实现上仍是采用串行传输方式,只是切换太快。这时,一旦发生丢包,就会进行重传,在重新接收到期望的数据包之前要一直等待,发生全部重传,容易造成队头阻塞。
3.4.2 QUIC / HTTP3.0(解决队头堵塞问题)
(1)优势:弱网传输,解决队头阻塞问题,组装思想
(2)为什么在用户态实现?
内核的更新迭代频率较低,不好推广,需要根据不同的操作系统分别进行实现,很麻烦。
(3)为什么用UDP?
TCP的队头阻塞问题不好解决,推倒重来&复用所有操作系统基本都支持的底层协议
3.4.3 网络路径优化
(1)数据中心建设
有边缘机房(靠近客户端)/汇聚机房/中心机房(服务器端)
(2)多运营商接入
同运营商内部访问,避免跨运营商的流量
(3)CDN 静态缓存系统
静态资源(图片视频) ,路径优化,边缘机房的建设,优先访问边缘机房,缓存命中视频/图片等静态内容
(4)DSA 动态加速系统
动态(API)(播放、评论接口) 路径优化,分四层/七层动态加速,核心在于利用可控节点做路径探测和规划。
e.g. 根据每个机房的时延,规划最优路径
3.5 网络稳定
3.5.1 容灾
3.5.2 网络容灾的具体案例
(1)机房专线故障
专线:连接各个机房的网络物理路径。内部机房不走外部 Internet,而是拉一根线之类的物理连接(交换机、集线器…),将机房直接相连,避免丢包,绕路
外网:跟 Internet 连接
环路容灾,转线走不通就走外网,避免某条专线故障导致机房孤岛问题
(2)单机房接入节点故障
DNS 容灾,摘除故障的节点 —— 字节GTM系统 :如果 A 机房挂了,直接在 DNS 过程中将节点 A 的 ip 剔除,替换成备用节点 B的 ip,同时,在替换成 B 之前完成故障感知,确保 B 的容量是足够承载从 A 流过来的访问量的,避免出现雪崩
(3)云控容灾
云端交互,服务器/云上下发命令到终端 —— 字节TNC系统
(4)cache容灾
源站不可用,降级到之前的缓存内容 —— 字节 TLB/ByteCDN 等系统的容灾建设
3.5.3 故障排查
(1)故障明确:出现什么故障?沟通是前提
- 什么业务?什么接口故障?
- 故障体现在哪里?
- 访问其他目标是否正常?
- 是否是修改导致的异常?
(2)故障止损:
- 要在第一时间做(灾备预案的建设) ,先止损再排查
- 如何止损?组件没有容灾,但是系统有没有?;降级;
(3)分段排查
-
客户端排查:
- 客户端访问其他服务没问题吗?
- 其他客户端访问目标服务没问题吗?
-
服务端排查:
- 服务端监控/指标都正常吗?
- 手动访问一下正常吗?
- 分组件排查
-
中间链路排查:
- 服务端跟客户端确保没有问题
- 中间网络设备有没有问题?(交换机/路由器/网关LB)
- 旁路的DNS有没有问题
(4)网络故障排查常用命令
- dig 查询 DNS 问题
- ping / telnet / nmap 查询三层 / 四层连通性
- Traceroute 排查中间链路,测试是否有丢包现象
- iptabels,查查有没有防火墙
- tcpdump 抓包
3.5.4 故障排查的具体案例
(1)服务端配置异常(健康检查异常)
(2)客户端某个例异常(客户端自己配置错误)
(3)外部运营商故障
(4)复杂故障的排查:需要抓包,具体问题具体分析
某 APP 故障 ----> 后端服务器反馈服务正常 ----> 网络转发设备异常 ----> 抓包 ----> 路由不对称
(四)课后思考
课后作业1- UDP socket 实现 ack,感知丢包重传
作业要求:
- 学会 UDP socket 编程
- 先从简单的 ack 学习,客户端等待 ack 再发包
- 什么时候客户端认为是丢包?
- 重传怎么考虑效率?
- 能不能不阻塞只穿丢掉的中间的段?
TCP socket 编程:
//服务器端
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args){
//创建一个服务器端的Socket,即ServerSocket,绑定需要监听的端口
try {
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = null;
//记录连接过服务器端客户端数量
int count = 0;
System.out.println("服务器即将启动,等待客户端的连接");
while(true){//循环监听新的客户端的连接
//调用accept()方法监听,等待客户端的连接以获取Socket实例
socket = serverSocket.accept();
//创建新线程
Thread thread = new Thread(new ServerThread(socket));
thread.start();
count++;
System.out.println("服务器端被连接过的次数:"+count);
InetAddress address = socket.getInetAddress();
System.out.println("当前客户端IP为:"+address.getHostAddress());
}
//服务器端的连接不用关闭。
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//客户端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client1 {
public static void main(String[] args) {
try{
Socket socket = new Socket("localhost",8888);
OutputStream os = socket.getOutputStream();
PrintWriter pw = new PrintWriter(os);
pw.write("用户名:jinxueling;密码:123");
pw.flush();
socket.shutdownOutput();
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is,"UTF-8");
BufferedReader br = new BufferedReader(isr);
String data = null;
while((data = br.readLine())!=null){
System.out.println("我是客户端,服务器端响应的数据为:"+data);
}
socket.close();
}catch (UnknownHostException e) {
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}
}
}
UDP Socket编程
//线程工具类
package UDPSocket;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPThread implements Runnable{
DatagramSocket socket = null;
DatagramPacket packet = null;
public UDPThread(DatagramSocket socket,DatagramPacket packet){
this.socket = socket;
this.packet = packet;
}
@Override
public void run() {
String info = null;
InetAddress address = null;
int port = 8800;
byte[] data2 = null;
DatagramPacket packet2 = null;
try{
//打印当前请求socket客户端的请求数据和信息。
info = new String(packet.getData(),0,packet.getLength());
System.out.println("我是服务器,客户端说:"+info);
//封装数据包,响应给当前socket实例的客户端
address = packet.getAddress();
port = packet.getPort();
data2 = "我在响应你".getBytes();
packet2 = new DatagramPacket(data2, data2.length,address,port);
socket.send(packet2);
}catch(Exception e){
e.printStackTrace();
}
}
}
//服务端
package UDPSocket;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
public class UDPServer {
public static void main(String[] args) throws IOException {
try {
//创建指定端口的DatagramSocket
DatagramSocket socket = new DatagramSocket(8800);
//声明数据报
DatagramPacket packet = null;
//声明字节数组
byte[] data = null;
//服务器响应的连接计数
int count = 0;
System.out.println("服务器启动,等待发送数据");
//等待客户端连接
while(true){
//初始化字节数组容量,指定接收的数据包的大小
data = new byte[1024];
//初始化数据包
packet = new DatagramPacket(data,data.length);
//等待接收来自服务端的数据包
socket.receive(packet);
//到达这一步,socket.receive方法停止阻塞了,说明有客户端在请求了
//给该客户端创建一个独立的线程,并根据接收到的包,给予响应。
Thread thread = new Thread(new UDPThread(socket,packet));
thread.start();
count++;
System.out.println("服务器端被连接过的次数:"+count);
//打印当前的客户端socket的ip
InetAddress address = packet.getAddress();
System.out.println("当前客户端的IP为:"+address.getHostAddress());
}
} catch (SocketException e) {
e.printStackTrace();
}
}
}
//客户端
package UDPSocket;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPClient {
public static void main(String[] args) throws IOException {
//定义服务器的地址,端口号,数据
InetAddress address = InetAddress.getByName("localhost");
int port = 8800;
byte[] data = "用户名:admin;密码:123".getBytes();
//创建数据报
DatagramPacket packet = new DatagramPacket(data,data.length,address,port);
//创建DatagramSocket,实现数据发送和接收
DatagramSocket socket = new DatagramSocket();
//向服务器发送数据报
socket.send(packet);
//接收服务器 响应数据
byte[] data2 = new byte[1024];
DatagramPacket packet2 = new DatagramPacket(data2,data2.length);
socket.receive(packet2);
String info = new String(data2,0,packet2.getLength());
System.out.println("我是客户端,服务器说:"+info);
socket.close();
}
}