Socket 创建与配置:socket() 函数详解、地址族与套接字类型、协议参数选择
写网络程序,第一件事就是创建 Socket。这就像你要打电话,得先找一部电话机。Python 里创建 Socket 就靠 socket() 这个函数。别看它简单,参数选不对,后面全是坑。
我个人习惯,写任何网络代码前,先把这三个参数想清楚:地址族、套接字类型、协议。今天咱们就把它们掰开揉碎了讲。
socket() 函数签名
先看原型:
socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
前三个参数是核心。fileno 是文件描述符,日常开发基本用不到,我做了这么多年项目,也就用过一两次。
返回值是一个 Socket 对象。嗯,这里要注意:如果创建失败,会抛出 OSError。我见过不少新手不处理这个异常,程序一跑就崩。
地址族(Address Family)
地址族决定了 Socket 用什么「地址格式」。说白了,就是告诉系统:我要跟谁通信,对方的地址长什么样。
| 常量 | 值 | 说明 |
|---|---|---|
AF_INET |
2 | IPv4,最常用,比如 192.168.1.1 |
AF_INET6 |
23 | IPv6,比如 ::1 |
AF_UNIX |
1 | 本地进程间通信,用文件路径 |
AF_PACKET |
17 | 底层网络接口,抓包用 |
我建议初学者先死磕 AF_INET。为什么?因为 90% 的网络编程场景都是 IPv4。你想想看,写个 Web 服务、连个数据库、做个聊天室,哪个不是 IPv4?
有一次我在项目中遇到一个诡异的问题:客户的内网环境只支持 IPv6,但代码里写死了 AF_INET。结果部署上去,Socket 创建成功,连接死活连不上。排查了半天才发现是地址族不匹配。从那以后,我写代码都会加个配置项,让运维人员可以切换地址族。
套接字类型(Socket Type)
套接字类型决定了数据传输的方式。这是整个 Socket 编程里最核心的概念之一。
| 常量 | 值 | 说明 |
|---|---|---|
SOCK_STREAM |
1 | 面向连接的可靠字节流(TCP) |
SOCK_DGRAM |
2 | 无连接的数据报(UDP) |
SOCK_RAW |
3 | 原始套接字,直接操作 IP 层 |
SOCK_STREAM 就像打电话。你先拨号(连接),然后双方建立一条通道,数据按顺序到达,不丢包。适合 HTTP、FTP、SSH 这些需要可靠传输的场景。
SOCK_DGRAM 就像寄明信片。你写好地址扔邮筒里,对方能不能收到、什么时候收到,你管不了。适合视频直播、DNS 查询、游戏状态同步这些可以容忍少量丢包的场景。
我个人习惯:能用 TCP 就别用 UDP。除非你明确知道自己在做什么,并且愿意处理丢包、乱序、重传这些麻烦事。我曾经接手过一个老项目,用 UDP 做文件传输,结果自己实现了一套可靠传输协议,代码量比 TCP 还大,性能还差。何必呢?
SOCK_RAW 需要 root 权限才能创建。我曾经在开发机上跑得好好的,部署到生产环境就报权限错误,排查了半天才发现是用户权限不够。生产环境千万别用 raw socket,除非你有充分的理由。
协议参数选择
第三个参数 proto 通常填 0,让系统自动选择。但有些场景需要手动指定。
| 协议常量 | 值 | 说明 |
|---|---|---|
IPPROTO_TCP |
6 | TCP 协议 |
IPPROTO_UDP |
17 | UDP 协议 |
IPPROTO_ICMP |
1 | ICMP 协议(ping 用) |
IPPROTO_RAW |
255 | 原始 IP 协议 |
为什么有时候要手动指定?举个例子:你想创建一个 ICMP 的原始套接字来做 ping 工具。这时候 type=SOCK_RAW,proto=IPPROTO_ICMP。如果不指定协议,系统不知道你要操作哪个上层协议。
但日常开发中,proto=0 就够了。系统会根据 type 自动匹配:SOCK_STREAM 配 TCP,SOCK_DGRAM 配 UDP。
常见组合方式
我整理了一张表,把最常用的组合列出来:
| 用途 | family | type | proto |
|---|---|---|---|
| TCP 客户端/服务端 | AF_INET |
SOCK_STREAM |
0 |
| UDP 客户端/服务端 | AF_INET |
SOCK_DGRAM |
0 |
| 本地 TCP 通信 | AF_UNIX |
SOCK_STREAM |
0 |
| ping 工具 | AF_INET |
SOCK_RAW |
IPPROTO_ICMP |
| 抓包工具 | AF_PACKET |
SOCK_RAW |
htons(ETH_P_ALL) |
socket 模块里这些常量都有定义。写代码时用 socket.AF_INET 而不是直接写数字 2,可读性会好很多。我见过有人写 socket(2, 1, 0),三个月后自己都看不懂。
代码示例:创建不同类型的 Socket
import socket
# 创建 TCP Socket(最常用)
tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 创建 UDP Socket
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 创建 IPv6 TCP Socket
tcp6_sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
# 创建本地 UNIX Socket
unix_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
# 创建 ICMP 原始 Socket(需要 root 权限)
icmp_sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
print("所有 Socket 创建成功")
运行这段代码,你会看到「所有 Socket 创建成功」。但注意,ICMP 那个如果没有 root 权限会报错。我建议你开个虚拟机或者容器来测试 raw socket,别在宿主机上乱搞。
核心逻辑流程图
下面这张图展示了 Socket 创建时的决策路径。我画这张图的时候,特意把最常见的路径标粗了:
从图里你能看到,创建 Socket 就是一条「先选地址族,再选类型」的路径。协议参数通常交给系统处理,除非你有特殊需求。
常见错误与避坑
- 地址族不匹配:用
AF_INET创建的 Socket 不能连接 IPv6 地址。我曾经犯过这个错,调试了整整一个下午。 - 类型与协议冲突:
SOCK_STREAM配IPPROTO_UDP会报错。系统会告诉你「协议不支持」。 - 忘记处理异常:
socket()可能因为权限、资源不足等原因失败。一定要用try/except包起来。 - 重复创建:每次通信都创建新 Socket,用完不关闭。这会导致文件描述符泄漏。我建议用
with语句自动管理生命周期。
socket(family, type, proto) 三个参数决定了 Socket 的「身份」。日常开发用 socket(AF_INET, SOCK_STREAM) 就够了。记住:地址族管「地址格式」,类型管「传输方式」,协议管「具体协议」。选对了,后面就顺了。