在两台主机A、B建立UDP连接,A主机(Client)利用Leap Motion采集手势信息,B主机(Server)接收数据,利用OpenGL绘图。
1、UDP协议
我的总结
UDP(User Datagram Protocol),用户数据报协议,UDP是一种简单协议,提供了基本的传输层功能。与TCP相比,UDP不建立握手,不建立对等网络,属于广播性质,UDP的开销极低,因为UDP是无连接的,并且不提供复杂的重新传输、排序和流量控制机制。UDP稳定性一般,尽最大努力交付,即不可靠交付,但优点是传输速度快。
UDP对一次传输的数据大小有要求,UDP数据报的数据区最大长度为1472字节。
UDP提供了无连接通信,且不对传送数据包进行可靠性保证,适合于一次传输少量数据,UDP传输的可靠性由应用层负责。
UDP报文没有可靠性保证、顺序保证和流量控制字段等,可靠性较差。但是正因为UDP协议的控制选项较少,在数据传输过程中延迟小、数据传输效率高,适合对可靠性要求不高的应用程序,或者可以保障可靠性的应用程序,如DNS、TFTP、SNMP等。
UDP使用
在选择使用协议的时候,选择UDP必须要谨慎。在网络质量令人十分不满意的环境下,UDP协议数据包丢失会比较严重。但是由于UDP的特性:它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。比如我们聊天用的QQ就是使用的UDP协议。
既然UDP是一种不可靠的网络协议,那么还有什么使用价值或必要呢?其实不然,在有些情况下UDP协议可能会变得非常有用。因为UDP具有TCP所望尘莫及的速度优势。虽然TCP协议中植入了各种安全保障功能,但是在实际执行的过程中会占用大量的系统开销,无疑使速度受到严重的影响。反观UDP由于排除了信息可靠传递机制,将安全和排序等功能移交给上层应用来完成,极大降低了执行时间,使速度得到了保证。
在程序中建立UDP连接
UDP通信代码参考
在使用时需要先开启Server端再开启Client端
Server端代码
//UdpNetSrv.cpp
#include <Winsock2.h>
#include <stdio.h>
void main()
{
//加载套接字库
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(1,1);
err = WSAStartup(wVersionRequested, &wsaData);//错误会返回WSASYSNOTREADY
if(err != 0)
{
return;
}
if(LOBYTE(wsaData.wVersion) != 1 || //低字节为主版本
HIBYTE(wsaData.wVersion) != 1) //高字节为副版本
{
WSACleanup();
return;
}
printf("server is operating!\n\n");
//创建用于监听的套接字
SOCKET sockSrv = socket(AF_INET,SOCK_DGRAM,0);//失败会返回 INVALID_SOCKET
//printf("Failed. Error Code : %d",WSAGetLastError())//显示错误信息
SOCKADDR_IN addrSrv; //定义sockSrv发送和接收数据包的地址
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(6000);
//绑定套接字, 绑定到端口
bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));//会返回一个SOCKET_ERROR
//将套接字设为监听模式, 准备接收客户请求
SOCKADDR_IN addrClient; //用来接收客户端的地址信息
int len = sizeof(SOCKADDR);
char recvBuf[100]; //收
char sendBuf[100]; //发
char tempBuf[100]; //存储中间信息数据
while(1)
{
//等待并数据
recvfrom(sockSrv,recvBuf,100,0,(SOCKADDR*)&addrClient,&len);
if('q' == recvBuf[0])
{
sendto(sockSrv,"q",strlen("q")+1,0,(SOCKADDR*)&addrClient,len);
printf("Chat end!\n");
break;
}
sprintf_s(tempBuf,"%s say : %s",inet_ntoa(addrClient.sin_addr),recvBuf);
printf("%s\n",tempBuf);
//发送数据
printf("Please input data: \n");
gets(sendBuf);
sendto(sockSrv,sendBuf,strlen(sendBuf)+1,0,(SOCKADDR*)&addrClient,len);
}
closesocket(sockSrv);
WSACleanup();
}
Client端代码
//UdpNetClient.cpp
#include <Winsock2.h>
#include <stdio.h>
void main()
{
//加载套接字库
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(1,1);
err = WSAStartup(wVersionRequested, &wsaData);
if(err != 0)
{
return;
}
if(LOBYTE(wsaData.wVersion) != 1 || //低字节为主版本
HIBYTE(wsaData.wVersion) != 1) //高字节为副版本
{
WSACleanup();
return;
}
printf("Client is operating!\n\n");
//创建用于监听的套接字
SOCKET sockSrv = socket(AF_INET,SOCK_DGRAM,0);
sockaddr_in addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//输入你想通信的她(此处是本机内部)
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(6000);
int len = sizeof(SOCKADDR);
char recvBuf[100]; //收
char sendBuf[100]; //发
char tempBuf[100]; //存储中间信息数据
while(1)
{
printf("Please input data: \n");
gets(sendBuf);
sendto(sockSrv,sendBuf,strlen(sendBuf)+1,0,(SOCKADDR*)&addrSrv,len);
//等待并数据
recvfrom(sockSrv,recvBuf,100,0,(SOCKADDR*)&addrSrv,&len);
if('q' == recvBuf[0])
{
sendto(sockSrv,"q",strlen("q")+1,0,(SOCKADDR*)&addrSrv,len);
printf("Chat end!\n");
break;
}
sprintf_s(tempBuf,"%s say : %s",inet_ntoa(addrSrv.sin_addr),recvBuf);
printf("%s\n",tempBuf);
//发送数据
}
closesocket(sockSrv);
WSACleanup();
}
在使用以上建立UDP协议的代码时,需要加上#pragma warning(disable:4996)
,避免一些较老的语法所报的错误,而且需要#pragma comment(lib,"ws2_32.lib")
导入所需的网络编程库。
Client端
Client端负责获取Leap Motion数据,打包成可以发送的字符串格式并发送。由于负责发送的sendto( )
只能发送const char*格式,所以要把数据序列化,转为字符串。将23个手部关节点的空间位置(float类型)转为字符串,每两个浮点数中间用‘#’号分割,一只手需要五百多个字节,UDP的上限是1400+,两只手的节点位置数据是可以发送的,但并没有手性、关节方向向量、速度、抓握强度、手指是否伸直等等数据,需要自己设计发送的串的格式内容。
目前只实现了一只手的数据进行获取发送,在Server端进行绘图时,是可以绘制两只手的,但其间会有闪烁(因为手的数据是一只一只发送的)。如果要发送两只手,可以尝试的方法是在Client端发送的时候,如果检测到两只手,则在数据里加入标识有几只手和手性的tag,把两只手的数据都打包发送,在Server进行拆包。
LEAP_TRACKING_EVENT* frame = GetFrame();
frame->nHands; // 表示画面里有几只手
//c++ 将float 类型转换成string 类型:
inline string floatToString(float Num)
{
ostringstream oss;
oss << Num;
string str(oss.str());
str += "#";
return str;
}
// 将frame序列化变成字符串
vector<string> frameSerialize(LEAP_TRACKING_EVENT* frame) {
vector<string> handData;
string oneHand;
for (uint32_t h = 0; h < frame->nHands; h++) {
oneHand = "";
// Draw the hand
LEAP_HAND* hand = &frame->pHands[h];
//elbow
oneHand += floatToString(hand->arm.prev_joint.x);
oneHand += floatToString(hand->arm.prev_joint.y);
oneHand += floatToString(hand->arm.prev_joint.z);
//wrist
oneHand += floatToString(hand->arm.next_joint.x);
oneHand += floatToString(hand->arm.next_joint.y);
oneHand += floatToString(hand->arm.next_joint.z);
//palm position
oneHand += floatToString(hand->palm.position.x);
oneHand += floatToString(hand->palm.position.y);
oneHand += floatToString(hand->palm.position.z);
//Distal ends of bones for each digit
for (int f = 0; f < 5; f++) {
LEAP_DIGIT finger = hand->digits[f];
for (int b = 0; b < 4; b++) {
LEAP_BONE bone = finger.bones[b];
oneHand += floatToString(bone.next_joint.x);
oneHand += floatToString(bone.next_joint.y);
oneHand += floatToString(bone.next_joint.z);
}
}
handData.push_back(oneHand);
}
return handData;
}
// main 函数
void main{
// ...
while (1)
{
// 获取手势信息并打包
LEAP_TRACKING_EVENT* frame = GetFrame();
cout << frame->nHands << endl;
// frame->pHands[0].index.is_extended
if (frame && frame->nHands>0) {
for (uint32_t h = 0; h < frame->nHands; h++) {
// LEAP_HAND* hand = &frame->pHands[h];
// frame序列化
vector<string> handData = frameSerialize(frame);
const char* sendBuf;
for (int i = 0; i < handData.size(); i++) {
sendBuf = handData[i].c_str();
sendto(sockSrv, sendBuf, strlen(sendBuf), 0, (sockaddr*)&addrSrv, sizeof(addrSrv));
}
}
}
}
// ...
}
Server端
在Server端里接收数据后,将其进行拆包,通过GLUT进行绘图。
设置回调函数,其中display是负责接收数据并拆包、绘制的回调函数。
// GLUT callbacks
glutIdleFunc(idle);
glutReshapeFunc(reshape);
glutDisplayFunc(display);
void display()
{
recvfrom(sockSrv, recvBuf, sizeof(recvBuf), 0, (SOCKADDR*)&addrClient, &len);
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glTranslatef(0, -300, -500); //"Camera" viewpoint
glClear(GL_COLOR_BUFFER_BIT);
//按照#划分数据
char* tokenPtr = strtok(recvBuf, "#");
float pos[3];
for (int i = 0; i < 23; i++) {
pos[0] = atof(tokenPtr);
tokenPtr = strtok(nullptr, "#");
pos[1] = atof(tokenPtr);
tokenPtr = strtok(nullptr, "#");
pos[2] = atof(tokenPtr);
tokenPtr = strtok(nullptr, "#");
glPushMatrix();
glTranslatef(pos[0], pos[1], pos[2]);
glutWireOctahedron();
glPopMatrix();
}
glFlush();
glPopMatrix();
}
在发送和接受数据时可以通过以下语句打印当前系统时间进行对比,可以发现延迟是毫秒级的。
SYSTEMTIME sys;
GetLocalTime(&sys);
printf("%4d/%02d/%02d %02d:%02d:%02d.%03d 星期%1d\n", sys.wYear, sys.wMonth, sys.wDay, sys.wHour, sys.wMinute, sys.wSecond, sys.wMilliseconds, sys.wDayOfWeek);
一些BUG
一开始Client端获取的数据和Server端绘制的手势有明显延时,起初以为是网络延迟或者GLUT调用回调函数频率不够,经过Client端和Server端打印时间对比后排除网络延迟问题,而GLUT调用display回调函数的频率也足够高(因为绘制的动画很流畅)。后来删除display函数中的cout打印输出后,延时消失,看来是因为cout打印输出耗时太多。
尚存在的问题
两只手的数据发送和拆包,以及更多手势信息的打包
Windows Socket异步怎么写?当前Server端只有获取到手势数据后,OpenGL window才进行绘制,窗口才可以拖动。
本机目录(供自己忘记时查看):
E:\Leap Motion\Leap Motion Demo\VS\Leap Motion UDP\UDP Demo