简介这份资源是面向高校计算机、自动化及相关专业学生的毕业设计参考资料围绕多服务器架构下的CARLA自动驾驶仿真展开适合正在准备毕业设计或期末大作业、需要搭建分布式仿真环境的同学。压缩包共251个文件约4.66MB以173个txt配置与说明文档、50个Python脚本为主另含9个xml、3个sumocfg仿真配置、1个xodr地图文件及若干png、md、yaml、json等辅助文件覆盖仿真场景配置、车辆控制脚本与数据定义说明等模块。目前已有198人学习下载。通过该资源读者可获取多服务器CARLA仿真的完整工程结构理解分布式节点间的通信与协同逻辑参考manual_control_sensor等脚本实现传感器数据采集与车辆控制并借助配置文档快速复现实验环境为毕业设计中的仿真验证、代码调试与论文撰写提供可落地的参考方案。1. 多服务器 CARLA 仿真毕业设计里最容易被低估的分布式坑如果你正在做自动驾驶相关的毕业设计单机跑 CARLA 大概率已经让你崩溃过了——场景稍微复杂一点帧率就掉到个位数传感器数据一多主进程直接卡死。这时候“多服务器 CARLA 仿真”这个方向就冒出来了把仿真负载拆到多台机器上一台跑世界模拟其余跑传感器、跑规划算法、跑数据采集。听起来很美好但真正动手你会发现坑不在 CARLA 本身而在“多服务器”这三个字带来的时钟同步、数据分发和进程编排问题。这篇笔记面向两类人一是毕业设计选题落在 CARLA 分布式仿真、需要一套能跑通的方案二是已经单机跑通、想扩展到多机但不知道从哪下手。我会按“先立住架构、再动手复现、最后排坑”的顺序讲所有代码和配置都可以直接抄。2. 多服务器 CARLA 仿真的架构选型同步模式还是异步模式在动手之前必须先做一个决定多台服务器之间是同步还是异步。这个选择直接决定了你后面所有代码的写法也决定了你的毕业设计能不能在答辩时自圆其说。2.1 同步模式与异步模式的本质区别CARLA 本身支持两种运行模式。单机情况下默认是异步模式服务器按自己的节奏推进客户端通过world.tick()或wait_for_tick()来获取最新状态。但在多服务器场景下异步模式会带来一个致命问题不同服务器上的传感器数据时间戳对不上。比如主服务器上车辆已经开到路口传感器服务器上采集到的还是上一帧的相机图像融合算法直接翻车。同步模式则要求所有客户端和服务器在同一个 tick 上对齐。CARLA 的同步模式通过settings.synchronous_mode True开启配合settings.fixed_delta_seconds设定固定步长。多服务器场景下通常由一台“主控服务器”负责推进仿真步其余“从属服务器”在每一步等待主控的信号。常见做法是主控服务器跑世界模拟和交通流从属服务器跑传感器渲染和算法推理两者通过一个轻量的协调进程来同步 tick。我一般会推荐毕业设计用同步模式原因有三第一数据对齐问题天然解决传感器融合、规划控制的结果可复现第二答辩时你可以明确说出“固定步长 0.05 秒即 20Hz 仿真频率”这比“大概每秒跑十几帧”听起来靠谱得多第三同步模式下更容易做确定性回放这对写论文里的实验对比章节至关重要。2.2 多服务器角色划分与通信拓扑角色划分没有标准答案但有一个原则把计算密集且对实时性要求高的任务分开。典型的划分方式是服务器角色主要任务硬件建议主控服务器世界模拟、交通流生成、车辆动力学高主频 CPU 中端 GPU传感器服务器相机、激光雷达、毫米波雷达渲染高端 GPU显存越大越好算法服务器感知、规划、控制算法推理带 GPU 的机器或纯 CPU 集群数据服务器数据采集、存储、回放大容量 SSD 高速网络通信拓扑上最常见的是星型结构主控服务器作为中心节点其余服务器通过 TCP 或共享内存与主控通信。如果传感器服务器和算法服务器在同一台机器上可以用共享内存减少拷贝开销如果跨机器TCP 是稳妥选择但要注意设置TCP_NODELAY避免 Nagle 算法带来的延迟。提示毕业设计里不要一上来就搞三台以上服务器。两台机器——一台主控、一台传感器算法——已经能覆盖绝大多数分布式仿真的核心问题而且调试成本可控。2.3 用 Python 搭建最小多服务器同步框架下面是一个最小可运行的多服务器同步框架。主控服务器负责推进 tick从属服务器在每一步等待主控的同步信号。代码基于 CARLA 的 Python API假设你已经安装好 CARLA 服务器和客户端库。# master_server.py # 主控服务器负责推进仿真步并向从属服务器广播 tick 信号 import carla import socket import struct import time # 连接本机 CARLA 服务器 client carla.Client(localhost, 2000) client.set_timeout(10.0) world client.get_world() # 开启同步模式固定步长 0.05 秒20Hz settings world.get_settings() settings.synchronous_mode True settings.fixed_delta_seconds 0.05 world.apply_settings(settings) # 创建 TCP 服务端用于向从属服务器广播 tick sync_server socket.socket(socket.AF_INET, socket.SOCK_STREAM) sync_server.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sync_server.bind((0.0.0.0, 9999)) sync_server.listen(5) print(等待从属服务器连接...) conn, addr sync_server.accept() print(f从属服务器已连接: {addr}) try: for step in range(1000): # 模拟 1000 步 # 推进 CARLA 世界一步 world.tick() # 获取当前帧号 frame world.get_snapshot().frame # 向从属服务器发送帧号8 字节大端整数 conn.sendall(struct.pack(Q, frame)) # 等待从属服务器确认 ack conn.recv(1) if ack ! b\x01: print(f第 {step} 步未收到确认退出) break time.sleep(0.001) # 轻微让出 CPU finally: conn.close() sync_server.close() # 恢复异步模式避免影响下次运行 settings.synchronous_mode False world.apply_settings(settings)# slave_server.py # 从属服务器等待主控 tick 信号然后执行传感器采集或算法推理 import carla import socket import struct # 连接本机 CARLA 服务器从属服务器上也有 CARLA 实例 client carla.Client(localhost, 2000) client.set_timeout(10.0) world client.get_world() # 从属服务器同样开启同步模式 settings world.get_settings() settings.synchronous_mode True settings.fixed_delta_seconds 0.05 world.apply_settings(settings) # 连接主控服务器的同步端口 sync_client socket.socket(socket.AF_INET, socket.SOCK_STREAM) sync_client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sync_client.connect((主控服务器IP, 9999)) try: while True: # 接收主控发来的帧号 data sync_client.recv(8) if len(data) 8: break frame struct.unpack(Q, data)[0] # 在从属服务器上推进到同一帧 world.tick() # 这里可以插入传感器数据采集或算法推理 # 例如camera.listen(lambda image: process(image)) # 处理完成后发送确认 sync_client.sendall(b\x01) finally: sync_client.close() settings.synchronous_mode False world.apply_settings(settings)这段代码的核心逻辑是主控服务器每推进一个 CARLA tick就通过 TCP 把帧号发给从属服务器从属服务器收到帧号后在自己的 CARLA 实例上推进一个 tick完成传感器采集或算法推理后回传确认。参数方面fixed_delta_seconds 0.05对应 20Hz这是自动驾驶仿真的常用频率如果你的传感器是相机可以调到 0.033 对应 30Hz但要注意网络传输和渲染能否跟上。TCP_NODELAY必须设置否则小包会被 Nagle 算法缓冲导致同步延迟飙升。注意从属服务器上的 CARLA 实例需要加载与主控服务器相同的地图。如果地图不一致world.tick()后的帧号会对不上同步直接失效。3. 传感器数据跨服务器分发从采集到落盘的完整链路多服务器架构里传感器数据的分发是最容易出问题的一环。相机图像、激光雷达点云、毫米波雷达目标列表这些数据要么在从属服务器上直接落盘要么回传给主控服务器做融合。选哪种方式取决于你的毕业设计侧重什么。3.1 数据分发方式的选择直接落盘还是回传直接落盘的意思是传感器服务器采集到数据后直接写入本地磁盘或网络存储主控服务器只记录帧号和文件路径的映射关系。回传的意思是传感器服务器把数据通过 TCP 或共享内存发给主控服务器由主控统一处理。我一般会推荐直接落盘原因很实际相机图像一帧 1920x1080 的 RGB 数据大约 6MB激光雷达一帧 64 线点云大约 2MB如果每秒 20 帧回传的带宽需求轻松超过 100MB/s。普通千兆网根本扛不住万兆网卡和交换机在毕业设计的预算里也不现实。直接落盘则只需要在最后做数据对齐时根据帧号把不同服务器上的文件关联起来。具体做法是在从属服务器上为每个传感器创建一个监听回调回调里把数据保存为文件文件名包含帧号、传感器 ID 和时间戳。主控服务器在每一步 tick 后记录当前帧号并生成一个索引文件。最后用索引文件把不同服务器上的数据关联起来。# sensor_recorder.py # 在从属服务器上运行负责采集传感器数据并落盘 import carla import os import numpy as np import struct # 数据保存根目录 SAVE_ROOT /data/carla_record os.makedirs(SAVE_ROOT, exist_okTrue) def save_camera_data(image, sensor_id, frame): 保存相机图像为二进制文件文件名包含帧号和传感器 ID # image.raw_data 是 BGRA 格式的字节流 array np.frombuffer(image.raw_data, dtypenp.uint8) array array.reshape((image.height, image.width, 4)) # 只保存 BGR 三个通道减少存储 bgr array[:, :, :3] filename f{SAVE_ROOT}/camera_{sensor_id}_frame_{frame:08d}.bin bgr.tofile(filename) # 同时保存一个元数据文件记录宽高和格式 with open(filename .meta, w) as f: f.write(fwidth{image.width}\nheight{image.height}\nformatBGR\n) def save_lidar_data(lidar_data, sensor_id, frame): 保存激光雷达点云为二进制文件 # lidar_data 是 carla.LidarMeasurement 对象 points np.frombuffer(lidar_data.raw_data, dtypenp.float32) points points.reshape((-1, 4)) # x, y, z, intensity filename f{SAVE_ROOT}/lidar_{sensor_id}_frame_{frame:08d}.bin points.tofile(filename) with open(filename .meta, w) as f: f.write(fnum_points{points.shape[0]}\nformatXYZI\n) # 假设已经连接到 CARLA 并获取了 world # 这里省略连接代码重点在回调逻辑 def attach_sensors(world, vehicle): 在车辆上挂载传感器并绑定回调 bp_lib world.get_blueprint_library() # 相机 camera_bp bp_lib.find(sensor.camera.rgb) camera_bp.set_attribute(image_size_x, 1920) camera_bp.set_attribute(image_size_y, 1080) camera_bp.set_attribute(fov, 90) camera world.spawn_actor(camera_bp, carla.Transform( carla.Location(x1.5, z2.0)), attach_tovehicle) # 用闭包捕获 sensor_id camera.listen(lambda image: save_camera_data(image, front, image.frame)) # 激光雷达 lidar_bp bp_lib.find(sensor.lidar.ray_cast) lidar_bp.set_attribute(channels, 64) lidar_bp.set_attribute(range, 100) lidar_bp.set_attribute(points_per_second, 1000000) lidar world.spawn_actor(lidar_bp, carla.Transform( carla.Location(x0, z2.5)), attach_tovehicle) lidar.listen(lambda data: save_lidar_data(data, top, data.frame)) return camera, lidar这段代码的关键点在于相机数据保存为 BGR 三通道二进制比保存 PNG 快得多而且后续用 NumPy 读取时不需要解码激光雷达点云保存为 XYZI 四通道浮点数直接np.fromfile就能读。元数据文件记录了宽高和格式方便后续处理时不用猜。参数方面points_per_second设为 1000000 对应 100 万点每秒64 线雷达在 20Hz 下每帧大约 5 万点这个值可以根据你的存储和算力调整。3.2 帧号对齐与索引文件生成数据落盘后下一步是生成索引文件把不同服务器上的数据按帧号关联起来。索引文件可以用 JSON Lines 格式每行一个 JSON 对象记录帧号、时间戳、各传感器文件路径。# index_builder.py # 在主控服务器上运行根据帧号生成索引文件 import json import os SAVE_ROOT /data/carla_record INDEX_FILE /data/carla_record/index.jsonl def build_index(start_frame, end_frame): 根据帧号范围生成索引文件 with open(INDEX_FILE, w) as f: for frame in range(start_frame, end_frame 1): entry { frame: frame, timestamp: frame * 0.05, # 固定步长 0.05 秒 sensors: {} } # 检查各传感器文件是否存在 camera_file f{SAVE_ROOT}/camera_front_frame_{frame:08d}.bin if os.path.exists(camera_file): entry[sensors][camera_front] camera_file lidar_file f{SAVE_ROOT}/lidar_top_frame_{frame:08d}.bin if os.path.exists(lidar_file): entry[sensors][lidar_top] lidar_file # 只写入至少有一个传感器数据的帧 if entry[sensors]: f.write(json.dumps(entry) \n) if __name__ __main__: build_index(0, 999)索引文件生成后后续做数据融合或算法训练时只需要读取index.jsonl就能知道每一帧有哪些传感器数据、文件在哪里。这个设计的好处是数据采集和数据处理解耦采集时不用关心算法需要什么处理时不用关心数据在哪台机器上。3.3 跨服务器数据读取与验证最后一步是验证数据是否对齐。写一个简单的读取脚本随机抽取几帧检查相机和激光雷达的时间戳是否一致。# verify_alignment.py # 验证跨服务器数据对齐情况 import json import numpy as np INDEX_FILE /data/carla_record/index.jsonl def verify(): with open(INDEX_FILE, r) as f: lines f.readlines() # 随机抽取 5 帧 indices np.random.choice(len(lines), 5, replaceFalse) for idx in indices: entry json.loads(lines[idx]) frame entry[frame] print(f帧号: {frame}, 时间戳: {entry[timestamp]:.3f}s) for sensor_name, filepath in entry[sensors].items(): # 读取文件头几个字节验证可读性 with open(filepath, rb) as sf: data sf.read(16) print(f {sensor_name}: {filepath}, 前16字节: {data.hex()}) print() if __name__ __main__: verify()运行这个脚本如果每一帧的相机和激光雷达文件都能正常读取且帧号连续说明数据对齐没问题。如果发现某些帧缺少某个传感器的数据需要回到采集脚本检查回调是否被正确触发。4. 多服务器 CARLA 仿真的避坑与排查这一章记录我在多服务器 CARLA 仿真里踩过的坑每条按“现象 → 原因 → 解决”写。这些坑在单机模式下不会遇到但一旦扩展到多服务器几乎必然出现。4.1 从属服务器 tick 卡死主控一直等待确认现象主控服务器发送帧号后从属服务器没有回传确认主控阻塞在conn.recv(1)整个仿真停滞。原因从属服务器上的world.tick()在某些情况下会阻塞比如地图加载未完成、传感器回调耗时过长、或者 CARLA 服务器本身崩溃。另外如果从属服务器的 CARLA 实例没有开启同步模式world.tick()的行为会和主控不一致。解决第一确保从属服务器的settings.synchronous_mode True和fixed_delta_seconds与主控完全一致第二在从属服务器的 tick 前后加日志确认卡在哪一步第三给conn.recv设置超时避免无限等待。# 在从属服务器上设置 socket 超时 sync_client.settimeout(5.0) try: data sync_client.recv(8) except socket.timeout: print(等待主控信号超时检查主控是否在运行) break4.2 传感器数据帧号与主控帧号不一致现象索引文件里记录的帧号是 100但相机文件名里的帧号是 98激光雷达是 102。原因CARLA 的传感器回调是异步的image.frame表示图像采集时的帧号但回调执行时世界可能已经推进了好几步。在多服务器同步模式下如果从属服务器在收到主控信号后立即 tick 并触发回调回调里的image.frame可能还是上一帧的。解决不要在回调里直接用image.frame作为文件名。正确做法是在从属服务器的主循环里收到主控帧号后先 tick然后主动从传感器获取最新数据而不是依赖回调。或者在回调里用一个共享变量记录当前主循环的帧号用这个帧号作为文件名。# 用共享变量记录当前帧号 current_frame 0 def save_camera_data(image): global current_frame # 使用主循环的帧号而不是 image.frame filename f{SAVE_ROOT}/camera_front_frame_{current_frame:08d}.bin # ... 保存逻辑 # 在主循环里更新 current_frame while True: data sync_client.recv(8) current_frame struct.unpack(Q, data)[0] world.tick() # 此时回调里的 current_frame 已经是正确的 sync_client.sendall(b\x01)4.3 网络带宽打满导致同步延迟累积现象仿真运行几分钟后主控和从属之间的同步延迟越来越大从属服务器落后主控几十帧。原因传感器数据回传或索引文件写入占用了大量带宽TCP 缓冲区堆积导致同步信号被延迟处理。另外如果从属服务器上同时运行多个传感器回调CPU 和磁盘 IO 也可能成为瓶颈。解决第一坚持直接落盘不要回传原始数据第二索引文件写入用追加模式不要每次重写整个文件第三如果必须回传只回传元数据帧号、文件路径、时间戳不回传原始数据第四在从属服务器上把传感器回调放到独立线程避免阻塞主循环。# 用独立线程处理传感器数据落盘 import threading import queue save_queue queue.Queue(maxsize100) def save_worker(): while True: item save_queue.get() if item is None: break # 执行落盘操作 save_camera_data(item[image], item[frame]) save_queue.task_done() # 启动工作线程 threading.Thread(targetsave_worker, daemonTrue).start() # 回调里只入队不落盘 def on_camera_image(image): try: save_queue.put_nowait({image: image, frame: current_frame}) except queue.Full: print(保存队列已满丢弃一帧)4.4 从属服务器 CARLA 实例内存泄漏现象从属服务器运行一段时间后内存占用持续上升最终 OOM 被杀。原因CARLA 的 Python API 在某些版本下存在内存泄漏尤其是传感器回调里创建的 NumPy 数组如果没有及时释放会累积占用内存。另外如果从属服务器上反复创建和销毁传感器也会导致内存碎片。解决第一传感器只创建一次不要每帧重建第二在回调里用del显式释放大数组第三定期重启从属服务器的 CARLA 实例比如每采集 10000 帧重启一次第四用gc.collect()手动触发垃圾回收。import gc def save_camera_data(image): array np.frombuffer(image.raw_data, dtypenp.uint8) array array.reshape((image.height, image.width, 4)) bgr array[:, :, :3] bgr.tofile(filename) # 显式释放 del array, bgr # 每 100 帧触发一次垃圾回收 if current_frame % 100 0: gc.collect()4.5 主控服务器地图与从属服务器地图不一致现象从属服务器 tick 后帧号与主控对不上或者传感器数据里的场景和主控完全不是同一个地方。原因CARLA 服务器启动时默认加载的地图可能不同。如果主控加载的是 Town01从属加载的是 Town02两者的世界状态完全独立同步毫无意义。解决在启动 CARLA 服务器时显式指定地图或者在 Python 脚本里用client.load_world(Town01)强制加载同一张地图。注意load_world会重置世界必须在创建车辆和传感器之前调用。# 在主控和从属服务器上都执行 client.load_world(Town01) # 等待地图加载完成 import time time.sleep(5) world client.get_world()5. 进阶用帧号做确定性回放与实验对比毕业设计里实验对比章节往往需要“同样的场景跑两次只改一个变量”。在多服务器 CARLA 仿真里这要求你能够精确回放某一帧的世界状态。CARLA 本身不提供完整的快照回放功能但你可以用帧号做确定性回放记录每一帧的车辆位置、速度、传感器数据然后在需要时重新加载这些状态。具体做法是在主控服务器上每一步 tick 后记录所有车辆的状态位置、旋转、速度、控制量写入一个 JSON Lines 文件。回放时读取这个文件在每一步 tick 前用vehicle.set_transform()和vehicle.set_target_velocity()恢复状态。传感器数据则直接读取之前落盘的文件不需要重新渲染。# state_recorder.py # 在主控服务器上记录车辆状态 import json STATE_FILE /data/carla_record/vehicle_states.jsonl def record_vehicle_states(world, frame): 记录当前帧所有车辆的状态 states [] for actor in world.get_actors().filter(vehicle.*): transform actor.get_transform() velocity actor.get_velocity() control actor.get_control() states.append({ id: actor.id, location: {x: transform.location.x, y: transform.location.y, z: transform.location.z}, rotation: {pitch: transform.rotation.pitch, yaw: transform.rotation.yaw, roll: transform.rotation.roll}, velocity: {x: velocity.x, y: velocity.y, z: velocity.z}, control: {throttle: control.throttle, brake: control.brake, steer: control.steer} }) with open(STATE_FILE, a) as f: f.write(json.dumps({frame: frame, states: states}) \n)回放时读取vehicle_states.jsonl按帧号恢复状态。注意回放必须在同步模式下进行且fixed_delta_seconds与录制时一致。如果录制时是 0.05 秒回放时也必须是 0.05 秒否则物理模拟的结果会不同。验证回放是否成功的方法录制一段 100 帧的状态回放后再次记录状态对比两次记录的位置和速度。如果误差在 1e-3 以内说明回放是确定性的。如果误差较大检查是否有随机因素比如交通流生成器的随机种子、传感器噪声模型等。我自己的习惯是每次跑实验前先跑一段 10 帧的短回放确认状态恢复正确再跑完整实验。这个习惯帮我省了很多“跑完两小时才发现回放没对齐”的后悔药。另外状态文件会随着帧数线性增长10000 帧大约 50MB毕业设计的存储完全够用但记得定期清理旧文件。希望帮到你。本文还有配套的精品资源点击获取