对UDP编程0基础的可以参考这篇记录博文。
我做的是同一个程序中接收指定IP地址和端口号的信息作为输入,通过程序的算法进行处理,处理后的信息再通过另一个指定IP地址和端口号进行发送。也就是需要做两个udp一个接收数据,另一个发送数据。
网上的教程都是两个或多个.CPP文件的代码,对于UDP零基础的不太友好,代码结构包括运行步骤也不清晰。
源码的话网上有很多资源,大多大同小异。博主是根据这个博文修改的:源码博文
该博文对于部分关键函数也有相应的介绍说明。
修改后源码:
// // Created by JinxBIGBIG on 2022/8/3. // #include详解: 2.1、代码运行可能BUG#include #include #include #include "positionInfo.h" #include "OdrManager.hh" #include "ShareMessage.h" //链接静态库 #pragma comment (lib,"ws2_32.lib") using namespace std; using namespace OpenDrive; //传参分别为:待发送信息;发送端口号;发送ip地址 int sendTo(char *sendBuf, short sendHostShort, const char *sendIP) { WSADATA wdata; WORD wVersion; wVersion = MAKEWORd(2, 2); WSAStartup(wVersion, &wdata); if (HIBYTE(wdata.wVersion) != 2 || LOBYTE(wdata.wVersion) != 2) { return -1; } sockaddr_in sClient; sClient.sin_family = AF_INET; sClient.sin_port = htons(sendHostShort); //此处或出现bug,解决办法详见下文 //inet_pton(AF_INET, "127.0.0.1", &sClient.sin_addr); sClient.sin_addr.S_un.S_addr = inet_addr(sendIP); SOCKET psock = socket(AF_INET, SOCK_DGRAM, 0); int len = sizeof(sClient); //char sendBuf[128]; while (1) { //memset(sendBuf, 0, sizeof(sendBuf)); //cout << "pelase input word:"; //cin.getline(sendBuf, 64); sendto(psock, sendBuf, sizeof(sendBuf), 0, (sockaddr*)&sClient, len); //cout << "Send over;" << endl; } return 0; } int main() { //以下为对接收数据进行处理的相关算法 OpenDrive::OdrManager manager; Point point; PointLaneInfo pointLaneInfo; string xodrPath = "..\map2.xodr"; //string xodrPath = "..\data\map.xodr"; bool xodrLoad = manager.loadFile(xodrPath); OpenDrive::Position* pos = manager.createPosition(); manager.activatePosition(pos); //接收IP和发送IP、接收端口号和发送端口号设置 char const *receiveIP = "127.0.0.1"; char const *sendIP = "127.0.0.1"; short receiveHostShort; short sendHostShort; cout << "Please input receiveHostPort(like:9999) and sendHostPort(like:9999)" << endl; cin >> receiveHostShort >> sendHostShort; //接收UDP编写 WSADATA wsData; int nret = WSAStartup(MAKEWORd(2, 2), &wsData); if(nret!=0) return nret; sockaddr_in sa,recSa; int len = sizeof(sa); sa.sin_addr.S_un.S_addr = inet_addr(receiveIP); sa.sin_family = AF_INET; sa.sin_port = htons(receiveHostShort); SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock==INVALID_SOCKET) return WSAGetLastError(); bind(sock, (sockaddr*)&sa, len); while (true) { char buf[1024]; memset(buf, 0, 1024); int nlen = recvfrom(sock, buf, 1024, 0, (sockaddr*)&recSa, &len); if (nlen>0) { //char sIP[20]; //此处或出现BUG,详见下文 //inet_ntop(AF_INET, &recSa.sin_addr, sIP, 20); inet_ntoa(recSa.sin_addr); cout << "The information :" << buf << endl; istringstream str(buf); double out; int i = 0; //接收的信息设置是一个三维坐标,类型为double,(x, y , z) double p[3]; //将接收的char类型的数据以空格为分隔符分别读取xyz值 while (str >> out) { //cout << out << endl; p[i] = out; i++; } point = {p[0], p[1], p[2]}; //对接收的数据进行处理 manager.setInertialPos(point.x, point.y, point.z); bool result = manager.inertial2lane(); cout << "Position initialing result :" << result << endl; //处理后的输出信息为roadID int roadID = manager.getRoadHeader()->mId; //转换为char *类型后进行发送 string str1 = ""; str1 += to_string(roadID); char* sendBuf = const_cast (str1.data()); cout << "sendBuf: "<< *sendBuf << endl; //此处对发送方法进行了封装直接调用sendTo即可,sendTo见main函数上方 sendTo(sendBuf, sendHostShort, sendIP); } } }
对于上述代码有两个函数的调用可能会出现BUG:
receive:
//inet_ntop(AF_INET, &recSa.sin_addr, sIP, 20); inet_ntoa(recSa.sin_addr);
这两个函数实现的都是将网络字节序列IP地址转化为字符串IP地址。都依赖于头文件#include
并且需要添加链接库#pragma comment (lib,"ws2_32.lib")。
可能BUG:添加头文件和链接库调用仍然出错的情况,可在cmakelist.txt中添加target_link_libraries(get_udp ws2_32或link_libraries(ws2_32)指定链接库。
但inet_ntop()在win下运行可能会无法调用,linux下可正常调用。
解决办法有3个:
其一:按上述代码所示通过函数inet_ntoa()替代,但其传参和返回值有所区别。
都无需返回值;
inet_ntop()传参分别为:第一个参数可以是AF_INET或AF_INET6:第二个参数是一个指向网络字节序的二进制值的指针;第三个参数是一个指向转换后的点分十进制串的指针;第四个参数是目标的大小,以免函数溢出其调用者的缓冲区。
inet_ntoa()则只需要指向网络字节序的二进制值的指针。
其二:通过WSAAddressToStringA()方法实现并封装为inet_ntop()方法:
PCSTR WSAAPI inet_ntop(INT Family,const VOID *pAddr,PSTR pStringBuf, size_t StringBufSize)
{
if(pStringBuf ==NULL || StringBufSize == 0)
{
WSASetLastError(ERROR_INVALID_PARAMETER);
return NULL;
}
if(Family == AF_INET6)
{
int ret=0;
ret=WSAAddressToStringA((PSOCKADDR)pAddr,sizeof(PSOCKADDR),NULL,pStringBuf,(LPDWORD)&StringBufSize);
if(ret!=0)
{
return NULL;
}
}
else if(Family == AF_INET)
{
struct in_addr a;
memcpy(&a,pAddr,sizeof(struct in_addr));
pStringBuf = inet_ntoa(a);
}
else
{
WSASetLastError(WSAEAFNOSUPPORT);
return NULL;
}
return pStringBuf;
}
封装后再调用inet_ntop()即可。
其三:直接使用WSAAddressToStringA(),代码见下方send中其三
。
send:
//inet_pton(AF_INET, sendIP, &sClient.sin_addr); sClient.sin_addr.S_un.S_addr = inet_addr(sendIP);
这两个函数实现的都是将字符串IP地址转化为网络字节序列IP地址。都依赖于头文件#include
并且需要添加链接库#pragma comment (lib,"ws2_32.lib")。但inet_pton()其在win下运行可能会无法调用,linux下可正常调用。
解决办法有两个:
其一:按上述代码所示通过函数inet_addr()替代,但其传参和返回值有所区别。inet_pton()无需返回值,转化后的IP地址直接存储在sClient.sin_addr中;inet_addr()需要返回sClient.sin_addr的子变量sClient.sin_addr.S_un.S_addr。传参就比较好理解,前者第一个参数为固定值,其余为字符串形式的发送IP地址和转化后IP地址;后者只需要传输字符串形式的发送IP地址。
其二:同理改写封装WSAStringToAddress。
其三:
#include2.2、输入输出设置相关#include #include #include using namespace std; int main() { WSADATA wsa_data; WORD sockversion = MAKEWORd(2,2); if(WSAStartup(sockversion, &wsa_data) != 0) { return 0; } struct sockaddr_in6 ser_addr; int addr_size=sizeof(struct sockaddr_in6); char ip_addr[100]=""; DWORD string_leng=100; int i; WSAStringToAddress( (LPSTR)"ff::1:ff:1", AF_INET6, NULL, (LPSOCKADDR) &ser_addr, &addr_size ); printf("16进制ip地址是:"); for(i=0;i<15;i=i+2) { printf("%x%x:",ser_addr.sin6_addr.u.Byte[i],ser_addr.sin6_addr.u.Byte[i+1]); } ser_addr.sin6_port=htons(5240); WSAAddressToStringA( (LPSOCKADDR)&ser_addr, // sockaddr类型指针 addr_size, //地址长度 NULL, //地址协议指针 (LPSTR) ip_addr, //转换后字符串地址 &string_leng //函数返回的字符串长度 ); printf("nipv6 address is"%s"n",ip_addr); memset(ip_addr,0,100); ser_addr.sin6_port=htons(0); WSAAddressToStringA( (LPSOCKADDR)&ser_addr, // sockaddr类型指针 addr_size, //地址长度 NULL, //地址协议指针 (LPSTR) ip_addr, //转换后字符串地址 &string_leng //函数返回的字符串长度 ); printf("n端口为0后显示 address is"%s"n",ip_addr); return 1; }
开篇说过两个udp只需要两对IP地址和端口号即可,那么只需要在代码:
char const *receiveIP = "127.0.0.1";
char const *sendIP = "127.0.0.1";
short receiveHostShort;
short sendHostShort;
此处进行修改。上述代码接收和发送IP地址均为本机(测试用),而输入和输出端口由控制台输入,若需要指定在此处直接赋值即可。
2.3、 接收信息后的处理和发送相关接收后的信息处理和发送均在while循环中。
receive:
while (true)
{
char buf[1024];
memset(buf, 0, 1024);
int nlen = recvfrom(sock, buf, 1024, 0, (sockaddr*)&recSa, &len);
if (nlen>0)
{
//char sIP[20];
//inet_ntop(AF_INET, &recSa.sin_addr, sIP, 20);
inet_ntoa(recSa.sin_addr);
cout << "The information :" << buf << endl;
istringstream str(buf);
double out;
int i = 0;
double p[3];
while (str >> out) {
//cout << out << endl;
p[i] = out;
i++;
}
point = {p[0], p[1], p[2]};
manager.setInertialPos(point.x, point.y, point.z);
bool result = manager.inertial2lane();
cout << "Position initialing result :" << result << endl;
int roadID = manager.getRoadHeader()->mId;
//转化为char *类型
string str1 = "";
str1 += to_string(roadID);
char* sendBuf = const_cast(str1.data());
cout << "sendBuf: "<< *sendBuf << endl;
sendTo(sendBuf, sendHostShort, sendIP);
}
}
上述代码中变量buf即为接收的信息,此处博主设置的接收信息的格式为三个double类型的数据,形如1 1 1,按空格划分;接收后为char类型的buf,首先将其读取为Point类型数据point,Point结构定义如下:
struct Point{
double x{0.};
double y{0.};
double z{0.};
Point() = default;
Point(double _x, double _y, double _z)
: x(_x)
, y(_y)
, z(_z)
{}
};
再将其作为算法输入进行处理,最后输出为一个int型的数据roadID;发送之前需要将其转换char *类型;最后再调用封装好的sendTo()方法。
send:
这块代码改动不大,写好入参皆可。
上述代码是本程序里边的源码,完成接收、处理和发送三个操作。直接将程序编译好运行即可。但是测试的时候还需要编写一个发送端的代码,负责发送本程序待接收的信息;另外,如果要测试是否能够正常接收,还可以再单独写一个接收端的代码。
3.1、发送端代码send.cpp:
// // Created by JinxBIGBIG on 2022/8/3. // #include#include using namespace std; #pragma comment(lib,"ws2_32.lib") int main() { WSADATA wdata; WORD wVersion; wVersion = MAKEWORd(2, 2); WSAStartup(wVersion, &wdata); if (HIBYTE(wdata.wVersion) != 2 || LOBYTE(wdata.wVersion) != 2) { return -1; } sockaddr_in sClient; sClient.sin_family = AF_INET; sClient.sin_port = htons(9999); //inet_pton(AF_INET, "127.0.0.1", &sClient.sin_addr); sClient.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); SOCKET psock = socket(AF_INET, SOCK_DGRAM, 0); int len = sizeof(sClient); char sendBuf[128]; while (1) { memset(sendBuf, 0, sizeof(sendBuf)); cout << "Please input the point(x, y, z):"; cin.getline(sendBuf, 64); sendto(psock, sendBuf, sizeof(sendBuf), 0, (sockaddr*)&sClient, len); } return 0; }
将此代码单独编译运行生成get_send.exe文件后,点击运行:
此处附上接收端代码:
receive.cpp:
// // Created by JinxBIGBIG on 2022/8/3. // #include#include #include #pragma comment (lib,"ws2_32.lib") using namespace std; int main() { WSADATA wsData; int nret=WSAStartup(MAKEWORd(2, 2), &wsData); if(nret!=0) { return nret; } sockaddr_in sa,recSa; int len = sizeof(sa); sa.sin_addr.S_un.S_addr = INADDR_ANY; sa.sin_family = AF_INET; sa.sin_port = htons(9999); SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock==INVALID_SOCKET) { return WSAGetLastError(); } bind(sock, (sockaddr*)&sa, len); while (true) { char buf[1024]; memset(buf, 0, 1024); int nlen = recvfrom(sock, buf, 1024, 0, (sockaddr*)&recSa, &len); if (nlen>0) { //char sIP[20]; //inet_ntop(AF_INET, &recSa.sin_addr, sIP, 20); inet_ntoa(recSa.sin_addr); cout << buf << endl; } } }
同理,将此代码单独编译运行生成get_receive.exe文件后,如有需要点击运行。
3.2、调试助手上述1解决测试时的发送端问题,但本程序在接收发送端发送的数据并处理后还需要进行发送,那么如何查看发送的消息是否有正确接收呢?
可以下载网络调试助手,
点集立即下载:
我选的是箭头所指这个就自动跳到迅雷下载了。下载后是一个单独的.exe文件,
双击即可运行:
协议选择udp协议,ip地址下拉选项可设置为本机地址,主机端口也可设置,此处博主设置的8080。即本程序中的处理后数据的发送目标是char const *sendIP = "127.0.0.1; sendHostShort = 8080;,而负责接收数据的目标地址是char const *receiveIP = "127.0.0.1; receiveHostShort = 9999;也就是本机。
3.3.1 运行本程序:可在编辑器里边运行也可以通过.exe文件运行,
本程序IP地址都直接给的本机,端口号则是手动输入9999 8080。
char const *receiveIP = "127.0.0.1";
char const *sendIP = "127.0.0.1";
short receiveHostShort;
short sendHostShort;
cout << "Please input receiveHostPort(like:9999) and sendHostPort(like:9999)" << endl;
cin >> receiveHostShort >> sendHostShort;
3.3.2 运行发送端程序get_send.exe给本程序发送坐标((5915.00, -2937.76, 0)):输入坐标后回车:
可在本程序的终端界面看到:
图中的:
The information :5915.00 -2937.76 0 Position initialing result :1 sendBuf: 4
第1行为接收的坐标信息,2、3行则是对接收的数据处理后的自定义显示结果。
3.3.3 通过调试助手接收本程序发送的数据:打开网络调试助手
点击打开:
即可看到已成功接收传递的数据40,3.2中的sendBuf: 4表示的是传递的char类型数据第一个元素的信息,也就是40的4。
3.3.4 另外可在图中箭头所示设置接收数据显示的类型,HEX为16进制。
如果传输的数据是1,则其HEX为:
也就是31,数字“1”被当做字符存储时,用的ASCII码,值是49(10进制),转化为16进制就是31(316^1 + 116 ^ 0)。
如果我们要想接收端接收的是16进制的字符,那么我们就需要在发送之前进行字符串转16进制操作。
至此结束。
四、补充如果是单独运行发送端和接收端的代码,则直接将三中的发送端代码send.cpp和接收端代码receive.cpp分别运行后执行生成的两个.exe文件再分别运行c测试即可。



