对UDP编程0基础的可以参考这篇记录博文。
我做的是同一个程序中接收指定IP地址和端口号的信息作为输入,通过程序的算法进行处理,处理后的信息再通过另一个指定IP地址和端口号进行发送。也就是需要做两个udp一个接收数据,另一个发送数据。
网上的教程都是两个或多个.CPP文件的代码,对于UDP零基础的不太友好,代码结构包括运行步骤也不清晰。
源码的话网上有很多资源,大多大同小异。
修改后源码:
//
// Created by JinxBIGBIG on 2022/8/3.
//
#include
#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<char *>(str1.data());
cout << "sendBuf: "<< *sendBuf << endl;
//此处对发送方法进行了封装直接调用sendTo即可,sendTo见main函数上方
sendTo(sendBuf, sendHostShort, sendIP);
}
}
/*int count = 1;
string str = "";
str += to_string(count);
char* roadID = const_cast(str.data());
sendTo(roadID, 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
。
其三:
/*
ipv6 address to string or string to ipv6 address;
Edited by Mr Zhu,email:[email protected] or weixin:40222865
*/
#include
#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地址均为本机(测试用),而输入和输出端口由控制台输入,若需要指定在此处直接赋值即可。
接收后的信息处理和发送均在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<char *>(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:
这块代码改动不大,写好入参皆可。
上述代码是本程序里边的源码,完成接收、处理和发送三个操作。直接将程序编译好运行即可。但是测试的时候还需要编写一个发送端的代码,负责发送本程序待接收的信息;另外,如果要测试是否能够正常接收,还可以再单独写一个接收端的代码。
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文件后,如有需要点击运行。
上述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测试即可。