4、Python Socket模块入门:socket()函数、bind()、listen()、accept()、connect()、send()、recv()

好,咱们正式开始动手写代码了。

前面几章聊了那么多理论,IP、端口、TCP 三次握手……说实话,光看概念容易晕。我当年学 Socket 的时候,看了三天书,脑子里还是一团浆糊。直到我打开 Python,敲了几行 socket(),才真正明白——哦,原来网络通信就是这么回事。

这一章,咱们就把 Python 的 socket 模块里最核心的几个函数,一个一个掰开揉碎了讲。你跟着我敲一遍,保证能跑通第一个网络程序。

4.1 一切从 socket() 开始

你要跟别人通信,总得先有个电话机吧?socket() 就是干这个的——它创建一个网络通信的端点,也就是一个套接字对象。

Python 里调用起来很简单:

import socket

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

这里有两个参数,我解释一下:

  • AF_INET:指定使用 IPv4 地址。你想想看,现在大部分网络还是 IPv4 的天下,所以这个最常用。如果你要搞 IPv6,就用 AF_INET6。
  • SOCK_STREAM:指定使用 TCP 协议,面向连接的、可靠的流式传输。如果你要做 UDP,就用 SOCK_DGRAM。

我的小习惯:我一般会把创建套接字的参数写成常量,比如 socket.AF_INET 而不是直接写数字。虽然 AF_INET = 2,但写数字可读性太差了,别人看代码还得猜。

创建完之后,这个 s 就是一个套接字对象。它现在还是个「裸机」,啥也没绑定,啥也没监听。接下来咱们要给它分配一个身份。

4.2 bind() —— 给套接字一个地址

套接字创建好了,你得告诉操作系统:「这个套接字归我管,我要用它监听某个 IP 和端口。」这就是 bind() 的工作。

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 8888))

bind() 接收一个元组,格式是 (IP地址, 端口号)。

  • IP 地址:写 '0.0.0.0' 表示监听本机所有可用的网络接口。如果你只想让某个特定 IP 访问,就写具体的 IP,比如 '192.168.1.100'。
  • 端口号:0-65535 之间的整数。注意,1024 以下的端口是「知名端口」,通常被系统服务占用,比如 HTTP 的 80、HTTPS 的 443。你自己测试的话,建议用 8000-9999 之间的端口,避免冲突。

我曾经踩过的坑:有一次我 bind 了一个端口,程序跑完没正常关闭,第二次再跑就报 OSError: [Errno 98] Address already in use。这是因为操作系统还没释放那个端口。解决办法是设置 SO_REUSEADDR 选项:

s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

这个设置告诉系统:「端口占用了没关系,我就是要复用它。」开发调试时特别好用。

4.3 listen() —— 开启监听模式

绑定好地址之后,套接字还不会主动接收连接。你得调用 listen(),告诉操作系统:「我准备好了,可以接受客户端的连接请求了。」

s.listen(5)

这个参数 5 是什么意思?它指定了「未完成连接队列」的最大长度。说白了,就是最多允许 5 个客户端在排队等待被处理。如果超过这个数,新的连接请求会被拒绝。

我解释一下这个「排队」的机制:

  • 客户端发来连接请求,操作系统先把它放到一个「半连接队列」里。
  • 等三次握手完成,这个连接就被移到「已完成连接队列」。
  • listen() 的参数控制的就是这个已完成队列的长度。

实际项目中的经验:并发量不大的服务,设 5 或 10 就够了。如果是高并发场景,比如 Web 服务器,一般会设到 128 甚至更高。但别设太大,否则内存占用会飙升。

4.4 accept() —— 接受客户端连接

这是服务端最核心的一步。accept() 会阻塞等待,直到有客户端连上来。一旦有连接,它会返回两个东西:

  1. 一个新的套接字对象 conn,专门用来和这个客户端通信。
  2. 客户端的地址 addr,是一个元组 (IP, 端口)。
conn, addr = s.accept()
print(f"客户端 {addr} 已连接")

注意,原来的 s 套接字还在继续监听,它不负责收发数据。真正干活的是 conn。这就像公司前台——前台接电话,然后把电话转给对应的同事去聊。

关键点:accept() 是阻塞的。如果一直没有客户端连接,程序就会停在这一行不动。所以实际项目中,通常会把 accept() 放在一个循环里,或者用多线程来处理多个客户端。

4.5 connect() —— 客户端主动出击

服务端那边准备好了,客户端怎么连上去?用 connect()。

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8888))

connect() 的参数也是一个元组 (IP, 端口)。客户端不需要 bind(),因为操作系统会自动给它分配一个临时端口。你只管指定要连谁就行。

这里有个细节:connect() 会触发三次握手。如果服务端没在监听,或者网络不通,它会抛出一个 ConnectionRefusedError。嗯,这个错误我见过太多次了——经常是服务端忘了启动,或者端口写错了。

4.6 send() 和 recv() —— 真正的数据传输

连接建立之后,就可以收发数据了。这是整个 Socket 编程里最直观的部分。

服务端接收数据:recv()

data = conn.recv(1024)
print(f"收到消息:{data.decode('utf-8')}")

recv() 的参数是缓冲区大小,单位是字节。这里设 1024,表示一次最多读 1024 个字节。如果对方发来的数据超过这个数,剩下的数据会留在缓冲区里,下次再读。

返回值是一个 bytes 对象。所以一般要调用 .decode() 转成字符串。

客户端发送数据:send()

client.send("你好,服务端!".encode('utf-8'))

send() 的参数必须是 bytes 类型。所以字符串要先用 .encode() 转一下。

注意:send() 不一定能把所有数据一次性发完。它返回的是实际发送的字节数。如果网络拥堵,可能只发了一部分。稳妥的做法是用 sendall(),它会循环发送直到全部发完。

client.sendall("这是一条完整消息".encode('utf-8'))

4.7 完整示例:一个来回通信

光说不练假把式。咱们写一个完整的 TCP 服务端和客户端,让它们互相说句话。

服务端代码(server.py):

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 8888))
server.listen(5)
print("服务端已启动,等待连接...")

conn, addr = server.accept()
print(f"客户端 {addr} 已连接")

# 接收客户端消息
data = conn.recv(1024)
print(f"收到:{data.decode('utf-8')}")

# 回复客户端
conn.send("消息已收到,谢谢!".encode('utf-8'))

conn.close()
server.close()

客户端代码(client.py):

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8888))

# 发送消息
client.send("你好,服务端!".encode('utf-8'))

# 接收回复
reply = client.recv(1024)
print(f"服务端回复:{reply.decode('utf-8')}")

client.close()

运行顺序:先跑 server.py,再跑 client.py。你会看到服务端打印出收到的消息,客户端打印出回复。

4.8 知识体系总览

为了让你对整个流程有个直观的印象,我画了一张图。这张图把服务端和客户端的函数调用顺序、以及它们之间的交互关系都串起来了。

Socket 编程核心函数调用流程 Socket 编程核心函数调用流程 服务端 (Server) socket() 创建套接字 bind() 绑定地址和端口 listen() 开启监听 accept() 等待连接 recv() / send() 收发数据 close() 关闭连接 客户端 (Client) socket() 创建套接字 connect() 连接服务端 send() / recv() 收发数据 close() 关闭连接 三次握手 数据流 服务端先启动,调用 accept() 阻塞等待;客户端调用 connect() 发起连接 三次握手完成后,accept() 返回,双方开始收发数据

4.9 函数对比速查表

为了方便你记忆,我把这几个核心函数的角色和特点整理成了表格:

函数 所属方 作用 是否阻塞
socket() 双方 创建套接字对象 否
bind() 服务端 绑定 IP 和端口 否
listen() 服务端 开启监听模式 否
accept() 服务端 等待并接受客户端连接 是
connect() 客户端 向服务端发起连接 是
send() 双方 发送数据 否(可能部分发送)
recv() 双方 接收数据 是

4.10 避坑指南

最后,我把自己这些年踩过的坑总结一下,你遇到了可以直接翻回来看看:

  • 端口被占用:跑完程序记得 close(),或者加上 SO_REUSEADDR。
  • 数据粘包:TCP 是流式协议,多次 send() 的数据可能合并成一次 recv() 收到。解决办法是自定义协议,比如在数据前面加上长度字段。
  • recv() 返回空字节:如果 recv() 返回 b'',说明对方已经关闭了连接。这时候你也该 close() 了。
  • send() 没发完:用 sendall() 代替 send(),省心。

好了,这一章的内容就到这儿。你把这些代码敲一遍,跑通一次,后面学多线程、非阻塞 IO 什么的,心里就有底了。