Socket网络编程详解

目录

[一、Socket 编程概述](#一、Socket 编程概述)

[1.1 什么是 Socket](#1.1 什么是 Socket)

[1.2 Python 中的 Socket 模块](#1.2 Python 中的 Socket 模块)

[1.3 两种主流 Socket 类型](#1.3 两种主流 Socket 类型)

[二、Socket 编程基础](#二、Socket 编程基础)

[2.1 地址族](#2.1 地址族)

[2.2 创建 Socket 对象](#2.2 创建 Socket 对象)

[2.3 地址表示](#2.3 地址表示)

[2.4 字节序问题](#2.4 字节序问题)

[三、TCP Socket 编程详解](#三、TCP Socket 编程详解)

[3.1 TCP 通信流程](#3.1 TCP 通信流程)

[3.2 服务端核心步骤](#3.2 服务端核心步骤)

[3.2.1 创建 Socket](#3.2.1 创建 Socket)

[3.2.2 绑定地址与端口](#3.2.2 绑定地址与端口)

[3.2.3 开始监听](#3.2.3 开始监听)

[3.2.4 接受客户端连接](#3.2.4 接受客户端连接)

[3.2.5 收发数据](#3.2.5 收发数据)

[3.2.6 关闭连接](#3.2.6 关闭连接)

[3.3 客户端核心步骤](#3.3 客户端核心步骤)

[3.4 完整示例:回显服务器](#3.4 完整示例:回显服务器)

[3.5 TCP 常见问题与解决方案](#3.5 TCP 常见问题与解决方案)

[3.5.1 粘包问题](#3.5.1 粘包问题)

[3.5.2 端口占用错误](#3.5.2 端口占用错误)

[3.5.3 并发处理多个客户端](#3.5.3 并发处理多个客户端)

[四、UDP Socket 编程详解](#四、UDP Socket 编程详解)

[4.1 UDP 特点](#4.1 UDP 特点)

[4.2 服务端流程](#4.2 服务端流程)

[4.3 客户端流程](#4.3 客户端流程)

[4.4 UDP 注意事项](#4.4 UDP 注意事项)

五、高级主题

[5.1 非阻塞 Socket](#5.1 非阻塞 Socket)

[5.2 I/O 多路复用](#5.2 I/O 多路复用)

[5.3 超时设置](#5.3 超时设置)

[5.4 广播与多播(UDP)](#5.4 广播与多播(UDP))

[5.5 使用 SSL/TLS 加密](#5.5 使用 SSL/TLS 加密)

六、错误处理与调试

[6.1 常见异常](#6.1 常见异常)

[6.2 调试工具](#6.2 调试工具)

[6.3 优雅关闭连接](#6.3 优雅关闭连接)

七、实战案例:简易聊天室

[7.1 服务器端](#7.1 服务器端)

[7.2 客户端](#7.2 客户端)

八、总结

一、Socket 编程概述

1.1 什么是 Socket

Socket(套接字)是网络通信的端点,可以理解为不同进程(本地或远程)之间进行数据交换的"虚拟通道"。它屏蔽了 TCP/IP 协议栈的复杂细节,为应用层提供了一套标准的文件描述符接口 ------ 打开、读写、关闭。

1.2 Python 中的 Socket 模块

Python 标准库中的 socket 模块是对底层操作系统 Socket API 的封装,语法简洁、跨平台,适合快速开发网络应用。无论是 Web 服务器、聊天程序、文件传输工具,还是网络爬虫,都离不开 Socket。

1.3 两种主流 Socket 类型

|--------------------|-----|---------------------|------------------|
| 类型 | 类型 | 核心特点 | 典型应用 |
| socket.SOCK_STREAM | TCP | 面向连接、可靠、按序、无边界、 有重传 | HTTP、FTP、SSH、数据库 |
| socket.SOCK_DGRAM | UDP | 无连接、不可靠、有消息边界、 效率高 | DNS、音视频、游戏 |

还有 SOCK_RAW(原始套接字),可以操作 IP 层,常用于实现 ping、traceroute 等工具,但需要 root 权限。

二、Socket 编程基础

2.1 地址族

socket.AF_INET:IPv4

socket.AF_INET6:IPv6

socket.AF_UNIX:Unix 域套接字(仅本机进程间通信)

2.2 创建 Socket 对象

python 复制代码
python
import socket

# TCP 套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# UDP 套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

2.3 地址表示

Python 中用元组 (ip, port) 表示网络地址:

IPv4:('127.0.0.1', 8080)

IPv6:('::1', 8080, 0, 0)(第四个元素为流标识)

特殊 IP:

0.0.0.0 或 ' ':绑定到本机所有可用网络接口

127.0.0.1:本地地址,只能本机访问

2.4 字节序问题

网络协议规定使用大端序(网络字节序),而多数 CPU(x86/ARM)使用小端序。Python 提供了转换函数:

python 复制代码
python
# 端口转换
port = 8080
net_port = socket.htons(port)   # host to network short
host_port = socket.ntohs(net_port)

# IP 转换(字符串 → 32位整数)
ip_str = '192.168.1.1'
ip_int = socket.htonl(int.from_bytes(socket.inet_aton(ip_str), 'big'))

不过在日常编程中,直接使用字符串和元组即可,底层函数会自动处理字节序。

三、TCP Socket 编程详解

3.1 TCP 通信流程

python 复制代码
mermaid
sequenceDiagram
    participant Client
    participant Server
    Server->>Server: socket() → bind() → listen()
    Client->>Client: socket()
    Client->>Server: connect()
    Server-->>Client: accept() 返回新socket
    Client->>Server: send() 请求数据
    Server->>Client: send() 响应数据
    Client->>Server: close()
    Server->>Server: close()

3.2 服务端核心步骤

3.2.1 创建 Socket

python 复制代码
python
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

3.2.2 绑定地址与端口

python 复制代码
python
server.bind(('0.0.0.0', 8888))

端口号范围 0~65535,1024 以下需要管理员权限。

绑定 `0.0.0.0` 表示监听所有网卡(包括 127.0.0.1 和局域网 IP)。

3.2.3 开始监听

python 复制代码
python
server.listen(5)   # 最大等待连接数(backlog)

backlog 是已完成三次握手但尚未被 accept() 取走的连接队列长度。

3.2.4 接受客户端连接

python 复制代码
python
client_socket, client_addr = server.accept()

accept() 会阻塞,直到有客户端连接,返回一个新的 Socket 对象(用于与该客户端通信)和客户端地址。

3.2.5 收发数据

python 复制代码
python
data = client_socket.recv(1024)        # 最多接收 1024 字节
if not data:
    break                              # 对方关闭连接
client_socket.sendall(b'Hello')        # 发送所有数据

recv 返回空字节串 b' ' 表示对方已正常关闭连接。

sendall 会循环发送直到所有数据发出,比 send 更安全。

3.2.6 关闭连接

python 复制代码
python
client_socket.close()
server.close()

3.3 客户端核心步骤

python 复制代码
python
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8888))     # 阻塞连接
client.sendall(b'Hello Server')
response = client.recv(1024)
client.close()

3.4 完整示例:回显服务器

服务端(echo_server.py)

python 复制代码
python
import socket

def run_server():
    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('Echo server listening on port 8888...')

    while True:
        client, addr = server.accept()
        print(f'New connection from {addr}')
        with client:
            while True:
                data = client.recv(1024)
                if not data:
                    break
                client.sendall(data)   # 原样返回
        print(f'Connection from {addr} closed')

if __name__ == '__main__':
    run_server()

客户端(echo_client.py)

python 复制代码
python
import socket

def run_client():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 8888))
    client.sendall(b'Hello, TCP server!')
    response = client.recv(1024)
    print(f'Server replied: {response.decode()}')
    client.close()

if __name__ == '__main__':
    run_client()

3.5 TCP 常见问题与解决方案

3.5.1 粘包问题

现象:连续多次 send 的小数据可能被一次 recv 全部读走,导致无法区分消息边界。

原因:TCP 是流式协议,没有消息边界,发送方有Nagle 算法会合并小包,接收方缓冲区可能一次性取出多个包。

解决方案:

  1. 固定长度消息:每个消息固定为 N 字节,不足补空格或 \0。

  2. 特殊分隔符:如 HTTP 的 `\r\n\r\n`,但消息体本身不能包含分隔符。

  3. 长度前缀法(最常用):每个消息前 4 字节表示消息体长度。

示例(长度前缀):

python 复制代码
python
# 发送端
def send_msg(sock, msg_bytes):
    length = len(msg_bytes)
    sock.sendall(length.to_bytes(4, 'big'))  # 发送长度
    sock.sendall(msg_bytes)                  # 发送内容

# 接收端
def recv_msg(sock):
    raw_len = sock.recv(4)
    if not raw_len:
        return None
    length = int.from_bytes(raw_len, 'big')
    data = b''
    while len(data) < length:
        chunk = sock.recv(min(1024, length - len(data)))
        if not chunk:
            raise ConnectionError('Connection broken')
        data += chunk
    return data

3.5.2 端口占用错误

原因:Socket 关闭后进入 TIME_WAIT 状态(默认 2MSL,约 1~4 分钟),此时重新绑定同一端口会失败。

解决:设置 SO_REUSEADDR 选项

python 复制代码
python
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

3.5.3 并发处理多个客户端

上面示例只能依次处理客户端(一个结束后才能处理下一个)。要实现并发,常见方案:

多线、多进程、异步 I/O

多线程示例:

python 复制代码
python
import threading

def handle_client(client_sock, addr):
    print(f'Handling {addr}')
    with client_sock:
        while True:
            data = client_sock.recv(1024)
            if not data:
                break
            client_sock.sendall(data)
    print(f'{addr} done')

def run_concurrent_server():
    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('Concurrent server running...')
    while True:
        client, addr = server.accept()
        t = threading.Thread(target=handle_client, args=(client, addr))
        t.start()

注意:Python 的 GIL 对 I/O 密集型任务影响不大,但线程数过多会增加上下文切换开销。对于高并发场景,建议使用 asyncio 或 epoll。

四、UDP Socket 编程详解

4.1 UDP 特点

  • 无连接,不需要 listen 和 accept。
  • 每个数据报独立发送,可能丢失、乱序。
  • 有消息边界:一次 recvfrom 对应一个 sendto。
  • 效率高,适合实时通信。

4.2 服务端流程

python 复制代码
python
import socket

udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_server.bind(('0.0.0.0', 9999))
print('UDP server ready')

while True:
    data, addr = udp_server.recvfrom(1024)   # 阻塞等待数据报
    print(f'Received from {addr}: {data.decode()}')
    udp_server.sendto(b'ACK', addr)          # 回复

4.3 客户端流程

python 复制代码
python
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(b'Hello UDP', ('127.0.0.1', 9999))
response, server_addr = client.recvfrom(1024)
print(response.decode())
client.close()

4.4 UDP 注意事项

数据报大小限制:IPv4 下最大 65507 字节(包含头部),一般建议不超过 1472 字节(避免分片)。

丢包处理:应用层需要实现超时重传、序列号等机制。

广播与多播:UDP 天然支持。

五、高级主题

5.1 非阻塞 Socket

默认情况下,recv、accept、connect 都是阻塞的。可以设置为非阻塞模式:

python 复制代码
python
sock.setblocking(False)

此时若没有数据可读,recv 会立即抛出 BlockingIOError。通常与 I/O 多路复用配合使用。

5.2 I/O 多路复用

用一个线程同时监控多个 Socket 的可读/可写事件,避免为每个客户端创建一个线程。

select / poll / epoll 对比

|-------|-------|------------------|------------|
| 方法 | 最大描述符 | 效率 | 平台支持 |
| elect | 1024 | O(n) 遍历,每次重新复制集合 | 全平台 |
| poll | 无限制 | O(n) 遍历 | Unix/Linux |
| epoll | 无限制 | O(1) 事件驱动,只返回活跃的 | Linux(推荐) |

使用 selectors 模块(推荐)

Python 3.4+ 提供了 selectors 模块,自动选择最高效的 I/O 多路复用机制。

python 复制代码
python
import selectors
import socket

sel = selectors.DefaultSelector()

def accept(sock, mask):
    conn, addr = sock.accept()
    conn.setblocking(False)
    sel.register(conn, selectors.EVENT_READ, read)

def read(conn, mask):
    data = conn.recv(1024)
    if not data:
        print('Closing connection')
        sel.unregister(conn)
        conn.close()
    else:
        conn.sendall(data)

server = socket.socket()
server.bind(('0.0.0.0', 8888))
server.listen()
server.setblocking(False)
sel.register(server, selectors.EVENT_READ, accept)

while True:
    events = sel.select()          # 阻塞等待事件
    for key, mask in events:
        callback = key.data
        callback(key.fileobj, mask)

5.3 超时设置

python 复制代码
python
sock.settimeout(5.0)   # 秒
try:
    data = sock.recv(1024)
except socket.timeout:
    print('Recv timeout')

或者使用 select 实现更精细的超时控制。

5.4 广播与多播(UDP)

广播:向网络内所有主机发送数据,需要设置 SO_BROADCAST 选项。

python 复制代码
python
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.sendto(b'Broadcast message', ('255.255.255.255', 9999))

多播(组播):加入一个多播组(如 224.0.0.1),只有加入该组的主机才能收到。

python 复制代码
python
import struct

multicast_group = '224.0.0.1'
multicast_port = 10000

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)  # TTL=2

# 发送多播
sock.sendto(b'Hello multicast', (multicast_group, multicast_port))

# 接收端需要加入多播组
sock.bind(('0.0.0.0', multicast_port))
mreq = struct.pack('4sl', socket.inet_aton(multicast_group), socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

5.5 使用 SSL/TLS 加密

Python 的 ssl 模块可以为 Socket 添加加密层,实现 HTTPS 等安全通信。

python 复制代码
python
import ssl

# 服务端
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certfile='server.crt', keyfile='server.key')
secure_sock = context.wrap_socket(sock, server_side=True)

# 客户端
context = ssl.create_default_context()
secure_sock = context.wrap_socket(sock, server_hostname='example.com')

六、错误处理与调试

6.1 常见异常

|------------------------|-----------------------|
| 异常类 | 典型原因 |
| socket.error | 通用错误(被更具体的异常替代 |
| socket.gaierror | DNS 解析失败(getaddrinfo) |
| socket.timeout | 操作超时 |
| ConnectionRefusedError | 目标端口未监听或防火墙拒绝 |
| ConnectionResetError | 对方异常关闭(发送 RST) |
| ConnectionAbortedError | 本地软件中止连接 |
| TimeoutError | 连接超时 |

6.2 调试工具

netstat、lsof -i:查看端口监听状态

tcpdump、Wireshark:抓包分析网络包

Python日志:记录收发数据和时间戳

python 复制代码
python
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug(f'Sent {len(data)} bytes')

6.3 优雅关闭连接

python 复制代码
python
# 先调用 shutdown 表示不再发送数据,让对方知晓
sock.shutdown(socket.SHUT_WR)   # 关闭写方向
sock.recv(1024)                 # 等待对方关闭读方向(可选)
sock.close()

shutdown 会立即发送 FIN 包,而 close 只是减少引用计数,可能不会立即关闭。

七、实战案例:简易聊天室

结合多线程和 TCP,实现一个群聊服务器。

7.1 服务器端

python 复制代码
python
import socket
import threading

clients = []

def broadcast(message, sender_sock=None):
    for client in clients:
        if client != sender_sock:
            try:
                client.send(message)
            except:
                client.close()
                clients.remove(client)

def handle_client(client_sock, addr):
    print(f'{addr} connected')
    while True:
        try:
            data = client_sock.recv(1024)
            if not data:
                break
            broadcast(data, client_sock)
        except:
            break
    clients.remove(client_sock)
    client_sock.close()
    print(f'{addr} disconnected')

def run_chat_server():
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(('0.0.0.0', 5555))
    server.listen(5)
    print('Chat server started on port 5555')
    while True:
        client, addr = server.accept()
        clients.append(client)
        t = threading.Thread(target=handle_client, args=(client, addr))
        t.start()

if __name__ == '__main__':
    run_chat_server()

7.2 客户端

python 复制代码
python
import socket
import threading

def receive_messages(sock):
    while True:
        try:
            data = sock.recv(1024)
            if not data:
                break
            print(data.decode())
        except:
            break

def run_chat_client():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 5555))
    threading.Thread(target=receive_messages, args=(client,), daemon=True).start()
    while True:
        msg = input()
        if msg.lower() == '/quit':
            break
        client.send(msg.encode())
    client.close()

if __name__ == '__main__':
    run_chat_client()

八、总结

  1. TCP 适用于需要可靠传输的场景:如文件传输、HTTP、数据库连接。注意处理粘包和并发。

  2. UDP 适用于实时性要求高、可容忍丢包的场景:如音视频、游戏、DNS。注意应用层实现丢包恢复。

  3. 高并发首选 asyncio 或 epoll:避免创建大量线程带来的开销。

  4. 始终处理异常:网络环境不可控,必须捕获 socket.error并合理重试或关闭连接。

  5. 使用 `SO_REUSEADDR`:开发调试时非常有用,避免 `Address already in use`。

  6. 生产环境务必加安全层:使用 SSL/TLS 或 SSH 隧道,防止数据被窃听。

相关推荐
码界筑梦坊2 小时前
324-基于Python的中国传染病数据可视化分析系统
开发语言·python·信息可视化
ZC跨境爬虫2 小时前
Playwright基础操作:元素坐标获取与坐标截图实战
python·microsoft·前端框架
源码之家2 小时前
计算机毕业设计:Python汽车销量智能分析与预测系统 Flask框架 scikit-learn 可视化 requests爬虫 AI 大模型(建议收藏)✅
人工智能·hadoop·python·算法·数据分析·flask·课程设计
普通网友2 小时前
使用Python处理计算机图形学(PIL/Pillow)
jvm·数据库·python
qiqiqi(^_×)2 小时前
pycharm Memory Settings无法保存
ide·python·pycharm
xzal122 小时前
Python 中,and 和 or 运算符的运算规则
笔记·python
gf13211112 小时前
【python_使用指定应用发送飞书卡片】
java·python·飞书
Dxy12393102162 小时前
Python转Word为PDF:办公自动化的高效利器
python·pdf·word
Thomas.Sir2 小时前
第十章:RAG知识库开发之【LangSmith 从入门到精通:构建生产级 LLM 应用的全链路可观测性平台】
人工智能·python·langsmith·langchian