Windows蓝牙串口通信实战:基于Winsock API实现RFCOMM服务端

发布时间:2026/10/14 1:50:59
Windows蓝牙串口通信实战:基于Winsock API实现RFCOMM服务端 最近一直在捣鼓 Windows 桌面端的蓝牙通信准确说是把 PC 变成一台蓝牙串口服务端再配合手机上的串口调试 App 收发数据。中间查资料的时候发现一个很尴尬的现象网上一搜 Windows 蓝牙编程跳出来的全是 WinRT 那一套Windows.Devices.Bluetooth代码风格是 C/CX 或者 C#对还在维护老工程、只想在 VS2017 里老老实实编一个 Win32 程序的人来说参考价值极其有限。所以我干脆用最传统的 Winsock 蓝牙扩展 API 走了一遍完整流程——从扫描附近蓝牙设备到注册 SDP 服务、建立 RFCOMM 串口服务再到收发字节流所有代码在 VS2017 里一次编译通过。这篇文章就把这套路子完整记录下来适合需要在 Windows 桌面端做蓝牙串口通信、又不想被 UWP 和 WinRT 绑架的开发者参考。1. 为什么选“老掉牙”的 Winsock 蓝牙 API而不是 WinRT先说结论如果你是维护一个传统的 Win32 / MFC / Qt 桌面程序要在 VS2017 里编译Winsock 的蓝牙扩展是最省事的选择没有之一。Windows 平台做蓝牙编程常见的有两条技术路线。一条是 WinRT 的Windows.Devices.Bluetooth这条 API 设计得确实现代支持 BLE、支持异步但问题是它面向的是 UWP 应用虽然在桌面应用里也能通过 C/WinRT 调用但牵扯到异步操作、委托、IAsyncOperation这类概念工程配置也麻烦——VS2017 对 C/WinRT 的支持不算友好稍不注意就给你整出一堆链接错误。另一条就是我们今天的主角Winsock 的蓝牙扩展核心就是AF_BTH地址族和BTHPROTO_RFCOMM协议。这套 API 从 XP 时代就有了微软一直没砍掉原因是它严格遵循了 Berkeley Socket 的模型你要创建一个蓝牙 socket本质上和创建 TCP socket 没什么两样。我自己的判断标准很简单项目要求的是“在 VS2017 里能编译通过”那就说明大概率是传统桌面工程没必要为了一个新 API 去改造整个工程结构。Winsock 蓝牙 API 的头文件ws2bth.h在 Windows SDK 里一直保留着链接ws2_32.lib和Bthprops.lib就能跑编译环境干净利落。还有一个容易被忽略的点蓝牙 SPPSerial Port Profile串口协议在协议栈里跑的就是 RFCOMM而 RFCOMM 在 Winsock 里的语义就是SOCK_STREAM和 TCP 的 socket 类型完全一致。这意味着你熟悉的bind、listen、accept、connect、recv、send这一套函数全部原样可用。唯一的区别是地址结构从sockaddr_in换成了SOCKADDR_BTH。提示如果你的工程是 VS2017 但用了较老的 Windows SDK比如 8.1ws2bth.h也是存在的不用担心。真正容易翻车的是_WIN32_WINNT宏版本太低导致AF_BTH没被定义后面第五节会专门说怎么处理。2. 扫描周围蓝牙设备WSALookupService 系列的正确打开方式2.1 思路把蓝牙设备当成“服务”来查询Winsock 蓝牙扫描用到的核心 API 是WSALookupServiceBegin、WSALookupServiceNext、WSALookupServiceEnd。这套 API 本来是用于 DNS 和 SLP 服务查询的微软把它扩展到了蓝牙命名空间NS_BTH上。理解这一点非常重要在 Winsock 的世界里一个蓝牙设备就是一条服务记录。你要找设备就是在NS_BTH命名空间里做一次服务查询。大概逻辑是这样发起查询时用一个WSAQUERYSET结构设置dwNameSpace NS_BTH然后用LUP_CONTAINERS标志告诉系统“我要枚举容器也就是蓝牙设备本身”而不是枚举设备上具体某个服务。这俩的区别非常坑我第一次写的时候就是因为没搞清楚结果不是查不到设备就是返回一堆看不懂的 GUID。查询标志dwFlags也很有讲究。最常用的组合是LUP_CONTAINERS枚举周围蓝牙设备LUP_RETURN_NAME返回设备名称LUP_RETURN_ADDR返回设备地址这个必须加否则拿不到SOCKADDR_BTH还有一个关键点查询缓冲区大小。设备名、地址信息都放在WSAQUERYSET里如果缓冲区给小了WSALookupServiceNext会返回WSAEFAULT这时候需要用返回值里的所需大小重新分配。实际开发里我直接分配一个WSAQUERYSET 4096字节的缓冲区省得反复调整。2.2 完整扫描代码骨架下面是我整理出来的最小可运行扫描逻辑每次调用EnumerateBluetoothDevices就会打印出附近所有可见蓝牙设备的名字和地址#include winsock2.h #include ws2bth.h #include stdio.h #pragma comment(lib, ws2_32.lib) #pragma comment(lib, Bthprops.lib) void EnumerateBluetoothDevices() { WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), wsaData); WSAQUERYSET querySet { 0 }; querySet.dwSize sizeof(querySet); querySet.dwNameSpace NS_BTH; querySet.lpcsaBuffer NULL; DWORD flags LUP_CONTAINERS | LUP_RETURN_NAME | LUP_RETURN_ADDR; HANDLE hLookup NULL; int ret WSALookupServiceBegin(querySet, flags, hLookup); if (ret ! 0) { printf(WSALookupServiceBegin failed: %d\n, WSAGetLastError()); return; } // 分配足够大的缓冲区 DWORD bufferLen sizeof(WSAQUERYSET) 4096; WSAQUERYSET* pResult (WSAQUERYSET*)malloc(bufferLen); if (!pResult) { WSALookupServiceEnd(hLookup); return; } while (true) { ZeroMemory(pResult, bufferLen); pResult-dwSize sizeof(WSAQUERYSET); pResult-dwNameSpace NS_BTH; ret WSALookupServiceNext(hLookup, flags, bufferLen, pResult); if (ret ! 0) { int err WSAGetLastError(); if (err WSA_E_NO_MORE || err WSAENOMORE) { break; // 枚举完毕 } else { printf(WSALookupServiceNext error: %d\n, err); break; } } const char* name pResult-lpszServiceInstanceName ? pResult-lpszServiceInstanceName : (unnamed); BTH_ADDR bthAddr 0; if (pResult-lpcsaBuffer) { SOCKADDR_BTH* pSockAddr (SOCKADDR_BTH*)pResult-lpcsaBuffer-RemoteAddr.lpSockaddr; if (pSockAddr) { bthAddr pSockAddr-btAddr; } } printf(Device: %s Addr: %016llX\n, name, (unsigned long long)bthAddr); } free(pResult); WSALookupServiceEnd(hLookup); WSACleanup(); }这段代码在 VS2017 里可以直接粘贴测试。需要提醒一句lpcsaBuffer指向的是CSADDR_INFO结构里面RemoteAddr才是一个SOCKET_ADDRESS再往里才是SOCKADDR_BTH。我看到不少人直接把lpcsaBuffer强转成SOCKADDR_BTH*虽然碰巧也能读对但严谨说是不对的因为CSADDR_INFO开头是LocalAddr和RemoteAddr两个SOCKET_ADDRESS结构体只有RemoteAddr.lpSockaddr才是地址的指针。2.3 扫描不出设备的常见原因按我实测的经验扫描最常见的坑有三个。第一个是LUP_CONTAINERS没加或者错误地加上了别的标志导致查询的是服务而不是设备返回结果要么空要么不完整。第二个是电脑蓝牙是“隐藏”状态也就是“允许设备查找此电脑”选项没开这是系统的隐私设置程序是改不了的必须在 Windows 设置里的蓝牙页面打开。第三个是 USB 蓝牙适配器的驱动不对很多免驱适配器装的是通用微软栈扫描函数返回 0但实际设备列表就是空的换一个自带驱动的适配器就好了。另外有一个很实际的经验如果WSALookupServiceBegin直接返回WSASERVICE_NOT_FOUND10101优先检查是不是用了 32 位程序调 64 位系统上的某些旧版驱动这种情况在 Windows 7 上很常见Win10 基本没有如果返回的是WSAEINVAL百分之九十九是querySet.dwSize没填或者填错。3. 建立串口服务端绑定 RFCOMM 通道并注册 SDP 记录3.1 先理解“串口服务”在蓝牙协议栈里的定位标题里说的“建立串口服务”放在蓝牙协议栈里就是 SPP RFCOMM。RFCOMM 协议模拟了 RS-232 串口的信号和数据通路应用层不关心底层的跳频、基带只需要把一个 socket 绑定到某个 RFCOMM 通道上然后监听连接。对远端设备来说它就看到了一个“虚拟串口”。这里有一个非常重要的点在 Windows 上如果只创建一个 RFCOMM socket 并 bind listen远端设备是搜不到你的服务的。原因是蓝牙服务发现依赖 SDP 协议你的程序必须向系统的 SDP 数据库注册一条服务记录声明“我支持 SerialPort Profile我监听在某个 RFCOMM 通道上”。这个动作通过WSASetServiceRNRSERVICE_REGISTER完成。如果不注册 SDP你拿着手机上的蓝牙串口 App 去搜索看到的只有你这台电脑的蓝牙适配器名字看不到任何可连接的串口服务。3.2 服务端核心代码先定义几个关键常量。SerialPort 服务的 UUID 是蓝牙标准里规定的// {00001101-0000-1000-8000-00805F9B34FB} static const GUID GUID_SerialPortServiceClass { 0x00001101, 0x0000, 0x1000, { 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB } };接下来是服务端流程SOCKET StartBluetoothSerialServer() { // 1. 初始化 Winsock WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), wsaData); // 2. 创建蓝牙 RFCOMM socket SOCKET serverSocket socket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM); if (serverSocket INVALID_SOCKET) { printf(socket failed: %d\n, WSAGetLastError()); return INVALID_SOCKET; } // 3. 绑定本地蓝牙适配器 SOCKADDR_BTH localAddr { 0 }; localAddr.addressFamily AF_BTH; localAddr.btAddr BDADDR_ANY; // 绑定本机任意蓝牙适配器 localAddr.serviceClassId GUID_SerialPortServiceClass; localAddr.port BT_PORT_ANY; // 让系统分配 RFCOMM 通道 int bindRet bind(serverSocket, (SOCKADDR*)localAddr, sizeof(localAddr)); if (bindRet SOCKET_ERROR) { printf(bind failed: %d\n, WSAGetLastError()); closesocket(serverSocket); return INVALID_SOCKET; } // 4. 查询实际分配的 RFCOMM 通道号 int nameLen sizeof(localAddr); getsockname(serverSocket, (SOCKADDR*)localAddr, nameLen); printf(RFCOMM channel: %d\n, localAddr.port); // 5. 注册 SDP 服务记录否则远端设备发现不了 WSAQUERYSET service { 0 }; service.dwSize sizeof(service); service.lpServiceClassId (LPGUID)GUID_SerialPortServiceClass; service.dwNameSpace NS_BTH; service.dwNumberOfCsAddrs 1; CSADDR_INFO csAddr { 0 }; csAddr.LocalAddr.lpSockaddr (LPSOCKADDR)localAddr; csAddr.LocalAddr.iSockaddrLength sizeof(localAddr); service.lpcsaBuffer csAddr; int regRet WSASetService(service, RNRSERVICE_REGISTER, 0); if (regRet ! 0) { printf(WSASetService(SDP register) failed: %d\n, WSAGetLastError()); } else { printf(SDP registered OK\n); } // 6. 监听 listen(serverSocket, 1); return serverSocket; }注意这里lpServiceClassId填的是 SerialPort 的 UUID不是随便一个自定义 GUID。如果你填错或省略这一步远端设备可能看到一条“未知服务”或者干脆看不到。我测试过同样的 bind listen注册了 SDP 之后手机蓝牙串口 App 能连上不注册就连不上报“目标设备拒绝连接”。3.3 频繁重启程序导致“服务注册冲突”怎么办开发阶段会反复跑程序这时候容易遇到WSASetService返回WSAEALREADY或者类似错误。原因是上一次程序异常退出SDP 记录没被注销系统里残留了同一条服务记录。解决办法有两个一是写一个清理函数在程序启动时尝试删除同名 SDP 记录也就是调用WSASetService时把标志换成RNRSERVICE_DELETE忽略“找不到记录”的错误二是换个serviceClassId——不太推荐因为标准串口服务 UUID 就这一个。我实际的做法比较简单服务端程序退出之前调用下面这个函数void UnregisterBluetoothService(SOCKADDR_BTH* localAddr) { WSAQUERYSET service { 0 }; service.dwSize sizeof(service); service.lpServiceClassId (LPGUID)GUID_SerialPortServiceClass; service.dwNameSpace NS_BTH; service.dwNumberOfCsAddrs 1; CSADDR_INFO csAddr { 0 }; csAddr.LocalAddr.lpSockaddr (LPSOCKADDR)localAddr; csAddr.LocalAddr.iSockaddrLength sizeof(SOCKADDR_BTH); service.lpcsaBuffer csAddr; WSASetService(service, RNRSERVICE_DELETE, 0); }就算这一步偶尔失败也没关系Windows 的 SDP 数据库不是永久存储的蓝牙适配器重启或者重启系统基本就清掉了只是开发时要注意别累积一堆残留记录。真遇到顽固残留最粗暴的办法是去设备管理器里禁用再启用蓝牙适配器实测有效。4. 接受连接并收发数据RFCOMM 的“串口语义”4.1 服务端 accept 和客户端连接listen之后accept的用法和 TCP 完全一样。唯一的区别是accept返回的第二个参数应该传SOCKADDR_BTH*这样你才能知道连进来的是哪台设备的哪个地址SOCKADDR_BTH clientAddr { 0 }; int addrLen sizeof(clientAddr); SOCKET clientSocket accept(serverSocket, (SOCKADDR*)clientAddr, addrLen); if (clientSocket ! INVALID_SOCKET) { printf(Client connected, btAddr: %016llX\n, (unsigned long long)clientAddr.btAddr); }如果要做反向的情况——PC 作为客户端去连一个蓝牙串口模块比如常见的 HC-05、HC-06或者手机上的蓝牙串口 App 开的服务——流程也一样套路只是把connect的目标地址填成扫描到的设备地址端口填目标 RFCOMM 通道。对于大多数标准 SPP 模块RFCOMM 通道号是 1可以直接写remoteAddr.port 1但更准确的做法是发起一次 SDP 查询去查目标设备上的 SerialPort 服务到底在哪个通道。Windows 里可以用WSALookupServiceBegin加上LUP_RETURN_BLOB来查逻辑稍微复杂一点如果你的目标设备固定是某个模块直接写死通道 1 是省事的但遇到某些安卓手机实现不标准的 SPP 服务最好还是做 SDP 查询。连接的本端代码SOCKET clientSocket socket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM); SOCKADDR_BTH remoteAddr { 0 }; remoteAddr.addressFamily AF_BTH; remoteAddr.btAddr targetDeviceAddr; // 扫描阶段拿到的 64 位蓝牙地址 remoteAddr.port 1; // 常见 SPP 服务默认通道 1 int ret connect(clientSocket, (SOCKADDR*)remoteAddr, sizeof(remoteAddr)); if (ret SOCKET_ERROR) { printf(connect failed: %d\n, WSAGetLastError()); }4.2 收发数据时要注意“串口”不是 TCP很多从 TCP 转过来的人在这里会踩坑他们以为send一次发 100 字节对面recv就能一次收到 100 字节。RFCOMM 虽然底层也带流控和重传但它是面向字节流的协议recv返回多少字节完全取决于链路层什么时候把数据递上来可能你发 100 字节对面分成 3 次收到 405010。这一点在写上位机和单片机通信的时候尤其重要因为单片机串口程序通常按帧收数据你必须在应用层做分包和组帧。我的做法是定义一个简单的接收循环把收到的数据累积到缓冲区同时用超时控制char buf[1024]; while (true) { int bytes recv(clientSocket, buf, sizeof(buf), 0); if (bytes 0) { // 把 bytes 字节追加到应用层缓冲区 // 然后按自己的帧协议解析 } else if (bytes 0) { printf(Remote closed connection\n); break; } else { int err WSAGetLastError(); if (err WSAETIMEDOUT || err WSAEWOULDBLOCK) { continue; } printf(recv error: %d\n, err); break; } }recv返回 0 表示对方正常关闭了 RFCOMM 连接返回SOCKET_ERROR通常是对端异常断开或者蓝牙链路断开。蓝牙链路有个特点如果对端设备突然关机或者走出覆盖范围TCP 那种“几分钟后才发现断开”的情况在蓝牙上相对好一些因为底层的基带断开会导致 RFCOMM 很快报错但具体延迟仍然依赖驱动实现不要指望它像本地 TCP 一样灵敏。4.3 一个值得留意的细节本机同时只能有一个进程绑定串口服务在调试多个程序同时监听同一个 RFCOMM 服务时会发现新进程 bind 失败报 10048WSAEADDRINUSE。这和 TCP 的端口占用是同一个错误但原因更隐蔽一个蓝牙适配器上同一个 SerialPort 服务 UUID 在同一时刻只能被一条 SDP 记录占有。如果你的旧进程还在监听新进程 bind 成功但注册 SDP 时会失败。调试时最干净的姿势是确保旧进程退出后再启动新进程或者用前面的RNRSERVICE_DELETE先清理。另外Windows 蓝牙对同一时刻的 SPP 客户端数量也有严格限制listen的 backlog 填 1 或 2 比较符合实际。蓝牙链路本身带宽有限即使你开多个并发客户端整体吞吐量也不会因为并发而有数量级提升。5. VS2017 编译通过的完整工程配置与报错修复5.1 工程属性配置新建一个 Win32 控制台项目或 Win32 项目这几个属性值得提前改好配置项推荐值说明平台工具集Visual Studio 2017 (v141)默认设置无需调整Windows SDK 版本10.0.17763.0 或更新老 SDK 也基本能编但建议用新的字符集使用多字节字符集如果你用 printf 输出调试信息多字节比 Unicode 省事预处理定义_WIN32_WINNT0x0601让 Windows SDK 暴露蓝牙相关定义C/C 预编译头不使用预编译头小 Demo 没必要开链接器 附加依赖项ws2_32.lib; Bthprops.lib;或者在代码里#pragma comment(lib, ...)最关键的其实是_WIN32_WINNT。ws2bth.h里AF_BTH、BTHPROTO_RFCOMM这些定义受版本宏保护默认值可能只有 0x0501XP导致你明明包含了头文件却报“BTHPROTO_RFCOMM未定义”。这一点特别坑因为报错时你不会想到去查这个宏。5.2 常见编译错误对照表我实际编译过程中遇到的报错列个清单方便排查错误信息原因解决方法error C2065: AF_BTH: undeclared identifier_WIN32_WINNT版本太低代码开头加#define _WIN32_WINNT 0x0601且必须放在所有头文件之前error LNK2019: unresolved external symbol __imp_WSALookupServiceBeginW没链接 ws2_32.lib工程链接器加ws2_32.lib或#pragma comment(lib, ws2_32.lib)error LNK2019: unresolved external symbol __imp_WSASetServiceW同上加ws2_32.liberror C2065: BTHPROTO_RFCOMM: undeclared identifierws2bth.h没包含或 SDK 版本过旧确认包含ws2bth.h并检查 SDK 版本warning C4005: _WIN32_WINNT: macro redefinition工程设置和代码里宏冲突统一在工程预处理器里定义代码里不再重复定义error C4996: WSAStartup: was declared deprecated项目启用了 SDL 安全检查但实际上不影响运行不需要处理或者关闭 SDL 检查补充一下虽然Bthprops.lib主要是蓝牙辅助函数库比如BluetoothFindFirstDevice但有的 SDK 版本里某些和 SDP 相关的导出也在里面把两个库都加上是最稳妥的。5.3 推荐的文件组织方式我建议把蓝牙封装成一个独立的模块不要在主程序里直接铺这些 API。最简单的做法是建bt_serial.h和bt_serial.cpp对外提供四个函数枚举设备、启动服务端、连接远端、关闭服务。原因有两点第一WSALookupServiceBegin这套 API 的缓冲区和句柄管理细节不少封装一次以后不用重复踩坑第二主程序如果只有 200 行那无所谓但你后面八成会加上界面、日志、协议解析分开写才能保证主程序不变成一坨。封装的接口可以长这样#pragma once #include winsock2.h #include ws2bth.h #ifdef __cplusplus extern C { #endif int bt_enum_devices(void(*callback)(const char* name, BTH_ADDR addr, void* ctx), void* ctx); SOCKET bt_start_server(unsigned short* channel_out); SOCKET bt_connect(BTH_ADDR remote_addr, unsigned short channel); void bt_close(SOCKET s); #ifdef __cplusplus } #endif5.4 一个“编译通过后立刻翻车”的典型问题第一次编译通过后我兴致勃勃地运行程序结果socket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM)直接返回INVALID_SOCKETWSAGetLastError是 10047WSAEAFNOSUPPORT。这个错误的意思是地址族不支持常见原因是程序运行在老的 Windows 7 上而机器的蓝牙驱动只支持 Bluetooth 2.0 但系统服务没起来或者虚拟机里没有蓝牙适配器。如果在 Win10 真机上还报这个错去设备管理器看看蓝牙适配器的驱动把微软的通用驱动换成厂商驱动比如 Intel 的蓝牙驱动通常能解决。另一个很容易让人误判的错误是 10049WSAEADDRNOTAVAIL出现在bind时btAddr填了一个具体的蓝牙地址而不是BDADDR_ANY。原因是本机可能有多个蓝牙适配器比如内置一个、USB 一个你填的那个地址恰好对应不存在的适配器。解决办法要么用BDADDR_ANY让系统选要么先用getsockname在一个BDADDR_ANYsocket 上拿到实际地址再绑定。6. 实测验证与调优怎么确定这套链路真的通了6.1 推荐用手机蓝牙串口 App 做对端服务端写好之后最直观的验证方式是用手机上的蓝牙串口 App 去连接电脑。手机支持 SPP 的 App 一大堆Google Play 商店里搜“Bluetooth Terminal”或者“串口助手”随便装一个。打开 App搜索设备会看到你电脑的蓝牙适配器名字点进去就能看到它探测到的服务——如果你的 SDP 注册正确服务名通常是“SerialPort”或者你注册时设置的lpszServiceInstanceName。连接成功后手机 App 会显示一个类似终端的界面。你在电脑服务端代码里send一段字符串手机上立刻能看到反过来手机发送电脑recv也能收到。这是最省事的端到端验证比拿两块 USB 蓝牙适配器互相测要方便得多。提示如果没有手机两个 USB 蓝牙适配器插在同一台电脑上也能测但 Windows 对同一适配器做“自己连自己”通常不灵必须两个物理适配器才行。6.2 关注几个性能和异常指标测试的时候不要只看“能连上”还要关注几个隐含指标。一是连接延迟。SPP 连接本身是秒级的accept返回一般在 100ms 到 3 秒之间如果超过 5 秒还没反应多半是 SDP 注册出了问题或者对端设备的服务发现超时。二是数据吞吐。RFCOMM 的理论带宽受蓝牙版本限制经典蓝牙BR/EDR的 SPP 实测大概在几十 KB/s 到一两百 KB/s 之间取决于适配器和环境干扰。如果你的应用需要连续传文件建议在应用层做分包每包 512 字节比较均衡太大反而会因为链路丢包重传导致延迟飙升。三是断线检测。蓝牙连接被意外断开比如对端走远时recv返回的错误码往往是WSAECONNRESET10054或者WSAENOTCONN10057。不要在这时候傻傻地 close socket更稳妥的做法是进入重连逻辑先closesocket释放句柄再重新 socket connect这比尝试在同一个 socket 上复用要可靠得多。6.3 数据粘包与拆包的实战建议串口通信场景几乎必然涉及二进制帧协议。我的习惯是定义一种极简的帧格式帧头两个字节0xAA 0x55紧接着两个字节的长度字段小端然后是载荷数据最后加一个字节的校验累加和。接收端维护一个环形缓冲区每次recv到数据后丢进缓冲区然后循环尝试解析完整帧。这里要说一句大实话串口协议解析的坑不在代码复杂而在你没有预见到“一条完整指令被拆成两半到达”这种情况。用上面这种无状态的解析逻辑天然就能处理拆包和粘包因为只要缓冲区里有完整的一帧取出来即可。代码层面大概就是这样void OnDataReceived(const char* data, int len) { // 追加到 recvBuf memcpy(recvBuf recvLen, data, len); recvLen len; int parsedLen 0; while (true) { int frameLen TryParseFrame(recvBuf parsedLen, recvLen - parsedLen); if (frameLen 0) break; // 处理一帧 [0, frameLen) HandleFrame(recvBuf parsedLen, frameLen); parsedLen frameLen; } if (parsedLen 0) { memmove(recvBuf, recvBuf parsedLen, recvLen - parsedLen); recvLen - parsedLen; } }6.4 重连和退出时的资源清理服务端程序的退出顺序也有讲究先closesocket(clientSocket)断开客户端连接再closesocket(serverSocket)关闭监听最后调用UnregisterBluetoothService删除 SDP 记录然后WSACleanup。顺序反了可能导致 SDP 记录删除失败因为系统还认为服务在监听中。实际开发里还有个更隐蔽的问题accept之后如果程序是单线程一旦进入recv阻塞你就没法在别处点击“停止服务”。我建议至少把服务端逻辑放到一个工作线程里主线程只负责启动和停止。停止时调用shutdown(serverSocket, SD_BOTH)或者closesocket(serverSocket)accept会立刻返回错误工作线程随之退出。这种用句柄关闭来中断阻塞调用的手段比用一个volatile标志位轮询要优雅得多也可靠得多。7. 我个人的几个心得和扩展方向整条链路跑通之后回头再看“Windows 蓝牙编程”这个题目会发现难点其实不在 API 本身而在三件事第一是把 Winsock 的查询模型和蓝牙的 SDP 模型对应起来第二是搞清楚 RFCOMM 的通道号和服务发现的因果关系第三是在真实设备上反复测试因为蓝牙的异常行为比 TCP 丰富得多。如果你想在这个基础上继续扩展我建议按这个顺序做先加一个 SDP 客户端查询用代码去查远端设备的 SerialPort 服务通道这样就彻底摆脱“写死端口 1”的做法然后封装一个简单的帧协议层把拆包逻辑和应用逻辑分开最后再考虑把它接到现有的串口调试工具上让工具既能走物理 COM 口也能走蓝牙 RFCOMM。如果你只是想把 Android 手机和 Windows PC 连起来传数据这套代码已经足够支撑一个小工具的开发了。还有一句实在话VS2017 虽然旧但在传统 Win32 蓝牙编程这条路上它反而是最顺手的工具。新版的 VS2022 我也试过工程配置基本兼容代码不用改就能编过只是如果你还在维护老项目没必要为了这个功能去升级整个 IDE。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询