赞
踩
交换机: 「数据链路层➡️物理层」把多个主机构成一个网络
集线器: 「物理层➡️物理层」上古时期老设备,网线分叉,同一时刻只能有一根网线工作
路由器: 「网络层➡️物理层」解决集线器同一时刻只能有一根网线工作的弊端,可以使得所有设备都有网络「路由器不仅能组成一个局域网,同时也链接了两个局域网的功能,让局域网之间由交换数据的功能」
局域网: 局部组建的一种私有网络
广域网: 通过路由器将多个局域网连接起来,在物理范围上组建成很大范围的网络。广域网内部的局域网都属于其子网
局域网和广域网: 没有特定的概念,都是相对而言「公司的局域网一定比家庭的广域网范围更大」
网络通信: 网络主机中不同进程间网络传输数据
LAN(Local Area NetWork)口: 路由器连接下级网络设备「路由器,电脑,电视等」
WAN(Wide Area NetWork)口: 路由器连接上级路由器「光猫」
路由器和交换机有什么区别:
MAC地址: 6字节,识别数据链层中相连的的节点。在网卡出厂的时候就设置了的不能被修改。MAC地址是唯一的「虚拟机中的MAC地址并不是真正的MAC,也有些网卡支持用户配置MAC地址」
IP地址: 4字节,定位主机的网络地址网络层「就像我们发送快递一样,需要知道对方的收货地址才能把包裹送达」
种类 | 定义 | 范围 | 私有地址 | 保留地址 | 适用范围 | 网络数 | 主机数 |
---|---|---|---|---|---|---|---|
A | 第1字节为网络地址固定位为0,其它3个为主机地址 | 0.0.0.0-126.255.255.255 | 10.0.0.0-10.255.255.255 | 127.0.0.1-127.255.255.255『主要利用内部网络通信性能高,方便测试一些网络诚信通信使用 | 大型 | 2 7 − 2 = 126 2^7-2=126 27−2=126 | 2 24 − 2 = 1677 _ 7214 2^{24}-2=1677\_7214 224−2=1677_7214 |
B | 第1和第2字节为网络地址固定为10,其它2个位主机地址 | 128.0.0.0-191.255.255.255 | 172.16.0.0-172.31.255.255 | 当IP是自动获取但又没有DHCP服务器,就从『169.254.0.0-169.254.255.255』中临时获得一个IP地址 | 中型 | 2 14 − 2 = 1 _ 6382 2^{14}-2=1\_6382 214−2=1_6382 | 2 16 − 2 = 6 _ 5534 2^{16}-2=6\_5534 216−2=6_5534 |
C | 第1,第2,第3字节为网络地址位固定为110,剩下的一个是主机地址 | 192.0.0.0-223.255.255.255 | 192.168.0.0-192.168.255.255 | 192.168.0.0-192.168.255.255 | 小型 | 2 21 − 2 = 209 _ 7150 2^{21}-2=209\_7150 221−2=209_7150 | 2 8 − 2 = 126 2^{8}-2=126 28−2=126 |
IP地址解决了网络通信时定位网络主机的问题,但是数据传输到主机后由哪个进城来管理这些数据呢?这就需要用到 端口号
端口号: 标记主机中发收发数据的进程
有了IP地址,端口,就可以定位到网络中唯一的一个进程。但存在一个问题:网络通信是基于光电信号,高低电平转换为二进制数据01传输的,我们如何知道对方送的什么数据呢?「图片,视屏,文本,音频对应的数据格式,编码方式也都不同」此时就需要有一个 协议 来规定双方发送接收数据
认识协议: 网络协议是网络通信 经过的所有设备 都要遵从的一组约定,规则。如怎么连接连接,怎么互相识别。只有遵从这个规定,多台计算机之间才能互相通信交流
三要素组成:
主要用来说明通信双方应当怎么做。用于协调和差错处理
主要定义了何时通信,先讲什么,后讲什么,讲话速度。比如采用同步传输还是异步传输
知名协议的默认端口: 系统端口范围是「0,65535」,知名端口「0,1023」,这些端口都是预留给服务端程序来绑定广泛使用的应用层协议。比如:
知名端口也可以配置「1024,65535」范围内的端口来定义知名端口
五元组: 在网络通信中用 五元组 来标示一个网络通信
协议分层: 把一个大的协议逐个拆分出来形成一个小协议。类似于达到面向接口编程这样的效果:定义好两层之间的接口规范,让双方遵守这个规范来对接数据。便于日后的维护和更新
OSI七层模型
只存在于教科书中「越往下越接近硬件设备,越往上越接近应用程序」
每一层都呼叫它的下一层来完成需求
层 | 功能 |
---|---|
应用层 | 应用程序间沟通,如简单的电子邮件传输SMTP,文件传输FTP,网络远程访问Telnet「网络编程主要在应用层,拿到数据之后你要干啥…」 |
传输层 | 两台主机之间的数据传输。如TCP,UDP「端到端:消费者和商家只关注某个快递是不是收到了」 |
网络层 | 管理和路由选择。在IP中识别主机,并通过路由表的方式规划处两台主机之间数据传输路线「点到点:快递公司,怎样运输才高效」 |
数据链路层 | 设备之间数据帧的传送和识别「帧同步,冲突检测,差错校验」 |
物理层 | 光电信号传输方式 |
传输层的端到端:只关注起点/终点,不关注中间过程「买家与卖家」
网络层的点到点:传输过程中经历的节点,需要关注中间过程的「快递公司」
网络设备所在分层
这里说的是传统意义上的交换机和路由器,也称为二层交换机(工作在TCP/IP五层模型的下两层)、三层路由器(工作在TCP/IP五层模型的下三层)
随着现在网络设备技术的不断发展,也出现了很多3层或4层交换机,4层路由器。我们以下说的网络设 备都是传统意义上的交换机和路由器。
封装和分用
数据封装图
数据分用图
ICMP:(Internet 控制消息协议,Internet Control Message Protocol)用来给IP协议提供控制服务,允许路由器或目标主机给数据的发送方提供反馈信息。需要发送反馈信息的情况包括:数据包不能被发送到 目标主机,路由器缓冲区溢出导致数据包被删除,路由器想要把流量重定向到另外一个更短的路由上等。ICMP协议是IP协议的一部分,任何实现了IP协议的 设备同时也被要求实现ICMP协议
IGMP:(Internet工作组管理协议,Internet Group Management Protocol)用来解决网络上广播时占用带宽的问题。当网络上的信息要传输给所有工作站时,就发出广播(broadcast)信息(即IP地址主机标识位全为1),交换机会将广播信息不经过滤地发给所有工作站;但当这些信息只需传输给某一部分工作站时,通常采用组播(multicast,也称多点广播)的方式,这就要求交换机支持IGMP。支持IGMP的交换机会识别组播信息并将其转发至相应的组,从而使不需要这些信息的工作站的网络带宽不被浪费。 IGMP对于提高多媒体传输时的网络性能尤为重要。
封装和分用不仅仅存在于发送方和接收方,中间设备「路由器/交换机」也会针对数据进行封装和分用
通常情况下:
交换机:只是封装分用到数据链路层就结束
数据链路层就针对这里数据解析并重新打包
路由器:只是封装分用到网络层就结束
网络层要根据这里的目的地址来规划接下来的传输路线
TCP | UDP |
---|---|
有链接「打电话」 | 无连接「发微信」 |
可靠传输「叮叮已读」 | 不可靠传输「叮叮未读」 |
面向字节流 | 面向数据报 |
全双工 | 全双工 |
全双工: 一个socket既可以用来发送也可以用来接收
半双工: 只能用来发送或者只能用来接收
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; // 此处指定的端口就是服务器自己的端口,ip 并没有指定,相当于市 0.0.0.0(绑定到当前主机的所有网卡上) public UdpEchoServer(int port) throws SocketException { this.socket = new DatagramSocket(port); } public void start() throws IOException { while (true) { // 1.读取客户端发来的请求,客户端发来请求之前这里的receive是阻塞的 DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); socket.receive(requestPacket); // 把收到的数据进行提取 String request = new String(requestPacket.getData(), 0, requestPacket.getLength()); String response = process(request); // 2.处理请求 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress()); // 3.出列结果返回给客户端 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; /* 此处指定的 ip 和 port 是服务器的 ip 和 port 客户端是不需要指定自己的 ip 和 端口 客户端 ip 就是本机 ip,客户端的端口就是操作系统自动分配 */ public UdpEchoClient(String ip, int port) throws SocketException { this.serverIP = ip; this.serverPort = port; /* 此处构造这个对象的时候不需要填参数了:绑定这个指定的端口(客户端是无需绑定端口的,端口系统给的) 前面记录的服务器 ip 和 port 是为了后面发送数据给服务器的准备工作 */ socket = new DatagramSocket(); } public void start() throws IOException { Scanner scanner = new Scanner(System.in); while (true) { // 1. 从控制台读取用户输入 System.out.print("-> "); String request = scanner.nextLine(); // 2. 把数据构成 UDP 数据报,发送给服务器 if (request.equals("exit")) { break; } DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIP), serverPort); socket.send(requestPacket); // 3. 从服务器读取响应数据 DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096); socket.receive(responsePacket); // 4. 把响应数据进行解析并显示 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(); } }
客户端
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; // 和UDP类似,只是多了个连接过程 public class TcpEchoClient { private Socket socket = null; public TcpEchoClient(String serverIP, int serverPort) throws IOException { // 客户端何时和服务器建立连接:在实例化 Socket 的时候 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) { // 1. 从控制台读取用户输入 System.out.print("-> "); String request = scanner.nextLine(); // 2. 把用户输入的数据,构造请求,发送给服务器 PrintWriter writer = new PrintWriter(outputStream); writer.println(request); writer.flush(); // 3. 从服务器读取响应 String response = respScanner.nextLine(); // 4. 把响应习显示出来 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()); /* 需要和客户端进行通信,和文件操作的字节流一模一样 通过 socket 对象拿到 输入流 对象,对这个 输入流 就相当于从网课读数据 通过 socket 对象拿到 输出流 对象,对这个 输出流 就相当于往网卡写数据 */ try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) { Scanner scanner = new Scanner(inputStream); while (true) { // 1.根据请求并解析 if (!scanner.hasNext()) { System.out.printf("[%s:%d] 客户端退出链接\n", clientSocket.getInetAddress(), clientSocket.getPort()); break; } String request = scanner.nextLine(); // 2.根据请求计算响应 String response = process(request); // 3.把响应写入到客户端 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 { // 此处的 clientSocket 的关闭是非常有必要的 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()); /* 需要和客户端进行通信,和文件操作的字节流一模一样 通过 socket 对象拿到 输入流 对象,对这个 输入流 就相当于从网课读数据 通过 socket 对象拿到 输出流 对象,对这个 输出流 就相当于往网卡写数据 */ try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) { Scanner scanner = new Scanner(inputStream); while (true) { // 1.根据请求并解析 if (!scanner.hasNext()) { System.out.printf("[%s:%d] 客户端退出链接\n", clientSocket.getInetAddress(), clientSocket.getPort()); break; } String request = scanner.nextLine(); // 2.根据请求计算响应 String response = process(request); // 3.把响应写入到客户端 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 { // 此处的 clientSocket 的关闭是非常有必要的 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(); // 和客户端进行通信了,通过这个方法来处理整个的连接过程 //「把 processConnection 作为一个任务,交给线程池处理」 pool.submit(() -> { processConnection(clientSocket); }); } } private void processConnection(Socket clientSocket) { System.out.printf("[%s:%d] 客户端建立连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); /* 需要和客户端进行通信,和文件操作的字节流一模一样 通过 socket 对象拿到 输入流 对象,对这个 输入流 就相当于从网课读数据 通过 socket 对象拿到 输出流 对象,对这个 输出流 就相当于往网卡写数据 */ try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) { Scanner scanner = new Scanner(inputStream); while (true) { // 1.根据请求并解析 if (!scanner.hasNext()) { System.out.printf("[%s:%d] 客户端退出链接\n", clientSocket.getInetAddress(), clientSocket.getPort()); break; } String request = scanner.nextLine(); // 2.根据请求计算响应 String response = process(request); // 3.把响应写入到客户端 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 { // 此处的 clientSocket 的关闭是非常有必要的 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(); } }
DNS其实就是一个域名解析作用「比如https:www.baidu.com对应的IP地址是14.215.177.38」
第一步:浏览器查找该域名的IP地址
第二步:浏览器和服务器建立TCP链接
6. 通过获取到的IP地址和服务器建立TCP链接
7. 三次挥手:浏览器所在的客户机向服务器发送请求链接报文「SYN=1」;服务器收到报文以后同意建立连接,并发送一个确认报文「SYN=2,ACK=1」;客户机收到服务器确认报文之后再次向服务器发送一个请求报文「SYN=3,ACK=2」,确认已经收到报文。「至此客户机和服务器建立连接开是通信」
第三步:浏览器通过HTTP协议发送给服务器请求
第四步:服务器处理请求并返回客户机一个响应
第五步:释放TCP链接
8. 浏览器所在客户机发出链接报文之后停止发送数据,给服务器发送一个释放报文
9. 服务器收到释放报文之后,等待一段时间,保证当前位发送完的数据发送完
10. 服务器数据传输完毕之后,给客户机发送一个释放报文
11. 客户机收到释放报文之后发送确认报文,等待一段时间之后断开TCP链接
第五步:浏览器显示页面
浏览器没有接收到完整的HTML文档时,就开是渲染页面,再根据服务器返回的数据包进行对应数据的渲染到前端页面
第六步:浏览器发送获取嵌入在HTML中的其他内容
css,js,图片之类的静态资源
我们也知道目前IPV4数量已经不够用,虽然IPV6在我国正在大力推崇中,但是目前的主力军依旧是IPV4+NAT
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」的时候就会自动生成这样的一个映射关系,以便于后续直接使用。当结束连接的时候机会自动删除
问题来了:子网中多个主机「A/B/C」访问同一个外网IP「163.22.120.9」,服务器响应数据却发现目的IP都是相同的「202.244.173.37」,该如何区分子网内部的设备呢?
IP+Port
这种映射关系也是TCP初次建立连接的时候生成的,由NAT路由器自动维护,当断开连接的时候就会自动删除
NAT: 路由器大多都具备NAT功能,完成子网内部设备和外界的沟通
代理服务器: 看起来和NAT挺像,客户端向代理服务器发送一个请求,代理服务器把请求发送给真正要访问的服务器;服务器返回结果给代理服务器,代理服务器在返回给客户端
NAT和代理服务器区别
NAT | 代理服务器 | |
---|---|---|
应用 | 解决的IP不足 | 翻墙: 广域网中的代理;负载均衡: 局域网中的代理 |
底层实现 | 工作在网络层,实现IP地址的更换 | 工作在应用层 |
适用范围 | 局域网的出口部署 | 局域网,广域网都可以使用甚至跨网 |
部署 | 防火墙,路由器等硬件设备上 | 部署在服务器上的一个软件程序 |
代理服务器又分为正向代理和反向代理
胖虎在宿舍不想去超市买辣条,在《小葵花大学6栋6小卖部》的QQ群里艾特李华帮忙买包辣条然后给他小费
后来胖虎一直让李华带零食,李华也开始偷了懒,抄起了Python,一顿数据分析猛如虎之后发现胖虎最爱大卫龙,可口可乐和乐事薯片。于是李华给了超市老板1元钱,从他那儿获取批发商联系方式,然后进了很多零食包括胖虎的最爱,自己在小葵花大学6栋当起了小老板,开启了贩卖零食的大学生活「此时李华就成了反向代理」
64k对于当下是否满足呢?
2字节=16位=216=65535byte=65535/1024=64k
2
16
÷
2
10
=
2
6
=
64
2^{16} \div 2^{10}=2^6=64
216÷210=26=64
发现数据量非常小,如果传输的数据很大就需要其他方法
是否可以扩展UDP:比如,把报头改成使用4个字节「42亿9千万」来表示长度「改不了,改就需要改系统内核」
单位 | 数据量「字节」 |
---|---|
K | 千「thousand」 |
M | 百万「million」 |
G | 十亿「billion」 |
UDP首部有一个16位的最大数据长度,也就是说一次UDP传输最多有64K「包含首部」,传输数据超过64K,则我们需要在应用层进行手动分包,多次发送并在接收端手动拼装。
五层模型中:程序员最关注的是应用层
下四层已经被操作硬件/驱动/系统实现好了,只要理解大概工作过程即可
对于应用层来说,不仅要理解工作过程,更要能设计出一些 协议「设计应用协议就是约定前后端交互的接口」
用来验证数据是否正确的一种手段「不能保证数据100%正确,但是校验和如果不正确则数据100%不正确」
背景:网络传输过成中,可能会涉及到一定的干扰,就可能会破坏原有要传输的信息。光信号/电信号可能会受到一些 电磁场/高能粒子 的影响,可能会影响到地球上的通信「bit 翻转」。
方法:crc,sha1,md5…
发送方和接收方利用同样的算法计算校验和
发送方式sum1,接受方是sum2.如果中途出现数据变动,则校验和大概率不同
应用层发送给UDP多长的数据,UDP原样不变、发送给网络层多长数据。既不拆分也不合并。
用UDP发送 1000 字节数据
发送方调用一次内核的 send,发送 1000 字节,接收方的内核就 receive 接受 1000 字节。而不会分成 100 次,每次发送 10 字节
这 6 位用 0/1 表示
每次客户端发送数据给服务器都会SYN请求服务器建立连接,服务器收到响应都会ACK给客户端确认应答
发送数据丢失的两种情况:
处理丢包问题:
就按照最坏情况下作为发送方 “我” 数据丢失,如果指定时间后还没有收到回信,我就再发一次
对于服务端发送给客户端的数据丢了,服务端可以重发一次而不会对客户端有影响;但是客户端发送给服务端的数据丢失了,会进行大量的重发,服务端如何处理重复消息呢?
处理服务端数据重复
TCP会自动对消息进去重
发过去的数据会先放在接收方的消息缓冲区里「内核中的一个数据结构,可以视为阻塞队列」。
任何一段消息都带有ACK确认序号,如果新来的消息序号和阻塞队列中的序号重复,TCP直接去重「多个消息只保留一份」。所以应用程序从接受缓冲区取数据的时候,肯定不是一个重复的数据。调用 socket api 得到的数据一定不重复。
有了以上的 确认应答,超时重传应该可以保证TCP的数据万无一失了吧?但最糟糕的问题来了:如果对于客户端和服务端任何一方而言,重传数据也丢失了该怎么办呢?
处理重传数据丢失问题
重传不会无休止的进行,尝试一定次数后就会放弃「如果重传的数据也丢失了就认为能够恢复链接的概率很低,重传次数再多也是浪费资源」
重传时间间隔也不相同,每次重传时间间隔都会变长
假设丢包概率是10%,则两次数据包都丢失的概率就是10% * 10% =1%
我们有了尝试一定次数和时间间隔来解决丢包难题,次数我们很容易规定,可以假设超过16次就认为传输失败,可以关闭连接。但是这个 重传时间间隔 该如何确定呢?
确定重传时间间隔
最理想的情况下是能够早找一个 最短回复时间,在这个时间内,数据的响应一定能返回
但是这个时间的长短是由网络环境决定的,各有差异
如果设置的超时时间太长,则会影响整个过程的传输效率
如果设置的超市时间太短,则会频繁的发送数据包,造成资源浪费
因此,TCP为了保证在任何环境下都能保持较高性能的通信效率,因此会动态计算这个 超市时间
Linux「Unix,Windows」也都是超时以 500ms 为一个单位进行超时控制,每次判定超时重传的时间间隔是 500ms的整数倍
第一次:500ms,第二次:2*500ms,第三次:3*500ms…
如果累积到一定次数之后就会认为当前网络环境已经无法恢复,就会强制关闭连接
正常情况下TCP要经历三次握手建立连接,四次挥手断开连接
三次握手
三次握手的原始连接
三次握手后的连接优化
因为对于服务端发给客户端的 ACK+SYN 可以合并在一起发送。
public TcpEchoClient(String serverIP, int serverPort) throws IOException { // 客户端何时和服务器建立连接:在实例化 Socket 的时候 this.socket = new Socket(serverIP, serverPort); }
- 1
- 2
- 3
- 4
还记得这段代码吗?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就是网络上两点之间传输消耗的最大时间」
TCP最原始的发送数据:发一个,确认一个这样的机制。等到ACK之后才能发送下一个数据,这样的话后续的数据大量会阻塞等待。
滑动窗口就是为了解决这个问题,减少等待ACK时间「其实就是将多段等待时间重叠在一起了」
假设一次发送长度为N的数据,然后等待一波ACK,此时这里的N就称为“窗口大小”
N越大,传输的速度越高,但是N也不能无限大,如果N无限大,此时确认应答就没有意义了,可靠性就形同虚设了
遇到丢包问题怎么办?
丢包问题分为两种,一种是确认应答ACk丢了,一种是数据丢了。我们需要分开讨论分析。
ACK丢了
这种情况下,丢ACK并不要紧,可以通过后续ACK来确认
数据丢了
当某一个数据包丢失的时候,发送端会一直发送此数据端的ACK,比如ACK1001。
如果客户端主机收到了 3次 同样的ACK,就认定次数据包已经丢失了,会根据ACK的提示发送对应的数据段。
当服务端主机收到了所需要的ACK的时候,则会返回客户端最后一次 没有丢包发送过来的数据的ACK「此处就是ACK6001」
因为2000-6000的数据已经被收到了,就被放到了操作系统内核的 接受缓冲区 了。
这样就构成了 滑动窗口下的快重传
也是在保证可靠性,对滑动窗口进行了制约。滑动窗口越大,就认为传输速率越高。但也并不是越大越好,接收方顶不住消息之后,额外发出的数据大概率是要丢包的,就会触发超时重传机制。所以一定是最合适的才是最好的。发送方和接收方速率理论上匹配最好。
主要是根据接收方处理数据的能力,来制约滑动窗口大小。发送方的话动窗口大小是变化的「不是固定的」
接收方处理数据的速率主要取决于应用程序,调用 socket api 读取数据的速率
如何衡量接收方的处理数据的速度
主要就是看接收方的应用程序调用 socket api 的读操作「read() 快不快」
刨根问底就是判断:通过接受缓冲区中剩余空间的大小
假设接受缓冲区一共是 4k。 当前使用了3k,还剩1k。此时接收方就会返回 ACK 的时候告知发送方说:我这个接受缓冲区还有 1k 空间;接下来会发现发送的时候就可以按照 1k 这样的窗口来发送数据…
在考虑一个极端情况:如果发送接收方缓冲区满了,发送方就不再发送数据
这时候接收方会在窗口满的时候发送一个 ACK 告知发送方窗口满了,然后发送方停止发送数据。由于发送方停止发送数据导致的接收方不会对发送方有任何响应。
所以这个时候发送方会 定期 发送一个 探测报文,接收方收到这个 探测报文段 之后就会把自己当前窗口大小响应给发送方「这个接收方有种需要发送方敲打的味道」
这个窗口只能存放65535个字节吗?
TCP首部40字节选项中还包含了一个扩大因子 M,实际窗口大小是 左移M位
和流量控制差不多,都是用来限制发送方传输速率的机制。防止发的太快处理不了。
虽然两台电脑处理和发送都很快,但是如果中间某个节点「路由器等网络设备」出问题,不能快速的转发
相比于流量控制,拥塞控制是更复杂的
流量控制:只考虑接收方和发送方
拥塞控制:考虑的是整个链路上有多少个设备,这些设备路径都是什么情况会很复杂「由于这个中间路径非常复杂,拥塞控制解决方案是把中间整个链路视为一个整体,通过 不断试错 的方式来找到一个合适的发送窗口大小。不断的尝试不同的窗口大小,在保证可靠性的前提下提高发送速率」
拥塞控制如何控制拥塞窗口的?
拥塞控制会设置出一个 拥塞窗口 这样的指标,通过拥塞窗口来影响滑动窗口的窗口大小
拥塞控制也是动态变化的,刚开始用一个比较小的值「让发送方发的慢点」如果通信非常顺利,也没有丢包就会逐渐放大窗口,加快发送速度的同时密切监视丢包情况,如果嫁到一定程度了,发生了丢包,说明当前接收方顶不住了;就立即减小窗口大小,让速度再慢下来,如果不丢包,再逐渐加速。反复重复以上步骤就会逐渐稳定在一个合适的速率
拥塞控制和流量控制都能影响滑动窗口,到底谁起决定作用
谁小谁说了算
拥塞窗口的变化规律
提升传输效率,考虑是否能子啊保证可靠性的前提下继续把滑动窗口调大一点「流量控制的延伸」
流程简述
记住:窗口越大,网络传输速率就越大,但是一定要在保证可靠性的前提下才可以调大窗口
那么所有的包都可以延迟应答吗?
当然不是,有数量和时间限制
数量:每隔 N 个包就应答一次
时间:超过最大延迟时间就应答一次
具体的数量和时间:不同操作系统都是不一样的。一般 数量N取2,最大延迟时间取200ms
在延迟应答的基础之上做了延伸
最典型的就是:一问一答
客户端发送一个请求,服务器就会响应一个客户端的请求
这俩操作是不同的时机,既然是不同实际也就不应该合并成一个数据报
但是TCP中的捎带应答机制导致B对A的回复并不是立即的,而是等待一段时间之后在发送。在等待的这段时间内,就导致了发送的时间可能就和应用程序返回A响应的时间就是同一时机了,也就可以合并了
把两个个TCP数据报合并成一个数据报,节约资源与时间,减少封装和分用
TCP的四次挥手有没有可能变为三次挥手?
B发给A的ACK是在内核中完成的、FIN是应用程序代码调用 close() 完成的,这俩操作看似是不同时机,但是如果有了捎带应答机制结果就不一样了。如果恰好触发了捎带应答,则会是 ACK+FIN 合二为一发送过去,此时的话就会是三次挥手
程序不一定100%触发捎带应答,如果设定延迟应答时间为200ms,如果200ms内恰好出发了捎带应答,则会执行到 close
创建一个 socket 的同时内核就会创建一个 接收/发送 缓冲区
发送数据
接收数据
由于有缓冲区的存在,TCP程序的读和写不是一一对应
多个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 个这么长的数据也就到达了边界
建立好通信的双方,在通信过程中突然有一方遇到了突发状况。
1.进程终止
A,B其中某个进程突然终止「崩溃或者被强制关闭」
如果直接关闭进程,看起来是猝不及防,但实际上操作系统早有准备「也就是每次打开任务管理器的时候,CPU占用资源瞬间高涨的一部分原因」
杀死某个进程,操作系统回释放这个进程的相关资源「TCP这里依赖 socket 文件,操作系统就会自动关闭这个 socket 文件。这个自动关闭的过程就相当于 socket.close()『触发了四次挥手』」
2.机器重启
按照操作系统既定的流程重启
就会由操作系统先把当前所有的应用程序,强制杀死「杀死进程就和上面的进程终止一样了,释放 socket 文件,发送 FIN」
『单纯的四次挥手』
3.断电/断网
这个情况才算 偷袭成功
接收方掉电
此时A不会收到B发送的ACK,接下来就会触发超时重传,重传一定次数之后认为连接不可恢复『尝试重新建立连接』,最终只能放弃链接『A就会释放自己所有保存连接的信息』
『就会放弃四次挥手断开连接』
发送方掉电
A发完第一条消息之后,B响应对应的ACK,但是A没有发送断开连接的请求导致B就会一直在等待A的请求
解决方案就是:TCP连接双方会周期性的给对方发送一个不包含业务数据的 探测报文,这个探测报文不传递实际的数据,只是用来检查对方是否正常工作。
优先保证可靠性,再进一步提高效率
**可靠性:**确认应答,超时重传,连接管理,流量控制,拥塞控制,TCP异常情况
**效率:**滑动窗口,延迟应答,捎带应答
**编码注意事项:**沾包问题
HTTP,HTTPS,SSH,FTP,SMTP,Telnet
TCP优势:可靠性
**UDP优势:**效率更高
经典面试题:如何用UDP保证可靠传输
「抄TCP的作业」
网络层里面最核心的协议叫做 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位标识,3位标志,13位片偏移都是为了拆分
8位生存时间:数据报到达目的地的最大跳数。一般是64,每经历一次转发TTL就会-1,直到0了还没有收到,那么就丢弃。主要为了防止循环路由出现
8位协议:表示传输层使用的哪个协议,TCP/UDP 会有不同的值,为了在分用的时候能够让网络层把数据提交给正确的传输层协议来处理
16位首部校验和:使用CRC来校验头部是否损坏
32位源地址:发送端IP地址
32位目的地址:接收段IP地址
UDP首部长度固定为8字节,IP首部长度固定为20字节
16位表示,3位标志,13位片偏移如何拆分64K的
生存时间
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地址
子网内部的一些设备
那么问题来了,手动管理子网内的IP地址是非常麻烦的
IPV4协议,是使用4个字节来表示IP地址,表示的地址个数只能是42亿9千万
如何应对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的网络建设「主要是针对国家安全和利益考虑」
特殊的IP
主机号全为0:当前局域网
主机号全为1(125):广播IP
以127开头的IP「环回」
内网IP是不要求唯一的「不同网段中会出现相同的IP地址」
除了10,172.16-172.31,192.168是私网IP以外,其余全是外网IP「外网IP要求是唯一的」
路由选择也就是规划一条通信传输的路径
客户端在和服务器搭建连接的过程中是一个很复杂的过程,中间会有很多台设备中转才连接到的
在这些线路中找到一个最合适的路线就是路由选择要做的工作
简约的查找流程
- 如果路由器直接认识目的IP,就可以直接转发到目的IP进而建立连接
- 如果路由器不认识目的IP,路由器就会把这个数据沿着一条默认的路径继续转发给下一个路由器
重复上述步骤,就能够找到一个合适的路由认识目的IP「通过6个人可以认识全世界的故事原理」
路由器如何认识目的IP的呢?这就用到了所谓的路由表的概念了。
路由表
这个是路由器内部维护的一个类似于 “通讯录电话本” 的功能,是以 key:vale 形式存储的
key:IP地址网络号
value:网络接口「从路由器的WAN口出还是LAN口出」
路由表又一个默认的电话本,称为 “下一跳”
路由表的实现也很复杂,一方面可以手动生成一方面可以动态设定
主要负责相邻的两个节点之间的通信
以太网:并非是一个真正的网络,而是一种技术规范「既包含了数据链路层也涵盖物理层:网络的拓扑结构,访问控制方式,传输速率…」
以太网中的网线必须使用双绞线「相同设备使用交叉线;不同设备使用直通线」
FCS:数据校验的方式「著名的是CRC冗余验证」
46-1500:所能承载的数据范围「单位是字节」
最多1500:首先与当前网络硬件的结构,不同数据链路层协议搭配不同的物理设备对因承载数据的能力也不同
MAC地址是6字节,表示范围较于4字节的IPV4多了6w倍「所以才可以财大气粗的给每个硬件在出厂的时候写死一个MAC地址」
MAC地址起到的的主要要作用就是在相邻的两个基点至简传输
当数据包到达局域网之后,根据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「准考证号」。两者缺一不可
对比发现:
MTU「最大传输单元:Maximum Transfer Unit」相当于对数据帧的限制,这个限制是数据链路层对下一层的物理层的管理也影响上一层网络层的协议。
如果IP数据报超过了1500字节,就无法被封装到一个以太网数据帧中,这个时候就会触发IP的分包操作「IP的分包一般不是因为报头中的64限制了数据报整体的大小,大概率是因为数据链路层以太网帧的MTU限制来分的」
以下是MTU对IP报如何分片的
以下是MTU对UDP的分片
TCP的数据报也不能无限大,主要还是受限于MTU。TCP单个数据报的最大长度是MSS「Maximum Segment Size」
好奇的童鞋有可能会问:为什么TCP没有数据长度限制呢?那UDP有吗?
在回顾一下TCP和UDP格式会发现,只有UDP又一个2字节数据长度「还记得计算的是不超过64K吗?」,而TCP协议格式中则没有对数据长度的限制
以下是MTU对TCP的分片
这个了解即可,ARP协议其实并非是一个单纯的数据链路层协议,而是作用在数据链路层和网络层之间的协议
用来简历IP地址和MAC地址之间的映射关系
先获取MAC地址
再来看一下使用ARP发送数据过程
应用层
传输层
网络层
数据链路层
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。