网络编程

Table of Contents

摘录自 → 网络编程·廖雪峰

网编编程是 Java 最擅长的方向之一,使用 Java 进行网络编程时,由虚拟机实现了底层复杂的网编协议,Java 程序只需要调用 Java 标准库提供的接口,就可以简单地编写网络程序。

网络编程基础

在学习 Java 网络编程之前,我们先来了解什么是计算机网络。

计算机网络是指两台或更多的计算机组成的网络,在同一个网络中,任意两台计算机都可以直接通信,因为所有计算机都需要遵循同一种网络协议。像这样,把很多计算机网络连接起来形成的一个全球统一的网络,就是 互联网

如果计算机网络各自在通讯协议不统一,就没法把不同的网络连接起来形成互联网,因此,为了把计算机网络接入互联网,就必须使用 TCP/IP 协议。

IP 地址

i.e. Internet Protocol

在互联网中,一个 IP 地址用于唯一标识一个网络接口(Network Interface),一台联入互联网的计算机至少有一个 IP 地址。

IP 地址分为 IPv4 和 IPv6 两种。IPv4 采用 32 位地址,类似于 101.202.99.12 ,而 IPv6 采用 128 位地址,类似 2001:0DA8:100A:0000:0000:1020:F2F3:1428 。IPv4 地址总共有 232 个(大约 42 亿),而 IPv6 地址则总共有 2128 个(大约 340 万亿亿亿亿),IPv4 的地址目前已经耗尽,而 IPv6 的地址是根本用不完的。

IP 地址又分为公网 IP 地址(可以直接被访问)和内网 IP 地址(只能在内网访问)。

内网 IP 地址类似于: 192.168.x.x10.x.x.x ,还有一个特殊的 IP 地址,称之为本机地址,它总是 127.0.0.1

IPv4 地址实际上是一个 32 位整数,如:

106717964 0x65ca630c
  65 ca 63 0c
  101.202.99.12

如果一台计算机只有一个网卡,并且接入了网络,那么,它有一个本机地址 127.0.0.1 ,还有一个 IP 地址,例如 101.202.99.12 ,可以通过这个 IP 地址接入网络。

如果一台计算机有两块网卡,那么除了本机地址,它可以有两个 IP 地址,可以分别接入两个网络。通常连接两个网络的设备是路由器或者交换机,它至少有两个 IP 地址,分别接入不同的网络,让网络之间连接起来。

如果两台计算机位于同一个网络,那么他们之间可以直接通信,因为他们的 IP 地址是相同的,也就是网络号是相同的。

网络号是 IP 地址通过子网掩码过滤后得到的。

例如:某台计算机的 IP 是 101.202.99.2 ,子网掩码是 255.255.255.0 ,那么该计算机的网络号是:

IP 101.202.99.2
Mask 255.255.255.0
Network (IP & Mask) 101.202.99.0

每台计算机都需要正确配置 IP 地址和子网掩码,根据这两个就可以计算网络号。如果两台计算机计算出的网络号相同,说明两台计算机在同一个网络,可以直接通信;如果网络号不同,说明两台计算机不在同个网络,不能直接通信。

不在同一网络(网络号不同)的计算机,必须通过 路由器或者交换机 这样的网络设备间接通信,我们把这种设备称为 网关

网关的作用就是连接多个网络,负责把来自一个网络的数据包发到另一个网络,这个过程叫 路由

Figure: 一台计算机的一个网卡的 3 个关键配置

域名

IP 地址不容易记忆,所以我们通常使用域名访问某个特定的服务, 域名解析服务器 DNS 负责把域名翻译成对应的 IP ,客户端再根据 IP 地址访问服务器。

nslookup 可以查看域名对应的 IP 地址:

$ nslookup www.rosesor.com
Server:  xxx.xxx.xxx.xxx
Address: xxx.xxx.xxx.xxx#53

Non-authoritative answer:
Name:    www.rosesor.com
Address: 192.112.245.112

有一个特殊的本机域名 localhost ,它对应的 IP 地址总是本机地址 127.0.0.1

网络模型

由于计算机网络从底层的传输到高层的软件设计十分复杂,要合理地设计计算机网络模型,必须采用分层模型,每一层负责处理自己的操作。

OSI(Open System Interconnect)网络模型是 ISO 组织定义的一个计算机互联的标准模型,注意它只是一个定义,目的是为了简化网络各层的操作,提供标准接口便于实现和维护。

互联网实际使用的 TCP/IP 模型并不是对应到 OSI 的 7 层模型,而是大致对应 OSI 的 5 层模型:

TCP/IP 四层模型 TCP/IP 五层模型 OSI 模型 描述
应用层 应用层 应用层 提供应用程序之间的通信
    表示层 处理数据格式,加解密等等
    会话层 负责建立和维护会话
传输层 传输层 传输层 负责提供端到端的可靠传输
互联网层 互联网层 (IP 层) 网络层 负责根据目标地址选择路由来传输数据
  数据链路层 数据链路层 负责把数据进行分片并且真正通过物理
网络接口层 物理层 物理层 网络传输,例如,无线网、光纤等

常用协议

IP 协议是一个分组交换传输协议,它不保证可靠传输,而 TCP 协议是传输控制协议,它是面向连接的协议,支持可靠传输和双向通信。

TCP 协议是建立在 IP 协议之上,简单地说,IP 协议只负责发数据包,不保证顺序和正确性,而 TCP 协议负责控制数据包传输,它在传输数据之前需要先 建立连接 ,连接建立后才能 传输数据 ,传输完成后还需要 断开连接

TCP 协议之所以能保证数据的可靠传输,是通过接收确认、超时重传这些机制实现的。并且,TCP 协议允许双向通信,即通信双方可以同时发送和接收数据。

TCP 协议也是应用最广泛的协议,许多高级协议都是建立在 TCP 协议之上的,例如 HTTP、SMTP 等。

UDP 协议(User Datagram Protocol)是一种数据报文协议,这是元连接协议,不保证可靠传输。因为 UDP 协议在通信前不需要进行连接,所以它的传输效率比 TCP 高,而且 UDP 协议比 TCP 协议要简单得多。

选择 UDP 协议时,传输的数据通常是能容忍丢失的,例如,一些语音视频通信的应用会选择 UDP 协议。

网络基本概念小结

  • 计算机网络:由两台或更多计算机组成的网络;
  • 互联网:连接网络的网络;
  • IP 地址:计算机的网络接口(通常是网卡)在网络中的唯一标识;
  • 网关:负责连接多个网络,并在多个网络之间转发数据的计算机,通常是路由器或交换机;
  • 网络协议:互联网使用 TCP/IP 协议,它泛指互联网协议簇;
  • IP 协议:一种分组交换传输协议;
  • TCP 协议:一种面向连接,可靠传输的协议;
  • UDP 协议:一种无连接,不可靠传输的协议。

TCP 编程

i.e. Transmission Control Protocol

在开发网络应用程序的时候,我们会遇到 Socket ,它是一个抽象概念,一个应用程序通过一个 Socket 来建立一个远程连接,而 Socket 内部通过 TCP/IP 协议把数据传输到网络:

Socket、TCP 和部分 IP 的功能都是由操作系统提供的,不同的编程语言只是提供了对操作系统调用的简单封装。例如,Java 提供的几个关于 Socket 相关的类就封装了操作系统提供的接口。

为什么需要 Socket 进行网络通信?

因为仅仅通过 IP 地址进行通信是不够的,同一台计算机同一时间会运行多个网络应用程序,如浏览器、QQ、音乐播放器等。当操作系统收到一个数据包的时候,如果只有 IP 地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出 Socked 接口,每个应用程序需要各自对应到不同的 Socket,数据包才能根据 Socket 正确地发到对应的应用程序。

一个 Socket 就是由 IP 地址和端口号(范围中 0~65535)组成 ,其中,小于 1024 的端口属于特权端口,需要管理员权限,大于 1024 的端口可以由任意用户的应用程序打开。如:

IE: 101.202.99.2:1201
QQ: 101.202.99.2:1304

使用 Socket 进行网络编程时,本质上就是两个进程之间的网络通信。

其中一个进程必须充当服务器端,它会主动监听某个指定的端口,另一个进程必须充当客户端,它必须主动连接服务器的 IP 地址和指定端口。如果连接成功,服务器和客户端就成功地建立了一个 TCP 连接,双方后续就可以随时发送和接收数据。

当 Socket 连接成功地在服务器端和客户端之间建立后:

  • 对服务器端来说,它的 Socket 是指定的 IP 地址和指定的端口号;
  • 对客户端来说,它的 Socket 是它所在计算机的 IP 地址和一个由操作系统分配的随端口号。

服务器端

要全用 Socket 编程,我们首先要编写服务器端程序。

Java 标准库提供了 ServerSocket 来实现对指定 IP 和指定端口的监听,其典型实现代码如下:

 1: public class Server {
 2:     public static void main(String[] args) throws IOException {
 3:         ServerSocket ss = new ServerSocket(6666); // 监听指定端口
 4:         System.out.println("Server is running...");
 5:         for (;;) {
 6:             Socket sock == ss.accept();
 7:             System.out.println("connected from " + sock.getRemoteSocketAddress());
 8:             Thread t = new Handler(sock);
 9:             t.start();
10:         }
11:     }
12: }
13: 
14: class Handler extends Thread {
15:     Socked sock;
16: 
17:     public Handler(Socket sock) {
18:         this.sock = sock;
19:     }
20: 
21:     @Override
22:     public void run() {
23:         try (InputStream input = this.sock.getInputStream()) {
24:             try (OutputStream output = this.sock.getOutputStream()) {
25:                 handle(input, output);
26:             }
27: 
28:         } catch (Exception e) {
29:             try {
30:                 this.sock.close();
31:             } catch (IOException ioe) {
32:             }
33:             System.out.println("client disconnected.");
34:         }
35:     }
36: }
37: 
38: private void handle(InputStream input, OutputStream output) throws IOException {
39:     var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
40:     var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
41:     writer.write("hello\n");
42:     writer.flush();
43:     for(;;) {
44:         String s = reader.readLine();
45:         if (s.equals("bye")) {
46:             writer.write("bye\n");
47:             writer.flush();
48:             break;
49:         }
50:         writer.write("ok: " + s + "\n");
51:         writer.flush();
52:     }
53: }

让我们来分析一下上面的代码吧。

服务器端通过代码:

1: ServerSocket ss = new ServerSocket(6666);

在指定端口 6666 监听,此处没有指定 IP 地址,表示在计算机的所有网络接口上进行监听。

如果 ServerSocket 监听成功,我们就使用一个无限循环来处理客户端的连接:

1: for(;;) {
2:     Socket sock = ss.accept();
3:     Thread t = new Handler(sock);
4:     t.start();
5: }

注意到代码 ss.accept() 表示每当有新的客户端连接进来后,就返回一个 Socket 实例,这个 Socket 实例就是用来和刚连接的客户端进行通信的。由于客户端很多,要实现并发处理,我们就必须为每个新的 Socket 创建一个新线程来处理,这样,主线程的作用就是接收新的连接,每当收到新连接后,就创建一个新线程进行处理。

我们还可以利用线程池来处理客户端连接,能大大提高运行效率。

如果客户端连接进来, accept() 方法会阻塞并一直等待。如果有多个客户端同时连接进来, ServerSocket 会把连接扔到队列里,然后一个一个处理。对于 Java 程序而言,只需要通过循环不断调用 accept() 就可以获取新的连接。

客户端

相比服务器端,客户端程序就要简单很多。一个典型的客户端程序如下:

 1: public class Client {
 2:     public static void main(String[] args) throws IOException {
 3:         Socket sock = new Socket("localhost", 6666); // 连接指定服务器和端口
 4:         try (InputStream input = sock.getInputStream()) {
 5:             try (OutputStream output = sock.getOutputStream()) {
 6:                 handle(input, output);
 7:             }
 8:         }
 9:     }
10: 
11:     private static void handle(InputStream input, OutputStream output) throws IOException {
12:         var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
13:         var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
14:         Scanner scanner = new Scanner(System.in);
15:         System.out.println("[server] " + reader.readLine());
16:         for (;;) {
17:             System.out.println(">>> ");    // 打印提示
18:             String s = scanner.nextLine(); // 读取一行输入
19:             writer.write(s);
20:             writer.newLine();
21:             writer.flush();
22:             String resp = reader.readLine();
23:             System.out.println("<<< " + resp);
24:             if (resp.equals("bye")) {
25:                 break;
26:             }
27:         }
28:     }
29: }

客户端程序通过:

1: Socked sock = new Socket("localhost", 6666);

连接到服务器端,注意上述代码的服务器地址是 localhost ,表示本机地址,端口号是 6666 ,如果连接成功,将返回一个 Socket 实例,用于后续通信。

Socket 流

当 Socket 连接创建后成功后,无论是服务器端,还是客户端,我们都使用 Socket 实例进行网络通信。

因为 TCP 是一种基于流的协议,因此,Java 标准库使用 InputStreamOutputStream 来封装 Socket 的数据流,这样我们使用 Socket 的流,和普通 IO 流类似:

1: // 用于读取网络数据:
2: InputStream in = sock.getInputStream();
3: // 用于写入网络数据:
4: OutputStream out = sock.getOutputstream();

最后,我们重点来看看,为什么写入网络数据时,要调用 flush() 方法。

如果不调用 flush() ,很可能会发现客户端和服务器都收不到数据,这并不是 Java 标准库的设计问题,而是我们以流的形式写入数据的时候,并不是一写入就立刻发送到网络,而是先写入内存缓冲区,直至缓冲区满了以后,才会一次性真正发送到网络,这样设计的目的是为了提高传输效率。

如果缓冲区很少,而我们又想强制把这些数据发送到网络,就必须调用 flush() 强制把缓冲区数据发送出去。

TCP 编程小结

使用 Java 进行 TCP 编程时,需要使用 Socket 模型:

  • 服务器端使用 ServerSocket 监听指定端口;
  • 客户端使用 Socket(InetAddress, port) 连接服务器;
  • 服务器端用 accept() 接收并返回 Socket
  • 双方通过 Socket 打开 InputStream/OutputStream 读写数据;
  • 服务器端通常使用多线程同时处理多个客户端连接,利用线程池可大幅提升效率;
  • flush() 用于强制输出缓冲区到网络。

UDP 编程

和 TCP 编程相比,UDP 编程就简单得多,因为 UDP 没有创建连接,数据包也是一次收发一个,所以没有流的概念。

在 Java 中使用 UDP 编程,仍然需要使用 Socket ,因为应用程序在使用 UDP 时必须指定网络接口(IP)和端口号。

*注:UDP 端口和 TCP 端口虽然都使用 0~65535 ,但他们是两套独立的端口,即一个应用程序用 TCP 占用了端口 1234 ,不影响另一个应用程序用 UDP 占用端口 1234

服务器端

在服务器端,使用 UDP 也需要监听指定的端口,Java 提供了 DatagramSocket 来实现这个功能,代码如下:

 1: DatagramSocket ds = new DatagramSocket(6666); // 监听指定端口
 2: for (;;) {
 3:     // 数据缓冲区
 4:     byte[] buffer = new byte[1024];
 5:     DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
 6:     ds.receive(packet);                       // 收取一个 UDP 数据包
 7:     // 收取到的数据存储在 buffer 中,由 packet.getOffset(),packet.getLength() 指定起始位置和长度
 8:     // 将其按 UTF-8 编码转换为 String
 9:     String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
10:     // 发送数据
11:     byte[]data = "ACK".getBytes(StandardCharsets.UTF_8);
12:     packet.setData(data);
13:     ds.send(packet);
14: }

服务器端首先使用如下语句指定的端口监听 UDP 数据包:

1: DatagramSocket ds = new DatagramSocket(6666);

如果没有其他应用程序占据这个端口,那么监听成功,我们就使用一个无限循环来处理收到的 UDP 数据包:

1: for (;;) {
2:     ...
3: }

要接收一个 UDP 数据包,需要准备一个 byte[] 缓冲区,并通过 DatagramPacket 实现接收:

1: byte[] buffer = new byte[1024];
2: DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
3: ds.receive(packet);

假设我们收取到的是一个 String ,那么,通过 DatagramPacket 返回的 packet.getOffset()packet.getLength() 确定数据在缓冲区的起止位置:

1: String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);

当服务器收到一个 DatagramPacket 后,通常必须立刻回复一个或多个 UDP 包,因为客户端地址在 DatagramPacket 中,每次收到的 DatagramPacket 可能是不同的客户端,如果不回复,客户端就收不到任何 UDP 包。

发送 UDP 包也是通过 DatagramPacket 实现的,发送代码非常简单:

1: byte[] data = ...
2: packet.setData(data);
3: ds.send(packet);

客户端

和服务端相比,客户端使用 UDP 时,只需要直接向服务器端发送 UDP 包,然后接收返回的 UDP 包:

 1: DatagramSocket ds = new DatagramSocket();
 2: ds.setSoTimeOut(1000);
 3: ds.connect(InetAddress.getByName("localhost"), 6666); // 连接指定服务器和端口
 4: // 发送:
 5: byte[] data = "Hello".getBytes();
 6: DatagramPacket packet = new DatagramPacket(data, data.length);
 7: ds.send(packet);
 8: // 接收:
 9: byte[] buffer = new byte[1024];
10: packet = new DatagramPacket(buffer, buffer.length);
11: ds.receive(packet);
12: String resp = new String(packet.getData(), packet.getOffset(), packet.getLength());
13: ds.disconnect();

客户端打开一个 DatagramSocket 使用以下代码:

1: DatagramSocket ds = new DatagramSocket();
2: ds.setSoTimeout(1000);
3: ds.connect(InetAddress.getByName("localhost"), 6666);

客户端创建 DatagramSocket 实例时并不需要指定端口,而是由操作系统自动指定一个当前未使用的端口。紧接着,调用 setSoTimeout(1000) 设定超时 1 秒,意思是后续接收 UDP 包时,等待时间最多不会超过 1 秒,否则在没有收到 UDP 包时,客户端会无限等待下去。这一点和服务端不一样,服务器端可以无限等待,因为它本来就被设计成长时间运行。

注意到客户端的 DatagramSocket 还调用了一个 connect() 方法“连接”到指定的服务器端。

不是说 UDP 是无连接的协议吗?为啥这里需要 connect()

其实,这个 connect() 方法不是真连接,它是为了在客户端的 DatagramSocket 实例中保存服务器的 IP 和端口号,确保这个 DatagramSocket 实例只能往指定的地址和端口发送 UDP 包,不能往其他地址和端口发送。这么做不是 UDP 的限制,而是 Java 内置了安全检查。

如果客户端希望向两个不同的服务器发送 UDP 包,那么它必须创建两个 DatagramSocket 实例。

后续的收发数据和服务器端是一致的。通常来说,客户端必须先发 UDP 包,因为客户端不发 UDP 包,服务端就根本不知道客户端的地址和端口号。如果客户端认为通信结束,就可以调用 disconnect() 断开连接:

1: ds.disconnect();

注意到 disconnect() 也不是真正地断开连接,它只是清除了客户端 DatagramSocket 实例记录的远程服务器地址和端口号,这样, DatagramSocket 实例就可以连接另一个服务器端。

UDP 编程小结

使用 UDP 协议通信时,服务器和客户端双方无需建立连接:

  • 服务器端用 DatagramSocket(port) 监听端口;
  • 客户端使用 DatagramSocket.connect() 指定远程地址和端口;
  • 双方通过 receive()send() 读写数据;
  • DatagramSocket 没有 IO 流接口,数据被直接写入 byte[] 缓冲区。

HTTP 编程

什么是 HTTP ?HTTP 就是目前使用最广泛的 Web 应用程序使用的基础协议,例如,浏览器访问网站,手机 App 访问后台服务器,都是通过 HTTP 协议实现的。

HTTP(HyperText Transfer Protocol)的缩写,超文本传输协议,它是基于 TCP 协议之上的一种“请求-响应”协议。

我们来看一下浏览器请求访问某个网站时发送的 HTTP “请求-响应”。当浏览器希望访问某个网站时,浏览器和网站服务器之间首先建立 TCP 连接,且服务器总是使用 80 端口和加密端口 443 ,然后,浏览器向服务器发送一个 HTTP 请求,服务器收到后,返回一个 HTTP 响应,并且在响应中包含了 HTML 有网页内容,这样,浏览器解析 HTML 后就可以给用户显示网页了。

一个完整的 HTTP 请求-响应如下:

HTTP 请求

HTTP 请求的格式是固定的,它由 HTTP Header 和 HTTP Body 两部分构成。

第一行总是 请求方法 路径 HTTP版本 ,例如, GET / HTTP/1.1 表示使用 GET 请求,路径是 / ,版本是 HTTP/1.1

后续的每一行都是固定的 Header: Value 格式,我们称为 HTTP Header, 服务器依靠某些特定的 Header 来识别客户端请求 。例如:

  • Host :表示请求的域名,因为一台服务器上可能有多个网站,所以有必要依靠 Host 来识别用于请求;
  • User-Agent :表示客户端自身标识信息,不同的浏览器有不同的标识,服务器依靠 User-Agent 判断客户端类型;
  • Accept :表示客户端处理的 HTTP 响应格式, */* 表示任意格式, text/* 表示任意文本, image/png 表示 PNG 格式的图片;
  • Accept-Language :表示客户端接收的语言,多种语言按优先级排序,服务器依靠该字段给用户返回特定语言的网页版本。

如果是 GET 请求,那么该 HTTP 请求只有 HTTP Header,没有 HTTP Body。如果是 POST 请求,那么该 HTTP 请求带有 Body,以一个空行分隔。一个典型的带 Body 的 HTTP 请求如下:

POST /login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30

username=hello&password=123456

POST 请求通常要设置 Content-Type 表示 Body 的类型, Content-Length 表示 Body 的长度,这样服务器就可以根据请求的 Header 和 Body 做出正确的响应。

请求的 Header 就是给服务器用的。

此外, GET 请求的参数必须附加在 URL 上,并以 URLEncoded 方式编码,例如: http://www.example.com/?a=1&b=K%26R ,参数分别是 a=1b=K&R

因为 URL 的长度限制, GET 请求的参数不能太多,而 POST 请求的参数就没有长度限制,因为 POST 请求的参数必须放到 Body 中。并且, POST 请求的参数不一定是 URL 编码,可以按任意格式编码,只需要在 Content-Type 中正确设置即可。

常见的发送 JSON 的 POST 请求如下:

POST /login HTTP/1.1
Content-Type: application/json
Content-Length: 38

{"username": "bob", "password": "123456"}

HTTP 响应

HTTP 响应也是由 Header 和 Body 两部分组成 ,一个典型的 HTTP 响应如下:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 133251

<!DOCTYPE html>
<html><body>
<h1>Hello</h1>
...

响应的第一行总是 HTTP版本 响应代码 响应说明 ,例如, HTTP/1.1 200 OK 表示版本是 HTTP/1.1 ,响应代码是 200 ,响应说明是 OK

客户端只依赖响应代码判断 HTTP 响应是否成功 ,HTTP 有固定的响应代码:

  • 1xx :表示一个提示性响应,例如 101 表示将切换协议,常见于 WebSocket 连接;
  • 2xx :表示一个成功的响应,例如 200 表示成功, 206 表示只发送了部分内容;
  • 3xx :表示一个重定向的响应,例如 301 表示永久重写向, 303 表示客户端应该按指定路径重新发送请求;
  • 4xx :表示一个因为客户端问题导致的错误响应,例如 400 表示因为 Content-Type 等各种原因导致的无效请求, 404 表示指定的路径不存在;
  • 5xx :表示一个因为服务器问题导致的错误响应,例如 500 表示服务器内部故障, 503 表示服务器暂时无法响应。

当浏览器收到第一个 HTTP 响应后,它解析 HTML 后,又会发送一系列 HTTP 请求,例如, GET /logo.jpg HTTP/1.1 请求一个图片,服务器响应请求后,会直接把二进制内容的图片发送给浏览器:

HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 18391

????JFIFHH??XExifMM?i&??X?...(二进制的JPEG图片)

因此,服务器总是被动地接收客户端的一个 HTTP 请求,然后响应它。客户端根据需要发送若干个 HTTP 请求。

对于最早期的 HTTP/1.0 协议,每次发送一个 HTTP 请求,客户端都需要先创建一个新的 TCP 连接,然后,收到服务器响应后,关闭这个 TCP 连接。由于建立 TCP 连接就比较耗时,因此,为了提高效率, HTTP/1.1 协议允许在一个 TCP 连接中反复“发送-响应”,这样就能大大提高效率。

因为 HTTP 协议是一个请求-响应协议,客户端在发送了一个 HTTP 请求后,必须等待服务器响应后,才能发送下一个请求,这样一来,如果某个响应太慢,它就会堵住后面的请求。

TCP 连接是能复用了,等待响应又成问题了……

所以,为了进一步提速, HTTP/2.0 允许客户端在没有收到响应的时候,发送多个 HTTP 请求,服务器返回响应的时候,不一定按顺序返回,只要双方能识别出哪个响应对应哪个请求,就可以做到并行发送和接收:

有了问题,也就有同时产生了解决问题的可能。

Date: 2020-10-05 Mon 09:53

Author: Jack Liu

Created: 2020-10-05 Mon 17:44

Validate