首页 / 资讯中心 / 文章详情

python 网络编程详解及简单实例

python 网络编程详解及简单实例 ★ FEATURED ARTICLE
前言「网络编程」这个词容易让人联想到很重的东西其实标准库给的门槛非常低socket模块让你直接操作传输层几十行代码就能跑起一个能收发数据的服务端和客户端。真正的难点不在于写出能跑的代码而在于理解你写出来的东西在什么情况下会不按预期工作。典型的误解有三个。第一把 TCP 当成消息通道以为一次发送对应一次接收——实际上 TCP 是字节流没有消息边界。第二以为recv(1024)一定会返回 1024 字节——它返回的是「最多 1024 字节」。第三以为socket模块里有serve_forever——那是socketserver模块的东西。这三条都会在下面反复出现。本文先对比 TCP 和 UDP 两种传输语义再各给一个最小实例然后用socketserver演示如何在标准库里写出并发服务器最后用一段「裸 socket 发 HTTP 请求」的代码说明应用层协议是怎么架在 TCP 之上的。所有代码以 Python 3 为准Python 2.7 已于 2020 年 1 月 1 日停止维护旧版socket返回str的行为在 3 里已经变成返回bytes。一、TCP 与 UDP两种传输语义维度TCPSOCK_STREAMUDPSOCK_DGRAM连接面向连接先握手无连接可靠性有序、不丢、不重不保证送达、不保证顺序数据形态字节流无边界数据报保留边界常用方法connect/listen/accept/sendall/recvsendto/recvfrom典型用途HTTP、SSH、数据库连接DNS、日志上报、实时音视频选择标准很直接结果必须准确、可以重传的用 TCP允许偶尔丢包、更在意延迟的用 UDP。绝大多数业务程序选 TCP。二、UDP 最小实例UDP 没有连接服务端绑定端口后直接收数据报。# 适用于 Python 3.8import socketwith socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:server.bind((127.0.0.1, 9001))print(UDP 服务端监听 9001)while True:data, addr server.recvfrom(1024)server.sendto(data.upper(), addr)客户端不需要connect# 适用于 Python 3.8import socketwith socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:client.sendto(bhello udp, (127.0.0.1, 9001))data, addr client.recvfrom(1024)print(来自, addr, 的回复, data.decode(utf-8))recvfrom返回的是「数据 来源地址」二元组因为 UDP 套接字可能收到来自任意地址的数据报。recv(1024)里的 1024 同样是上限超出的部分会被直接丢弃UDP 不会帮你保留或重传。三、用 socketserver 写并发服务器第二小节的 UDP 服务端一次只能处理一个请求。要同时服务多个连接socketserver提供了现成的框架。它的结构是「一个服务器类 一个请求处理类」# 适用于 Python 3.8import socketserverclass EchoHandler(socketserver.BaseRequestHandler):每个连接会被交给一个 handler 实例处理。def handle(self):self.data self.request.recv(1024)self.request.sendall(self.data.upper())class ThreadedServer(socketserver.ThreadingTCPServer):# 允许服务器重启时立即复用端口避免 Address already in useallow_reuse_address True# 主线程退出时不等待工作线程便于 CtrlC 停掉daemon_threads Trueif __name__ __main__:with ThreadedServer((127.0.0.1, 9002), EchoHandler) as server:print(服务端启动, server.server_address)server.serve_forever()要点serve_forever()在这里才存在。它属于socketserver的服务器类socket模块里没有这个方法。handler 里有三个现成属性self.request是与客户端通信的套接字self.client_address是客户端地址self.server指回服务器实例。ThreadingTCPServer由ThreadingMixIn和TCPServer组合而成每个连接起一个线程。由于处理器里主要是等 I/O线程模型在这里是合适的如果处理逻辑是 CPU 密集的受 GIL 限制多线程不会带来加速应当换用多进程方案。想停止服务器调用server.shutdown()。官方文档特别指出shutdown()必须在serve_forever()运行于另一个线程时调用在同一线程里调用会互相等待。配套的客户端就是普通 TCP 客户端# 适用于 Python 3.8import socketwith socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:s.connect((127.0.0.1, 9002))s.sendall(bhello tcp)print(s.recv(1024).decode(utf-8))四、应用层协议是搭在 TCP 上的为了让「字节流之上还有协议」这件事可见下面用裸 socket 发一个 HTTP 请求。先在本地起一个静态文件服务器标准库自带# 需要 Python 3.7在某个目录下执行会以 8000 端口对外提供该目录python -m http.server 8000然后用最原始的 socket 去请求它# 适用于 Python 3.8import sockethost, port 127.0.0.1, 8000request (GET / HTTP/1.1\r\nfHost: {host}:{port}\r\nConnection: close\r\n\r\n)with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:s.connect((host, port))s.sendall(request.encode(ascii))chunks []while True:chunk s.recv(4096)if not chunk:breakchunks.append(chunk)response b.join(chunks)head, _, body response.partition(b\r\n\r\n)print(head.decode(latin-1).splitlines()[0])print(正文字节数, len(body))这段代码展示了三件事HTTP 请求就是一串有约定格式的文本头部之间用\r\n分隔头部与正文之间用空行\r\n\r\n分隔——这正是「分隔符定界」的应用层实现。Connection: close告诉服务端响应完就关连接于是客户端可以用「recv返回空字节串」作为结束标志。如果不写这一行用 HTTP/1.1 的默认长连接这个循环会一直等下去。循环累加chunks是必需的。任何一次recv都可能只拿到响应的一部分。顺带说明真实场景不应该这样手写 HTTP标准库有http.client、urllib.request等更高层的模块。这里手写只是为了看清 TCP 之上那一层的结构。五、超时与错误处理网络程序必须假设「对端随时可能消失」所以超时是标配而不是可选项。# 适用于 Python 3.8import socketwith socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:s.settimeout(5.0)try:s.connect((127.0.0.1, 9002))s.sendall(bping)print(s.recv(1024))except socket.timeout:print(超时)except ConnectionRefusedError:print(目标端口没有服务在监听)except OSError as exc:print(网络错误, exc)socket.timeout是OSError的子类所以except OSError也能兜住它但把它单独写在前面的分支里能让日志更清楚。连接被拒绝ConnectionRefusedError和超时是两类完全不同的故障排查方向也不同前者通常是端口没人监听或被防火墙拒绝后者是对方没响应。关于超时异常的类型还得多说一句从 Python 3.10 起socket.timeout已是内置异常TimeoutError的弃用别名两者指向同一个类但在 3.9 及更早版本里它们是两个不同的类在那些版本上写except TimeoutError:是捕获不到套接字超时的。上面的代码写socket.timeout是为了兼容 3.8 这类较老的版本如果你确定只在 3.10 上运行改成TimeoutError更符合新代码的写法。常见坑点以为一次recv能收全❌ 写data s.recv(4096)就当拿到了完整响应。✅recv(n)返回「最多 n 字节」短读是常态。要收全就循环累加直到对端关闭或读够预期长度。在socket模块里找serve_forever❌ 写sock.serve_forever()报AttributeError。✅serve_forever()属于socketserver的服务器类socket只提供底层原语。用send以为发全了❌ 调一次s.send(data)就继续下一步实际上只发出去一部分。✅ 用s.sendall(data)或者自己处理send的返回值做循环。UDP 套接字上调用recv而不是recvfrom❌ 用recv收 UDP 数据拿不到来源地址也没法回复。✅ 面向无连接的数据报场景用recvfrom/sendto。服务器重启报Address already in use❌ 调试时反复启动端口没释放报OSError。✅ 在服务器类上设allow_reuse_address True或在bind前设SO_REUSEADDR。在同一个线程里调用server.shutdown()❌ 想优雅退出直接在serve_forever()后面写shutdown()程序卡死。✅ 官方文档要求shutdown()在serve_forever()运行于另一线程时调用把serve_forever()放进子线程主线程负责触发shutdown()。忘了设超时❌ 对端不回数据程序无限阻塞还以为是死循环。✅ 用settimeout()给套接字设超时并捕获socket.timeout。把Connection: close当成可有可无❌ 用「读到空字节串就结束」的循环读 HTTP/1.1 响应却又不声明关闭连接结果一直等下去。✅ 用Connection: close明确让服务端关连接或者按协议里的Content-Length精确读够字节数。总结场景该用什么关键提醒可靠传输socketSOCK_STREAM字节流无边界短读是常态低延迟、可丢包socketSOCK_DGRAM用sendto/recvfrom多个连接同时服务socketserver.ThreadingTCPServerserve_forever在这里I/O 密集并发线程或多路复用受 GIL 限制的是 CPU 密集场景收全一段数据循环recv直到满足条件别假设一次就够网络编程的实例代码都很短真正决定程序稳不稳的是那几条隐含假设数据会不会分片、连接会不会断、对端会不会永远不回。把这三件事在代码里显式处理掉才算真的写对了。
阅读完成 · 觉得有帮助?
咨询建站