前言: 笔者正在备赛,新规是树莓派可以与笔电进行通讯,也就是说可以利用笔电进行yolo等模型的计算。 于是就有了实时传输视频流这一需求。
但是,我啥也不会啊,我openCV numpy TCP都不会,于是vibe了一版,发现是可以实现的。好,开始学习!下面将利用TCP协议进行传输
TCP简介
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层协议。通信前需要通过"三次握手"建立连接,通信结束后通过"四次挥手"断开。它通过序列号、确认应答(ACK)、超时重传、滑动窗口等机制,保证数据不丢失、不重复、按序到达。代价是延迟比 UDP 高——握手/ACK/重传/拥塞控制都会增加时间开销。你项目里用它传检测结果(数据量小、必须可靠),就是典型的 TCP 适用场景。
TCP 流程图
一、TCP 三次握手(建立连接)
sequenceDiagram
participant C as 客户端 (树莓派)
participant S as 服务端 (笔记本)
Note over C,S: 初始状态: CLOSED
C->>S: SYN (seq=x)
Note right of C: 状态: SYN_SENT
Note left of S: 状态: LISTEN
S->>C: SYN+ACK (seq=y, ack=x+1)
Note left of S: 状态: SYN_RCVD
C->>S: ACK (ack=y+1)
Note right of C: 状态: ESTABLISHED ✅
Note left of S: 状态: ESTABLISHED ✅
Note over C,S: 连接建立,可以开始传输数据
三次握手核心逻辑:
| 次序 | 方向 | 标志位 | 含义 |
|---|---|---|---|
| 1 | 客户端 → 服务端 | SYN | "我想建立连接" |
| 2 | 服务端 → 客户端 | SYN+ACK | "同意,我也准备好了" |
| 3 | 客户端 → 服务端 | ACK | "好的,确认建立" |
为什么是三次而不是两次? 防止"历史重复的连接"——如果客户端之前发的一个旧 SYN 迟到了,服务端收到后误以为是新连接,两次握手时服务端就进入 ESTABLISHED,但客户端知道这是过期的,不会用。三次握手让客户端可以发 RST 拒绝。
二、TCP 四次挥手(断开连接)
sequenceDiagram
participant C as 客户端 (树莓派)
participant S as 服务端 (笔记本)
Note over C,S: 状态: ESTABLISHED
C->>S: FIN (seq=u)
Note right of C: 状态: FIN_WAIT_1
Note left of S: 状态: CLOSE_WAIT
S->>C: ACK (ack=u+1)
Note left of S: 状态: CLOSE_WAIT (还可发数据)
Note right of C: 状态: FIN_WAIT_2
S->>C: FIN (seq=w)
Note left of S: 状态: LAST_ACK
C->>S: ACK (ack=w+1)
Note right of C: 状态: TIME_WAIT (等 2MSL)
Note left of S: 状态: CLOSED
Note over C,S: 等待 2MSL 后...
Note right of C: 状态: CLOSED ✅
四次挥手核心逻辑:
| 次序 | 方向 | 标志位 | 含义 |
|---|---|---|---|
| 1 | 客户端 → 服务端 | FIN | "我没数据要发了" |
| 2 | 服务端 → 客户端 | ACK | "收到,但我可能还有数据要发" |
| 3 | 服务端 → 客户端 | FIN | "我也发完了" |
| 4 | 客户端 → 服务端 | ACK | "好的,确认关闭" |
为什么是四次而不是三次? 因为 TCP 是全双工的,每个方向的关闭是独立的。服务端收到 FIN 时可能还有数据没发完,所以 ACK 和 FIN 不能合并(不像握手时 SYN+ACK 可以合在一起)。
为什么客户端关闭前要等 2MSL(Maximum Segment Lifetime)? 确保最后一个 ACK 能到达服务端(如果丢了,服务端会重发 FIN);同时让网络中残留的旧数据包消散,避免被新连接误收。
省流
优点:丢包率低 缺点:要经常确认,延时比较高
python与TCP理论
重要工具socket
概述
Socket(套接字)本质上是操作系统提供的一个通信端点,你可以把它想象成墙上的电源插座——每台电脑就像一个布满插座的房间,每个插座有个编号(端口号),有的插座提供网页服务(80端口),有的提供文件传输(21端口),有的提供你项目的推理结果回传(8800端口)。客户端程序拿着"插头"插到对应编号的插座上,就能获取那种服务。
技术上说,Socket 是 BSD UNIX 发明的进程间通信机制,用来描述一对 IP 地址 + 端口,是网络连接的一个端点。两台电脑上的程序要通信,各自都得打开一个 Socket,然后这两个 Socket 之间就形成了一条双向的数据通道——数据从这头进去,从那头出来,反过来也行。
简单说:Socket = 网络通信的接口,
socket()函数解析
socket.socket([family[, type[, proto]]])
方括号 [] 括起来的内容,表示“可写可不写”的参数。这个嵌套的意思是:前面的参数如果不写,后面的参数也不能写(因为后面的参数依赖前面的位置)。
- family: 套接字家族可以使用 AF_UNIX(本机) 或者 AF_INET(IPv4)或者AF_INET6(IPv6)。
- type: 套接字类型可以根据是面向连接的还是非连接分为
SOCK_STREAM(TCP) 或SOCK_DGRAM(UDP)。 - protocol: 一般不填默认为 0。
为了直观,下面使用流程图展示初始化及后续函数使用
socket流程图
flowchart TD
subgraph SERVER["TCP 服务端 (笔记本 laptop_server.py)"]
S1["socket(AF_INET, SOCK_STREAM)<br/>创建 TCP 套接字"]
S2["bind(('0.0.0.0', 8800))<br/>绑定 IP + 端口"]
S3["listen(5)<br/>开始监听, 排队上限 5"]
S4["accept()<br/>阻塞等待客户端连接"]
S5["recv() / recv_exact()<br/>接收数据"]
S6["sendall()<br/>发送数据"]
S7["close()<br/>关闭连接"]
S1 --> S2 --> S3 --> S4
S4 -->|"客户端连入<br/>返回 conn, addr"| S5
S5 --> S6 -->|"继续通信"| S5
S5 -->|"对方关闭"| S7
end
subgraph CLIENT["TCP 客户端 (树莓派 remote_client.py)"]
C1["socket(AF_INET, SOCK_STREAM)<br/>创建 TCP 套接字"]
C2["connect((host, 8800))<br/>发起连接 (触发三次握手)"]
C3["sendall()<br/>发送数据"]
C4["recv() / recv_exact()<br/>接收数据"]
C5["close()<br/>关闭连接 (触发四次挥手)"]
C1 --> C2
C2 -->|"连接成功"| C3
C3 --> C4 -->|"继续通信"| C3
C4 -->|"通信结束"| C5
end
CLIENT --"三次握手建立连接"--> SERVER
CLIENT --"数据交换"--> SERVER
CLIENT --"四次挥手断开连接"--> SERVER
用DeepSeek的话来说就是: 就像插座是用电的接口一样。你写代码时 socket.socket() 创建的就是这样一个接口,bind() 把它绑到某个端口(给插座编号),connect() 或 accept() 建立连接(把插头插上去),之后就能 send()/recv() 收发数据了。
实操部分
传输一帧
下面使用服务器收,客户端发,这也是最符合我比赛的情况
代码展示
服务端(发起者,中心, 在本示例中是收照片的)
import socket
import numpy
import cv2
# 由于socket自带的recv接受函数只能接收简单的字符串,
# 并不能直接地接收图片,于是我们需要进行改造
def recv_size(conn,count):
"""接收图片大小工具函数"""
length = conn.recv(count)
return length
def recv_all(conn,count):
"""接收整张图片,因为发可以一次性发完, 但是收不可以一次性收完"""
data = b''
while len(data) < count:
newdata = conn.recv(count-len(data))
if not newdata:
print("已断开连接")
return None
data += newdata
return data
# 首先创建一个socket
# socket.AF_INET是IPv4, socket.SOCK_STREAM是TCP
soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定端口和地址
host = socket.gethostname()
soc.bind((host, 8010))
# 开始监听, 最多5个设备
soc.listen(5)
# 接受TCP连接并返回(conn, addr), 其中conn是新的套接字对象,
# 可以用于接受和发送数据,addr是客户端的地址
conn, addr = soc.accept()
# 接收一帧
length = recv_size(conn,16)
if isinstance(length, bytes):
int_length = int(length)
data = recv_all(conn,int_length)
# 开始解码
imgdata = numpy.frombuffer(data, dtype='uint8')
img = cv2.imdecode(imgdata,1)
cv2.imshow('SERVER0', img)
cv2.waitKey(0)
print("succeed")
else :
print("错误")
soc.close()
cv2.destroyAllWindows()
客户端(请求者,在本示例中是发照片的)
import socket
import cv2
import numpy
from picamera2 import Picamera2
# 建立连接
IPaddress = ("192.168.1.7", 8010)
soc = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
soc.connect(IPaddress)
# 采集一帧图像
picam = Picamera2(0)# 0是CAMERA_NUM
config = picam.create_video_configuration(
main={"size":(820, 616),"format": "RGB888"}
)
picam.configure(config)
picam.start()
print("摄像头已开启")
frame = picam.capture_array()
# 暂时只发一张照片
result, imgencode = cv2.imencode(".jpg", frame)
soc.send(str(len(imgencode.tobytes())).ljust(16).encode())
soc.sendall(imgencode)
soc.close()
cv2.destroyAllWindows()
代码详解
服务端初始化
-
创建一个socket实例
-
绑定地址和端口。其中地址是本机的地址,不是客户端的,你不需要知道客户端的地址,客户端会主动来找你的
-
开始监听请求
-
接受请求并且返回一个客户端的实例
客户端初始化
-
创建一个socket实例
-
填入服务器的地址和端口,申请连接
发送函数
发送函数有两个,一个是send()一个是sendall(),用sendall()会比较稳妥,因为IPv4一次传输有最大的额度,send发一次没发完就不发了,sendall可以循环发送到发完为止。想要了解更多可以看这篇文章
两个函数发的都是bytes或者bytearray或者memoryview,就是字节或者字节数组或者实现了缓冲区协议的对象(类似于C语言的指针)
所以,发送字符串在python3中是非法的,发字符串之前必须要调用一遍encode()。如 sock.send("Hello".encode('utf-8')),或者直接构造 b"Hello"。
结合OpenCV的库函数,就可以将一张图片变为字节数据了。例如result, imgencode = cv2.imencode(".jpg", frame),其中result接收转化成功与否的布尔值,imgencode接收压缩后的JPEG数据,其实就是可以直接塞进sendall了(第三类)。
在发数据之前先将数据大小发过去,其中先转为bytes,再len计算长度,再转为str,再左对齐补充到16位,然后再用encode转为字节
接收函数
很可惜啊,有sendall,但是没有recvall,搞半天还要自己写
实现方式也很简单,首先规定客户端发数据之前要把图片大小发过来,然后再循环接收图片直到到达目标大小
- recv接受一个参数,
bufsize,就是要接受的字节长度,这个数可以很大,但不能保证收完
-
先写一个
recv_size, 上文提到,发过来的大小是16位的,所以我们只要指定16位即可,收到这个是字节型的,后面还要转成int -
再写一个
recv_all,传count,不断接受数据,拼接数据,直到到达count为止
解码图片
-
首先利用numpy将字节数据转为数组,然后利用OpenCV将数组解码为图片,并show出来。
-
然后加上waitKey等待键盘操作再关闭视图。其中waitKey接收一个参数,如果是0值,就无限等待键盘输入,如果是正数则等待那么多毫秒再关闭
comment 评论区
star_outline 咱快来抢个沙发吧!