1. Socket 基础回顾:TCP/IP 协议栈、Socket 通信模型、客户端/服务器架构
好,咱们正式开始。第一节课,我不会一上来就扔一堆安全攻防的东西。你得先站稳了,才能跑。
Socket 安全,说白了就是「在网络上安全地收发数据」。但如果你连数据怎么发、怎么收、谁先发、谁先收都搞不清楚,那安全防护就是空中楼阁。所以,咱们先花点时间,把地基夯实。
1.1 TCP/IP 协议栈:数据是怎么「包」起来的?
你想想看,你在浏览器里输入一个网址,敲回车,到网页显示出来,中间发生了什么?数据不是一口气从你的电脑飞到服务器的。它得一层一层地打包,再一层一层地拆包。
这个打包拆包的规则,就是 TCP/IP 协议栈。我习惯把它理解成一个「俄罗斯套娃」的过程。
核心概念:TCP/IP 协议栈分为四层,每一层各司其职。数据从应用层往下走,每经过一层,就加上一个头部(Header),这叫「封装」。到了接收端,从下往上走,每层拆掉一个头部,这叫「解封装」。
具体是哪四层?我列个表,你一看就明白。
| 层级 | 核心协议 | 职责 | 我眼中的它 |
|---|---|---|---|
| 应用层 | HTTP, FTP, SMTP | 定义数据格式和业务逻辑 | 「我要发什么内容」 |
| 传输层 | TCP, UDP | 提供端到端的通信,保证可靠性或实时性 | 「怎么发,要不要确认」 |
| 网络层 | IP | 寻址和路由,找到目标主机 | 「发给谁,走哪条路」 |
| 网络接口层 | 以太网, Wi-Fi | 物理传输,把数据变成电信号或光信号 | 「怎么传过去」 |
嗯,这里要注意。我们做 Socket 编程,打交道最多的是传输层。你写代码时,选择 socket.SOCK_STREAM 就是 TCP,选择 socket.SOCK_DGRAM 就是 UDP。这个选择,直接决定了你后面要处理的安全模型。
我的经验:我在项目中遇到过不少新手,用 TCP 传实时视频流,结果卡得要死。后来换成 UDP,虽然丢了几帧,但流畅度上去了。选协议,本质是在「可靠」和「实时」之间做取舍。安全防护也一样,TCP 有 TCP 的防法,UDP 有 UDP 的招数。
1.2 Socket 通信模型:一个「插座」的故事
Socket 这个词,直译就是「插座」。我觉得这个比喻特别形象。你想让两台机器通信,就像要让两个电器通电。你得在每台机器上都有一个「插座」,然后拿一根「线」(网络)把它们连起来。
这个「插座」在代码里长什么样?就是一个文件描述符。你可以对它进行读、写、关闭操作。我刚开始学的时候,总觉得它很神秘,后来发现,它本质上就是一个「管道」的两端。
咱们来看看一个典型的 TCP Socket 通信流程。我画了一张图,帮你理清思路。
这张图里,客户端和服务器各自调用的函数,就是 Socket 编程的「七步拳」。你把这个流程背下来,写代码就不会乱。
避坑指南:我曾经犯过一个低级错误。在服务器端,我调用了 bind() 之后,忘了调 listen(),直接去调 accept()。结果程序直接报错 OSError: [Errno 22] Invalid argument。嗯,这个错误信息很隐晦,我当时排查了半天。记住,listen() 是把主动套接字变成被动套接字的关键一步,不能省。
1.3 客户端/服务器架构:谁主动,谁被动?
客户端/服务器架构,简称 C/S 架构。这是网络世界里最基础的「生产关系」。客户端是服务的消费者,服务器是服务的提供者。
我举个例子。你打开,给朋友发一条消息。你的手机就是客户端,腾讯的服务器就是服务器。客户端主动发起连接,服务器被动等待连接。这个「主动」和「被动」,决定了它们在安全防护上的侧重点完全不同。
- 客户端:核心是「信任验证」。你怎么确认你连的服务器是真的?不是钓鱼的?所以客户端要做证书校验、域名校验。
- 服务器:核心是「访问控制」。你怎么知道发请求的是真用户?不是黑客?所以服务器要做身份认证、权限校验、防暴力破解。
咱们用一段最简短的 Python 代码,感受一下这个模型。
# 服务器端代码 (server.py)
import socket
# 1. 创建套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 绑定地址和端口
server_socket.bind(('0.0.0.0', 8888))
# 3. 开始监听
server_socket.listen(5)
print("服务器已启动,等待客户端连接...")
# 4. 接受连接
client_socket, client_addr = server_socket.accept()
print(f"客户端 {client_addr} 已连接")
# 5. 接收数据
data = client_socket.recv(1024)
print(f"收到数据: {data.decode()}")
# 6. 发送响应
client_socket.send(b"Hello, Client!")
# 7. 关闭连接
client_socket.close()
server_socket.close()
# 客户端代码 (client.py)
import socket
# 1. 创建套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 连接服务器
client_socket.connect(('127.0.0.1', 8888))
# 3. 发送数据
client_socket.send(b"Hello, Server!")
# 4. 接收响应
data = client_socket.recv(1024)
print(f"收到响应: {data.decode()}")
# 5. 关闭连接
client_socket.close()
我的习惯:我写 Socket 代码时,一定会把 bind() 的 IP 地址写成 '0.0.0.0',而不是 '127.0.0.1'。前者表示监听所有网络接口,后者只监听本地回环。如果你在服务器上只绑定了 127.0.0.1,那外网是永远连不进来的。这个坑,我见过不少人踩过。