udp.h
#ifndef _UDP_H_20230628
#define _UDP_H_20230628
/**************************************************************
* @brief UDP通讯底层接口封装,适配多平台
* @author luotao
* @date 20230628
* @note UDP是面向非连接的协议,它不与对方建立连接,用户可以一对多,传输数据量大,速度快;UDP信息包只有8字节相对TCP的20字节开销小。
* @refer https://blog.csdn.net/sinat_38102206/article/details/80320111
* https://blog.csdn.net/weixin_44522306/article/details/117734571
***************************************************************/
#include <stdio.h>
#include <string.h>
#ifdef WIN32
#include <winsock2.h>
#include <Windows.h>
#pragma comment(lib,"ws2_32.lib")
#define SOCKET_LEN int
#else
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#define SOCKET_LEN socklen_t
#endif
/**
* @brief UDP服务器端类
*/
class CServerUdp{
public:
/**
* @brief 构造函数
*/
CServerUdp();
~CServerUdp();
//绑定接收端口进行监听,返回true表示绑定成功
bool Bind(int nRecvPort);
//接收客户端数据,返回实际接收数据长度
int RecvData(char* pBuff, int nMaxLen);
//回复数据给最近时间发送过来的客户端,成功:返回发送成功的数据长度; 失败: -1
int SendTo(const char* pData, int nLen);
//获取最近发送客户端信息,(存在多个客户端)
void GetClientInfo(char ip[16], int* pPort)const;
//指定ip端口发送数据,(存在多个客户端)
int SendTo(const char* ip, int nPort, const char* pData, int nLen);
private:
#ifdef WIN32
SOCKET m_socket; //套接字
#else
int m_socket;
#endif
sockaddr_in m_clientAddr; //客户端地址
};
/**
* @brief UDP客户端类
*/
class CClientUdp
{
public:
/**
* @brief 构造函数
*/
explicit CClientUdp(const char* serverIP/*服务器IP*/, int nServerPort/*服务器端口*/);
~CClientUdp();
//发送数据,成功:返回发送成功的数据长度; 失败: -1
int SendTo(const char* pData, int nLen);
//接收客户端数据,返回实际接收数据长度
int RecvData(char* pBuff, int nBuffLen);
//建立与指定socket的连接 (可选)
bool Connect();
private:
#ifdef WIN32
SOCKET m_socket; //套接字
#else
int m_socket;
#endif
char m_serverIP[16]; //服务器IP
int m_nServerPort; //服务器端口
};
#endif
udp.cpp
#include "udp.h"
CServerUdp::CServerUdp()
{
#ifdef WIN32
//初始化网络环境
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
printf("WSAStartup failed\n");
return;
}
//建立一个UDP的socket
//建立socket参数:socket(协议域,指定socket类型,指定协议)(和TCP协议后两个参数不同,都为IP协议族)
m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (m_socket == SOCKET_ERROR)
{
printf("create socket failed\n");
return;
}
#else
if( (m_socket = socket(AF_INET, SOCK_DGRAM, 0) ) == -1) //socket函数类似于open函数,打开并创建一个socket,AF_INET使用IPV4协议族,SOCK_STREAM为TCP套接口
{
perror("socket create failed!");
return;
}
#endif
}
CServerUdp::~CServerUdp()
{
#ifdef WIN32
//关闭SOCKET连接
closesocket(m_socket);
//清理网络环境
WSACleanup();
#else
close(m_socket);
#endif
}
//绑定接收端口进行监听,返回true表示绑定成功
bool CServerUdp::Bind(int nRecvPort)
{
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET; // 协议簇为IPV4的
serverAddr.sin_port = htons(nRecvPort); // 端口 因为本机是小端模式,网络是大端模式,调用htons把本机字节序转为网络字节序
serverAddr.sin_addr.s_addr= htonl(INADDR_ANY); // ip地址,INADDR_ANY表示绑定电脑上所有网卡IP
//bind(socket描述字, 绑定给listenfd的协议地址,地址长度)
return bind(m_socket, (sockaddr*)&serverAddr, sizeof(sockaddr)) == 0;
}
//接收客户端数据,返回实际接收数据长度
int CServerUdp::RecvData(char* pBuff, int nBuffLen)
{
// 网络节点的信息,用来保存客户端的网络信息
memset(&m_clientAddr, 0, sizeof(m_clientAddr));
size_t nClientAddrLen = sizeof(sockaddr);
//接收客户端发来的数据
//recvfrom参数:socket名称,接收数据的缓冲区,缓冲区大小,标志位(调用操作方式),sockaddr结构地址,sockaddr结构大小地址
//sockaddr地址用来保存从哪里发来,和发送到哪里的地址信息
int ret =recvfrom(m_socket, pBuff, nBuffLen, 0, (sockaddr*)&m_clientAddr, (SOCKET_LEN*)&nClientAddrLen);
//inet_ntoa函数转化为ip,ntohs函数转化为端口号
printf("Recv msg:%s from IP:[%s] Port:[%d]\n", pBuff, inet_ntoa(m_clientAddr.sin_addr), ntohs(m_clientAddr.sin_port));
return ret;
}
//回复数据给最近时间发送过来的客户端,成功:返回发送成功的数据长度; 失败: -1
int CServerUdp::SendTo(const char* pData, int nLen)
{
printf("Send msg back to IP:[%s] Port:[%d]\n", inet_ntoa(m_clientAddr.sin_addr), ntohs(m_clientAddr.sin_port));
// 发一个数据包返回给客户
//sendto参数:socket名称,发送数据的缓冲区,缓冲区大小,标志位(调用操作方式),sockaddr结构地址,sockaddr结构大小地址
return sendto(m_socket, pData, nLen, 0, (sockaddr*)&m_clientAddr, sizeof(sockaddr));
}
//获取最近发送客户端信息,(存在多个客户端)
void CServerUdp::GetClientInfo(char ip[16], int* pPort)const
{
strcpy(ip, inet_ntoa(m_clientAddr.sin_addr));
*pPort = ntohs(m_clientAddr.sin_port);
}
//指定ip端口发送,(存在多个客户端)
int CServerUdp::SendTo(const char* ip, int nPort, const char* pData, int nLen)
{
sockaddr_in addr = { 0 };
addr.sin_family = AF_INET; // 协议簇为IPV4的
addr.sin_port = htons(nPort); // 端口 因为本机是小端模式,网络是大端模式,调用htons把本机字节序转为网络字节序
addr.sin_addr.s_addr = inet_addr(ip); // 客户端的ip地址
// 发一个数据包返回给客户
//sendto参数:socket名称,发送数据的缓冲区,缓冲区大小,标志位(调用操作方式),sockaddr结构地址,sockaddr结构大小地址
return sendto(m_socket, pData, nLen, 0, (sockaddr*)&addr, sizeof(sockaddr));
}
/*****************************客户端UDP***************************************************/
CClientUdp::CClientUdp(const char* serverIP/*服务器IP*/, int nServerPort/*服务器端口*/)
{
strcpy(m_serverIP, serverIP);
m_nServerPort = nServerPort;
#ifdef WIN32
//初始化网络环境
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
printf("WSAStartup failed\n");
return;
}
//建立一个UDP的socket
//建立socket参数:socket(协议域,指定socket类型,指定协议)(和TCP协议后两个参数不同,都为IP协议族)
m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (m_socket == SOCKET_ERROR)
{
printf("create socket failed\n");
return;
}
#else
if( (m_socket = socket(AF_INET, SOCK_DGRAM, 0) ) == -1) //socket函数类似于open函数,打开并创建一个socket,AF_INET使用IPV4协议族,SOCK_STREAM为TCP套接口
{
perror("socket create failed!");
return;
}
#endif
}
CClientUdp::~CClientUdp()
{
#ifdef WIN32
//关闭SOCKET连接
closesocket(m_socket);
//清理网络环境
WSACleanup();
#else
close(m_socket);
#endif
}
//发送数据,成功:返回发送成功的数据长度; 失败: -1
int CClientUdp::SendTo(const char* pData, int nLen)
{
sockaddr_in addr = { 0 };
addr.sin_family = AF_INET; // 协议簇为IPV4的
addr.sin_port = htons(m_nServerPort); // 端口 因为本机是小端模式,网络是大端模式,调用htons把本机字节序转为网络字节序
addr.sin_addr.s_addr = inet_addr(m_serverIP); // 服务器的ip地址
//发送数据
//sendto参数:socket名称,接收数据的缓冲区,缓冲区大小,标志位(调用操作方式),sockaddr结构地址,sockaddr结构大小地址
int dwSent = sendto(m_socket, pData, nLen, 0, (sockaddr *)&addr, sizeof(sockaddr));
if (dwSent == 0)
{
printf("send %s failed\n", pData);
return -1;
}
return dwSent;
}
//接收客户端数据,返回实际接收数据长度
int CClientUdp::RecvData(char* pBuff, int nBuffLen)
{
memset(pBuff, 0, nBuffLen);
sockaddr_in addrSever = { 0 };
size_t nServerAddrLen = sizeof(sockaddr_in);
// 接收数据
//recvfrom参数:socket名称,接收数据的缓冲区,缓冲区大小,标志位(调用操作方式),sockaddr结构地址,sockaddr结构大小地址
int dwRecv = recvfrom(m_socket, pBuff, nBuffLen, 0, (sockaddr *)&addrSever, (SOCKET_LEN*)&nServerAddrLen);
printf("Recv msg from server : %s\n", pBuff);
return dwRecv;
}
//建立与指定socket的连接 (可选)
bool CClientUdp::Connect()
{
sockaddr_in addr = { 0 };
addr.sin_family = AF_INET; // 协议簇为IPV4的
addr.sin_port = htons(m_nServerPort); // 端口 因为本机是小端模式,网络是大端模式,调用htons把本机字节序转为网络字节序
addr.sin_addr.s_addr = inet_addr(m_serverIP); // 服务器的ip地址
return connect(m_socket, (sockaddr *)&addr, sizeof(sockaddr_in)) == 0;
}
linux客户端测试代码
#include "udp.h"
int main()
{
CClientUdp clientUdp("192.168.10.160", 5000);
if (clientUdp.Connect())
{
printf("connect server success\n");
}
clientUdp.SendTo("hello world", strlen("hello world"));
char szBuf[512] = "";
clientUdp.RecvData(szBuf, 512);
return 0;
}
linux gcc编译:gcc -o main main.cpp udp.cpp -lstdc++
客户端运行结果:connect server success
Recv msg from server : hello world
服务器端 ip:192.168.10.160 ,windows Qt 部分测试代码
#include <QDebug>
#include <thread>
#include "util/udp.h"
void RecvThread(int nPort)
{
printf("%d", nPort);
CServerUdp serverUdp;
if (serverUdp.Bind(nPort))
{
qDebug() << "udp port Bind success";
}
else
{
qDebug() << "udp port Bind failed";
}
char szBuff[4096] = "";
int nRecvLen = 0;
while(1)
{
nRecvLen = serverUdp.RecvData(szBuff, 4096);//堵塞
if (nRecvLen)
{
serverUdp.SendTo(szBuff, nRecvLen);
}
}
}
//开启线程接收
std::thread t(RecvThread, 5000);
t.detach();
测试流程:先启动服务器程序监听端口,再启动客户端。
测试问题:虽然功能都正常但是异常情况还是和预期有点差异
1. 客户端SendTo即使给ping不通的ip发送也是同样返回成功,估计因为UDP是无连接的,只是成功发出了本机端口,对方是否接收成功不管;
2. Connect接口按理是通信方只有一个时使用,只是将IP地址和端口号进行了存储,发送时不会对数据包再进行 IP地址和Port的安全检查,更快速,适用于发送频繁场景。测试发现ip不通Connect接口都是返回成功,估计底层返回的是存储结果,并不是连接对端的结果。