如何使用 WSASetService 在 Windows 设备上使用 WSAQUERYSET 结构来宣传蓝牙服务
How to use WSASetService to advertise bluetooth service using WSAQUERYSET structure on Windows devices
我从 https://docs.microsoft.com/en-us/windows/win32/bluetooth/bluetooth-and-wsasetservice 了解到有两种方法可以将服务提交到本地 SDP 服务器:
应用程序可以让系统通告一个简单的蓝牙 SDP 服务记录,该记录由 WSAQUERYSET 结构中的标准成员构成。
应用程序可以让系统通过在 WSAQUERYSET 结构的 lpBlob 成员中传递一个 BTH_SET_SERVICE 结构来公布自己的蓝牙 SDP 记录。这是一种更复杂的方法。
对我来说,第一个是首选,但我现在能找到的所有代码示例都使用第二个。我想大概是在WSAQUERYSET成员中设置服务的地址、端口、服务uuid等信息:
// Service Information
int port = 4;
BTH_ADDR address = xxx;
GUID serviceUuid = {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx};
char* serviceName = "xxx";
//....
// Putting service information into WSAQUERYSET
WSAQUERYSET Service;
memset( &Service, 0, sizeof( Service ) );
Service.dwSize = sizeof( Service );
Service.dwNameSpace = NS_BTH;
//Service.xxx = xxx
//...
if ( WSASetService( &Service, RNRSERVICE_REGISTER, 0 ) == SOCKET_ERROR ) {
return WSAGetLastError();
}
else {
return 0;
}
目前,我正在猜测如何设置这些属性,但不知何故我无法成功宣传可以检测到的服务。我真的希望有一些可行的代码示例。
首先你不需要硬编码port = 4;
,而是使用bind
获取动态空闲端口,使用getsockname
获取端口号。例如(没有错误检查)
SOCKADDR_BTH asi = { AF_BTH, 0, {}, BT_PORT_ANY };
SOCKET s = WSASocket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM,
0, 0, WSA_FLAG_NO_HANDLE_INHERIT|WSA_FLAG_OVERLAPPED);
bind(s, (PSOCKADDR)&asi, sizeof(asi));
int len = sizeof(asi);
getsockname(s, (PSOCKADDR)&asi, &len);
然后您需要调用 WSASetService,如 Bluetooth and WSAQUERYSET for Set Service 中所述。这里描述了 WSAQUERYSET
中使用的所有参数。只有一个例外 - lpszServiceInstanceName 描述为
Optional, but recommended.
但如果不使用它 - 我们得到 提供了无效参数。所以这是强制参数。
ULONG BthRegisterService(LPCGUID lpServiceClassId, PCWSTR ServiceName, PSOCKADDR_BTH asi)
{
CSADDR_INFO csa = {
{ (PSOCKADDR)asi, sizeof(SOCKADDR_BTH) }, {}, SOCK_STREAM, BTHPROTO_RFCOMM
};
WSAQUERYSET wqs = { sizeof(wqs) };
wqs.lpszServiceInstanceName = const_cast<PWSTR>(ServiceName);
wqs.lpServiceClassId = const_cast<PGUID>(lpServiceClassId);
wqs.dwNameSpace = NS_BTH;
wqs.dwNumberOfCsAddrs = 1;
wqs.lpcsaBuffer = &csa;
return WSASetService(&wqs, RNRSERVICE_REGISTER, 0) ? WSAGetLastError() : NOERROR;
}
这种方式的缺点 - 我们无法取回 HANDLE
我们注册的记录,用于延迟调用 WSASetService
和 RNRSERVICE_DELETE
- 为此需要使用 BLOB
指向 BTH_SET_SERVICE
。为此,我们需要通过自己的原始 SDP 记录流进行格式化。
所以调用BthRegisterService
我们可以这样:
struct __declspec(uuid("00112233-4455-6677-8899-aabbccddeeff")) MyServiceClass;
BthRegisterService(&__uuidof(MyServiceClass), L"*", &asi);
但是,如果我们可以格式化SDP记录,我们也可以直接使用IOCTL_BTH_SDP_SUBMIT_RECORD
for register and later IOCTL_BTH_SDP_REMOVE_RECORD
注销
打开蓝牙设备需要使用 CM_Get_Device_Interface_ListW
和 GUID_BTHPORT_DEVICE_INTERFACE
由 NspSetService 格式化的 SDP 记录看起来像
UCHAR SdpRecordTmplt[] = {
// [//////////////////////////////////////////////////////////////////////////////////////////////////////////
0x35, 0x33, //
//
// UINT16:SDP_ATTRIB_CLASS_ID_LIST //
0x09, 0x00, 0x01, //
// [///////////////////////////////////////////////////////////////////////////////////////////////// //
0x35, 0x11, // //
// UUID128:{guid} // //
0x1c, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, // //
// ]///////////////////////////////////////////////////////////////////////////////////////////////// //
//
// UINT16:SDP_ATTRIB_PROTOCOL_DESCRIPTOR_LIST //
0x09, 0x00, 0x04, //
// [///////////////////////////////// //
0x35, 0x0f, // //
// (L2CAP, PSM=PSM_RFCOMM) // //
// [///////////////////////// // //
0x35, 0x06, // // //
// UUID16:L2CAP_PROTOCOL_UUID16 // // //
0x19, 0x01, 0x00, // // //
// UINT16:PSM_RFCOMM // // //
0x09, 0x00, 0x03, // // //
// ]///////////////////////// // //
// (RFCOMM, CN=Port) // //
// [///////////////////////// // //
0x35, 0x05, // // //
// UUID16:RFCOMM_PROTOCOL_UUID16 // // //
0x19, 0x00, 0x03, // // //
// UINT8:CN // // //
0x08, 0x**, // // //
// ]///////////////////////// // //
// ]///////////////////////////////// //
//
// UINT16:SDP_ATTRIB_SERVICE_NAME //
0x09, 0x01, 0x00, //
// STR:4 "ABCD" //
0x25, 0x04, 0x41, 0x42, 0x43, 0x44 //
// ]//////////////////////////////////////////////////////////////////////////////////////////////////////////
};
这里只有UUID128(ServiceClassId),rfcomm端口号和名字都改了
即使我们自己格式化原始 SDP 记录 - windows 添加 2 个属性:
(UINT16:ServiceRecordHandle) (UINT32:xxxxx)
和
(UINT16:BrowseGroupList) [ (UUID16:PublicBrowseRoot) ]
例如,如果我们传递下一个 sdp 记录:
[ // 30
(UINT16:ServiceClassIDList)
[ // 11
(UUID128:00112233-4455-6677-8899-aabbccddeeff)
]
(UINT16:ProtocolDescriptorList)
[ // C
[ // 3
(UUID16:L2CAP_PROTOCOL_UUID16)
]
[ // 5
(UUID16:PSM_RFCOMM)
(UINT8:CN=Port)
]
]
(UINT16:ServiceName)
("ABCD")
]
windows 将其转换(扩展)为
[ // 40
(UINT16:ServiceRecordHandle) (UINT32:10001)
(UINT16:ServiceClassIDList)
[ // 11
(UUID128:00112233-4455-6677-8899-aabbccddeeff)
]
(UINT16:ProtocolDescriptorList)
[ // C
[ // 3
(UUID16:L2CAP_PROTOCOL_UUID16)
]
[ // 5
(UUID16:PSM_RFCOMM)
(UINT8:CN=Port)
]
]
(UINT16:BrowseGroupList)
[ // 3
(UUID16:PublicBrowseRoot)
]
(UINT16:ServiceName)
("ABCD")
]
或原始字节 - 记录:
UCHAR sdp[] = {
0x35 ,0x30 ,0x09 ,0x00 ,0x01 ,0x35 ,0x11 ,0x1c
,0x00 ,0x11 ,0x22 ,0x33 ,0x44 ,0x55 ,0x66 ,0x77
,0x88 ,0x99 ,0xaa ,0xbb ,0xcc ,0xdd ,0xee ,0xff
,0x09 ,0x00 ,0x04 ,0x35 ,0x0c ,0x35 ,0x03 ,0x19
,0x01 ,0x00 ,0x35 ,0x05 ,0x19 ,0x00 ,0x03 ,0x08
,0x04 ,0x09 ,0x01 ,0x00 ,0x25 ,0x04 ,0x41 ,0x42
,0x43 ,0x44
};
转换为
UCHAR sdp_final[] = {
0x35 ,0x40 ,0x09 ,0x00 ,0x00 ,0x0a ,0x00 ,0x01
,0x00 ,0x01 ,0x09 ,0x00 ,0x01 ,0x35 ,0x11 ,0x1c
,0x00 ,0x11 ,0x22 ,0x33 ,0x44 ,0x55 ,0x66 ,0x77
,0x88 ,0x99 ,0xaa ,0xbb ,0xcc ,0xdd ,0xee ,0xff
,0x09 ,0x00 ,0x04 ,0x35 ,0x0c ,0x35 ,0x03 ,0x19
,0x01 ,0x00 ,0x35 ,0x05 ,0x19 ,0x00 ,0x03 ,0x08
,0x04 ,0x09 ,0x00 ,0x05 ,0x35 ,0x03 ,0x19 ,0x10
,0x02 ,0x09 ,0x01 ,0x00 ,0x25 ,0x04 ,0x41 ,0x42
,0x43 ,0x44
};
我从 https://docs.microsoft.com/en-us/windows/win32/bluetooth/bluetooth-and-wsasetservice 了解到有两种方法可以将服务提交到本地 SDP 服务器:
应用程序可以让系统通告一个简单的蓝牙 SDP 服务记录,该记录由 WSAQUERYSET 结构中的标准成员构成。
应用程序可以让系统通过在 WSAQUERYSET 结构的 lpBlob 成员中传递一个 BTH_SET_SERVICE 结构来公布自己的蓝牙 SDP 记录。这是一种更复杂的方法。
对我来说,第一个是首选,但我现在能找到的所有代码示例都使用第二个。我想大概是在WSAQUERYSET成员中设置服务的地址、端口、服务uuid等信息:
// Service Information
int port = 4;
BTH_ADDR address = xxx;
GUID serviceUuid = {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx};
char* serviceName = "xxx";
//....
// Putting service information into WSAQUERYSET
WSAQUERYSET Service;
memset( &Service, 0, sizeof( Service ) );
Service.dwSize = sizeof( Service );
Service.dwNameSpace = NS_BTH;
//Service.xxx = xxx
//...
if ( WSASetService( &Service, RNRSERVICE_REGISTER, 0 ) == SOCKET_ERROR ) {
return WSAGetLastError();
}
else {
return 0;
}
目前,我正在猜测如何设置这些属性,但不知何故我无法成功宣传可以检测到的服务。我真的希望有一些可行的代码示例。
首先你不需要硬编码port = 4;
,而是使用bind
获取动态空闲端口,使用getsockname
获取端口号。例如(没有错误检查)
SOCKADDR_BTH asi = { AF_BTH, 0, {}, BT_PORT_ANY };
SOCKET s = WSASocket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM,
0, 0, WSA_FLAG_NO_HANDLE_INHERIT|WSA_FLAG_OVERLAPPED);
bind(s, (PSOCKADDR)&asi, sizeof(asi));
int len = sizeof(asi);
getsockname(s, (PSOCKADDR)&asi, &len);
然后您需要调用 WSASetService,如 Bluetooth and WSAQUERYSET for Set Service 中所述。这里描述了 WSAQUERYSET
中使用的所有参数。只有一个例外 - lpszServiceInstanceName 描述为
Optional, but recommended.
但如果不使用它 - 我们得到 提供了无效参数。所以这是强制参数。
ULONG BthRegisterService(LPCGUID lpServiceClassId, PCWSTR ServiceName, PSOCKADDR_BTH asi)
{
CSADDR_INFO csa = {
{ (PSOCKADDR)asi, sizeof(SOCKADDR_BTH) }, {}, SOCK_STREAM, BTHPROTO_RFCOMM
};
WSAQUERYSET wqs = { sizeof(wqs) };
wqs.lpszServiceInstanceName = const_cast<PWSTR>(ServiceName);
wqs.lpServiceClassId = const_cast<PGUID>(lpServiceClassId);
wqs.dwNameSpace = NS_BTH;
wqs.dwNumberOfCsAddrs = 1;
wqs.lpcsaBuffer = &csa;
return WSASetService(&wqs, RNRSERVICE_REGISTER, 0) ? WSAGetLastError() : NOERROR;
}
这种方式的缺点 - 我们无法取回 HANDLE
我们注册的记录,用于延迟调用 WSASetService
和 RNRSERVICE_DELETE
- 为此需要使用 BLOB
指向 BTH_SET_SERVICE
。为此,我们需要通过自己的原始 SDP 记录流进行格式化。
所以调用BthRegisterService
我们可以这样:
struct __declspec(uuid("00112233-4455-6677-8899-aabbccddeeff")) MyServiceClass;
BthRegisterService(&__uuidof(MyServiceClass), L"*", &asi);
但是,如果我们可以格式化SDP记录,我们也可以直接使用IOCTL_BTH_SDP_SUBMIT_RECORD
for register and later IOCTL_BTH_SDP_REMOVE_RECORD
注销
打开蓝牙设备需要使用 CM_Get_Device_Interface_ListW
和 GUID_BTHPORT_DEVICE_INTERFACE
由 NspSetService 格式化的 SDP 记录看起来像
UCHAR SdpRecordTmplt[] = {
// [//////////////////////////////////////////////////////////////////////////////////////////////////////////
0x35, 0x33, //
//
// UINT16:SDP_ATTRIB_CLASS_ID_LIST //
0x09, 0x00, 0x01, //
// [///////////////////////////////////////////////////////////////////////////////////////////////// //
0x35, 0x11, // //
// UUID128:{guid} // //
0x1c, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, // //
// ]///////////////////////////////////////////////////////////////////////////////////////////////// //
//
// UINT16:SDP_ATTRIB_PROTOCOL_DESCRIPTOR_LIST //
0x09, 0x00, 0x04, //
// [///////////////////////////////// //
0x35, 0x0f, // //
// (L2CAP, PSM=PSM_RFCOMM) // //
// [///////////////////////// // //
0x35, 0x06, // // //
// UUID16:L2CAP_PROTOCOL_UUID16 // // //
0x19, 0x01, 0x00, // // //
// UINT16:PSM_RFCOMM // // //
0x09, 0x00, 0x03, // // //
// ]///////////////////////// // //
// (RFCOMM, CN=Port) // //
// [///////////////////////// // //
0x35, 0x05, // // //
// UUID16:RFCOMM_PROTOCOL_UUID16 // // //
0x19, 0x00, 0x03, // // //
// UINT8:CN // // //
0x08, 0x**, // // //
// ]///////////////////////// // //
// ]///////////////////////////////// //
//
// UINT16:SDP_ATTRIB_SERVICE_NAME //
0x09, 0x01, 0x00, //
// STR:4 "ABCD" //
0x25, 0x04, 0x41, 0x42, 0x43, 0x44 //
// ]//////////////////////////////////////////////////////////////////////////////////////////////////////////
};
这里只有UUID128(ServiceClassId),rfcomm端口号和名字都改了
即使我们自己格式化原始 SDP 记录 - windows 添加 2 个属性:
(UINT16:ServiceRecordHandle) (UINT32:xxxxx)
和
(UINT16:BrowseGroupList) [ (UUID16:PublicBrowseRoot) ]
例如,如果我们传递下一个 sdp 记录:
[ // 30
(UINT16:ServiceClassIDList)
[ // 11
(UUID128:00112233-4455-6677-8899-aabbccddeeff)
]
(UINT16:ProtocolDescriptorList)
[ // C
[ // 3
(UUID16:L2CAP_PROTOCOL_UUID16)
]
[ // 5
(UUID16:PSM_RFCOMM)
(UINT8:CN=Port)
]
]
(UINT16:ServiceName)
("ABCD")
]
windows 将其转换(扩展)为
[ // 40
(UINT16:ServiceRecordHandle) (UINT32:10001)
(UINT16:ServiceClassIDList)
[ // 11
(UUID128:00112233-4455-6677-8899-aabbccddeeff)
]
(UINT16:ProtocolDescriptorList)
[ // C
[ // 3
(UUID16:L2CAP_PROTOCOL_UUID16)
]
[ // 5
(UUID16:PSM_RFCOMM)
(UINT8:CN=Port)
]
]
(UINT16:BrowseGroupList)
[ // 3
(UUID16:PublicBrowseRoot)
]
(UINT16:ServiceName)
("ABCD")
]
或原始字节 - 记录:
UCHAR sdp[] = {
0x35 ,0x30 ,0x09 ,0x00 ,0x01 ,0x35 ,0x11 ,0x1c
,0x00 ,0x11 ,0x22 ,0x33 ,0x44 ,0x55 ,0x66 ,0x77
,0x88 ,0x99 ,0xaa ,0xbb ,0xcc ,0xdd ,0xee ,0xff
,0x09 ,0x00 ,0x04 ,0x35 ,0x0c ,0x35 ,0x03 ,0x19
,0x01 ,0x00 ,0x35 ,0x05 ,0x19 ,0x00 ,0x03 ,0x08
,0x04 ,0x09 ,0x01 ,0x00 ,0x25 ,0x04 ,0x41 ,0x42
,0x43 ,0x44
};
转换为
UCHAR sdp_final[] = {
0x35 ,0x40 ,0x09 ,0x00 ,0x00 ,0x0a ,0x00 ,0x01
,0x00 ,0x01 ,0x09 ,0x00 ,0x01 ,0x35 ,0x11 ,0x1c
,0x00 ,0x11 ,0x22 ,0x33 ,0x44 ,0x55 ,0x66 ,0x77
,0x88 ,0x99 ,0xaa ,0xbb ,0xcc ,0xdd ,0xee ,0xff
,0x09 ,0x00 ,0x04 ,0x35 ,0x0c ,0x35 ,0x03 ,0x19
,0x01 ,0x00 ,0x35 ,0x05 ,0x19 ,0x00 ,0x03 ,0x08
,0x04 ,0x09 ,0x00 ,0x05 ,0x35 ,0x03 ,0x19 ,0x10
,0x02 ,0x09 ,0x01 ,0x00 ,0x25 ,0x04 ,0x41 ,0x42
,0x43 ,0x44
};