我写了500行代码,终于搞懂了Socket到底是怎么回事!
看完《网络是怎样连接的》,还是一头雾水?别怕,我写了可运行的Python代码,一步步带你拆解!
为什么还是看不懂?
很多文章用各种类比解释Socket:
-
有人说像打电话
-
有人说像寄快递
-
有人说像管道
但看完之后,你还是不知道:
-
描述符到底是啥?
-
bind和listen干什么用?
-
accept返回的新套接字和原来的有什么区别?
-
客户端怎么知道服务器关闭了连接?
这次不一样,我们用真实可运行的代码,一步步拆解!
先运行看效果
把下面的代码保存为 socket_demo.py,运行起来:
python socket_demo.py
你会看到完整的通信过程,从创建套接字到关闭连接,每一步都有输出。
完整代码(可运行)
import socket
import threading
import time
def start_server():
"""启动一个简单的TCP服务器"""
print("========== 服务器端 ==========")
# 1. 创建服务器套接字
print("1. 创建服务器套接字...")
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(f" 服务器套接字描述符: {server_socket.fileno()}")
# 设置SO_REUSEADDR选项,避免"Address already in use"错误
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2. 绑定地址和端口
print("2. 绑定地址和端口...")
server_address = ('127.0.0.1', 9999)
server_socket.bind(server_address)
print(f" 绑定到: {server_address[0]}:{server_address[1]}")
# 3. 开始监听连接
print("3. 开始监听连接...")
server_socket.listen(1) # 最多允许1个等待连接
print(" 服务器正在监听,等待客户端连接...")
def handle_client(client_socket, client_address):
"""处理客户端连接"""
print(f"\n[服务器] 接收到来自 {client_address} 的连接")
print(f"[服务器] 客户端套接字描述符: {client_socket.fileno()}")
try:
# 4. 接收客户端消息
print("[服务器] 等待接收客户端消息...")
data = client_socket.recv(1024)
if data:
message = data.decode('utf-8')
print(f"[服务器] 收到客户端消息: {message}")
# 5. 发送响应给客户端
response = f"服务器已收到你的消息: '{message}'"
print(f"[服务器] 发送响应: {response}")
client_socket.sendall(response.encode('utf-8'))
except Exception as e:
print(f"[服务器] 处理客户端时出错: {e}")
finally:
# 6. 关闭客户端套接字
print("[服务器] 关闭客户端连接")
client_socket.close()
return server_socket, handle_client
def start_client():
"""启动客户端连接服务器"""
print("\n========== 客户端 ==========")
# 1. 创建客户端套接字
print("1. 创建客户端套接字...")
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(f" 客户端套接字描述符: {client_socket.fileno()}")
# 2. 设置服务器地址
server_address = ('127.0.0.1', 9999)
print(f"2. 设置服务器地址: {server_address[0]}:{server_address[1]}")
try:
# 3. 连接到服务器
print("3. 连接到服务器...")
client_socket.connect(server_address)
print(" 连接成功!")
# 获取连接后的本地和远程地址
local_addr = client_socket.getsockname()
remote_addr = client_socket.getpeername()
print(f" 本地地址: {local_addr[0]}:{local_addr[1]}")
print(f" 远程地址: {remote_addr[0]}:{remote_addr[1]}")
# 4. 发送消息到服务器
message = "Hello Server! 这是来自客户端的消息。"
print(f"4. 发送消息到服务器: {message}")
client_socket.sendall(message.encode('utf-8'))
# 5. 接收服务器响应
print("5. 等待服务器响应...")
data = client_socket.recv(1024)
response = data.decode('utf-8')
print(f" 收到服务器响应: {response}")
# 6. 关闭连接
print("6. 关闭连接...")
except ConnectionRefusedError:
print("连接被拒绝,请确保服务器已启动")
except Exception as e:
print(f"客户端出错: {e}")
finally:
client_socket.close()
print(" 连接已关闭")
def demo_socket_connection():
"""演示完整的Socket连接过程"""
print("Socket连接全过程演示")
print("=" * 50)
# 启动服务器
server_socket, client_handler = start_server()
# 在新线程中启动服务器接受连接
def run_server():
while True:
client_socket, client_address = server_socket.accept()
# 在新线程中处理客户端
client_thread = threading.Thread(
target=client_handler,
args=(client_socket, client_address)
)
client_thread.daemon = True
client_thread.start()
# 只处理一个连接然后退出循环
break
server_thread = threading.Thread(target=run_server)
server_thread.daemon = True
server_thread.start()
# 等待服务器启动
time.sleep(0.5)
# 启动客户端
start_client()
# 等待所有操作完成
time.sleep(1)
# 关闭服务器套接字
server_socket.close()
print("\n" + "=" * 50)
print("演示完成!")
def test_socket_functions():
"""测试和演示socket相关函数"""
print("\n========== Socket函数测试 ==========")
# 创建一个UDP套接字对比
print("\n1. 创建TCP和UDP套接字对比:")
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
print(f" TCP套接字类型: {tcp_socket.type}")
print(f" UDP套接字类型: {udp_socket.type}")
print(f" TCP套接字描述符: {tcp_socket.fileno()}")
print(f" UDP套接字描述符: {udp_socket.fileno()}")
# 获取套接字选项
print("\n2. 获取套接字选项:")
print(f" TCP_NODELAY: {tcp_socket.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY)}")
print(f" SO_REUSEADDR: {tcp_socket.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)}")
# 获取地址族和协议
print("\n3. 套接字基本信息:")
print(f" 地址族: {tcp_socket.family}")
print(f" 协议: {tcp_socket.proto}")
tcp_socket.close()
udp_socket.close()
if __name__ == "__main__":
# 运行完整的Socket连接演示
demo_socket_connection()
# 运行Socket函数测试
test_socket_functions()
print("\n" + "=" * 50)
print("关键概念总结:")
print("1. 描述符: 操作系统返回的整数标识,用于标识套接字")
print("2. bind(): 将套接字绑定到特定IP和端口")
print("3. listen(): 开始监听传入连接")
print("4. accept(): 接受客户端连接,返回新的套接字")
print("5. connect(): 客户端连接到服务器")
print("6. send()/recv(): 发送和接收数据")
print("7. close(): 关闭套接字释放资源")
运行这个代码,你会看到:
Socket连接全过程演示
* 服务器端 *
- 创建服务器套接字...
服务器套接字描述符: 3
- 绑定地址和端口...
绑定到: 127.0.0.1:9999
- 开始监听连接...
服务器正在监听,等待客户端连接...
* 客户端 *
- 创建客户端套接字...
客户端套接字描述符: 4
-
设置服务器地址: 127.0.0.1:9999
-
连接到服务器...
连接成功!
本地地址: 127.0.0.1:12345
远程地址: 127.0.0.1:9999
[服务器] 接收到来自 ('127.0.0.1', 12345) 的连接
[服务器] 客户端套接字描述符: 5
[服务器] 等待接收客户端消息...
- 发送消息到服务器: Hello Server! 这是来自客户端的消息。
[服务器] 收到客户端消息: Hello Server! 这是来自客户端的消息。
[服务器] 发送响应: 服务器已收到你的消息: 'Hello Server! 这是来自客户端的消息。'
- 等待服务器响应...
收到服务器响应: 服务器已收到你的消息: 'Hello Server! 这是来自客户端的消息。'
- 关闭连接...
[服务器] 关闭客户端连接
连接已关闭
现在一步步拆解!
🤔 第一个问题:客户端和服务器都需要套接字吗?
这是一个非常常见的问题!很多人认为:
-
"服务器才需要套接字,客户端直接连接就行"
-
或者 "客户端不需要创建套接字,因为套接字是用来监听的"
答案是:客户端和服务器都需要套接字!
为什么都需要?
套接字的本质:套接字是通信的接口,就像电话的听筒。
打电话的场景:
┌─────────────────┐ ┌─────────────────┐
│ 客户端(你) │ │ 服务器(朋友) │
│ │ │ │
│ 📱 电话机 │◀────电话线─────▶│ 📱 电话机 │
│ (套接字) │ │ (套接字) │
└─────────────────┘ └─────────────────┘
类比理解:
-
客户端套接字 = 你的电话听筒
-
服务器套接字 = 朋友的电话听筒
-
双方都需要电话听筒才能通话!
代码验证
服务器创建套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(f"服务器套接字描述符: {server_socket.fileno()}")
输出: 服务器套接字描述符: 3
客户端也创建套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(f"客户端套接字描述符: {client_socket.fileno()}")
输出: 客户端套接字描述符: 4
看到没有?双方都调用了 *socket() *!
两者的区别
虽然都需要套接字,但它们的用途不同:
| 方面 | 服务器套接字 | 客户端套接字 |
| 创建目的 | 被动等待连接 | 主动发起连接 |
| 使用流程 | socket → bind → listen → accept | socket → connect |
| 数量 | 可以服务多个客户端 | 通常连接一个服务器 |
| 类比 | 餐厅(接待很多客人) | 食客(去一个餐厅吃饭) |
完整的双向通信模型
客户端 服务器
│ │
├─ 创建套接字 │
│ client_socket = socket() │
│ (握住电话听筒) │
│ │
│ ├─ 创建套接字
│ │ server_socket = socket()
│ │ (握住电话听筒)
│ │
├─ 连接到服务器 │
│ client_socket.connect() │
│ (拨号) │
│ │
│ ├─ 接受连接
│ │ accept()
│ │ (接电话)
│ │
│ 【双方都通过套接字通信】 │
│ │
├─ send() ──────────────────────→ recv()
│ (说话) │ (听)
│ │
├─ recv() ←────────────────────── send()
│ (听) │ (说话)
│ │
└─ close() close()
(挂电话) (挂电话)
记忆技巧
套接字就像电话听筒:
- 双方都需要握着听筒才能通话
- 服务器 = 电话亭(可以接多个电话)
- 客户端 = 普通电话(打一个电话)
第一步:创建套接字和描述符
服务器端
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(f"服务器套接字描述符: {server_socket.fileno()}")
输出: 服务器套接字描述符: 3
客户端
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(f"客户端套接字描述符: {client_socket.fileno()}")
输出: 客户端套接字描述符: 4
🎯 关键概念:套接字(Socket)
是什么?
-
套接字是通信的抽象接口
-
就像电话的听筒,握住它才能开始通信
-
客户端和服务器都需要创建套接字
参数说明:
-
AF_INET:IPv4地址族
-
SOCK_STREAM:TCP流式套接字(可靠连接)
关键理解:
客户端套接字 ≠ 服务器套接字
两者是独立的对象,分别运行在:
-
客户端进程
-
服务器进程
类比:
套接字 = 电话听筒
客户端套接字 = 你的听筒
服务器套接字 = 朋友的听筒
双方都需要听筒才能通话!
🎯 关键概念:描述符(Descriptor)
是什么?
-
描述符是操作系统给你的整数门牌号
-
就像快递公司给你的"运单号:SF123456789"
-
操作系统用这个号来管理你的套接字
为什么是3、4、5?
0 → 标准输入(stdin)
1 → 标准输出(stdout)
2 → 标准错误(stderr)
3 → 第一个套接字
4 → 第二个套接字
...
类比:
你创建套接字 = 填寄货单
描述符 = 运单号 SF123456789
操作系统 = 快递公司内部系统
第二步:绑定地址和端口
只有服务器需要绑定
server_address = ('127.0.0.1', 9999)
server_socket.bind(server_address)
输出: 绑定到: 127.0.0.1:9999
🎯 关键概念:bind()
为什么服务器需要bind?
-
服务器需要告诉操作系统:"我要监听这个IP和端口"
-
客户端就知道往哪里发连接请求
类比:
bind() = 在门口挂个牌子
"营业时间:9:00-18:00"
"服务地址:127.0.0.1:9999"
IP地址和端口的作用:
-
127.0.0.1:本地回环地址(只在本机通信)
-
9999:端口号(可以理解为一栋楼的房间号)
为什么客户端不需要bind?
- 客户端是主动连接的一方
- 操作系统会自动分配一个随机端口
- 就像:你是去餐厅吃饭,餐厅需要门牌号,你不需要
第三步:开始监听
server_socket.listen(1)
print("服务器正在监听,等待客户端连接...")
🎯 关键概念:listen()
做什么?
-
将套接字从"普通模式"切换到"监听模式"
-
开始等待客户端的连接请求
参数1是什么?
-
最大等待队列长度
-
如果有2个客户端同时连接,第2个会排队,第3个会被拒绝
类比:
listen() = 服务员站在门口
"欢迎光临!请取号排队"
(最多允许1个人等待)
第四步:客户端连接
客户端
client_socket.connect(server_address)
print("连接成功!")
获取连接后的地址
local_addr = client_socket.getsockname() # 本地地址
remote_addr = client_socket.getpeername() # 远程地址
print(f"本地地址: {local_addr[0]}:{local_addr[1]}")
print(f"远程地址: {remote_addr[0]}:{remote_addr[1]}")
输出:
连接成功!
本地地址: 127.0.0.1:54321
远程地址: 127.0.0.1:9999
🎯 关键概念:connect()
发生了什么?(三次握手)
客户端 服务器
│ │
│ ① SYN: "我想建立连接" ──────────────→ │
│ │
│ ←──────────────────── ② SYN-ACK │
│ "好的,我也想建立连接" │
│ │
│ ③ ACK: "确认收到" ─────────────────→ │
│ │
│ 连接建立! │
类比:
你:服务员,我要点餐! (SYN)
服务员:好的,请问点什么? (SYN-ACK)
你:我要一份宫保鸡丁 (ACK)
→ 订单建立
getsockname() vs getpeername():
- getsockname():自己的地址(我:127.0.0.1:54321)
- getpeername():对方的地址(对方:127.0.0.1:9999)
第五步:服务器接受连接
服务器端
client_socket, client_address = server_socket.accept()
print(f"接收到来自 {client_address} 的连接")
print(f"客户端套接字描述符: {client_socket.fileno()}")
输出:
[服务器] 接收到来自 ('127.0.0.1', 54321) 的连接
[服务器] 客户端套接字描述符: 5
🎯 关键概念:accept()
返回了什么?
client_socket, client_address = server_socket.accept()
# ↓
# 新的套接字(描述符5)
# ↓
客户端的地址信息
为什么要返回新的套接字?
server_socket(描述符3)= 主管,负责门口迎接客人
client_socket(描述符5)= 服务员,负责服务具体客人
类比:
餐厅老板(server_socket):站在门口,等客人来
→ 客人来了
老板叫来服务员(client_socket)
服务员专门服务这个客人
老板继续在门口等下一个客人
关键理解:
- server_socket(描述符3):只负责accept,收新的客户端
- client_socket(描述符5):负责和具体客户端通信
第六步:发送和接收数据
客户端发送
message = "Hello Server!"
client_socket.sendall(message.encode('utf-8'))
服务器接收
data = client_socket.recv(1024)
message = data.decode('utf-8')
print(f"收到客户端消息: {message}")
🎯 **关键概念:send() 和 recv()
数据流向:
客户端send() → 网络 → 服务器recv()
↓
数据在这里
recv(1024)是什么意思?
-
最多接收1024字节
-
如果对方发了2000字节,需要调用2次recv
-
如果对方发了500字节,只会收到500字节
类比:
send() = 把信投进邮筒
recv() = 去信箱取信(一次最多拿1024封信)
第七步:关闭连接
客户端
client_socket.close()
服务器
client_socket.close()
server_socket.close()
🎯 关键概念:close()
发生了什么?(四次挥手)
客户端(client_socket) 服务器(client_socket)
│ │
│ ① FIN: "我要关闭连接" ──────────────→ │
│ (挂电话) │
│ │
│ ←──────────────────── ② ACK │
│ "收到,你说完了吗" │
│ │
│ ←──────────────────── ③ FIN │
│ "我也说完了" │
│ (挂电话) │
│ │
│ ④ ACK: "好的,再见" ────────────────→ │
│ │
│ 连接关闭! │
类比:
你(客户端):我说完了,挂电话 (FIN)
朋友(服务器):收到,你说完了吗 (ACK)
朋友(服务器):我也说完了,挂电话 (FIN)
你(客户端):好的,再见 (ACK)
→ 通话结束
注意:双方都有自己的套接字,都需要关闭!
关键问题:客户端怎么知道服务器关闭了连接?
问题场景
客户端代码
while True:
data = client_socket.recv(1024)
if data:
print(f"收到: {data.decode('utf-8')}")
else:
print("服务器关闭了连接")
break
关键点: *recv() ***返回空字符串时,说明连接已关闭!
演示代码
def demo_close_detection():
"""演示客户端如何检测服务器关闭连接"""
print("\n* 检测连接关闭 *")
# 服务器
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('127.0.0.1', 9998))
server_socket.listen(1)
def server_task():
client_sock, _ = server_socket.accept()
# 发送一条消息
client_sock.sendall("第一条消息".encode('utf-8'))
time.sleep(1)
# 发送第二条消息
client_sock.sendall("第二条消息".encode('utf-8'))
time.sleep(1)
# 关闭连接
print("[服务器] 关闭连接")
client_sock.close()
server_thread = threading.Thread(target=server_task)
server_thread.daemon = True
server_thread.start()
time.sleep(0.5)
# 客户端
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(('127.0.0.1', 9998))
print("[客户端] 开始接收消息...")
count = 0
while True:
try:
data = client_socket.recv(1024)
if not data:
# recv返回空字符串,说明连接已关闭
print(f"[客户端] 收到空数据,连接已关闭!")
break
count += 1
print(f"[客户端] 收到第{count}条消息: {data.decode('utf-8')}")
except Exception as e:
print(f"[客户端] 异常: {e}")
break
client_socket.close()
server_socket.close()
print("[客户端] 客户端关闭")
if __name__ * "__main__":*
* demo_close_detection()*
运行结果:
检测连接关闭 ==
[客户端] 开始接收消息...
[客户端] 收到第1条消息: 第一条消息
[客户端] 收到第2条消息: 第二条消息
[服务器] 关闭连接
[客户端] 收到空数据,连接已关闭!
[客户端] 客户端关闭
总结:检测连接关闭的3种方法
重要:recv()返回空字符串说明对方(无论是客户端还是服务器)关闭了它的套接字!
方法1:recv()返回空字符串(最常用)
data = client_socket.recv(1024)
if not data:
print("对方关闭了连接")
# 对方调用了 close()
理解:
-
服务器调用 client_socket.close() → 客户端recv()返回空
-
客户端调用 client_socket.close() → 服务器recv()返回空
-
双方都需要关闭自己的套接字
-
break
方法2:捕获异常
try:
data = client_socket.recv(1024)
except ConnectionResetError:
print("连接被重置")
方法3:使用select/poll(高级)
import select
ready = select.select([client_socket], [], [], timeout)
if ready[0]:
data = client_socket.recv(1024)
完整流程图总结
【服务器】 【客户端】
│ │
├─1. socket() → 描述符3 │
│ ├─1. socket() → 描述符4
├─2. bind(127.0.0.1:9999) │
│ ├─2. connect()
├─3. listen(1) │ (三次握手)
│ │
├─4. accept() → 描述符5 │
│ (主管叫来服务员) │
│ ├─3. send()
├─5. recv() ───────────────────────→ │ "Hello"
│ │
│ ├─4. recv()
├─6. send() ←─────────────────────── │ (收到响应)
│ "Thanks" │
│ │
└─7. close() └─5. close()
(四次挥手)
常见误区解答
❌ **误区1:只有服务器需要套接字
错误理解: 只有服务器需要套接字,客户端不需要
正确理解:
-
客户端和服务器**都需要套接字
-
套接字 = 通信接口(电话听筒)
-
双方都需要握住听筒才能通话
类比:
打电话:
你需要电话听筒 ←→ 朋友也需要电话听筒
(客户端套接字) (服务器套接字)
❌ **误区2:描述符就是套接字
错误理解: 描述符 = 套接字
正确理解:
-
套接字 = 通信对象(就像一个邮箱)
-
描述符 = 访问套接字的索引(就像邮箱号码)
操作系统内部:
描述符3 → 客户端套接字对象(包含IP、端口、缓冲区等)
描述符4 → 服务器监听套接字
描述符5 → 服务器与客户端通信的套接字
...
❌ **误区3:accept()返回的套接字和原来的套接字一样
错误理解: accept返回的client_socket和server_socket是同一个
正确理解:
server_socket (描述符4) = 主管(只负责accept新连接)
client_socket (描述符5) = 服务员(负责具体通信)
每个accept都返回新的套接字!
❌ **误区3:connect()后就建立了物理连接
错误理解: connect()后,两个电脑之间拉了一根物理线
正确理解:
-
TCP连接是逻辑连接,不是物理连接
-
通过IP地址、端口号、序列号等来标识连接
-
就像:电话连接是逻辑的,不是真的拉了一根电话线
❌ **误区4:recv(1024)保证收到1024字节
错误理解: recv(1024)一定会收到1024字节
正确理解:
-
recv(1024) = 最多接收1024字节
-
可能收到0字节(对方关闭连接)
-
可能收到1-1024字节(取决于网络情况)
正确的做法
data = b''
while len(data) < 1024:
chunk = client_socket.recv(1024 - len(data))
if not chunk:
break
data += chunk
记忆技巧:一张表格搞定
| 概念 | 类比 | 一句话记住 |
| socket | 电话听筒 | 通信的接口,双方都需要 |
| 描述符 | 运单号 | 操作系统给的索引 |
| bind() | 挂营业牌子 | 告诉别人我在哪 |
| listen() | 站门口等客人 | 开始监听连接 |
| connect() | 拨电话 | 主动发起连接 |
| accept() | 接电话 | 返回新的套接字 |
| send() | 说话 | 发送数据 |
| recv() | 听 | 接收数据 |
| close() | 挂电话 | 关闭连接 |
| recv返回空 | 对方挂了 | 对方关闭了套接字 |
关键公式
客户端和服务器都需要套接字 = 双方都需要电话听筒
套接字 + 描述符 = 通信的访问方式
bind + listen = 服务器准备就绪
connect = 客户端主动连接
accept = 服务器接受连接,返回新套接字
send + recv = 双向通信
close = 四次挥手,关闭连接
recv返回空字符串 = 对方关闭连接
总结
看完这篇文章和代码,你应该理解了:
✅ 客户端和服务器都需要套接字:双方都需要电话听筒才能通信
✅ 套接字:通信的抽象接口
✅ 描述符:操作系统给的整数索引
✅ bind() :服务器绑定IP和端口
✅ listen() :开始监听连接
✅ connect() :客户端发起连接(三次握手)
✅ accept() :服务器接受连接,返回新套接字
✅ send() / recv() :发送和接收数据
✅ close() :关闭连接(四次挥手)
✅ 检测连接关闭:recv()返回空字符串(对方关闭了它的套接字)
记住最关键的一点:
客户端和服务器都需要套接字,就像双方都需要电话听筒才能通话!
描述符是门牌号,套接字是房间。描述符帮你找到套接字,套接字负责实际的通信。
如果这篇文章对你有帮助,请点个赞👍 ,让更多人看到!