🐍 网络基础 — socket 编程入门
🕐 预计用时:2-3 小时 | 🎯 目标:掌握 TCP/IP 概念、socket 模块、简易客户端/服务端
📖 今日目录
1. 网络通信基础
网络通信的本质就是两台电脑之间收发数据。
# 一次网络通信的流程
# 服务端: 客户端:
# 1. 创建 socket 1. 创建 socket
# 2. 绑定 IP:端口 2. 连接服务端
# 3. 监听 3. 发送数据
# 4. 接受连接 4. 接收响应
# 5. 收发数据 5. 关闭连接
# 6. 关闭连接
2. TCP vs UDP
3. socket 模块入门
import socket
# 创建 socket 对象
# AF_INET = IPv4, SOCK_STREAM = TCP
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 查看本机 IP
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
print(f"主机名: {hostname}")
print(f"本机 IP: {local_ip}")
# 关闭 socket
s.close()
# 常用 socket 方法
# 服务端:
# s.bind((host, port)) — 绑定地址
# s.listen(n) — 开始监听
# s.accept() — 接受连接,返回 (conn, addr)
# conn.recv(bufsize) — 接收数据
# conn.send(data) — 发送数据
# conn.close() — 关闭连接
# 客户端:
# s.connect((host, port)) — 连接服务端
# s.send(data) — 发送数据
# s.recv(bufsize) — 接收数据
# s.close() — 关闭连接
4. 简易 TCP 服务端
import socket
def start_server(host="127.0.0.1", port=9999):
"""简易 TCP 服务端"""
# 1. 创建 socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 允许端口复用(避免 "Address already in use")
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2. 绑定地址
server.bind((host, port))
# 3. 开始监听(参数是等待队列的最大长度)
server.listen(5)
print(f"🖥️ 服务器启动: {host}:{port}")
print(f"📡 等待客户端连接...\n")
try:
while True:
# 4. 接受客户端连接(阻塞)
conn, addr = server.accept()
print(f"✅ 客户端已连接: {addr}")
# 5. 收发数据
while True:
data = conn.recv(1024) # 最多接收 1024 字节
if not data:
break # 客户端断开
message = data.decode("utf-8")
print(f"📩 收到: {message}")
# 回复客户端
response = f"服务器已收到: {message}"
conn.send(response.encode("utf-8"))
# 6. 关闭连接
conn.close()
print(f"❌ 客户端断开: {addr}\n")
except KeyboardInterrupt:
print("\n🛑 服务器关闭")
finally:
server.close()
# 运行服务器
start_server()
5. 简易 TCP 客户端
import socket
def start_client(host="127.0.0.1", port=9999):
"""简易 TCP 客户端"""
# 1. 创建 socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# 2. 连接服务器
client.connect((host, port))
print(f"✅ 已连接到 {host}:{port}")
while True:
# 3. 发送数据
message = input("📤 输入消息 (quit退出): ")
if message.lower() == "quit":
break
client.send(message.encode("utf-8"))
# 4. 接收响应
data = client.recv(1024)
print(f"📩 响应: {data.decode('utf-8')}")
except ConnectionRefusedError:
print("❌ 连接失败:服务器未启动")
finally:
client.close()
print("🔌 已断开连接")
# 运行客户端
start_client()
⚠️ 测试方法:开两个终端窗口,一个运行服务器,一个运行客户端。先启动服务器,再启动客户端。
6. TCP 聊天室
# ---- 服务端:多客户端聊天室 ----
import socket
import threading
clients = {} # {conn: name}
lock = threading.Lock()
def broadcast(message, exclude=None):
"""广播消息给所有客户端"""
with lock:
for conn in clients:
if conn != exclude:
try:
conn.send(message.encode("utf-8"))
except:
pass
def handle_client(conn, addr):
"""处理单个客户端"""
try:
conn.send("请输入你的昵称: ".encode("utf-8"))
name = conn.recv(1024).decode("utf-8").strip()
with lock:
clients[conn] = name
broadcast(f"📢 {name} 加入了聊天室!")
print(f"✅ {name} ({addr}) 已连接")
while True:
data = conn.recv(1024)
if not data:
break
message = data.decode("utf-8")
broadcast(f"[{name}] {message}", exclude=conn)
print(f"[{name}] {message}")
except Exception as e:
print(f"❌ {addr} 错误: {e}")
finally:
with lock:
name = clients.pop(conn, "未知")
conn.close()
broadcast(f"📢 {name} 离开了聊天室")
print(f"❌ {name} 已断开")
def start_chat_server(host="127.0.0.1", port=9999):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen(10)
print(f"💬 聊天室服务器启动: {host}:{port}")
try:
while True:
conn, addr = server.accept()
thread = threading.Thread(target=handle_client, args=(conn, addr))
thread.daemon = True
thread.start()
except KeyboardInterrupt:
print("\n🛑 服务器关闭")
finally:
server.close()
# start_chat_server()
7. UDP 通信
# ---- UDP 服务端 ----
import socket
def udp_server(host="127.0.0.1", port=9998):
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # DGRAM = UDP
server.bind((host, port))
print(f"🖥️ UDP 服务器启动: {host}:{port}")
while True:
data, addr = server.recvfrom(1024)
message = data.decode("utf-8")
print(f"📩 [{addr}] {message}")
response = f"已收到: {message}"
server.sendto(response.encode("utf-8"), addr)
# ---- UDP 客户端 ----
def udp_client(host="127.0.0.1", port=9998):
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
message = input("📤 消息: ")
if message == "quit":
break
client.sendto(message.encode("utf-8"), (host, port))
data, addr = client.recvfrom(1024)
print(f"📩 响应: {data.decode('utf-8')}")
client.close()
💡 TCP vs UDP socket 区别:
• TCP: SOCK_STREAM,用 send/recv
• UDP: SOCK_DGRAM,用 sendto/recvfrom
8. socket 超时处理
import socket
def connect_with_timeout(host, port, timeout=5):
"""带超时的连接"""
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.settimeout(timeout) # 设置超时时间
try:
client.connect((host, port))
print(f"✅ 连接成功: {host}:{port}")
return client
except socket.timeout:
print(f"⏰ 连接超时: {host}:{port}")
client.close()
return None
except ConnectionRefusedError:
print(f"❌ 连接被拒绝: {host}:{port}")
client.close()
return None
# 使用
conn = connect_with_timeout("127.0.0.1", 9999, timeout=3)
if conn:
conn.send("Hello".encode())
conn.settimeout(3) # 接收也设超时
try:
data = conn.recv(1024)
print(f"收到: {data.decode()}")
except socket.timeout:
print("⏰ 接收超时")
conn.close()
9. 实战:文件传输
# ---- 文件传输服务端 ----
import socket
import os
def file_server(host="127.0.0.1", port=9997):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen(1)
print(f"📁 文件服务器启动: {host}:{port}")
conn, addr = server.accept()
print(f"✅ 客户端连接: {addr}")
# 接收文件名和大小
meta = conn.recv(1024).decode()
filename, filesize = meta.split("|")
filesize = int(filesize)
print(f"📄 接收文件: {filename} ({filesize} bytes)")
conn.send(b"READY")
# 接收文件内容
received = 0
with open(f"received_{filename}", "wb") as f:
while received < filesize:
data = conn.recv(min(4096, filesize - received))
if not data:
break
f.write(data)
received += len(data)
print(f"✅ 接收完成: {received} bytes")
conn.close()
server.close()
# ---- 文件传输客户端 ----
def file_client(filepath, host="127.0.0.1", port=9997):
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, port))
filename = os.path.basename(filepath)
filesize = os.path.getsize(filepath)
# 发送文件信息
client.send(f"{filename}|{filesize}".encode())
response = client.recv(1024) # 等待 "READY"
# 发送文件内容
sent = 0
with open(filepath, "rb") as f:
while True:
data = f.read(4096)
if not data:
break
client.send(data)
sent += len(data)
print(f"✅ 发送完成: {sent} bytes")
client.close()
10. 今日小结
| | |
|---|
| | socket(AF_INET, SOCK_STREAM) |
| | s.bind(("127.0.0.1", 9999)) |
| | s.listen(5) |
| | conn, addr = s.accept() |
| | send/recv |
| | .encode() / .decode() |
🎯 练习建议:
1. 给聊天室添加"私聊"功能(@用户名 消息)
2. 实现一个简易 FTP:客户端可以列出服务器上的文件、上传、下载
3. 用 UDP 实现一个简易 DNS 查询工具
📚 Day39 完成!明天学习 HTTP 协议 — 理解 Web 的基础