☰
C#蓝牙通讯实战:基于32feet.NET的RFCOMM连接与避坑指南
2026/10/17 2:34:28 网站建设 项目流程

简介:SimpleBTComms 是一份面向 C# 初学者与物联网爱好者的蓝牙通信入门示例,核心演示如何让 C# 程序与 Arduino 等蓝牙设备进行无线数据交换,适合作为课程项目参考或蓝牙通信练手素材。资源包共 12 个文件,以 6 个 cs 源码文件为主体,辅以 resx 资源文件、sln 解决方案、csproj 工程文件及 settings 配置,压缩包约 125KB,结构轻量,便于直接打开研究。项目围绕蓝牙设备发现与枚举、基于服务 UUID 的连接建立、Stream 数据收发、异常处理与连接关闭等关键环节展开,并涉及 BLE 兼容性与跨平台支持等注意事项。由于项目年代较早,代码可能未跟进最新蓝牙标准,实际使用时需评估兼容性并酌情更新。目前已有 409 人学习下载,适合希望快速理解 C# 蓝牙通信基本流程、并以此为起点进行二次开发的开发者参考。

1. 从一次串口调试翻车说起:这个 C# 蓝牙通讯示例到底能干什么

去年帮一个做工业手持终端的朋友调蓝牙通讯,他拿了一份 C# 写的示例代码,说“跑不起来,搜不到设备”。我打开一看,代码里用的是SerialPort类去连蓝牙串口,但设备根本没做 SPP 配对,Windows 也没生成出站 COM 口,自然搜不到。这个场景其实很典型:很多人以为 C# 做蓝牙通讯就是开个串口读写,结果卡在设备发现、配对、RFCOMM 通道绑定这些环节上。SimpleBTComms 这个示例的价值就在这儿——它把 C# 里做经典蓝牙(Bluetooth Classic)通讯的最小闭环拆出来了,从设备发现、服务查询到 RFCOMM 流读写,每一步都有对应代码。它适合两类人:一是刚接触 Windows 蓝牙开发、想找个能跑通的起点的人;二是手头有蓝牙串口模块或蓝牙设备,需要用 C# 上位机做数据交互的工程师。下面我按自己拆项目的习惯,把这份示例的骨架、参数、坑点过一遍。

2. 拆开 SimpleBTComms:设备发现、服务查询与 RFCOMM 流读写

2.1 为什么不用 SerialPort 而用 32feet.NET

C# 里操作蓝牙,常见做法有三种:一是用System.IO.Ports.SerialPort走虚拟串口,二是调 Win32 API,三是用 32feet.NET 这类托管库。SimpleBTComms 选的是 32feet.NET(InTheHand.Net.Bluetooth),原因很直接:SerialPort 方案依赖系统先配对并生成 COM 口,换台机器、换个设备就得重新配,部署时很被动;Win32 API 调用繁琐,P/Invoke 结构体一堆,调试成本高。32feet.NET 把设备发现、服务发现、RFCOMM 连接封装成托管对象,代码量少,跨 Windows 版本表现也稳定。

示例里核心用到的几个类:

类/方法作用典型调用
BluetoothClient蓝牙客户端主入口new BluetoothClient()
DiscoverDevices()同步发现周边设备返回BluetoothDeviceInfo集合
DiscoverDevicesAsync()异步发现,带事件回调适合 UI 线程不卡
GetServiceRecords()查询设备支持的服务拿 RFCOMM 通道号
Connect()建立 RFCOMM 连接传BluetoothEndPoint
GetStream()拿到网络流读写字节

选型理由说清楚:如果你做的是 Windows 桌面上位机,且设备是经典蓝牙(不是 BLE),32feet.NET 是省事的选择。BLE 不在这个示例覆盖范围内,别拿它去连手环、心率带这类 BLE 设备,协议栈不一样。

2.2 设备发现:同步与异步的取舍

示例里设备发现给了两种写法。同步版适合控制台工具或后台服务,代码直白:

using InTheHand.Net.Sockets; // 创建客户端实例,后续所有操作都基于它 var client = new BluetoothClient(); // 同步发现:阻塞当前线程,直到扫描完成 // 参数 true 表示带认证扫描,false 为匿名扫描 BluetoothDeviceInfo[] devices = client.DiscoverDevices(10, true, true, true); foreach (var dev in devices) { // DeviceName 可能为空,需判空 Console.WriteLine($"名称: {dev.DeviceName}"); Console.WriteLine($"地址: {dev.DeviceAddress}"); Console.WriteLine($"已配对: {dev.Authenticated}"); }

DiscoverDevices的参数含义容易搞混:第一个是最大返回数量,后面三个 bool 分别控制是否返回已认证设备、是否返回未认证设备、是否返回 remembered 设备。实际用的时候,如果只想看已配对设备,把第二个参数设 true、第三个设 false,扫描会快很多。异步版用DiscoverDevicesAsync配合DiscoverDevicesProgressChanged事件,适合 WinForms/WPF 里更新列表,避免 UI 卡死。

提示:设备发现阶段拿不到 RFCOMM 通道号,通道号要在服务查询阶段才出现,别在这一步就急着 Connect。

2.3 服务查询与 RFCOMM 连接

发现设备后,下一步是查这个设备支持哪些服务,从中找到 RFCOMM 对应的服务记录,拿到通道号:

// 假设 target 是上一步选中的 BluetoothDeviceInfo var services = target.GetServiceRecords(BluetoothService.SerialPort); foreach (var record in services) { // 从服务记录里取出 RFCOMM 通道号 var rfcommAttr = record.GetAttributeById(BluetoothAttributeId.RfcommChannelNumber); if (rfcommAttr != null) { byte channel = (byte)rfcommAttr.Value; Console.WriteLine($"RFCOMM 通道: {channel}"); } }

BluetoothService.SerialPort是标准 SPP 服务的 UUID。如果设备用的是自定义 UUID,这里要换成对应的 Guid。拿到通道号后建立连接:

// 用设备地址和通道号构造终结点 var endpoint = new BluetoothEndPoint(target.DeviceAddress, BluetoothService.SerialPort, channel); // 连接超时建议自己控制,Connect 本身没有超时参数 client.Connect(endpoint); // 拿到流,后续读写都走这个 Stream var stream = client.GetStream();

Connect没有内置超时,设备不在线时会卡住。常见做法是把它放到 Task 里加Wait(5000),或者用异步连接ConnectAsync。连接成功后GetStream()返回的是NetworkStream,读写方式和 TCP 流一样,但要注意蓝牙 MTU 通常比以太网小,一次别写太大块数据。

2.4 数据读写与线程处理

读写部分示例给的是最基础的同步流操作:

// 发送:把字符串转成字节数组 byte[] sendData = System.Text.Encoding.UTF8.GetBytes("HELLO\r\n"); stream.Write(sendData, 0, sendData.Length); // 接收:循环读取,直到拿到完整帧 byte[] buffer = new byte[1024]; int bytesRead = stream.Read(buffer, 0, buffer.Length); string received = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead); Console.WriteLine($"收到: {received}");

这里有个血泪经验:stream.Read是阻塞的,放在 UI 线程里界面直接假死。正确做法是开一个后台线程或 Task 专门做接收循环,收到数据后通过Invoke或Dispatcher回传 UI。另外蓝牙数据是流式的,不保证一次 Read 就是一条完整消息,协议层要自己定帧(比如用\r\n结尾或长度头)。示例里没做粘包处理,实际项目里必须补上,否则数据一多就乱。

3. 从零跑通示例:环境、配对与第一个连接

3.1 环境准备与依赖引入

跑这个示例前,先确认几件事:Windows 10/11 自带蓝牙驱动正常,设备管理器里能看到蓝牙适配器;目标蓝牙设备处于可发现状态(很多模块默认不可见,要按一下配对键)。开发环境用 Visual Studio 2019 以上,新建一个 .NET Framework 4.7.2 或 .NET 6 的控制台/WinForms 项目都行。

依赖引入用 NuGet,包名是InTheHand.Net.Bluetooth:

# Package Manager Console 里执行 Install-Package InTheHand.Net.Bluetooth # 或者用 dotnet CLI dotnet add package InTheHand.Net.Bluetooth

装完后在代码文件顶部加using InTheHand.Net.Sockets;和using InTheHand.Net;。如果编译报找不到BluetoothClient,八成是包没装对,检查一下项目引用里有没有InTheHand.Net.Bluetooth.dll。

3.2 系统配对与代码连接的关系

这里有个容易翻车的点:32feet.NET 的Connect在多数情况下要求设备已经在 Windows 里配对过。没配对直接连,可能抛SocketException或卡住。稳妥流程是:先在 Windows 设置里手动配对一次,确认系统生成了蓝牙设备条目,再跑代码。配对时如果提示输入 PIN,常见模块默认是1234或0000,具体看设备手册。

配对完成后,代码里可以用BluetoothDeviceInfo的Authenticated属性判断是否已认证。如果要做代码内配对,32feet.NET 提供BluetoothSecurity.PairRequest,但它在不同 Windows 版本上行为不一致,我一般不建议在示例阶段折腾,先用系统配对把链路跑通。

3.3 一个最小可跑的控制台流程

把前面几段拼起来,就是一个能跑的最小流程:

using System; using System.Net.Sockets; using System.Text; using InTheHand.Net; using InTheHand.Net.Bluetooth; using InTheHand.Net.Sockets; class Program { static void Main() { var client = new BluetoothClient(); // 1. 发现设备 var devices = client.DiscoverDevices(10, true, true, true); BluetoothDeviceInfo target = null; foreach (var d in devices) { Console.WriteLine($"{d.DeviceName} - {d.DeviceAddress}"); // 按名称筛选,实际项目可按地址 if (d.DeviceName != null && d.DeviceName.Contains("HC")) target = d; } if (target == null) { Console.WriteLine("未找到目标设备"); return; } // 2. 查询 SPP 服务,拿通道号 byte channel = 1; // 默认通道,部分设备是 1 var records = target.GetServiceRecords(BluetoothService.SerialPort); foreach (var r in records) { var attr = r.GetAttributeById(BluetoothAttributeId.RfcommChannelNumber); if (attr != null) channel = (byte)attr.Value; } // 3. 连接 var ep = new BluetoothEndPoint(target.DeviceAddress, BluetoothService.SerialPort, channel); client.Connect(ep); Console.WriteLine("已连接"); // 4. 发一条数据 var stream = client.GetStream(); byte[] data = Encoding.UTF8.GetBytes("AT\r\n"); stream.Write(data, 0, data.Length); // 5. 读回显 byte[] buf = new byte[256]; int n = stream.Read(buf, 0, buf.Length); Console.WriteLine("收到: " + Encoding.UTF8.GetString(buf, 0, n)); client.Close(); } }

这段代码里,DiscoverDevices的筛选条件按实际设备名改,channel默认给 1 是兜底,正常应该从服务记录里取。Connect如果卡住,先确认设备是否已配对、是否在范围内。跑通这一步,说明链路没问题,后面就是协议层的事。

4. 避坑与排查:蓝牙连接失败的五个常见原因

4.1 现象:DiscoverDevices 返回空列表

原因通常有三个:适配器没开、设备不可发现、权限不足。Windows 上蓝牙适配器被禁用时,DiscoverDevices不报错但返回空。解决:先查设备管理器,确认蓝牙适配器启用;再确认目标设备处于可发现模式(很多模块上电后只广播几秒);最后检查应用是否有蓝牙权限,.NET Framework 项目一般不需要额外声明,但 .NET 6+ 在某些环境下要加清单。

4.2 现象:Connect 抛 SocketException,错误码 10060 或 10061

10060 是超时,10061 是拒绝。超时多半是设备不在范围或没配对;拒绝通常是通道号不对或服务没启动。解决:先用系统蓝牙设置配对一次,再用GetServiceRecords确认通道号,别硬编码。如果设备是自定义 UUID,BluetoothService.SerialPort查不到记录,要换成设备实际的 UUID。

4.3 现象:连接成功但 Read 一直阻塞

这是流式通讯的正常表现——没数据就阻塞。如果确定对方发了数据还是读不到,检查两点:一是对方是否真的往这个 RFCOMM 通道写;二是编码和帧格式是否匹配。常见翻车是对方发的是十六进制裸数据,这边用 UTF8 解,解出来是乱码,看起来像没收到。解决:先用stream.Read拿原始字节,打印十六进制确认。

4.4 现象:换一台电脑就跑不起来

32feet.NET 依赖 Windows 蓝牙栈,不同厂商的适配器驱动行为有差异。有的适配器不支持某些 RFCOMM 操作,换机器就翻车。解决:优先用 Windows 原生驱动,别装厂商自带的全家桶;如果必须用特定适配器,在目标机器上先跑一遍发现和连接测试,别等部署了才发现。

4.5 现象:多设备同时连接时互相干扰

BluetoothClient实例不是线程安全的,多个连接共用一个实例会出问题。解决:每个连接用独立的BluetoothClient实例,接收线程各自独立。另外 Windows 蓝牙栈对并发 RFCOMM 连接数有限制,一般 7 个左右,超过要排队。做多设备采集时,连接池和重连机制要自己写。

5. 进阶:把示例改成能用的上位机骨架

示例跑通只是起点,真正做项目还要补几块。第一块是异步接收循环,用Task.Run包一个while循环,配合CancellationToken做退出:

private async Task ReceiveLoop(NetworkStream stream, CancellationToken token) { byte[] buf = new byte[1024]; while (!token.IsCancellationRequested) { // ReadAsync 避免阻塞线程池线程 int n = await stream.ReadAsync(buf, 0, buf.Length, token); if (n == 0) break; // 对端关闭 var chunk = new byte[n]; Array.Copy(buf, chunk, n); // 交给协议解析器处理粘包 OnDataReceived(chunk); } }

第二块是断线重连。蓝牙链路不稳定,掉线是常态。我一般会记录最后一次连接的BluetoothEndPoint,在接收循环退出后延时 2 秒重连,重连失败次数超过阈值再告警。第三块是协议解析,按帧头帧尾或长度字段拆包,别指望一次 Read 就是一条完整消息。

验证方法上,我习惯用手机上的蓝牙串口 App 做对端,先确认手机能收到上位机发的数据,再反过来发,双向都通才算链路 OK。参数方面,RFCOMM 的 MTU 常见在 600 到 1000 字节之间,单次 Write 别超过这个数,超了要自己分片。

从那以后我每次接蓝牙项目,都强制先跑一遍“发现-配对-查服务-连通道-收发”这五步,任何一步不通就不往下写业务代码。这个习惯帮我省了很多返工。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询