Python Socket实现PC与树莓派双向通信:TCP/UDP选型与实战
1. 从一个真实需求说起为什么PC和树莓派之间需要一条看不见的线很多人手里都有一块树莓派可能是做家庭自动化控制、可能是跑一个小型数据采集节点、也可能是当作学习Linux和嵌入式的练手平台。但真正用起来之后一个绕不开的问题很快就冒出来了树莓派通常放在角落里默默跑着屏幕、键盘、鼠标都不接我怎么在PC上实时看到它的状态怎么给它下发指令怎么把采集到的数据传回来有人会说用SSH不就行了。SSH确实能远程登录但它是交互式的适合手动敲命令。如果你想让PC上的一个Python程序自动跟树莓派交换数据比如PC端每隔一秒把新的控制参数推给树莓派树莓派把传感器读数实时回传SSH就显得笨重了。这时候Socket通信就是最直接、最轻量的方案。Socket本质上就是两台设备之间的一条看不见的线。你可以把它想象成一根水管一端往里灌水另一端就能接到水。PC和树莓派各自有一个IP地址只要它们在同一局域网内就能通过TCP或UDP协议建立连接然后像读写文件一样收发数据。Python标准库自带的socket模块把底层TCP/IP的复杂性封装得很好几十行代码就能跑通一个可用的通信链路。这篇文章面向的是有基础Python语法知识、手里有树莓派、想让两者真正对话起来的开发者。我会从通信模型的选择讲起把服务端和客户端的代码逐段拆开补充实际调试中会遇到的各种坑最后给出一个可以直接复用的双向通信框架。不管你是做物联网原型、实验室数据采集还是单纯想学网络编程这套东西都能直接拿去改。2. TCP还是UDP先搞清楚你的数据怕不怕丢2.1 两种协议的本质差异在动手写代码之前必须先做一个决定用TCP还是UDP。这个选择不是拍脑袋定的它直接决定了你后面代码的结构和整个系统的可靠性。TCP是面向连接的协议。通信之前客户端和服务端要先握手建立连接之后所有数据按顺序到达丢了会重传错了会校验。你可以把它理解成打电话拨通了才能说话对方没听清会让你重复。UDP是面向无连接的协议发送方直接把数据包扔出去不保证到达、不保证顺序。它更像寄明信片你投进邮筒就不管了对方收没收到、什么时候收到你都不知道。对于PC和树莓派的互动场景大部分情况下我推荐TCP。原因很简单控制指令和数据采集通常要求可靠传输。你不想出现PC发了停止指令但树莓派没收到这种情况。UDP的优势在于低延迟和广播能力适合高频传感器数据流或者一对多场景但代价是你得自己在应用层处理丢包和乱序。2.2 用一张表把选型逻辑说清楚对比维度TCPUDP连接方式需要三次握手建立连接无连接直接发送可靠性保证数据到达且有序不保证到达不保证顺序传输速度相对较慢有确认机制相对较快适用场景控制指令、文件传输、配置下发高频传感器流、视频流、广播Python实现复杂度低socket库直接支持低但需自行处理丢包典型代码行数服务端约20行客户端约15行各约10行我自己的经验是如果你做的是PC下发指令、树莓派执行并回传结果这种请求-响应模式无脑选TCP。如果你做的是树莓派以100Hz频率持续上报加速度数据那可以考虑UDP但要在应用层加序号和时间戳来检测丢包。2.3 一个容易忽略的点端口选择不管选哪种协议都要指定端口号。端口号的范围是0到65535其中0到1023是系统保留端口需要管理员权限才能绑定。实际开发中建议选1024以上的端口比如8888、9999、12345这类。但要注意有些端口可能被系统或其他服务占用了绑定的时候会报Address already in use。我一般习惯在代码里把端口号定义成常量方便统一修改也避免在多个地方硬编码导致不一致。提示树莓派上如果开了某些常见服务比如Web服务占80端口选端口时避开这些。可以用ss -tlnp命令查看当前已监听的端口列表。3. 服务端放在哪一端树莓派做Server还是PC做Server3.1 两种架构的取舍这个问题看起来简单但实际项目中选错了会带来不少麻烦。服务端Server的角色是绑定端口、等待连接、接受请求。客户端Client的角色是主动发起连接。把树莓派作为服务端是最常见的做法。原因有几个树莓派通常固定放在某个位置IP地址相对稳定可以在路由器里绑定静态IP它开机后就启动服务端程序一直监听PC随时连上来都行。而且树莓派作为数据采集端天然适合等待被查询或者接受指令的角色。反过来PC作为服务端也有它的场景。比如PC上跑着一个计算密集型的模型树莓派采集到原始数据后主动推送给PC去处理处理完再回传结果。这种架构下PC的IP可能变化比如笔记本换网络树莓派就需要处理连不上的情况代码里得加重试逻辑。3.2 树莓派做服务端的完整代码拆解先看服务端代码我把它拆成几个关键步骤import socket import threading HOST 0.0.0.0 PORT 8888 def handle_client(conn, addr): print(f[新连接] {addr} 已接入) try: while True: data conn.recv(1024) if not data: break msg data.decode(utf-8) print(f[收到] {msg}) reply f树莓派已收到: {msg} conn.sendall(reply.encode(utf-8)) except ConnectionResetError: print(f[断开] {addr} 异常断开) finally: conn.close() print(f[关闭] {addr} 连接已释放) def start_server(): server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((HOST, PORT)) server.listen(5) print(f[启动] 服务端监听 {HOST}:{PORT}) while True: conn, addr server.accept() t threading.Thread(targethandle_client, args(conn, addr)) t.daemon True t.start() if __name__ __main__: start_server()逐段解释一下。HOST 0.0.0.0表示监听本机所有网络接口这样局域网内任何设备都能连过来。如果你只想让特定网段访问可以改成具体的IP。SO_REUSEADDR这个选项很重要它允许端口在关闭后立即被重新绑定否则你调试时频繁重启程序会遇到端口被占用的报错。server.listen(5)中的5是等待队列长度表示最多允许5个未处理的连接排队。对于PC和树莓派一对一的场景这个值够用了。accept()会阻塞在那里直到有客户端连进来返回一个新的socket对象和客户端地址。我用了一个线程来处理每个客户端连接这样服务端可以同时接受多个连接。虽然PC和树莓派通常是一对一但调试时你可能同时开多个客户端测试多线程处理更灵活。t.daemon True让线程随主线程退出而结束避免程序关不掉。3.3 客户端代码与连接逻辑PC端的客户端代码import socket SERVER_IP 192.168.1.100 SERVER_PORT 8888 def start_client(): client socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.settimeout(5) try: client.connect((SERVER_IP, SERVER_PORT)) print(f[已连接] {SERVER_IP}:{SERVER_PORT}) except socket.timeout: print([超时] 连接服务端超时请检查IP和网络) return except ConnectionRefusedError: print([拒绝] 服务端未启动或端口未开放) return while True: msg input(请输入要发送的内容输入quit退出: ) if msg quit: break client.sendall(msg.encode(utf-8)) reply client.recv(1024) print(f[回复] {reply.decode(utf-8)}) client.close() print([关闭] 连接已断开) if __name__ __main__: start_client()settimeout(5)设置了5秒超时避免网络不通时程序卡死。connect()发起连接如果树莓派的服务端没启动会立刻抛出ConnectionRefusedError如果IP不对或者网络不通会等到超时。这两种异常分开处理方便定位问题。sendall()和send()的区别值得说一下。send()返回实际发送的字节数可能小于你传入的数据长度需要循环发送。sendall()会一直发送直到全部数据发完代码更简洁推荐使用。4. 让通信真正双向实时从请求-响应到全双工4.1 请求-响应模式的局限上面那个例子是典型的请求-响应模式PC发一句树莓派回一句。但实际项目中你往往需要树莓派主动上报数据比如每隔一秒把温度读数推给PC同时PC也能随时下发控制指令。这就不是简单的一问一答了。如果只用单线程recv()会阻塞导致你没法同时处理发送和接收。解决办法有两种一是用多线程一个线程专门收数据一个线程专门发数据二是用select或selectors模块做I/O多路复用。对于PC和树莓派这种场景多线程更直观代码也更好维护。4.2 双线程全双工通信框架先看树莓派服务端的改造import socket import threading import time import random HOST 0.0.0.0 PORT 8888 def receive_loop(conn): while True: try: data conn.recv(1024) if not data: break print(f[PC指令] {data.decode(utf-8)}) except Exception as e: print(f[接收异常] {e}) break def send_loop(conn): while True: temp round(20 random.uniform(-2, 2), 2) msg fTEMP:{temp} try: conn.sendall(msg.encode(utf-8)) except Exception as e: print(f[发送异常] {e}) break time.sleep(1) def start_server(): 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[启动] 等待PC连接...) conn, addr server.accept() print(f[已连接] {addr}) t_recv threading.Thread(targetreceive_loop, args(conn,)) t_send threading.Thread(targetsend_loop, args(conn,)) t_recv.daemon True t_send.daemon True t_recv.start() t_send.start() t_recv.join() t_send.join() if __name__ __main__: start_server()这里用两个线程分别负责收和发。receive_loop阻塞在recv()上等待PC的指令send_loop每隔一秒模拟采集一次温度并发送。两个线程共享同一个conn对象TCP是全双工的同时读写没有问题。PC端的客户端也做类似改造import socket import threading SERVER_IP 192.168.1.100 SERVER_PORT 8888 def receive_loop(client): while True: try: data client.recv(1024) if not data: break print(f\n[树莓派上报] {data.decode(utf-8)}) except Exception: break def start_client(): client socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((SERVER_IP, SERVER_PORT)) print([已连接]) t threading.Thread(targetreceive_loop, args(client,)) t.daemon True t.start() while True: msg input() if msg quit: break client.sendall(msg.encode(utf-8)) client.close() if __name__ __main__: start_client()4.3 粘包问题为什么你的消息会粘在一起TCP是字节流协议它不保留消息边界。也就是说你调了两次sendall()对方可能一次recv()就把两条消息都收到了或者你发了一条很长的消息对方分两次才收完。这就是所谓的粘包和拆包问题。在小数据量、低频率的场景下这个问题可能不明显。但一旦数据量上来或者发送频率变高就一定会遇到。解决办法是在应用层定义消息边界。常见方案有三种第一种是固定长度消息每条消息长度固定接收方按固定长度读取。简单但浪费带宽适合定长指令。第二种是用分隔符比如每条消息以\n结尾接收方按行读取。Python的socket.makefile()可以很方便地实现按行读取。第三种是在消息头部加长度字段比如前4个字节表示消息体长度接收方先读4字节再根据长度读消息体。这是最通用的方案。我一般推荐分隔符方案因为实现简单且可读性好。改造后的接收代码def receive_loop(conn): buffer while True: data conn.recv(1024) if not data: break buffer data.decode(utf-8) while \n in buffer: line, buffer buffer.split(\n, 1) if line: print(f[收到] {line})发送时每条消息末尾加\nconn.sendall((msg \n).encode(utf-8))注意分隔符方案要求消息内容本身不能包含分隔符。如果你的数据里可能有换行符要么先做转义要么改用长度字段方案。5. 调试路上一定会遇到的几个坑5.1 连不上从网络层到应用层逐层排查Connection refused 和 Connection timed out 是两个最常见的报错但原因完全不同。前者说明网络是通的但目标端口没有服务在监听——要么树莓派上的服务端程序没启动要么绑定的IP不对。后者说明数据包根本没到达目标通常是IP写错了、两台设备不在同一网段、或者防火墙拦了。排查顺序我一般是这样的先在PC上ping 树莓派IP确认网络层通不通。通了之后在树莓派上ss -tlnp | grep 8888确认端口在监听。如果监听地址是127.0.0.1:8888而不是0.0.0.0:8888那只有本机能连外部连不上需要把HOST改成0.0.0.0。最后检查树莓派的防火墙规则有些系统默认开了防火墙会拦截入站连接。5.2 树莓派IP变了怎么办家用路由器分配的IP通常是动态的树莓派重启后IP可能变。解决办法有两个一是在路由器管理界面里给树莓派的MAC地址绑定一个固定IP二是在树莓派上装一个mDNS服务比如avahi然后PC端用hostname.local来代替IP地址。第二种方案更灵活不依赖路由器配置Python的socket也能直接解析主机名。5.3 程序异常退出后端口被占用调试时频繁重启服务端经常会遇到OSError: [Errno 98] Address already in use。这是因为TCP连接关闭后操作系统会保持一段时间TIME_WAIT状态。解决办法就是前面代码里提到的setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)加上这一行基本就能解决。5.4 中文乱码问题Python 3的字符串默认是Unicodeencode(utf-8)和decode(utf-8)要成对使用。如果一端用utf-8编码另一端用gbk解码中文就会乱码。统一用utf-8是最省心的做法。另外要注意recv()返回的是bytes必须先decode再处理。6. 从能跑到好用几个值得加的工程化改进6.1 心跳机制检测连接状态TCP连接建立后如果一方断电或网络中断另一方可能不会立刻感知到。recv()会一直阻塞直到TCP超时可能长达几分钟。加一个心跳机制可以快速检测连接是否还活着。做法很简单双方每隔几秒发送一个特殊的心跳包接收方收到后更新最后活跃时间如果超过一定时间没收到任何数据就主动关闭连接并尝试重连。6.2 断线自动重连客户端代码里加一个重连循环连接断开后等待几秒再尝试重新连接。这在树莓派偶尔重启或者网络波动的场景下非常实用。服务端也可以在accept()循环里持续等待新连接而不是处理完一个就退出。6.3 用JSON格式化消息纯文本消息在简单场景够用但一旦消息类型多了解析起来就麻烦。用JSON格式可以让消息自带结构import json msg {type: sensor, name: temperature, value: 23.5} conn.sendall((json.dumps(msg) \n).encode(utf-8))接收方json.loads()一下就能拿到字典根据type字段分发处理。这样扩展新消息类型时不用改解析逻辑。6.4 日志记录代替print调试阶段用print没问题但长期运行的程序应该用logging模块。可以同时输出到控制台和文件方便事后排查问题。日志级别也能控制生产环境只记录warning以上调试时开debug。import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(comm.log), logging.StreamHandler() ] )这套Socket通信框架我从最简单的请求-响应版本开始逐步加到双线程全双工、粘包处理、心跳重连前后在好几个项目里用过。最深的体会是网络编程的代码量不大但边界情况特别多。把超时、异常、重连这些处理好了程序才能真正稳定跑起来而不是演示五分钟就崩。另外如果你后续想扩展到多个树莓派节点服务端用selectors模块做I/O多路复用会比多线程更高效那是另一个话题了等有需要再展开。

相关新闻

二叉树实战指南:从存储设计到遍历推演与工程应用

二叉树实战指南:从存储设计到遍历推演与工程应用

最近帮一个学弟看代码,他卡在表达式求值那一关——中缀转后缀、括号优先级处理,折腾了一整天还是满屏 bug,最后破防问我:这东西到底为什么要搞这么复杂?我说你先别急着转后缀,把 3 4 * 5 按优先级画成一…

2026/10/13 13:08:59 阅读更多 →
Python+MLP虚假新闻检测器:从TF-IDF到PyTorch实战

Python+MLP虚假新闻检测器:从TF-IDF到PyTorch实战

简介:这是一份面向计算机相关专业学生与项目实战学习者的课程设计资料,围绕Python与MLP多层感知机实现互联网虚假新闻检测器,适合正在准备期末大作业、课程设计或需要机器学习实战练习的人群,可帮助理解文本预处理、特征提取、模型…

2026/10/13 13:09:03 阅读更多 →
MATLAB调用CUDA的mex接口实战:绕过gpuArray实现GPU原生计算

MATLAB调用CUDA的mex接口实战:绕过gpuArray实现GPU原生计算

1. 项目概述:为什么MATLAB原生计算会“卡”在GPU门口?你有没有遇到过这样的场景:用MATLAB写完一个图像去噪算法,输入一张512512的灰度图,CPU上跑一次要8秒;换成10241024,直接涨到35秒——而你手…

2026/10/13 13:09:06 阅读更多 →

最新新闻

AI时代个人量化:从零开发专属交易策略全流程

AI时代个人量化:从零开发专属交易策略全流程

AI时代最大的红利:个人做量化,也能开发出一套适合你的策略这几年AI的热度不用我多说,但很多人一提AI就想到大模型聊天、画图、写代码,真正把AI转化成个人生产力的场景,反而被我周围的量化交易玩家给玩明白了。我自己从…

2026/10/14 11:03:10 阅读更多 →
执行沙箱隔离实战:从命名空间到seccomp的多层防御设计

执行沙箱隔离实战:从命名空间到seccomp的多层防御设计

1. 执行沙箱隔离到底在解决什么问题第一次接触“执行沙箱隔离”这个概念,是在做一个代码在线评测功能的时候。用户提交一段代码,服务端要跑起来验证结果,跑完把输出返回去。听起来很简单对吧?但真正上线之后才发现,噩梦…

2026/10/14 11:03:10 阅读更多 →
商品列表接口设计:分页、筛选、排序背后的工程权衡

商品列表接口设计:分页、筛选、排序背后的工程权衡

上一篇讲了商品详情接口——它的难点在「各平台字段不一样」。 列表接口是它的姊妹,但难点完全不同。详情接口是「点查」:给一个确定的 ID,取一条确定的记录;列表接口是「扫描」:在一堆数据里,按用户给的任…

2026/10/14 11:03:10 阅读更多 →
Python在AI与数据分析中的核心应用与实践

Python在AI与数据分析中的核心应用与实践

1. 为什么Python是AI入门的首选语言 作为一个从2012年开始接触Python的老程序员,我至今记得第一次用10行代码完成C语言需要100行才能实现的数据处理时的震撼。Python凭借其近乎伪代码的语法设计,成为了人工智能领域当之无愧的" lingua franca"…

2026/10/14 11:03:10 阅读更多 →
RAG不评测等于闭眼调参:从指标原理到落地实践

RAG不评测等于闭眼调参:从指标原理到落地实践

做 RAG 不评测,等于闭着眼睛调参:一份从原理到落地的完整测评指南我见过太多做 RAG(检索增强生成)的同学,上线前说得头头是道,一问到“怎么证明你的系统比上个版本好”,立刻支支吾吾。RAG 项目最…

2026/10/14 11:03:10 阅读更多 →
Python实现重复文档查找与清理工具

Python实现重复文档查找与清理工具

1. 重复文档查找工具的设计与实现在日常工作中,我们经常会遇到文档重复的问题,特别是从QQ、微信等平台接收的文件,经常会在不同文件夹中保存多份相同内容但不同名称的文件。这些重复文件不仅占用宝贵的存储空间,还会导致文件管理混…

2026/10/14 11:02:10 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →