Seamoonsbird

Seamoonsbird



TCP传输视频流

Seamoonsbird · 2026-08-16 · 73浏览 · 折腾



前言: 笔者正在备赛,新规是树莓派可以与笔电进行通讯,也就是说可以利用笔电进行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()

代码详解

服务端初始化

  1. 创建一个socket实例

  2. 绑定地址和端口。其中地址是本机的地址,不是客户端的,你不需要知道客户端的地址,客户端会主动来找你的

  3. 开始监听请求

  4. 接受请求并且返回一个客户端的实例

客户端初始化

  1. 创建一个socket实例

  2. 填入服务器的地址和端口,申请连接

发送函数

发送函数有两个,一个是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,就是要接受的字节长度,这个数可以很大,但不能保证收完
  1. 先写一个recv_size, 上文提到,发过来的大小是16位的,所以我们只要指定16位即可,收到这个是字节型的,后面还要转成int

  2. 再写一个recv_all,传count,不断接受数据,拼接数据,直到到达count为止

解码图片

  1. 首先利用numpy将字节数据转为数组,然后利用OpenCV将数组解码为图片,并show出来。

  2. 然后加上waitKey等待键盘操作再关闭视图。其中waitKey接收一个参数,如果是0值,就无限等待键盘输入,如果是正数则等待那么多毫秒再关闭



©

comment 评论区

添加新评论

face表情



  • ©2026 bilibili.com

textsms
内容不能为空
昵称不能为空
email
邮件地址格式错误
web
beach_access
验证码不能为空
keyboard发表评论


star_outline 咱快来抢个沙发吧!




©2026 Seamoonsbird

Theme Romanticism2.2 by Akashi
Powered by Typecho