一万字❤ 计算机网络知识✔ 带你全面了解网络原理【详解 + 代码演示 + 图解】(期末考试、面试必备)

1. 网络基础

1.1 局域网

通过路由器,将多个电脑连接到一起,构成了局域网(交换机,是对路由器的端口进行扩展的)

1.2 广域网

将多个局域网连接到一起,构成了更加庞大的网络,覆盖范围也更大

局域网和广域网是相对而言的,没有明确的界限

1.3 IP 地址

描述一台设备在网络上的位置。使用一个四字节的数字来表示,点分十进制

1.4 端口号

区分一个主机上不同的应用程序(2个字节)。一个端口号只能被一个程序绑定,但一个程序可以绑定多个端口

范围:0 - 65535

  • 0 一般不使用
  • 1-1023 范围的端口号系统留作特殊用途,其他程序不能占用(22:ssh;23:telnet;80:http;443:https)

网络上本质是通过 光 / 电 信号来传输数据的

1.5 协议

通信双方按照什么样的规则来进行通信

五元组:源 IP,源端口,目的 IP,目的端口,协议类型

1.6 协议分层

网络通信场景比较复杂,很多问题都需要通过协议来解决,如果只有一个大的协议来解决所有问题,这个协议就会非常复杂,不利于学习、理解、维护。将大的协议拆分成多个小的协议,每个协议负责一小块,这样做的话,由于网络协议太复杂,拆分出来的小的协议太多,也不好管理和维护,此时就需要对协议进行分层。

按照协议的 定位/作用 分类,并约定不同层次之间的调用关系,“上层协议调用下层协议,下层协议给上层协议提供服务”,此时即使协议比较多,也可以顺利完成任务

优势

  1. 协议分层之后,上下层彼此之间就进行了封装(使用上层协议,不必过多关注下层,使用下层,也不必过多关注上层)
  2. 每层协议都可以根据需要灵活替换

1.7 网络模型

1.7.1 OSI 七层模型

从下到上分别为 物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

分层名称功能
应用层针对特定应用的协议
表示层设备固有数据格式和网络标准数据格式的转换
会话层通信管理。负责建立和断开通信连接(数据流动的逻辑通路)。管理传输层以下的分层
传输层管理两个节点之间的数据传输。负责可靠传输(确保数据被可靠地传送到目标地址)
网络层地址管理与路由选择
数据链路层互连设备之间传送和识别数据帧
物理层以 “0”、“1”代表电压的高低、灯光的闪灭。界定连接器和网线的规格。
1.7.2 TCP/IP 五层模型
分层名称功能实现
应用层应用程序如何使用这个数据应用程序
传输层关注起点和终点操作系统的内核
网络层进行路径规划操作系统的内核
数据链路层两个相邻节点之间的数据传输驱动程序 + 硬件
物理层描述网络通信的硬件设备(网线、光纤的规格等)。将数据转为 “0”、“1” 信号,通过 光信号/电信号进行传输。对于接收到的数据,将光信号/电信号转换成二进制数据得到以太网数据报交给数据链路层驱动程序 + 硬件

对于一台主机,操作系统内核实现了从传输层到物理层的内容,即 TCP/IP 五层模型的下四层

对于一台路由器,实现了从网络层到物理层,即 TCP/IP 五层模型的下三层

对于一台交换机,实现了从数据链路层到物理层,即 TCP/IP 五层模型的下两层

对于集线器,只实现了物理层

2. 网络编程

网络编程,需要使用操作系统提供的一组 API 来完成编程。可以认为是应用层和传输层之间交互的方式,成为 Socket API,可以完成不同主机、不同系统之间的网络通信。

传输层的协议主要是 TCP 和 UDP,这两个协议的特性差别很大,操作系统针对这两个协议分别提供了两组 API

2.1 TCP 和 UDP 的区别

  1. TCP 是有连接的,UDP 是无连接的

    有连接:建立连接的双方,各自保存了对方的信息

    • TCP 要通信的话,就得先和对方建立连接,然后才能进行通信

    • UDP 要通信,无需建立连接,直接发送数据即可。不需要征得对方的同意,也不会保存对方的信息。应用层调用 UDP 的 API 的时候,将对方信息传过去就行。

  2. TCP 是可靠传输,UDP 是不可靠传输

    可靠传输:发送方发送数据,是否到达接收方,发送方能够感知到,因此发送方可以在发送失败时采取相应的措施(如重传)

    TCP 内置了可靠传输机制,UDP 没有内置可靠传输机制

    虽然 TCP 是可靠传输,但是并非就更好,因为可靠传输的代价就是机制会更复杂、传输效率会降低

  3. TCP 是面向字节流的,UDP 是面向数据报的

    数据报(Datagram)

    数据包(Packet)

    数据帧(Frame)

    数据段(Segment)

  4. TCP 和 UDP 都是全双工通信

    全双工:可以同时进行双向通信

    半双工:可以进行双向通信,但同一时间只有一个方向可以进行通信

    一根网线中有 8 根线,4个一组,每组都可以独自完成通信,2 组可以实现,当某一根线坏掉时,剩下的也能继续工作。

2.2 UDP的 Socket API

2.2.1 DatagramSocket

Socket 本质上是一个特殊的文件,将网卡这个设备抽象成了文件,往 Socket 中写数据就相当于是发送数据,往 Socket 中读数据,就相当于接收数据。在 Java 中使用 DatagramSocket 来表示系统内部的 Socket 文件

2.2.1.1 构造方法
方法签名解释
DatagramSocket()创建一个 UDP 数据报套接字,绑定到本机任意一个随机端口
DatagramSocket(int port)创建一个 UDP 数据报套接字,绑定到本机指定的端口
2.2.1.2 主要方法
方法签名解释
void receive(DatagramPacket p)从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)。参数 p 是一个输出型参数,该方法会将接收到的数据填充到 p 中
void send(Datagram Packet p)从此套接字发送数据报(不会阻塞等待,直接发送)
void close()关闭此数据包套接字

客户端和服务器通信,两者都需要创建 Socket 对象,但服务器的 Socket 需要显式指定端口号,客户端的 Socket 一般不显式指定,由客户端操作系统自动分配

服务器端手动指定端口号,是因为开发者清楚该服务器上有什么程序,可以使用哪些端口号,是可控的

而客户端程序在用户电脑上,如果手动指定端口号,有可能这个端口号已经被用户端电脑上的其他程序占用了,就会重现端口冲突,而系统自动分配的话,分配到的肯定是空闲端口

2.2.2 DatagramPacket

用来作为接收和发送的数据报

2.2.2.1 构造方法
签名解释
DatagramPacket(byte[] buf, int length)构造一个DatagramPacket来接收数据报,接收到的数据存储于字节数组 buf 中,length 表示接收指定长度的数据(最多接收的数据的字节个数)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)构造一个DatagramPacket来发送数据报,发送的数据为字节数组 buf 中,从 0 到指定长度length。address 用于指定目的主机 IP 和 端口号
2.2.2.2 主要方法
签名解释
SocketAddress getSocketAddress()从数据报中获取发送端主机 IP 地址或接收端主机 IP 地址
int getPort()获取发送端主机或接收端主机端口号
byte[] getData()获取数据报中的数据
2.2.3 回显服务器

服务器端

package udp;

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 {
        System.out.println("服务器启动!!!");

        while (true) {
            // 1. 读取请求并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);

            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            // 2. 根据请求计算响应 (实际的业务逻辑)
            String response = process(request);

            // 3. 将响应写回客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress());
            socket.send(responsePacket);

            // 4. 打印日志
            System.out.printf("[%s:%d] req=%s, resp=%s", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
        }
    }

    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
        udpEchoServer.start();
    }
}

为什么这里不需要调用 close() 关闭文件

  • 这个 socket 是整个进程运行过程中一直都需要用到的
  • 当 socket 不需要使用的时候,意味着进程就结束了,进程的结束,PCB 也被销毁了,文件描述符表也被销毁了,因此不存在文件资源泄露

客户端

package udp;

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;

    private String serverIp = "";
    private int serverPort = 0;

    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.socket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);

        while (true) {
            // 1. 读取数据作为请求
            System.out.print("->");
            String request = scanner.next();
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(this.serverIp), serverPort);

            // 2. 发送请求
            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.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
        udpEchoClient.start();
    }
}

2.3 TCP 的Socket API

2.3.1 ServerSocket

给服务器使用的类,可以绑定端口号

2.3.1.1 构造方法
签名解释
ServerSocket(int port)创建一个服务器端 Socket,并绑定端口号
2.3.1.2 主要方法
签名解释
Socket accept()开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务器端 Socket 对象,并基于该 Socket 建立与客户端的连接,否则阻塞等待(将内核中的应用程序拿到应用程序中)
void close()关闭当前 Socket

服务器端通过 accept 将内核中的应用程序拿到应用程序中

如果有客户端和服务器建立连接,这个时候服务器的应用程序是不需要做出任何操作的,内核直接就完成了连接建立的过程(三次握手),之后就会在内核的队列中排队(每个 serverSocket 都有这样一个队列)

应用程序要和客户端通信,就通过 accept 方法将内核队列中建立好的连接对象,拿到应用程序中

ServerSocket 专门接收连接,ClientSocket 专门和客户端进行通信(每个服务器和客户端的连接被接收都会创建一个clientSocket)

TCP 是面向字节流的,而 InputStream 和 OutputStream 就是字节流,因此可以借助这两个对象完成数据的发送和接收。通过 InputStream 进行 read 操作,就是 “接收”,通过 OutputStream 进行 write 操作,就是 “发送”

2.3.2 Socket
2.3.2.1 构造方法
签名解释
Socket(String host, int port)创建一个客户端 Socket,并于对应 IP 的主机上对应端口的进程建立连接
2.3.2.2 主要方法
签名解释
InetAddress getInetAddress()返回 Socket 所连接的地址
InputStream getInputStream()返回 Socket 的输入流
OutputStream getOutputStream()返回 Socket 的输出流
2.3.3 回显服务器

服务器端

如果有很多客户端和服务器的连接,那么每个连接被接收(accept)都会创建 clientSocket,当连接断开之后,这个 clientSocket 就没用了,但若不手动 close,这个 clientSocket 对象会占用文件描述符的位置,造成文件资源泄露

package tcp;

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.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    private ServerSocket serverSocket = null;
    
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    
    public void start() throws IOException {
        System.out.println("服务器启动!!!");
        ExecutorService service = Executors.newCachedThreadPool();
        while (true) {
            // 从内核中将连接拿到应用程序中
            Socket clientSocket = serverSocket.accept();
            // 和客户端交互
//            Thread t = new Thread(() -> {
//                processConnection(clientSocket);
//            });
//            t.start();
            // 很多客户端频繁建立连接/断开连接,就导致服务器频繁创建/销毁线程,开销很大,使用线程池来解决
            // 如果同一时刻有大量客户端连接,就会使系统上出现大量线程,机器容易挂掉(可以通过协程/IO 多路复用/IO 多路转接来实现)
            service.submit(() -> {
                processConnection(clientSocket);
            });
        }
    }

    // 通过 processConnection 来和客户端交互
    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            // 客户端可能会发送多条数据,因此需要训练处理
            while (true) {
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress(), clientSocket.getPort());
                    break;
                }
                // TCP 是面向字节流的,每次传输多少字节,读取多少字节,都可以灵活设置,往往会手动约定一个数据报的开始和结束标志
                // 这里约定使用 \n 作为一个数据报的结束标志
                // 1. 读取请求并解析
                String request = scanner.next();
                // 2. 计算响应
                String response = process(request);
                // 3. 将响应写回客户端
                // 将响应写入 outputStream,也就是写入到了 clientSocket 里面了,进而通过网络发送出去
                PrintWriter printWriter = new PrintWriter(outputStream);
                // 此处使用 println,使得客户端也可以通过 \n 来区分数据报
                printWriter.println(response);
                printWriter.flush();
                System.out.printf("[%s:%d] req=%s, resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
                        request, response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } 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 {
        TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
        tcpEchoServer.start();
    }
}

客户端

package tcp;

import com.sun.deploy.security.SandboxSecurity;

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 scannerNetwork = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            Scanner scanner = new Scanner(System.in);
            while (true) {
                // 1. 从控制台读取用户输入
                System.out.println("-> ");
                String request = scanner.next();
                // 2. 发送给服务器
                printWriter.println(request);
                printWriter.flush();

                // 3. 接收服务器的响应
                String response = scannerNetwork.next();
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
        tcpEchoClient.start();
    }
}

3. TCP/IP 五层模型

3.1 应用层

3.1.1 自定义协议

应用层很多时候都是程序员自定义应用层协议(如在代码中约定如何进行数据传输)

  1. 根据需求,明确要传输的信息
  2. 约定好信息要按照什么格式来组织

传输格式

  1. 比较粗糙的方式:比如在登录功能中,约定,前端需要传用户名和密码,并且将用户名和密码处理成以逗号分隔的字符串,后端也按照逗号来解析字符串,得到用户名和密码,计算请求,返回给前端 0 或 1 来表示登录失败或登录成功,前端获取到 0 或 1 之后分别按照登录失败和登录成功来处理。

  2. xml

    数据可读性好,但标签编写繁琐且消耗额外网络带宽

  3. json

    数据可读性好,比 xml 更简洁,但 key 的传输也需要消耗额外网络带宽

  4. protobuffer

    使用二进制的方式来组织数据,保证带宽占用最低(将要传输的数据按二进制形式压缩了)

    占用带宽低,传输效率高,适用于对性能要求高的场景,但可读性不好,影响开发效率

3.1.2 HTTP 协议
3.1.2.1 报文格式

请添加图片描述

3.1.2.2 方法

GET:通常将要传给服务器的数据放在 URL 的 query string 中

POST:通常将要传给服务器的数据放在 body 中

GET 和 POST 没有本质区别,双方可以替换对方的场景

  1. 使用习惯:GET 通常将要传给服务器的数据放在 URL 的 query string 中,POST 通常将要传给服务器的数据放在 body 中
  2. 语义上:GET 大多数时候用来获取数据,POST 大多时候用来提交数据

错误理解:

  1. GET 请求能传递的数据量有上限,POST 传递的数据量没有上限

    早期浏览器,针对 GET 请求的 URL长度做了限制,但 RFC 标准文档中并未明确规定 URL 的长度。目前的浏览器和服务器的实现中,URL 可以非常长

  2. GET 请求数据不安全,POST 请求数据更安全(GET 请求的用户名和密码在 URL 中,可以显示出来)

  3. GET 只能给服务器传文本数据,POST 可以传文本和二进制数据

    GET 的 body 中可以直接放二进制数据,也可以将二进制的数据进行 base64 转码,然后放到 query string 中

  4. GET 是幂等的,POST 是不幂等的(不准确)

    是否幂等,取决于代码的实现(广告推荐),RFC 文档中,建议 GET 实现成幂等的

  5. GET 请求可以被浏览器缓存,POST 不能被缓存

    如果请求时幂等的,那么就可以缓存

3.1.2.3 Header
1. Host

服务器的 IP 地址和端口号

2. Content-Length 和 Content-Type

分别是 body 中数据的长度、body 中数据的格式(只有请求中有 body 时,才会有这两个属性)

HTTP 是基于 TCP 的,使用同一个 TCP 连接,传输多个 HTTP 数据包,则这多个 HTTP 数据包会在接收缓冲区中挨在一起,接收方解析的时候,就需要能够知道各个 HTTP 数据包的边界,通过空行和 Content-Length就能分辨出边界(没有 body 时,通过空行来分辨)。

Content-Type:body 中数据的格式

请求格式

  1. json

    Content-Type:application/json;charset=UTF-8

  2. form 表单

    相当于将 GET 的 query string 放到了 body 中

    Content-Type:application/x-www-form-urlencoded;charset=UTF-8

  3. form-data

    上传文件(也可能是 form 表单)

响应格式

  1. html

    Content-Type:text/html; charset=utf-8

  2. css

    Content-Type:text/css

  3. js

    Content-Type:application/javascript; charset=utf-8

  4. json

    Content-Type:application/json; charset=utf-8

  5. 图片

    Content-Type:image/png

  6. 视频

  7. 音频

3. User-Agent

描述了当前在使用什么设备上网(操作系统版本 + 浏览器版本)

开发者可以根据 UA 的信息,返回不同的内容,来兼容不同版本的浏览器以及设备

4. Referer

描述当前页面是从哪个页面跳转过来的

5. Cookie

浏览器在本地存储数据的一种机制

为了安全,网页不能直接访问电脑的文件系统,浏览器对操作文件进行了封装,网页只能往 Cookie 中存储键值对,每个域名下存储自己的 Cookie,各个域名的 Cookie 互不影响

后续请求服务器的时候,会自动带上 Cookie 中的内容

构造请求

  1. 浏览器地址栏 url 是 GET
  2. html 中的 a、img 等标签会用到 GET
  3. form 表单可以构造 GET 和 POST 请求(form 会触发页面跳转)
  4. ajax
3.1.3 HTTPS

HTTPS 是在 HTTP 的基础上引入了加密机制,针对 Header 和 body 进行加密

  1. 对称加密加密业务数据
  2. 非对称加密加密对称密钥
  3. 证书校验服务器的公钥(防止中间人攻击)
3.1.3.1 对称加密

加密和解密使用的是同一个密钥

  1. 客户端给服务器发送数据之前,先进行加密,服务器收到数据之后再使用相同的密钥来进行解密,此时黑客截获到的数据就无法解密。

  2. 但通常都是有多个客户端和服务器进行通信,如果多个客户端使用相同的密钥来加密,相同的密钥容易扩散,被黑客拿到,因此每个客户端的密钥都必须是不同的,这样的话,服务器就需要维护每个客户端和密钥之间的关系,需要客户端和服务器连接的时候,生成密钥,并传给服务器(密钥协商),但密钥也很容易被黑客截获。即使给密钥再加密也存在同样的问题。

为了解决这个问题,就引入了非对称加密

3.1.3.2 非对称加密

有一对密钥(配对的),一个是 “公钥”,一个是 “私钥”,用一个密钥加密,另一个密钥解密。可以是用公钥加密,私钥解密,也可以是私钥加密,公钥解密

  1. 服务器生成一对公钥和私钥,将公钥传给客户端(黑客能拿到),私钥自己保存
  2. 客户端生成对称密钥,用公钥加密对称密钥,然后将加密后的对称密钥传给服务器
  3. 服务器通过私钥对加密后的对称密钥进行解密,就得到了对称密钥的明文
  4. 客户端和服务器使用对称密钥加密传输数据

使用非对称加密只加密对称加密的密钥,因为非对称加密运算成本高,速度慢,而对称加密运算成本低,速度快

但这个方法,还有漏洞,即中间人攻击,在服务器给客户端发送公钥的过程中,黑客将数据包中的公钥获取到,黑客自己也生成一对非对称密钥,并将其中的公钥换成自己的公钥,然后发给客户端,在客户端使用黑客的公钥加密对称密钥并发给服务器时,黑客便可以使用自己的私钥解密,从而获得对称密钥,再将该数据包原封不动地传给服务器,使得客户端和服务器都感知不到中间黑客地存在,然而黑客却能对服务器和客户端的数据进行解密。

3.1.3.3 证书

为了解决中间人攻击的问题,引入了第三方机构来辨别这个客户端收到的公钥是否真的是服务器发过来的公钥。

  1. 搭建好服务器,生成公钥私钥之后,向公证机构提交域名、公钥、厂商等信息,来申请证书

  2. 公证机构审核通过后,会给服务器颁发一个证书(一段结构化数据,包含域名、公钥、证书过期时间、数字签名(被公证机构使用自己的私钥加密过的校验和)等)

    数字签名是由公证机构自己生成的私钥对校验和进行加密,公钥会发布给各个客户端设备(内置到了操作系统中)

  3. 客户端访问服务器,不会索要公钥,会直接索要证书,服务器会将证书返回给客户端。客户端需要进行证书校验,通过系统内置的公证机构的公钥对数字签名进行解密,拿到校验和,再通过校验和来得知证书是否被篡改过,则公钥是可信的。

    问题:

    1. 黑客修改证书中的公钥:客户端通过数字签名就能辨别
    2. 黑客同时修改证书中的公钥和数字签名:数字签名是用公证机构自己的私钥加密的,要替换数字签名,那就必须得用公证机构的私钥加密,黑客没有这个私钥,若黑客用自己的私钥加密,客户端就无法使用公证机构的公钥解密,公证机构的公钥是客户端系统自带的,黑客无法替换

3.2 传输层

3.2.1 UDP
3.2.1.1 报文格式

UDP 报头分为四个部分,每个部分两个字节。前两个字段分别为源端口号和目标端口号,第三个字段表示 UDP 报文长度,2个字节最多 64KB,即 UDP 数据报最长 64KB,第四个字段为校验和

请添加图片描述

很多应用层的协议都是基于 UDP 来实现的,如 NFS(网络文件系统)、TFTP(简单文件传输协议)、DHCP(动态主机配置协议)、BOOTP(启动协议)、DNS(域名解析协议)

3.2.1.2 校验和

网络传输中可能会出现传输出错的情况(比特翻转),需要通过校验和的方式来检查是否传输出错。

本质上也是一个字符串,通过原始数据生成,但体积比原始数据更小。原始数据相同,得到的校验和肯定相同,校验和相同,原始数据大概率也相同

请添加图片描述

计算方式

  1. 循环冗余检测(CRC,UDP 中使用的方式)

    将当前要传输的数据,每个字节进行累加,结果存入校验和字段(累加过程中溢出也可以)

    该算法对两个不同数据得到相同 CRC 校验和的概率较大

  2. md5 / sha1

    md5 特点

    • 定长。不论要计算的数据多长,得到的结果都是定长的16/32位
    • 分散。两个数据,哪怕大部分内容都相同,只有很少一部分不同,得到的结果差异也会很大
    • 不可逆。要根据 md5 的结果还原出原始数据,计算量会非常大(现有的计算机,理论上是不行的)
3.2.2 TCP
3.2.2.1 报文格式

请添加图片描述

  • TCP 报头长度是不固定的,报头最短 20 字节,最长 60 字节

  • 4 位首部长度,单位为 4 字节

  • 6 位保留位:暂时不用,先占个位,为后续的扩展留下了余地

TCP 最重要的特性就是可靠传输

3.2.2.2 可靠传输

1. 确认应答

确认应答是保证可靠传输最核心的机制(TCP 的可靠传输是以确认应答为核心,借助其他机制辅助来实现的)

发送方把数据发给接收方后,接收方收到数据就会给发送方返回一个 “应答报文”(Acknowledge,Ack),发送方收到了应答报文就知道数据发送成功了

TCP 需要完成:

  1. 确保应答报文和发出去的数据,能对应上,不会出现歧义
  2. 确保在出现后发先至的情况时,能够让应用程序按照正确顺序来理解数据

解决:

通过序号和确认需要来对发送的数据和应答数据进行编号,使得发送和应答数据能够对应起来,并且解决了 “后发先至” 的问题

由于 TCP 是面向字节流的,因此序号不是按照每条数据来编号,而是按照字节来编号的,每个字节都有一个编号

发送:TCP 报头中记录的 32 位序号只记录这一次传输的载荷中第一个字节的序号,剩下的其他字节的序号需要推出来。如传输 1000 个字节,32 位序号中填写的是 1

接收:TCP 报头中记录的 32 位确认序号中填写 1001,表示前 1000 个字节都收到了,期望后面发送方从 1001 开始发送

请添加图片描述

通过 TCP 报头中的 ACK 来判断该数据报是普通报文还是应答报文,若该字段为 1,则为应答报文,报头中的确认序号生效,否则不生效

2. 超时重传

若网络传输过程中丢包,发送方将无法收到 ACK,发送方无法区别丢包的具体情况,发送方只能是重传

  1. 两种丢包情况:
  1. 发送方发送的数据丢失
  2. 接收方发送的 ACK 丢失

TCP 相比 UDP 更可靠,但降低了效率,传输层也有其他协议来进行可靠传输和传输效率之间的权衡(quic、kcp 等)

发送方发出数据之后,会等待一段时间(可配置,也可动态变化),若等待之间之内收到了 ACK,则发送的数据到达了接收方,若没有在相应时间内到达,就会触发超时重传

等待时间会随着重传次数的增加而增加,直到时间延长到一定程度,即认为数据再怎么重传也没用,就放弃 TCP 连接(触发 TCP 的重置连接)

  1. 发送方未收到 ACK,于是重传,接收方收到了两条相同的数据,如何处理?

    TCP 有接收缓冲区,保存当前已经收到的数据以及序号,若接收方发现发送方发来的数据已经在接收缓冲区中存在了,接收方会把后来的重复数据丢掉,确保应用程序 read 操作读到的数据只有一条

    接收缓冲区:去重、重排序(确保发送的数据顺序和程序读取的数据顺序一致)

3. 连接管理

通过握手和挥手来实现建立连接(通信双方记录了对方的信息)和断开连接

握手:给对方发送一个简短的、没有业务数据的数据包,依次来引起对方的注意,从而触发后续操作(打招呼)

三次握手

通信双方打三次招呼完成连接的建立。发送的数据包是同步报文段(SYN),通过报头中的SYN字段来标识

在这里插入图片描述

发送接收双方都需要给对方发送 SYN 和 ACK(四次握手),但中间的 ACK 可以和 SYN 合并成一条数据报,即三次握手

三次握手核心作用

  1. 投石问路,确认当前网络是否畅通

  2. 确认发送和接收双方的发送和接收能力正常

  3. 让通信双方在握手过程中,针对一些重要的参数进行协商(如数据的序号从几开始)

    序号协商:在网络出现问题,进行 TCP 重连之后,上次连接发送的数据到达了接收方,需要丢弃掉,可以通过序号来区分是上次连接的数据还是本次连接中发送的数据

    每次建立连接都会协商一个比较大的,和上次不一样的值

重要的中间状态

  1. Listen:服务器端的状态,在服务器创建好 Socket 并绑定好端口号就会进入 Listen 状态,等待客户端的建立连接以及请求
  2. Established:客户端和服务器都会有的状态,表示连接建立完成

四次挥手

建立连接一般是由客户端发起,而断开连接,客户端、服务器端都可以发起

四次挥手通信双方发送的报文是 FIN(结束报文段)

请添加图片描述

中间的 ACK 和 FIN 不能合并

ACK 和 第二次 FIN 的时机不同,ACK 是内核响应的,而 FIN 是应用程序代码触发的(调用 Socket 的 close 方法)

TCP 的延时应答机制,可以拖延 ACK 的回应时间,那么将 ACK 滞后,就可能可以和 FIN 合并了

重要的中间状态

  1. TIME_WAIT:哪一方主动断开连接,哪一方就会进入 TIME_WAIT,在收到对方的 FIN 之后会进入该状态,等待一定时间(2MSL,一个 MSL 就是网络上两个节点通信消耗的最大时间)之后进入 CLOSED 状态(即不会收到 对方 FIN 之后立即删除对方信息),主要是为了防止最后一个 ACK 丢失,处理对方超时重传的 FIN
  2. CLOSED:断开连接

前三个是用来保证 TCP 的可靠性的

4. 滑动窗口

TCP 的可靠传输会影响传输效率(多出了等待 ACK 的时间,单位时间内传输的数据就会变少)

滑动窗口就是用来提高效率的,让可靠传输对性能的影响小点

通过批量传输,缩短确认应答的等待时间。并不是 “无限” 的传输,存在上限(不等待的情况下,批量发送多少数据,成为窗口大小),达到上限之后再统一等待 ACK。窗口越大传输效率越高

请添加图片描述

请添加图片描述

请添加图片描述

滑动窗口对丢包情况的处理

  1. ACK 丢包

    无需重传,因为即使 1001 的 ACK 丢了,但后面的 2001 ACK 收到了,那么表明 2001 之前的数据都接收到了

  2. 数据包丢失

    当接收方发现 1001 数据丢失的时候,无论后面收到的数据序号是多少,都会返回索要 1001 数据的 ACK,发送方(连续3次及以上收到重复 ACK)就明白是 1001 数据丢失了,然后就重传。在收到 1001 之后,若此时最高序号 7001 的数据已经到达了,那就发送索要 8001 的 ACK。

    这个过程中,只重传丢失的数据,未丢失的无需重传,即快速重传

    • 通信双方传输的数据量较小,不频繁,则使用普通的确认应答和超时重传

    • 若数据量较大,比较频繁,就使用滑动窗口,快速重传

5. 流量控制

站在接收方的角度,反向的制约发送方的发送速度,即发送方的发送速率不能超过接收方的处理能力(通过接收缓冲区的剩余空间大小来衡量接收方的数据处理能力)

TCP 的 Socket 对象上带有接收缓冲区,数据会先道到接收方操作系统内核 Socket 对象的接收缓冲区,接收方调用 read 方法,将数据读出来进行处理(一旦读出来,接收缓冲区的数据就会删掉),

接收方每次收到数据后会把接收缓冲区剩余空间大小通过 ACK (ACK 中的 16 位窗口大小来表示,并且选项中有一项是 “窗口扩展因子”,通过这个可以让16位窗口大小向左移位)发给发送方,发送方收到后,就可以根据这个来确定下一轮发送的窗口大小。

当接收方的接收缓冲区满了之后,发送的 ACK 携带的窗口大小为 0,此时发送方停止发送,定期的发送不携带业务数据的探测包。为了触发 ACK,查询接收方的接收缓冲区剩余大小。

6. 拥塞控制

考虑的是中间传输节点的能力。

由于中间节点结构复杂,难以直接进行量化,因此可以使用 “实验” 的方式来得到一个合适的值

发送方先以较低的速度(小窗口)发送数据,若没有丢包,传输顺利,就逐步尝试更大的窗口,直到随着窗口的增大,中间节点出现了问题,出现丢包,发送方发现丢包了,就尝试把窗口大小变小,若还是丢包,则继续变小,直到不再丢包,然后又尝试将窗口变大。通过这样的过程不断调整窗口大小,达到 “动态平衡”

探索出中间节点的传输瓶颈

请添加图片描述

7. 延时返回

延时返回 ACK,给接收方更多的时间读取接收缓冲区的数据,接收方从接收缓冲区读取数据之后,接收缓冲区剩余空间变大,ACK 返回的窗口剩余大小也就更大了。

8. 捎带应答

在延时应答的基础上进一步提高传输效率

TCP 引入了延时应答,接收方的 ACK 不一定会立即返回,可能要等一会儿,在等的过程中,接收方正好计算好了响应,就会返回响应,顺便带上延时的 ACK

9. 面向字节流

存在 “粘包” 问题

若同时有多个应用层数据包被传输过去,就容易出现 “粘包” 问题。接收缓冲区中各个数据包紧紧挨在一起,应用程序不知道从哪里到哪里是一个完整的数据包

解决:定义好应用层协议,明确各个数据包之间的边界

  1. 引入分隔符
  2. 引入长度

10. 异常情况的处理

  1. 进程崩溃

    进程异常终止,文件描述符表释放了,相当于调用了 Socket.close(),此时会触发 FIN(四次挥手)

    TCP 的连接可以独立于进程存在

  2. 主机关机

    关机会触发强制终止进程操作,此时会触发 FIN,对方收到之后,返回 FIN 和 ACK。关机时,不仅进程销毁了,整个系统也可能关闭了,若在系统关闭前,收到了对端返回的 ACK 和 FIN,则系统可以继续返回 ACK,正常进行四次挥手。若系统已关闭,ACK 和 FIN 迟到了,无法进行后续 ACK 的响应,对端以为是自己的 FIN 丢包了,则会尝试重传,重传几次之后都未收到响应,则会放弃连接

  3. 主机掉电(非正常)

    来不急销毁进程以及发送 FIN 的操作

    • 若对端是发送方(接收方掉电),发送的数据就会一直等待 ACK,触发超时重传,重传几次之后仍得不到响应,触发 TCP 连接重置功能,发起 “复位报文段”,若发过去之后仍没有响应,就会释放连接。
    • 若对端是接收方(发送方掉电),对端还在等数据到达,无法区分是对端没发消息还是故障了。TCP 中提供了 “心跳包” 机制,接收方定期给发送方发一个特殊的不携带业务数据的数据包,若对方没有应答,重复多次之后也没应答,就认为对方出现故障,之后就单方面释放连接
  4. 网线断开

    和上述 3 中的情况类似

3.3 网络层

3.3.1 地址管理

制定一系列规则,通过地址描述一个设备在网络上的位置

3.3.2 路由选择

一个节点到另一个节点之间存在多个路径,需要通过路由选择,制定/规划出更合适的路径进行数据传输

3.3.3 IP 协议

核心功能就是地址管理和路由选择

请添加图片描述

  • 4 位版本:4(IPv4)/ 6(IPv6)

  • 4 位首部长度:单位为 4 字节,其值为 0 - 0xF。报头是变长的,和选项有关

  • 8 位服务类型(TOS):3 位优先权字段(已弃用)、4 位 TOS 字段、1 位保留字段(必须置为 0)

    4 位 TOS 字段中彼此冲突,只能有一位设为 1,不同位表示 IP 协议的不同形态

    1. 最小延时
    2. 最大吞吐量
    3. 最高可靠性
    4. 最小成本:系统开销最小
  • 16 位总长度:描述了 IP 数据包最长长度(64KB)(虽然有长度限制,但 IP 协议自身支持 “拆包、组包”功能,通过 16 位标识,3 位标志,13 位片偏移来进行)

    若一个大的 IP 数据包拆分为多个小包(分片,大概率是因为数据链路层最大长度 MTU 的限制引起的),多个小包都有相同的 16 位标识,且通过 13 位片偏移来区分各个分片的相对位置

    三个标志位:其中 1 位保留位,1 位表示是否允许拆包,1 位表示是否是最后一个分片

  • 8 位生存时间:描述了这个数据包在网络上还能存活多久(单位:次数),当前数据包在网络上最多能经过多少次转发。数据包刚构造出来时,TTL 被赋予了一个初始值(32、64、128),没经过一个路由器转发,TTL - 1。若这个数据包的 TTL 已经成 0 了,还未到达对方,就会被丢弃掉

    若有一个数据包,目的 IP 为不存在的 IP,则不可能到达对方,这样的包也不允许在网络上一直存在,此时 TTL 就发挥了很大的作用

  • 8 位协议:IP 数据包的载荷部分是 TCP 数据包还是 UDP 数据包,来决定数据包分用时,将载荷交给传输层的 TCP 还是 UDP

  • 16 位首部校验和:只校验 IP 数据包首部。TCP 和 UDP 自身都有校验和

3.3.3.1 IP 地址不够用

IPv4 地址是 4 个字节 32 位

1. 动态分配

若当前设备需要上网,就为其分配 IP,若不需要上网,就无需分配 IP

2. NAT 机制(网络地址转换)

将 32 位 IP 地址分为两大类,即内网 IP 和外网 IP

  1. 内网 IP

    以 10.*,172.16.* - 172.31.*,192.168.* 开头

    同一个局域网内部内网 IP 地址不能重复,不同局域网内网 IP 地址可以重复

    一个局域网中可能有成千上万的设备(小区,公司),这个局域网只使用一个外网 IP 即可

  2. 外网 IP

    不能重复

当局域网中的设备要访问外网的服务器时,运营商路由器会将数据包中的源 IP 替换为外网 IP,收到服务器的响应数据包再将目的 IP 转为局域网 IP(记录了一张局域网 IP 和 外网 IP 的映射关系,通过源端口号区分不同主机的不同进程,同一局域网内部的不同设备的不同进程,分配的端口号大概率不同,若相同,则路由器主动将相同端口替换成不同端口)

3. IPv6

使用 16 个字节来表示 IP 地址,从根本上解决了问题,大幅度提高了 IP 地址的数量(可以给地球上的每一粒沙子分配一个 IP 地址)。但 IPv6 普及程度比较低,和 IPv4 不兼容

3.3.3.2 网段划分

将 IP 地址分为 2 部分,网络号(标识了一个局域网) + 主机号(标识了局域网中的一个设备)

同一个局域网中的设备,网络号必须相同,主机号必须不同。两个相邻的局域网网络号不能相同

通过子网掩码来确定哪部分是网络号,哪部分是主机号

若一个 IP 地址,主机号全为 0,则表示这是 “网络号”,代表一个局域网

若一个 IP 地址,主机号全为 1,则表示这是 “广播地址”

若一个 IP 地址,以 127 开头,则表示这是 “环回 IP”(loopback),都表示设备自身。(操作系统提供了一个特殊的虚拟网卡,关联到了这个 IP 上。可以用来进行测试性工作,排除了网络上的干扰因素,更好的排查代码问题)

3.3.3.3 路由选择

发送 IP 数据报的时候,每个路由器都无法知道整个网络的全貌,只知道局部信息,因此 IP 数据报的转发是 “探索式”、“启发式” 的

路由器的网络层,决定走哪个网络接口,是抽象的概念,最终还是要再数据链路层根据 MAC 地址才能决定走哪个具体的网口

3.4 数据链路层

最常见的协议就是 “以太网协议”

通过网线 / 光纤通信,使用的就是 ”以太网协议“

以太网关注于局部,相邻设备之间的通信过程,相邻节点之间的转发。整个从发送方到接收方的通信过程中,IP 地址是不变的,而 源 Mac 和目的 MAC 会根据当前转发过程每次到达一个节点,往下一个节点转发的时候,进行改变。

3.4.1 以太网数据帧格式

请添加图片描述

  • MAC 地址:每个网络设备的唯一标识(网卡出厂的时候就确定了)

    最开始网络层和数据链路层协议是被独立发明的,因此都包含地址

  • 类型:描述载荷数据是什么类型

    ARP 协议:用来构造转发表的内容

3.5 应用层

3.5.1 DNS

由于 IP 地址不方便使用,因此使用域名来代表相应的 IP 地址,这就需要在网络通信过程中将域名转换为 IP 地址。

最早的域名解析系统就是通过 hosts 文件来实现的。记录了域名和 IP 地址的映射关系

DNS 系统就是一组服务器,保存了域名和 IP 地址的映射关系。如果想访问某个域名,就得先访问 DNS 服务器,查询该域名对应的 IP 地址,然后再访问该 IP 地址的服务器。后面如果有域名更新,只需要更新这组服务器即可,无需修改每个用户电脑上的 hosts 文件

直接访问 DNS 服务器(根域名服务器),高并发如何解决?

  1. 开源

    各个网络运营商搭建了多组 ”DNS 镜像服务器“,镜像服务器中的数据都从 DNS 服务器中同步,用户访问的时候优先访问距离自己最近的镜像服务器

  2. 节流

    每个网络设备建立本地缓存,在访问域名时,若本地缓存中存在,则很长时间内都直接使用本地缓存中域名对应的 IP 地址,若本地缓存中不存在,再访问镜像服务器

作,排除了网络上的干扰因素,更好的排查代码问题)

3.3.3.3 路由选择

发送 IP 数据报的时候,每个路由器都无法知道整个网络的全貌,只知道局部信息,因此 IP 数据报的转发是 “探索式”、“启发式” 的

路由器的网络层,决定走哪个网络接口,是抽象的概念,最终还是要再数据链路层根据 MAC 地址才能决定走哪个具体的网口

3.4 数据链路层

最常见的协议就是 “以太网协议”

通过网线 / 光纤通信,使用的就是 ”以太网协议“

以太网关注于局部,相邻设备之间的通信过程,相邻节点之间的转发。整个从发送方到接收方的通信过程中,IP 地址是不变的,而 源 Mac 和目的 MAC 会根据当前转发过程每次到达一个节点,往下一个节点转发的时候,进行改变。

3.4.1 以太网数据帧格式

[外链图片转存中…(img-N9fLwuad-1725517584813)]

  • MAC 地址:每个网络设备的唯一标识(网卡出厂的时候就确定了)

    最开始网络层和数据链路层协议是被独立发明的,因此都包含地址

  • 类型:描述载荷数据是什么类型

    ARP 协议:用来构造转发表的内容

3.5 应用层

3.5.1 DNS

由于 IP 地址不方便使用,因此使用域名来代表相应的 IP 地址,这就需要在网络通信过程中将域名转换为 IP 地址。

最早的域名解析系统就是通过 hosts 文件来实现的。记录了域名和 IP 地址的映射关系

DNS 系统就是一组服务器,保存了域名和 IP 地址的映射关系。如果想访问某个域名,就得先访问 DNS 服务器,查询该域名对应的 IP 地址,然后再访问该 IP 地址的服务器。后面如果有域名更新,只需要更新这组服务器即可,无需修改每个用户电脑上的 hosts 文件

直接访问 DNS 服务器(根域名服务器),高并发如何解决?

  1. 开源

    各个网络运营商搭建了多组 ”DNS 镜像服务器“,镜像服务器中的数据都从 DNS 服务器中同步,用户访问的时候优先访问距离自己最近的镜像服务器

  2. 节流

    每个网络设备建立本地缓存,在访问域名时,若本地缓存中存在,则很长时间内都直接使用本地缓存中域名对应的 IP 地址,若本地缓存中不存在,再访问镜像服务器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Undefined name!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值