Leap Motion开发(五)基于UDP协议,跨设备采集手势信息并用OpenGL绘图

在两台主机A、B建立UDP连接,A主机(Client)利用Leap Motion采集手势信息,B主机(Server)接收数据,利用OpenGL绘图。

1、UDP协议

参考1
参考2

我的总结

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

猜你喜欢

转载自blog.csdn.net/qq_39006214/article/details/120904601
今日推荐