1. 网络基础

1.1 浏览器是如何工作的?

浏览器的工作过程大致可以分为以下几个主要步骤:

  1. 解析 URL: 分析 URL 所需要使用的传输协议和请求的资源路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,则对非法字符进行转义后在进行下一过程。
  2. 缓存判断: 浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里且没有失效,那么就直接使用,否则向服务器发起新的请求。
  3. DNS 解析: 如果资源不在本地缓存,首先需要进行 DNS 解析。浏览器会向本地 DNS 服务器发送域名解析请求,本地 DNS 服务器会逐级查询,最终找到对应的 IP 地址。
  4. 获取 MAC 地址:当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相结合,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
  5. 建立 TCP 连接: 主机将使用目标 IP 地址和目标 MAC 地址发送一个 TCP SYN 包,请求建立一个 TCP 连接,然后交给路由器转发,等路由器转到目标服务器后,服务器回复一个 SYN-ACK 包,确认连接请求。然后,主机发送一个 ACK 包,确认已收到服务器的确认,然后 TCP 连接建立完成。
  6. HTTPS 的 TLS 四次握手: 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的四次握手。
  7. 发送 HTTP 请求: 连接建立后,浏览器会向服务器发送 HTTP 请求。请求中包含了用户需要获取的资源的信息,例如网页的 URL、请求方法(GET、POST 等)等。
  8. 服务器处理请求并返回响应: 服务器收到请求后,会根据请求的内容进行相应的处理。例如,如果是请求网页,服务器会读取相应的网页文件,并生成 HTTP 响应。
  9. 浏览器接收响应: 浏览器接收服务器返回的响应数据。
  10. 解析 HTML 和构建 DOM 树: 浏览器解析收到的 HTML 文档,构建文档对象模型(DOM)树,以表示页面的结构。
  11. 下载和处理 CSS 样式: 浏览器获取并解析 CSS 文件,应用样式到 DOM 树中的元素,构建 CSSOM 树。
  12. 执行 JavaScript 脚本: 浏览器执行页面中的 JavaScript 代码,这可能会修改 DOM 树、执行动态效果等。
  13. 布局和绘制: 根据 DOM 树、CSS 样式和 JavaScript 的操作结果,浏览器进行页面布局计算,确定每个元素的位置和大小,然后进行绘制,将页面显示在屏幕上。
  14. 后续交互: 用户与页面进行交互,如点击链接、提交表单等,浏览器重复上述过程获取新的页面或处理用户操作。

一个 URL(统一资源定位符)的访问过程中,涉及到了多个协议。下面是一些常见的协议:

  1. HTTP(超文本传输协议):用于在 Web 浏览器和服务器之间传输和交互数据,是互联网上常用的协议。
  2. HTTPS(HTTP 安全):是在 HTTP 的基础上增加了安全性的协议,通过使用 SSL 或 TLS 加密通信内容,保证了数据传输的机密性和完整性。
  3. FTP(文件传输协议):用于在客户端和服务器之间传输文件,支持上传、下载等文件操作。
  4. SMTP(简单邮件传输协议):用于发送和传输电子邮件的协议。
  5. POP3(邮局协议版本 3):用于从服务器接收电子邮件的协议。
  6. IMAP(Internet 邮件访问协议):也是用于接收邮件的协议,相比于 POP3,它具有更多的功能,如在服务器上保留电子邮件的副本等。
  7. DNS(域名系统):用于将 URL 中的域名映射为 IP 地址,以便进行网络通信。
  8. TCP/IP(传输控制协议/互联网协议):是一组用于在网络中传输数据的协议,包括 IP(Internet Protocol)用于寻址和路由,以及 TCP(Transmission Control Protocol)用于可靠数据传输。

1.2 DNS 查询

DNS 解析流程分为递归查询和迭代查询,递归查询是以本地名称服务器为中心查询,递归查询是默认方式,迭代查询是以 DNS 客户端,也就是客户机器为中心查询。

由于递归查询对于被查询的域名服务器负担太大,通常采用下面右图的模式:从请求主机到本地域名服务器的查询是递归查询,而其余的查询是迭代查询。

为了提高 DNS 的查询效率,并减轻根域名服务器的负荷和减少因特网上的 DNS 查询报文数量,在域名服务器中广泛地使用了高速缓存。高速缓存用来存放最近查询过的的域名以及从何处获得域名映射信息的记录。

DNS 域名解析服务使用的默认端口号是 53,DNS 的标准端口号为 53/UDP 和 53/TCP。

UDP 通常用于一般的 DNS 查询,而 TCP 通常用于大型数据传输或特殊情况下的 DNS 查询。

1.3 TCP 基础知识

1.3.1 TCP 连接

TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。三次握手的过程如下图:

  1. 浏览器向发送一个 TCP 同步包(SYN)给服务器,等待确认。
  2. 服务器收到 SYN,返回一个同步确认包(SYN-ACK)。
  3. 浏览器收到服务器这个同步确认包(SYN-ACK),向服务器发送一个最终的连接确认包(ACK),当服务器收到 ACK 确认包,连接建立。

1.3.2 TCP 和 UDP 的区别及应用

TCP 和 UDP 区别:

  1. 连接
    • TCP 是面向连接的传输层协议,传输数据前先要建立连接。
    • UDP 是不需要连接,即刻传输数据。
  2. 服务对象
    • TCP 是一对一的两点服务,即一条连接只有两个端点。
    • UDP 支持一对一、一对多、多对多的交互通信
  3. 可靠性
    • TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
    • UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议。
  4. 拥塞控制、流量控制
    • TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
    • UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
  5. 首部开销
    • TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。
    • UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
  6. 传输方式
    • TCP 是流式传输,没有边界,但保证顺序和可靠。
    • UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
  7. 分片不同
    • TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
    • UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。

TCP 和 UDP 应用场景:

由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:

  • FTP 文件传输;
  • HTTP / HTTPS;

由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:

  • 包总量较少的通信,如 DNS 、SNMP 等;
  • 视频、音频等多媒体通信;
  • 广播通信;

1.3.3 OSI 和 TCP/IP 网络模型

TCP/IP 的四层网络模型由以下四层组成:

  1. 网络接口层(Network Interface Layer)
    • 也称为数据链路层和物理层。
    • 负责处理与物理网络介质的连接,包括将数据转换为适合在网络上传输的比特流,以及处理网络硬件(如网卡)的通信。
  2. 网络层(Internet Layer)
    • 主要协议是 IP(Internet Protocol,网际协议)。
    • 负责为数据包选择路径,将数据包从源主机路由到目标主机,实现网络中的寻址和路由功能。
  3. 传输层(Transport Layer)
    • 主要协议包括 TCP(Transmission Control Protocol,传输控制协议)和 UDP(User Datagram Protocol,用户数据报协议)。
    • 为应用程序提供端到端的通信服务,负责数据的分段、传输和重组,以及流量控制和错误控制。
  4. 应用层(Application Layer)
    • 包含了各种应用程序协议,如 HTTP、FTP、SMTP、DNS 等。
    • 为用户提供网络服务,如网页浏览、文件传输、电子邮件等。

1.4 HTTP 请求

请求报文格式:

1
2
3
  请求行     {     请求方法(get/post等)+     URL     +     协议版本号     }     
+ 头部 { 按照规范,根据自己需要来选择性添加 }
+ 包体 { 即数据内容,post请求时,这部分才有数据;get请求时,数据附在URL参数里,格式为【URL?参数名=参数值】 }

响应报文格式:

1
2
3
  状态行     {     协议版本号     +     状态码     +     状态码说明     }     
+ 头部 { 按照规范,根据自己需要来选择性添加 }
+ 包体 { 一般为服务端返回给客户端的数据 }

状态码:

2. Socket 网络编程

2.1 本质与内核

2.1.1 网络编程定义

网络编程是指使用编程语言来实现计算机之间通过网络进行通信和数据交换的过程。

通过网络编程,可以让不同的计算机程序在网络环境中相互协作、共享信息和资源。它涉及到对网络协议(如 TCP/IP 等)的理解和运用,以及使用编程语言提供的相关接口和库来实现诸如建立连接、发送和接收数据、处理错误、实现并发等功能。

2.1.2 常见的网络编程应用

  • 客户端 - 服务器应用:如 Web 服务器和浏览器之间的交互,邮件客户端和邮件服务器之间的通信。
  • 分布式系统:多个计算机协同工作完成一个共同的任务,通过网络进行通信和协调。
  • 即时通讯应用:如聊天软件。
  • 文件共享和下载应用。
  • 网络游戏:多个玩家通过网络连接进行互动。

2.1.3 Scoket 套接字

  • 基本概念: Socket 是通信的基石,是支持 TCP/IP 协议的网络通信的基本操作单元,在网络通信过程中端点的一种抽象表示。网络中使用 Socket 传输数据是一种特殊的网络 I/O。
  • 工作模式: 打开(open)-> 读写(write/read)-> 关闭(close)
  • 五种信息: socket 包括了数据传输必须的五元组,分别为源 IP、源端口、目的 IP、目的端口和协议号
  • 通信机制: 基于流(stream)或者基于数据报(datagram)
  • python 中使用:
1
2
3
from socket import *

scoket(...)

2.2 UDP 实现

2.2.1 UDP 实现原理

  • 服务端和客户端初始化 socket,得到文件描述符;
  • 服务端调用 bind,将 socket 绑定在指定的 IP 地址和端口;
  • 客户端调用 sendto 写入数据;服务端调用 recvfrom 读取数据;
  • 客户端断开连接时,会调用 close,那么服务端 recvfrom 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

2.2.2 UDP 服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import datetime
from socket import *

# 创建 UDP 套接字
server_socket = socket(AF_INET, SOCK_DGRAM)

# 绑定服务器的 IP 和端口
server_address = ('127.0.0.1', 5000)
server_socket.bind(server_address)
print("开启 UDP 服务,等待接收数据...")

while True:
# 接收客户端数据
data, client_address = server_socket.recvfrom(1024)
print(f"在{datetime.datetime.now()},从 {client_address} 接收到: {data.decode('gbk')}")

if data.decode('gbk') == "exit":
print("聊天到此结束")
break

# 发送响应
response = "已收到你的消息!"
server_socket.sendto(response.encode('gbk'), client_address)

2.2.3 UDP 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from socket import *

# 创建 UDP 套接字
client_socket = socket(AF_INET, SOCK_DGRAM)

while True:
data = input("请输入内容:")
server_address = ('127.0.0.1', 5000) # 服务器的 IP 和端口
client_socket.sendto(data.encode('gbk'), server_address)

if data == "exit":
print("聊天结束...")
break

data, server = client_socket.recvfrom(1024) # 接收服务器的响应
print(f"从服务器接收到: {data.decode('gbk')}")

# 关闭套接字
client_socket.close()

2.2.4 运行结果

  1. 先运行服务端代码

  2. 再运行客户端代码,并发送内容

  3. 回到服务端,可以发现接收到了客户端发送的信息

  4. 在客户端输入 exit 退出程序

  5. 服务端收到客户端退出指令,同样也退出程序

2.3 TCP 实现

2.3.1 TCP 实现原理

下面主要是讲 TCP 的实现原理,在 Python 具体实现的时候,将下面的 write 和 read 函数分别替换成 send 和 recv 函数即可。

  • 服务端和客户端初始化 socket,得到文件描述符;
  • 服务端调用 bind,将 socket 绑定在指定的 IP 地址和端口;
  • 服务端调用 listen,进行监听;
  • 服务端调用 accept,等待客户端连接;
  • 客户端调用 connect,向服务端的地址和端口发起连接请求;
  • 服务端 accept 返回用于传输的 socket 的文件描述符;
  • 客户端调用 write 写入数据;服务端调用 read 读取数据;
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。

所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。

成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

2.3.2 TCP 服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from socket import *

# 创建一个 TCP 套接字
server_socket = socket(AF_INET, SOCK_STREAM)

# 绑定服务器的 IP 和端口
server_address = ('127.0.0.1', 5001)
server_socket.bind(server_address)

# 开始监听连接,允许最多 5 个未处理的连接请求在队列中等待
server_socket.listen(5)

print("TCP 服务器正在监听...")

while True:
# 等待客户端连接
client_socket, client_address = server_socket.accept()
print(f"与客户端 {client_address} 建立连接")

# 接收客户端发送的数据
data = client_socket.recv(1024)
print(f"从客户端收到: {data.decode('utf-8')}")

# 向客户端发送响应
response = "已收到你的消息!"
client_socket.send(response.encode('utf-8'))

# 关闭与客户端的连接
client_socket.close()

2.3.3 UDP 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import socket

# 创建一个 TCP 套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 服务器的 IP 和端口
server_address = ('127.0.0.1', 5001) # 请确保与服务器端的端口一致

# 连接到服务器
client_socket.connect(server_address)

# 要发送的数据
message = "Hello, TCP Server!"
client_socket.send(message.encode('utf-8'))

# 接收服务器的响应
data = client_socket.recv(1024)
print(f"从服务器接收到: {data.decode('utf-8')}")

# 关闭套接字
client_socket.close()

2.3.4 运行结果

  1. 先启动服务端,然后再启动两个客户端并向服务端发送信息

  2. 在服务端就可以接收到客户端发送来的信息