1.概念篇
交换机: 一些学校,公司搭建的一个网络的时候就会用到交换机。交换机就是把多个主机构成一个网络。
集线器: 上古时期老设备,网线分叉,同一时刻只能有一根网线工作
路由器: 解决集线器同一时刻只能有一根网线工作的弊端,可以使得所有设备都有网络「路由器不仅能组成一个局域网,同时也链接了两个局域网的功能,让局域网之间由交换数据的功能」
局域网: 局部组建的一种私有网络。
广域网: 通过路由器将多个局域网连接起来,在物理范围上组建成很大范围的网络。广域网内部的局域网都属于其子网
局域网和广域网: 没有特定的概念,都是相对而言。「公司的局域网一定比家庭的广域网范围更大」
网络通信: 进行网络数据传输。更详细点是:网络主机中不同进程间机遇网络传输数据
路由器上面有两类网口
LAN(Local Area NetWork) 口:连接下级网络设备「路由器,电脑,电视等」
WAN 口(Wide Area NetWork):连接上级路由器「光猫」
路由器和交换机有什么区别:
「上古时代的面试题角度」:实际使用角度来看,交换机和路由器已经没区别了(路由器功能越来越强大)
「学校考试角度:」交换机负责二层转发,功能是组建一个局域网(二层指的是数据链路层);路由器负责三层转发,功能是连接两个局域网(三层指的是网络层)
IP地址
概念:4字节,定位主机的网络地址网络层「就像我们发送快递一样,需要知道对方的收货地址才能把包裹送达」
格式:被分割为 4 个 8位二进制数,通常用点分进制表示。a.b.c.d 每个数据范围[0, 255]
私有地址:互联网上不使用,而被用在局域网络中的地址
A类
- 第1字节为个网络地址,其它3个为主机地址。第1个字节最高位为固定位为0
- 范围:1.0.0.1-126.255.255.254
- 私有地址:10.0.0.0-10.255.255.255
- 保留地址:127.0.0.1-127.255.255.255『主要利用内部网络通信性能高,方便测试一些网络诚信通信使用』
B类
- 第1和第2字节为网络地址,其它2个位主机地址。第1个字节前两位固定为10
- 范围:128.0.0.1-191.255.255.254
- 私有地址:172.16.0.0-172.31.255.255
- 保留地址:当IP是自动获取但又没有DHCP服务器,就从『169.254.0.0-169.254.255.255』中临时获得一个IP地址
C类
- 第1,第2,第3字节为网络地址,剩下的一个是主机地址。第1个字节的前3位固定为110
- 范围:192.0.0.1-192.168.255.254
- 私有地址:192.168.0.0-192.168.255.255
IP地址解决了网络通信时定位网络主机的问题,但是数据传输到主机后由哪个进城来管理这些数据呢?这就需要用到 端口号
MAC地址
6字节,识别数据链层中相连的的节点
在网卡出厂的时候就设置了的不能被修改。MAC地址是唯一的「虚拟机中的MAC地址并不是真正的MAC,也有些网卡支持用户配置MAC地址」
端口号
概念:标记主机中发送数据,接收数据的进程
范围:「0-65535」
注意事项:两个不同的进程不能绑定同一个端口,但一个进程可以绑定多个端口号「两个收货地址不能同时接受同一个包裹,但一个收货地址可以接受多个不同的包裹」
了解:一个进程启动成功后,系统会随机分配一个端口号「启动端口」,程序代码中需要绑定一个端口来进行收发数据。
有了IP地址,端口,就可以定位到网络中唯一的一个进程。但存在一个问题:网络通信是基于光电信号,高低电平转换为二进制数据01传输的,我们如何知道对方送的什么数据呢?「图片,视屏,文本,音频对应的数据格式,编码方式也都不同」此时就需要有一个 协议 来规定双方发送接收数据饿。
认识协议
网络协议是网络通信 经过的所有设备 都要遵从的一组约定,规则。如怎么连接连接,怎么互相识别。只有遵从这个规定,多台计算机之间才能互相通信交流。
三要素组成:
-
语法:数据与控制结构信息的格式「打电话约定双方使用:普通话」 -
语义:需要发出何种控制信息,何种动作,何种响应「女朋友:喝奶茶;男朋友:走一起」
主要用来说明通信双方应当怎么做。用于协调和差错处理
-
时许:时间实现顺序的详细说明「打电话的时候,男生发起,聊天…,然后由女生挂断」
主要定义了何时通信,先讲什么,后讲什么,讲话速度。比如采用同步传输还是异步传输
知名协议的默认端口
系统端口范围是「0,65535」,知名端口「0,1023」,这些端口都是预留给服务端程序来绑定广泛使用的应用层协议。比如:
21:FTP
22:SSH
23:Telnet
80:HTTP
443:HTTPS
服务器也可以使用「1024,65535」范围内的端口来定义知名端口
五元组
在网络通信中用 五元组 来标示一个网络通信
- 源IP:标识源主机
- 源端口:标识源主机本次通信的进程
- 目的IP:标识目的主机
- 目的端口:标示本次通信发送到目的主机接收数据的目的进程
- 协议号:本次通信过程中双方约定的发送的数据格式
协议分层
把一个大的协议逐个拆分出来形成一个小协议
分层作用:类似于达到面向接口编程这样的效果:定义好两层之间的接口规范,让双方遵守这个规范来对接数据。便于日后的维护和更新。
OSI七层模型
只存在于教科书中「越往下越接近硬件设备,越往上越接近应用程序」
每一层都呼叫它的下一层来完成需求
层 | 功能 |
---|
应用层 | 应用程序间沟通,如简单的电子邮件传输SMTP,文件传输FTP,网络远程访问Telnet「网络编程主要在应用层,拿到数据之后你要干啥…」 | 传输层 | 两台主机之间的数据传输。如TCP,UDP「端到端:消费者和商家只关注某个快递是不是收到了」 | 网络层 | 管理和路由选择。在IP中识别主机,并通过路由表的方式规划处两台主机之间数据传输路线「点到点:快递公司,怎样运输才高效」 | 数据链路层 | 设备之间数据帧的传送和识别「帧同步,冲突检测,差错校验」 | 物理层 | 光电信号传输方式 |
传输层的端到端:只关注起点/终点,不关注中间过程
网络层的点到点:传输过程中经历的节点,需要关注中间过程的
网络设备所在分层
主机:操作系统内核实现了从物理层到传输层「TCP/IP下四层」
路由器:实现了网络层到物理层「下三层」
交换机:数据链路层到物理层「下两层」
集线器:物理层
注意:这里说的是传统意义上的交换机和路由器,也称为二层交换机(工作在TCP/IP五层模型的下两层)、三层路由器(工作在TCP/IP五层模型的下三层)。
随着现在网络设备技术的不断发展,也出现了很多3层或4层交换机,4层路由器。我们以下说的网络设 备都是传统意义上的交换机和路由器。
封装和分用
- 不同的协议层对数据包有不同的称谓。传输层:段「segment」;网络层:数据报「datagram」;链路层:帧「frame」
- 应用层数据通过协议栈发送出去的时候,每层协议都要加一个首部「header」,称为封装「Encapsulation」
- 首部:包含首部有多长,载荷多大,上层协议…
- 数据封装成帧后发到传输介质上,到达目的主机后每层都会剥掉首部,根据首部中上层协议字段,将数据交给对应的上层处理
数据封装图
数据分用图
封装和分用4不仅仅存在于发送方和接收方,中间设备「路由器/交换机」也会针对数据进行封装和分用
通常情况下:
交换机:只是封装分用到数据链路层就结束
A 的数据发给 交换机,交换机 物理层再进一步处理交给 数据链路层,数据链路层就针对这里数据解析并重新打包
路由器:只是封装分用到网络层就结束
网络层要根据这里的目的地址来规划接下来的传输路线,规划好了之后再重新交给数据链路层和物理层进行封装分用
2. 网络编程套接字
TCP | UDP |
---|
有链接「打电话」 | 无连接「发微信」 | 可靠传输「叮叮已读」 | 不可靠传输「叮叮未读」 | 面向字节流 | 面向数据报 | 全双工 | 全双工 |
全双工: 一个socket既可以用来发送也可以用来接收
半双工: 只能用来发送或者只能用来接收
2.1 UDP
DatagramSocket() 构造方法
UDP Socket 发送/接受数据
方法 | 含义 |
---|
DatagramSocket() | 创建一个套接字对象,绑定一个随机端口 | DatagramSocket(int port) | 创建一个套接字对象,绑定一个指定端口 |
DatagramSocket() 方法
方法 | 含义 |
---|
void receive(DatagramPacket p) | 从此套接字p只接收数据,如果没有收到数据就阻塞等待 | void send(DatagramPacket p) | 从此套接字p只接发数据,如果没有发送数据就阻塞等待 | void close() | 关闭此数据报套接字 |
DatagramPacket() 构造方法
UDP Socket 发送/接受数据
方法 | 含义 |
---|
DatagramPacket(byte[] buf, int length) | DatagramPacket把接收指定长度length的数据保存在字节数组buf中 | DatagramPacket(byte[] buf, int length, SocketAddress address) | DatagramPacket把长度length的字节数组buf数据发送到address |
DatagramPacket() 方法
方法 | 含义 |
---|
InetAddress getAddress() | 从接受的数据报中获取发送端 IP地址;从发送的数据报中获取接收端 IP地址 | int getPort() | 从接受的数据报中获取发送端 端口;从发送的数据报中获取接收端 端口 | byte[] getData() | 获取数据报中的数据 |
构造UDP发送数据报的时候,需要传入SocketAddress,该对象可以使用 InetSocketAddress 来创建
InetSocketAddress
方法 | 含义 |
---|
InetSocketAddress(InetAddress, int port) | 创建一个 Socket 对象,包含 IP地址 和 端口 |
服务端
package net;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
this.socket = new DatagramSocket(port);
}
public void start() throws IOException {
while (true) {
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
String response = process(request);
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s, %d]req:%s; resp:%s\n", requestPacket.getAddress(), requestPacket.getPort(), request, response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
客户端
package net;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
public UdpEchoClient(String ip, int port) throws SocketException {
this.serverIP = ip;
this.serverPort = port;
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("-> ");
String request = scanner.nextLine();
if (request.equals("exit")) {
break;
}
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIP), serverPort);
socket.send(requestPacket);
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.printf("req:%s; resp:%s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}
带有 “翻译功能” 的服务端
package net;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
public class UdpDictServer extends UdpEchoServer {
private HashMap<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("pig", "小猪");
dict.put("fuck", "卧槽");
}
@Override
public String process(String req) {
return dict.getOrDefault(req, "没有找到翻译");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
2.2 TCP
客户端
package net;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIP, int serverPort) throws IOException {
this.socket = new Socket(serverIP, serverPort);
}
public void start() {
System.out.println("启动客户端");
try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(System.in);
Scanner respScanner = new Scanner(inputStream);
while (true) {
System.out.print("-> ");
String request = scanner.nextLine();
PrintWriter writer = new PrintWriter(outputStream);
writer.println(request);
writer.flush();
String response = respScanner.nextLine();
System.out.printf("req:%s, resp:%s\n", request, response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
服务端
package net;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
this.serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket){
System.out.printf("[%s:%d] 客户端建立连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
while (true) {
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端退出链接\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
String request = scanner.nextLine();
String response = process(request);
PrintWriter writer = new PrintWriter(outputStream);
writer.println(response);
writer.flush();
System.out.printf("[%s:%d]req:%s, resp:%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
翻译功能的客户端
package net;
import java.io.IOException;
import java.util.HashMap;
public class TcpDictEchoServer extends TcpEchoServer {
private HashMap<String, String> dict = new HashMap<>();
public TcpDictEchoServer(int port) throws IOException {
super(port);
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("pig", "小猪");
dict.put("fuck", "卧槽");
}
@Override
public String process(String request) {
return dict.getOrDefault(request, "翻译失败");
}
public static void main(String[] args) throws IOException {
TcpDictEchoServer server = new TcpDictEchoServer(9090);
server.start();
}
}
利用多线程解决普通客户端一次只能链接一个用户的BUG
package net;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpThreadEchoServer {
private ServerSocket serverSocket = null;
public TcpThreadEchoServer(int port) throws IOException {
this.serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(() -> {
processConnection(clientSocket);
});
t.start();
}
}
private void processConnection(Socket clientSocket){
System.out.printf("[%s:%d] 客户端建立连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
while (true) {
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端退出链接\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
String request = scanner.nextLine();
String response = process(request);
PrintWriter writer = new PrintWriter(outputStream);
writer.println(response);
writer.flush();
System.out.printf("[%s:%d]req:%s, resp:%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoServer server = new TcpThreadEchoServer(9090);
server.start();
}
}
利用线程池进行优化
package net;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpThreadPoolEchoServer {
private ServerSocket serverSocket = null;
public TcpThreadPoolEchoServer(int port) throws IOException {
this.serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService pool = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
pool.submit(() -> {
processConnection(clientSocket);
});
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端建立连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
while (true) {
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端退出链接\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
String request = scanner.nextLine();
String response = process(request);
PrintWriter writer = new PrintWriter(outputStream);
writer.println(response);
writer.flush();
System.out.printf("[%s:%d]req:%s, resp:%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadPoolEchoServer server = new TcpThreadPoolEchoServer(9090);
server.start();
}
}
3. 理论知识「八股文」
3.1 应用层
3.1.2 DNS
DNS其实就是一个域名解析作用「比如https:www.baidu.com对应的IP地址是180.101.49.41」
这样的点分十进制IP大家很难记住于是就起了个朗朗上口的 www.baidu.com 方便人们记忆,www.baidu.com就是一个域名对应的IP地址是180.101.49.41。把 IP 解析成对应域名 www.baidu.com 就是域名解析
DNS底层使用UDP解析
浏览器会缓存DNS结果
DNS是应用层协议
3.1.3 浏览器中输入url后按下回车发生了什么
3.1.4 NAT 技术
我们也知道目前IPV4数量已经不够用,虽然IPV6在我国正在大力推崇中,但是目前的主力军依旧是IPV4+NAT
3.1.4.2 NAT IP转换过程
NAT IP转换过程
就是子网内部的设备访问外网的时候,路由器会把子网内部的请求IP全局替换为路由器内部一个 全球IP地址 作为出口去访问外网IP「图中:10.0.0.10-----『202.244.174.37』----->163.221.120.9」
外网IP响应给路由器全局IP数据时候,路由器内有一个 路由表 就会把对应的数据响应给子网内部对应的私有IP「图中:163.221.120.9----->『202.244.174.37』----->10.0.0.10」
当首次访问外网IP「163.221.120.9」的时候就会自动生成这样的一个映射关系,以便于后续直接使用。当结束连接的时候机会自动删除
3.1.4.3 NAPT
问题来了:子网中多个主机「PCA/B/C」访问同一个外网IP「163.22.120.9」,服务器响应数据却发现目的IP都是相同的「202.244.173.37」,该如何区分子网内部的设备呢?
IP+Port
这种映射关系也是TCP初次建立连接的时候生成的,由NAT路由器自动维护,当断开连接的时候就会自动删除
3.1.4.4 NAT 技术缺陷
- 外界无法访问内部「162.221.120.9无法访问子网内的10.0.0.10/11/12这些设备,也就是为何你的电脑无法访问我电脑上的127.0.0.1的原因」
- 路由表的维护,创建,销毁也是有一定的开销
- 通信过程中一旦有NAT设备异常,内网的设备连接都会断开
3.1.4.4 NAT 和 代理服务器
**NAT:**路由器大多都具备NAT功能,完成子网内部设备和外界的沟通
**代理服务器:**看起来和NAT挺像,客户端像代理服务器发送一个请求,代理服务器把请求发送给真正要访问的服务器;服务器返回结果给代理服务器,代理服务器在返回给客户端
NAT和代理服务器区别
| NAT | 代理服务器 |
---|
应用 | 解决的IP不足 | 翻墙: 广域网中的代理;负载均衡: 局域网中的代理 | 底层实现 | 工作在网络层,实现IP地址的更换 | 工作在应用层 | 适用范围 | 局域网的出口部署 | 局域网,广域网都可以使用甚至跨网 | 部署 | 防火墙,路由器等硬件设备上 | 部署在服务器上的一个软件程序 | | | |
代理服务器又分为正向代理和反向代理
胖虎在宿舍不想去超市买辣条,于是乎…在 《小葵花大学666栋666小卖部》的QQ群里艾特李华帮忙买包辣条然后给他小费
李华完成了胖虎的任务并获得了100元跑腿费「此时李华就是胖虎的正像代理」
后来胖虎一直让李华带零食,李华也开始偷了懒,抄起了Python,数据分析一顿操作猛如虎之后发现胖虎最爱大卫龙,可口可乐和乐事薯片。于是李华给了超市老板1元钱,从他那儿获取批发商联系方式,然后进了很多零食包括胖虎的最爱,自己在小葵花大学666栋当起了小老板,开启了贩卖零食的大学生活「此时李华就成了反向代理」
正向代理:敲一下,反馈一下这样的请求。。。
翻向代理:相当于正向代理的缓存
3.2 传输层
3.2.1 UDP
3.2.1.1 UDP首部格式「使用注意」
64k对于当下是否满足呢?
2字节=16位=216=65535byte=65535/1024=64k
非常小,如果传输的数据很大就需要其他方法。
- 应用层中手动对应应用数据进行拆分,接受放在重新组装「代码写起来容易出现问题」
- 是否可以扩展UDP:比如,把报头改成使用4个字节「42亿9千万」来表示长度「改不了,改就需要改-系统内核」
- 千:thousand–>k
- 百万:million–>M
- 十亿:billion–>G
- 直接使用TCP
UDP首部有一个16位的最大数据长度,也就是说一次UDP传输最多有64K「包含首部」,传输数据超过64K,则我们需要在应用层进行手动分包,多次发送并在接收端手动拼装。
五层模型中:程序员最关注的是应用层
下四层已经被操作系统/硬件/驱动实现好了,只要理解大概工作过程即可
对于应用层来说,不知要理解工作过程,更要能设计出一些 协议「设计应用协议就是Servlet约定前后端交互的接口」
3.2.1.2 UDP 校验和
用来验证数据是否正确的一种手段「不能保证数据100%正确,但是校验和如果不正确则数据100%不正确」
背景:网络传输过成中,可能会涉及到一定的干扰,就可能会破坏原有要传输的信息。光信号/电信号可能会受到一些 电磁场/高能粒子 的影响,可能会影响到地球上的通信「bit 翻转」。
方法:crc,sha1,md5…
发送方和接收方利用同样的算法计算校验和
发送方式sum1,接受方是sum2.如果中途出现数据变动,则校验和大概率不同
3.2.1.3 UDP特点
- 无链接:知道对方的 IP,port,但不需要建立连接就可以实现传输数据
- 不可靠:没有确认应答机制、重传机制,如果出现网络状况,UDP的传输层也不会给应用层任何错误信息
- 面向数据报:不能灵活控制数据报读写数据的大小和次数
3.2.1.4 面向数据报
应用层发送给UDP多长的数据,UDP原样不变、发送给网络层多长数据。既不拆分也不合并。
用UDP发送 1000 字节数据
发送方调用一次内核的 send,发送 1000 字节,接收方的内核就 receive 接受 1000 字节。而不会分成 100 次,每次发送 10 字节
3.2.1.5 UDP缓冲区
- UDP发送方没有真正的缓冲区:调用 send,内核会把数据交给 网络层协议,进行后续传输。
- UDP接收方有真正的缓冲区:但是这个缓冲区不能保证收到的数据报的发送和接收的顺序一致,可能会出现错乱。如果缓冲区满了,则会丢掉后续的UDP数据报。
3.2.1.6 基于UDP层的协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- BOOTP:启动协议「无盘启动」
- DHCP:动态网络IP分配协议
- DNS:域名解析协议
3.2.2 TCP
3.2.2.1 TCP首部格式
这 6 位用 0/1 表示
3.2.2.2 确认应答
每次客户端发送数据给服务器都会SYN请求服务器建立连接,服务器收到响应都会ACK给客户端确认应答
3.2.2.3 超时重传
发送数据丢失的两种情况:
- 客户端SYN丢失
- 服务端ACK丢失
处理丢包问题:
就按照最坏情况下作为发送方 “我” 数据丢失,如果指定时间后还没有收到回信,我就再发一次
对于服务端发送给客户端的数据丢了,服务端可以重发一次而不会对客户端有影响;但是客户端发送给服务端的数据丢失了,会进行大量的重发,服务端如何处理重复消息呢?
处理服务端数据重复
TCP会自动对消息进去重
发过去的数据会先放在接收方的消息缓冲区里「内核中的一个数据结构,可以视为阻塞队列」。
任何一段消息都带有ACK确认序号,如果新来的消息序号和阻塞队列中的序号重复,TCP直接去重「多个消息只保留一份」。所以应用程序从接受缓冲区取数据的时候,肯定不是一个重复的数据。调用 socket api 得到的数据一定不重复。
有了以上的 确认应答,超时重传应该可以保证TCP的数据万无一失了吧?但最糟糕的问题来了:如果对于客户端和服务端任何一方而言,重传数据也丢失了该怎么办呢?
处理重传数据丢失问题
-
重传不会无休止的进行,尝试一定次数后就会放弃「如果重传的数据也丢失了就认为能够恢复链接的概率很低,重传次数再多也是浪费资源」 -
重传时间间隔也不相同,每次重传时间间隔都会变长
假设丢包概率是10%,则两次数据包都丢失的概率就是10% * 10% =1%
我们有了尝试一定次数和时间间隔来解决丢包难题,次数我们很容易规定,可以假设超过16次就认为传输失败,可以关闭连接。但是这个 重传时间间隔 该如何确定呢?
确定重传时间间隔
最理想的情况下是能够早找一个 最短回复时间,在这个时间内,数据的响应一定能返回
但是这个时间的长短是由网络环境决定的,各有差异
如果设置的超时时间太长,则会影响整个过程的传输效率
如果设置的超市时间太短,则会频繁的发送数据包,造成资源浪费
因此,TCP为了保证在任何环境下都能保持较高性能的通信效率,因此会动态计算这个 超市时间
Linux「Unix,Windows」也都是超时以 500ms 为一个单位进行超时控制,每次判定超时重传的时间间隔是 500ms的整数倍
第一次:500ms,第二次:2*500ms,第三次:3*500ms…
如果累积到一定次数之后就会认为当前网络环境已经无法恢复,就会强制关闭连接
3.2.2.4 连接管理「面试问的最多」
正常情况下TCP要经历三次握手建立连接,四次挥手断开连接
三次握手
-
三次握手的原始连接 -
三次握手后的连接优化
因为对于服务端发给客户端的 ACK+SYN 可以合并在一起发送。
public TcpEchoClient(String serverIP, int serverPort) throws IOException {
this.socket = new Socket(serverIP, serverPort);
}
还记得这段代码吗?TCP的客户端什么时候建立连接呢?是在实例化 socket 对象的时候,自动连接。ACK和SYN操作都是操作系统同一时机内核完成的。因此对于服务端而言:可以把ACK的确认和SYN的请求建立通过一次网络请求执行完毕而不是通过两次网络请求「这样做有利于节约网络带宽」
分两条发送后,分别进行封装和分用,实际上这两条数据正好可以合并一起就没必要分开了
四次挥手
对于建立连接来说,中间的两次ACK+SYN可以合二为一,断开连接也可以合二为一吗?
抓蛇先抓七寸:TCP什么时候断开连接呢?「也就是说什么时候触发FIN呢?」
- 手动调用 scoket.close()
- 退出进程
当客户端触发 FIN 之后,服务器只要收到 FIN 就会立马返回 ACK「内核完成的」
当服务器的代码中运行到 socket.close() 操作的时候,就会触发 FIN
这两个操作在不同的时机,中间有一定的间隔。
两个重要的状态
CLOSE_WAIT
TIME_WAIT
这四次挥手过程中的任意一个包也是会丢的。
第一组 FIN 或者 ACK 丢了。此时 A 都没有收到 B 的 ACK,A 就会重传 FIN
第二组 FIN 或者 ACK 丢了。此时 B 都没有收到 A 的ACK,B 就会重传 FIN
如果 A 收到了 FIN 之后,立即发送 ACK,并且释放链接「变成CLOSE状态」,此时就会出现无法处理重传 FIN 的 ACK 情况,此时就僵硬了。
等一段时间之后,确保当前 FIN 不被重传了,然后才真的释放链接。
所以当 A「客户端这边」发送完 FIN 之后,不要立马释放,先等一等。等一段时间之后,确保当前 FIN 不被重传才会真正释放链接
TIME_WAIT等待的时间叫做 2MSL「MSL就是网络上两点之间传输消耗的最大时间」
3.2.2.5 滑动窗口
TCP最原始的发送数据:发一个,确认一个这样的机制。等到ACK之后才能发送下一个数据,这样的话后续的数据大量会阻塞等待。
滑动窗口就是为了解决这个问题,减少等待ACK时间「其实就是将多段等待时间重叠在一起了」
假设一次发送长度为N的数据,然后等待一波ACK,此时这里的N就称为“窗口大小”
N越大,传输的速度越高,但是N也不能无限大,如果N无限大,此时确认应答就没有意义了,可靠性就形同虚设了
- 上图的窗口大小就是 3000字节「3个字段」
- 发送前 3 个字段的时候无需等待,直接发送
- 发送第四个字段的时候等待需要阻塞等待第一个ACK「下一个是1001」才能继续发送,依此类推
- 操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前数据还有哪些没有应答;只有确认应答过的数据才能从缓冲区删除掉
遇到丢包问题怎么办?
丢包问题分为两种,一种是确认应答ACk丢了,一种是数据丢了。我们需要分开讨论分析。
ACK丢了
这种情况下,丢ACK并不要紧,可以通过后续ACK来确认
数据丢了
当某一个数据包丢失的时候,发送端会一直发送此数据端的ACK,比如ACK1001。
如果客户端主机收到了 3次 同样的ACK,就认定次数据包已经丢失了,会根据ACK的提示发送对应的数据段。
当服务端主机收到了所需要的ACK的时候,则会返回客户端最后一次 没有丢包发送过来的数据的ACK「此处就是ACK6001」
因为2000-6000的数据已经被收到了,就被放到了操作系统内核的 接受缓冲区 了。
这样就构成了 滑动窗口下的快重传
3.2.2.6 流量控制
也是在保证可靠性,对滑动窗口进行了制约。滑动窗口越大,就认为传输速率越高。但也并不是越大越好,接收方顶不住消息之后,额外发出的数据大概率是要丢包的,就会触发超时重传机制。所以一定是最合适的才是最好的。发送方和接收方速率理论上匹配最好。
主要是根据接收方处理数据的能力,来制约滑动窗口大小。发送方的话动窗口大小是变化的「不是固定的」
接收方处理数据的速率主要取决于应用程序,调用 socket api 读取数据的速率
如何衡量接收方的处理数据的速度
主要就是看接收方的应用程序调用 socket api 的读操作「read() 快不快」
刨根问底就是判断:通过接受缓冲区中剩余空间的大小
假设接受缓冲区一共是 4k。 当前使用了3k,还剩1k。此时接收方就会返回 ACK 的时候告知发送方说:我这个接受缓冲区还有 1k 空间;接下来会发现发送的时候就可以按照 1k 这样的窗口来发送数据…
在考虑一个极端情况:如果发送接收方缓冲区满了,发送方就不再发送数据
这时候接收方会在窗口满的时候发送一个 ACK 告知发送方窗口满了,然后发送方停止发送数据。由于发送方停止发送数据导致的接收方不会对发送方有任何响应。
所以这个时候发送方会 定期 发送一个 探测报文,接收方收到这个 探测报文段 之后就会把自己当前窗口大小响应给发送方「这个接收方有种需要发送方敲打的味道」
这个窗口只能存放65535个字节吗?
TCP首部40字节选项中还包含了一个扩大因子 M,实际窗口大小是 左移M位
3.2.2.7 拥塞控制
和流量控制差不多,都是用来限制发送方传输速率的机制。防止发的太快处理不了。
- 流量控制是根据接收方的处理速率来进行衡量的
- 拥塞控制是根据发送方到接收方这一些列通信链路的处理速率来衡量的。
虽然两台电脑处理和发送都很快,但是如果中间某个节点「路由器等网络设备」出问题,不能快速的转发
相比于流量控制,拥塞控制是更复杂的
流量控制:只考虑接收方和发送方
拥塞控制:考虑的是整个链路上有多少个设备,这些设备路径都是什么情况会很复杂「由于这个中间路径非常复杂,拥塞控制解决方案是把中间整个链路视为一个整体,通过 不断试错 的方式来找到一个合适的发送窗口大小。不断的尝试不同的窗口大小,在保证可靠性的前提下提高发送速率」
拥塞控制如何控制拥塞窗口的?
拥塞控制会设置出一个 拥塞窗口 这样的指标,通过拥塞窗口来影响滑动窗口的窗口大小
拥塞控制也是动态变化的,刚开始用一个比较小的值「让发送方发的慢点」如果通信非常顺利,也没有丢包就会逐渐放大窗口,加快发送速度的同时密切监视丢包情况,如果嫁到一定程度了,发生了丢包,说明当前接收方顶不住了;就立即减小窗口大小,让速度再慢下来,如果不丢包,再逐渐加速。反复重复以上步骤就会逐渐稳定在一个合适的速率
拥塞控制和流量控制都能影响滑动窗口,到底谁起决定作用
谁小谁说了算
拥塞窗口的变化规律
- 慢启动「慢开始」:刚开始的时候先慢点传输
- 指数增长:指数函数又称为爆炸函数,可以短时间内把窗口大小给加到最大值
- 线性规律增长:当指数增长到一定程度「超过设定的阈值24」,就会变为线性增长以此进行拥塞避免
- 当遇到了网络阻塞,导致丢包。立即就让窗口大小回到一个最初的很小值,同时修改下一次的阈值为刚才的阈值一半「24/2=12」
- 然后再重复刚才的步骤…
3.2.2.8 延迟应答
提升传输效率,考虑是否能子啊保证可靠性的前提下继续把滑动窗口调大一点「流量控制的延伸」
流程简述
- 假设接收方窗口大小为3M「3000字节」,如果接受了某次收到了2.5M数据就,如果立即返回,发送给发送方的窗口的大小为0.5M
- 但实际情况是可能处理数据速度很快,不到50ms就把2.5M数据处理掉了,这种情况下接收端还远远没有达到自己的极限,因此可以把窗口调大一些
- 接收端等待一会儿再应答,比如过200ms再返回。就会返回一个3M的窗口大小。
记住:窗口越大,网络传输速率就越大,但是一定要在保证可靠性的前提下才可以调大窗口
那么所有的包都可以延迟应答吗?
当然不是,有数量和时间限制
数量:每隔 N 个包就应答一次
时间:超过最大延迟时间就应答一次
具体的数量和时间:不同操作系统都是不一样的。一般 数量N取2,最大延迟时间取200ms
3.2.2.9 捎带应答
在延迟应答的基础之上做了延伸
最典型的就是:一问一答
客户端发送一个请求,服务器就会响应一个客户端的请求
- B收到一个TCP请求,就会立即返回一个ACK「内核」
- 应用程序根据请求,计算响应,把响应构造好之后,返回给浏览器
这俩操作是不同的时机,既然是不同实际也就不应该合并成一个数据报
但是TCP中的捎带应答机制导致B对A的回复并不是立即的,而是等待一段时间之后在发送。在等待的这段时间内,就导致了发送的时间可能就和应用程序返回A响应的时间就是同一时机了,也就可以合并了
把两个个TCP数据报合并成一个数据报,节约资源与时间,减少封装和分用
TCP的四次挥手有没有可能变为三次挥手?
B发给A的ACK是在内核中完成的、FIN是应用程序代码调用 close() 完成的,这俩操作看似是不同时机,但是如果有了捎带应答机制结果就不一样了。如果恰好触发了捎带应答,则会是 ACK+FIN 合二为一发送过去,此时的话就会是三次挥手
程序不一定100%触发捎带应答,如果设定延迟应答时间为200ms,如果200ms内恰好出发了捎带应答,则会执行到 close
3.2.2.10 面向字节流
创建一个 socket 的同时内核就会创建一个 接收/发送 缓冲区
发送数据
- 调用 write 写的时候,数据先会被发送到 发送缓冲区
- 如果发送的数据过长,就被拆分成很多段小的数据包;如果发送的数据过短,就会等待发送缓冲区数据长度差不多了一并发送
接收数据
- 接收数据的时候,网卡驱动程序先从内核中接受缓冲区读取数据
- 然后调用 read 拿 接收缓冲区 的数据
由于有缓冲区的存在,TCP程序的读和写不是一一对应
- 写100字节:可以一次 write(new byte[1000]). 也可以循环 1000次 write(new byte[1])
- 读100字节:可以一次 read(new byte[1000]). 也可以循环 1000次 read(new byte[1])
3.2.2.11 沾包问题
多个TCP数据报到达的时候,如果不显示的约定应用层数据的包和包之间边界,就很容易对数据产生混淆
这种情况就是 沾包问题,多个数据包混在一起
沾包问题并不是TCP独有的问题,任何的 面向字节流 传输机制都会涉及到这个沾包问题「读写普通文件也是面向字节流的」
解决方案:给每个数据包结尾片接一个特殊符号表示结束
一个简单协议就是用 ; 来分隔
在HTTP「应用层协议」中如何解决沾包问题呢?
不带body带head的GET请求
不带head带body的POST请求
在浏览器检查中,**Request Headers中的每一栏会以换行来区分,但在请求中以 \n来结束 **
GET /index.html/HTTP/1.1\nHOST127.0.0.1:8080\nUser-agent:xxx\nReferer:HTTP://www.baidu.com\n\n
如果接收方的接受缓冲区里有多条 HTTP GET 请求,就可以根据这个空行来区分多个HTTP请求了
HTTP没有body的时候以 空行结尾
POST /index.html/HTTP/1.1\nHOST127.0.0.1:8080\nContent-Type:text/html\nContent-Length:3277\n\…
如果接受方的接受缓冲区有很多条 HTTP POST 请求,还是先找到空行
在空行之前能够找到 Content-Length:3277,再从 Content-Length:3277 往后找 3277 个这么长的数据也就到达了边界
3.2.2.12 TCP异常情况
建立好通信的双方,在通信过程中突然有一方遇到了突发状况。
1.进程终止
A,B其中某个进程突然终止「崩溃或者被强制关闭」
如果直接关闭进程,看起来是猝不及防,但实际上操作系统早有准备「也就是每次打开任务管理器的时候,CPU占用资源瞬间高涨的一部分原因」
杀死某个进程,操作系统回释放这个进程的相关资源「TCP这里依赖 socket 文件,操作系统就会自动关闭这个 socket 文件。这个自动关闭的过程就相当于 socket.close()『触发了四次挥手』」
2.机器重启
按照操作系统既定的流程重启
就会由操作系统先把当前所有的应用程序,强制杀死「杀死进程就和上面的进程终止一样了,释放 socket 文件,发送 FIN」
『单纯的四次挥手』
3.断电/断网
这个情况才算 偷袭成功
接收方掉电
此时A不会收到B发送的ACK,接下来就会触发超时重传,重传一定次数之后认为连接不可恢复『尝试重新建立连接』,最终只能放弃链接『A就会释放自己所有保存连接的信息』
『就会放弃四次挥手断开连接』
发送方掉电
A发完第一条消息之后,B响应对应的ACK,但是A没有发送断开连接的请求导致B就会一直在等待A的请求
解决方案就是:TCP连接双方会周期性的给对方发送一个不包含业务数据的 探测报文,这个探测报文不传递实际的数据,只是用来检查对方是否正常工作。
3.2.2.13 TCP小结
优先保证可靠性,再进一步提高效率
**可靠性:**确认应答,超时重传,连接管理,流量控制,拥塞控制,TCP异常情况
**效率:**滑动窗口,延迟应答,捎带应答
**编码注意事项:**沾包问题
3.2.2.14 基于TCP层的协议
HTTP,HTTPS,SSH,FTP,SMTP,Telnet
3.2.3 TCP与UDP对比
TCP优势:可靠性
**UDP优势:**效率更高
经典面试题:如何用UDP保证可靠传输
「抄TCP的作业」
- 引入序列号,保证数据的顺序
- 引入确认应答,保证收到数据
- 引入超时重传,保证收到数据
- …
3.3 网络层
3.3.1 IP协议
网络层里面最核心的协议叫做 IP协议,分为两个版本:IPV4 和 IPV6
-
4位版本号:对于IPV4来说就是4 -
4位首部长度:类似于TCP。IP协议包头也是变长的,单位是4字节。4bit最大是15,所以IP头部最大长度就是4*15=60字节。 -
服务类型:3位优先权已被弃用,1位保留字必须为0,4位TOS字段分别代表:最小延时,最大吞吐量,最高可靠性,最小成本「对于SSH/Telnet这样的程序最小延迟比较用重要;对于FTP,最大吞吐量比较重要」。这4个特性互斥的,用的时候对应的比特位设置为1,其余必须为0 -
16位总长度:IP数据报整体占多少个字节
- 16位–>64K,难道说一个 IP 数据包最大只能表示 64K 吗?「是,又不完全是」
- 因为在IP里,协议内部实现了数据报的拆分「当超过64K,IP协议就会自动的对这个大的包进行拆分,拆成多个小的包,保证每个小的包不会超过64K」
-
16位标识,3位标志,13位片偏移都是为了拆分
- 16位唯一的标识主机发送的报文。如果IP报文在数据链路层被分片了,那么每一个片里面的这个id都都是相同的
- 3位标志:第1位保留;第二位:值为1则是禁止分片;值为0则是允许分片;第3位:只有最后一个分片值为1、其余均为0、用来设置结束标志
- 13位片偏移:是分片相对于原始IP报文开始处的偏移. 其实就是在表 示当前分片在原报文中处在哪个位置。实际偏移的字节数是这个值 * 8 得到的。因此,除了最后一个报文之外, 其他报文的长度必须是8的整数倍(否则报文就不连续)
-
8位生存时间:数据报到达目的地的最大跳数。一般是64,每经历一次转发TTL就会-1,直到0了还没有收到,那么就丢弃。主要为了防止循环路由出现 -
8位协议:表示传输层使用的哪个协议,TCP/UDP 会有不同的值,为了在分用的时候能够让网络层把数据提交给正确的传输层协议来处理 -
16位首部校验和:使用CRC来校验头部是否损坏 -
32位源地址:发送端IP地址 -
32位目的地址:接收段IP地址
UDP首部长度固定为8字节,IP首部长度固定为20字节
16位表示,3位标志,13位片偏移如何拆分64K的
生存时间
3.3.4 网段划分
IP地址分为两部分,网络号和主机号
- 网络号:保证两个相连接的网段具有不同的身份标识
- 主机号:同一网段内,主机具有相同的网络号,但是必须有不同的主机号
- 不同的子网其实就是把网络号相同的主机连接在一起
- 如果在子网中新增一台主机,则这台主机的网络号和子网中网络号相同,但是主机号不能和子网中其它主机号相同
通过合理的设置网络号和主机号,就可以保证网络中的主机IP地址不会重复
本机网络详情
子网掩码:和IP地址一样,也是一个32位整数,由网络号+主机号组成。通过 点分十进制 的形式划分为 4部分,每部分1个字节长度 来表达
子网掩码网络号:用二进制1来表示,1的数目代表网络号的长度
子网掩码主机号:用二进制0来表示,0的数目代表住几号的长度
网络号:子网掩码和IP地址进行按位与运算
假设有一个IP地址:191.100.0.0,子网掩码为:255.255.128.0来划分子网
B类子网掩码本来是255.255.0.0,所以此子网掩码网络号向主机号借了一位即17位,因此可以划分21个子网,但实际使用0个「去掉全0全1」,这个网段可以容纳215个主机
计算方式
网络号:IP地址与子网掩码按位与计算
主机号:IP地址与取反后的子网掩码按位与计算
| 十进制 | 二进制 |
---|
IP地址 | 180.210.242.131 | 10110100.11010010.11110010.10000011 | 子网掩码 | 255.255.248.0 | 11111111.11111111.11111000.00000000 | 网络号 | 180.210.240.0 | 10110100.11010010.11110000.00000000 | 主机号 | 0.0.2.131 | 00000000.00000000.00000010.10000011 |
几个特殊的IP地址
- 如果主机号为0:网络号
- 如果主机号为1:通常表示的是这个局域网的 网关「局域网的出入口,通常也就是路由器的LAN口IP」
- 如果主机号全1:广播这个IP,往这个IP上发送数据,局域网中的所有设备都能收到
子网内部的一些设备
那么问题来了,手动管理子网内的IP地址是非常麻烦的
3.3.5 IP地址的数量限制
IPV4协议,是使用4个字节来表示IP地址,表示的地址个数只能是42亿9千万
如何应对IP不够用
- 动态分配IP地址,一个设备连了网就分配IP;不联网就不分配IP
- NAT机制:把IP分成内网和外网IP两种。要求外网IP必须是唯一的,内网IP在不同局域网中可以重复。如果局域网内部的设备想上网,在数据报经过带有外网IP的路由器的时候就会自动的使用这个路由器的外网IP来代表这个局域网的设备「本质上是一个一大堆局域网里的设备,共同使用一个外网IP」
NAT机制图
设备1和设备2如果出现了端口重复该怎么办?
也是通过路由器的端口替换「NAPT」。
路由器检测到同一个子网内有两台相同端口的设备,会自动进行端口映射「在路由器内部维护这样的关系」,同一个子网内IP不会重复所以可以区分对应的主机。
NAT机制缺陷
虽然一定程度上解决了IP地址不够用的问题,但也引来了一些重要的缺陷。
子网内的设备 可以 访问一个外网IP的设备
子网内的设备 不可以 访问另外一个子网内的设备
由此诞生了 云服务器,作为第三方可以让大家共同访问「买服务器的本质是购买了一个外网IP」,如果初学,对服务器搭建Tomcat+MySQL不熟悉的可以查看我的 博客链接
真正解决IP地址不够用的技术:IPV6
拥有16字节来表示IP地址「2^128」
号称地球上的每一粒沙子都可以分配一个IP地址
为何当下还是IPV4+NAT呢?
主要是当下支持IPV4的设备「主要是路由器」大概率不兼容IPV6,要升级到IPV6,势必要把大量的设备换成IPV6,成本比较高
国家也在大力推进IPV6的网络建设「主要是针对国家安全和利益考虑」
3.3.6 私有IP地址和公网IP地址
特殊的IP
3.3.7 路由
路由选择也就是规划一条通信传输的路径
客户端在和服务器搭建连接的过程中是一个很复杂的过程,中间会有很多台设备中转才连接到的
在这些线路中找到一个最合适的路线就是路由选择要做的工作
简约的查找流程
- 如果路由器直接认识目的IP,就可以直接转发到目的IP进而建立连接
- 如果路由器不认识目的IP,路由器就会把这个数据沿着一条默认的路径继续转发给下一个路由器
重复上述步骤,就能够找到一个合适的路由认识目的IP「通过6个人可以认识全世界的故事原理」
路由器如何认识目的IP的呢?这就用到了所谓的路由表的概念了。
路由表
这个是路由器内部维护的一个类似于 “通讯录电话本” 的功能,是以 key:vale 形式存储的
key:IP地址网络号
value:网络接口「从路由器的WAN口出还是LAN口出」
路由表又一个默认的电话本,称为 “下一跳”
路由表的实现也很复杂,一方面可以手动生成一方面可以动态设定
3.4 数据链路层
主要负责相邻的两个节点之间的通信
3.4.1 认识以太网
3.4.1.1 以太网帧格式
以太网:并非是一个真正的网络,而是一种技术规范「既包含了数据链路层也涵盖物理层:网络的拓扑结构,访问控制方式,传输速率…」
以太网中的网线必须使用双绞线「相同设备使用交叉线;不同设备使用直通线」
FCS:数据校验的方式「著名的是CRC冗余验证」
46-1500:所能承载的数据范围「单位是字节」
最多1500:首先与当前网络硬件的结构,不同数据链路层协议搭配不同的物理设备对因承载数据的能力也不同
3.4.1.2 认识MAC地址
MAC地址是6字节,表示范围较于4字节的IPV4多了6w倍「所以才可以财大气粗的给每个硬件在出厂的时候写死一个MAC地址」
MAC地址起到的的主要要作用就是在相邻的两个基点至简传输
3.4.2 对比理解MAC地址和IP地址
当数据包到达局域网之后,根据IP+Port可以直接放送给目标主机的目标应用程序。好奇的朋友可能会问:为什么有了IP地址还要发明一个MAC地址呢?
举个例子「重复了又好像没重复…」:
高考发送录取通知书的时候,邮政小哥会提前打个电话通知李华,因为邮件地址填写的是光明小区,收件人是李华。李华满怀激动地下楼后等待快递小哥的到来,快递小哥为了验证身份会问到 “李华童鞋,你的准考证号是多少?”,因为李华这个名字全国都会重复,所以报了不会重复的准考证号,于是李华报了自己的证件号 “123”,于是快递小哥又说道 “好,准考证123号童鞋来拿你的录取通知书”。于是李华拿了小葵花大学的录取通知书离开了。
整个过程感觉重复了但又没有没重复的感觉…
来下面的解析:
其实是历史遗留问题,理论上讲:IP+Port就可以连接两台设备。
首先从实际出发,IP解决的事互联网上所有设备的联网问题。假设换句话说IP地址用MAC地址替代
MAV地址248 「2.81474976710656E14:281万亿字节,换算为存储就是262144G也就是256T的存储才能装完所有MAC地址」,这显然是不科学的。这也就是为何IP地址替代MAC的原因而MAC地址不能替代IP地址的原因
随着互联网的发展,路由也变得越来越复杂和困难了,于是聪明的人类发明了子网,把互联网分成很多个子网。在路由的时候,路由器就可以把其它子网看成一个整体。对于目的地还在其它其它其它的子网时候,路由器只负责把数据报发送到子网内部即可。然后在其子网内部完成剩余的数据报发送工作。这样做可以做到路径选择上接近最优而不是最优解。不过还是利大于弊,所以被采用了。
和MAC地址不同的是,IP地址和地域相关「类似于邮政编号,根据不同地区划分不同的邮政编码」。对于同一个子网内部的设备IP,它们的前缀都是相同的。现在路由器只记录每个子网的位置,就知道设备在哪个子网上了,这样大大的节约了路由器的存储空间
既然IP不能缺掉,那么这个MAC地址又显得多余,能不能去掉呢?
答案肯定是不可以
因为IP地址必须是设备上线后才能获得一个路由器根据设备对应的子网来动态分配一个私有IP地址,在离线的时候我们还需要通过MAC地址来管理设备的
总之IP地址相当于一个大范围的身份表示「光明小区李华」,而MAC地址就相当于一个属于自己的ID「准考证号」。两者缺一不可
对比发现:
- IP地址主要是用来表示转发过程中的起点和终点
- MAC地址主要是用来表示任意一次转发过程中的起点和终点
3.4.3 认识MTU
MTU「最大传输单元:Maximum Transfer Unit」相当于对数据帧的限制,这个限制是数据链路层对下一层的物理层的管理也影响上一层网络层的协议。
- MTU范围 [46, 1500] 闭区间上
- 最大值1500被称为MTU,不同网络类型有不同的MTU
- 不同数据链路层MTU标准不同
- 如果一个数据包从以太网上到达了数据链路层,若数据包超过MTU,这个数据就被 分片处理
3.4.3.1 MTU对IP协议的影响
如果IP数据报超过了1500字节,就无法被封装到一个以太网数据帧中,这个时候就会触发IP的分包操作「IP的分包一般不是因为报头中的64限制了数据报整体的大小,大概率是因为数据链路层以太网帧的MTU限制来分的」
以下是MTU对IP报如何分片的
- 先把一个大的IP数据包拆分成多个小包,并打标签
- 由于IP格式中16位标识的存在,被分片的小包都会具有相同的标识身份
- IP格式中3位标志在对每个小包贴标签「第一位保留位,第二位设置为0、表示允许分片,第3位,如果是最后一个小包则设置为1,其余全是0」
- 接受方收到数据后再将这些小包按顺序组合在一起,完成拼装后在一起 发送给传输层
- 中途某个小包出现问题,接收方就会重组失败,网络层也没有传输层超时重传机制,所以不会重新传输数据
3.4.3.1 MTU对UDP协议的影响
以下是MTU对UDP的分片
- 一旦UDP携带的数据超过1472(1500-IP首部长度20-UDP首部长度8)就会在网络层分成多个数据包
- 这个IP数据包中有任意一个丢失,都会引起接受端网络重组失败「意味着:如果UDP被分片,网络出现状况的概率大大增加」
3.4.3.1 MTU对TCP协议的影响
TCP的数据报也不能无限大,主要还是受限于MTU。TCP单个数据报的最大长度是MSS「Maximum Segment Size」
好奇的童鞋有可能会问:为什么TCP没有数据长度限制呢?那UDP有吗?
在回顾一下TCP和UDP格式会发现,只有UDP又一个2字节数据长度「还记得计算的是不超过64K吗?」,而TCP协议格式中则没有对数据长度的限制
以下是MTU对TCP的分片
- MSS协商:TCP在建立连接的时候,通信双方会进行协商使用谁的MSS「理想MSS:最理想的情况下就是MSS刚好达到IP不被分片处理的最大长度」
- 告知对方MSS:双方在发送SYN的时候会在报头写入自己能支持的MSS值
- 选取较小MSS:取双方最小值的MSS
- 存储MSS值:MSS的值就在TCP40字节的变长选项中
3.4.4 ARP协议
这个了解即可,ARP协议其实并非是一个单纯的数据链路层协议,而是作用在数据链路层和网络层之间的协议
3.4.4.1 ARP协议的作用
用来简历IP地址和MAC地址之间的映射关系
- 在网络通信是,发送端知道接收端的 IP+端口,却不知道接收端的 硬件地址「MAC地址」
- 站在接收端的角度来看:数据包先是被网卡驱动程序接收再去处理上层「网络层,传输层这些」协议,如果发现数据包的硬件地址和本机地址不符,则会直接丢掉
- 因此在通信前,还需要或读接收端的MAC地址
3.4.4.2 ARP协议工作流程
先获取MAC地址
再来看一下使用ARP发送数据过程
3.5 总结
应用层
- 应用层的作用:满足我们日常使用的网络程序
- DNS解析
- NAT技术结合应用程序访问外界IP
传输层
- 传输层的作用:负责端到端的数据传输
- 由端口号区分应用程序
- UDP协议及特点
- TCP协议的可靠性「两个状态的转化CLOSE,TIME_WAIT」
- TCP安全性:确认应答,连接管理,超时重传,流量控制,拥塞控制,TCP异常机制
- TCP的效率:滑动窗口,捎带应答,延迟应答
- TCP面向字节流,沾包问题的解决方案
- 基于UDP抄TCP作业实现可靠传输
- MTU对IP,TCP,UDP影响
网络层
- 网络层的作用:负责端到端过程中每个点到点的数据传输
- IP地址,MAC地址
- IP协议格式
- 网段划分
- IP数量不足的解决办法
- IP数据包地址路由的选择过程「如何跨网段送达目的地」
- IP数据包分片原因
- NAT设备工作原理
数据链路层
- 数据链路层的作用:两个设备之间的数据传输
- 以太网的理解
- 以太网帧格式
- MAC地址
- ARP协议
- MTU初识
|