UDP穿透原理
需求的由来:我们要实现的功能是两个设备跨局域网进行通信,先说一下这里面存在的一些问题
存在的问题
问题一:两个不同局域网下的设备不太可能互相通信
我们知道两个局域网的设备进行通信是不太可能的,比如说我们有两台不同局域网上的机器一个IP地址是192.168.0.164,一个是192.168.0.213这两个机器进行通信如果不进行设置的话是不可能访问到的,就算是访问到了那也可能是你这个局域网下的机器有一个是这样的IP地址。学过计算机网络的大佬可能知道解决方案就是在路由器上进行匹配也可以解决这个问题,但是我们现在的场景是面向广大用户,一般的用户他是不会去如何配置路由器的,所以我们必须找出一种可行性的方案在解决这个需求问题
问题二:个人设备的IP地址是临时的且会经常变化
我们知道个人设备的IP地址是由所在的路由器或者所属的运营商给你临时分配的IP地址,也就是说今天一个IP地址是192.168.2.45说不定明天的IP地址就变成192.168.2.54了,这都是有可能的,基于这个原因你就不可能让用户在使用产品的时候每天还得配置一遍转发表吧,那是不太现实的。所以我们需要有一个IP固定不变的公网地址来承载这个。
现在我们这个了两个局域网设备进行通信可能会语言的问题,接下来我们站在计算机网络的基础上来分析一下两大最基础的协议:TCP和UDP
协议问题
TCP
TCP协议他是可靠的、面向连接的、稳定的,并且在TCP协议进行连接时会先进行三次握手的操作以及在发送数据时会等待设备是否成功接收的应答,对于网络防火墙来说,你在设备上开启了一个TCP的端口并且以及与其它设备连接上了,那么网络防火墙就会严格控制改端口上发来的数据的地址,如果发来数据的设备不是与你建立连接的设备,则防火墙就会将数据包丢弃不会传到指定的进程上去的并且不进行三次握手发来的数据是不可能进行处理的,但是TCP协议可靠以及有序的,通常通过TCP协议来传输比如像文件或者一些要求数据不能出现错误的东西。
UDP
UDP协议就恰恰与TCP协议相反,他是不可靠的、不稳定的,它只管像指定地址去发送数据不管数据是否发送到也不会管数据有没有错误,基于这些特点UDP协议它的传输速度是比较快的,因为它不需要去确认你是否接收成功并且也没有重传的机制(这里说明一下,有些基于UDP通信的设备,其在开发的时候可能会给UDP设计一个重传机制,但是这种重传机制与TCP的重传机制也是不太一样的,UDP是丢那个发那个,TCP是丢那个发丢的以及它后面的所以包都要重发),所以UDP的实时性是比较强的,一般用在一些直播、视频、音频的播放上,因为这些东西丢了几帧数据是不会影响整体效果的
下面我们说一下防火墙对UDP的管理方案,当你的端口是使用UDP进行数据传输并且也已经成功接收和成功发送了数据,那么网络防火墙就会给你将这个口子打开,就是以后在有TCP数据往这个端口来的话,防火墙不管是谁发送过来的数据都会转发给对应的进程
UDP穿透
基于上面的分析,想必大家心中也都有答案了吧。
要解决的问题:要适应局域网上随时可变的地址,并且客户端和服务器接收数据可以来自任意的IP地址
所以我们的解决方案就是,需要一个固定不变的IP地址并且是跑在公网上的,并且客户端服务器进行数据接收要通过UDP来实现(注意因为UDP不能确定用户是否在线,所以在实际的开发中UDP是与TCP配合使用)
UDP穿透的原理就是,我们让两个局域网内的设备通过UDP连接到我们固定不变的公网IP服务器上,然后互相告诉对方的IP地址,之后两个设备就可以通过UDP进行互相通信
UDP穿透图解:

C++代码实现
首先说明一下我们这里实现了一个什么功能,我们这个代码的功能是两个局域网内的机器可以进行通信,我们通过UDP穿透进行实现,先让用户连接到UDP穿透服务器上,然后UDP穿透服务器分别向客户端发送对方的IP地址,之后就双方就可以进行通信。
注意,我这里实现的是一个非常粗糙的UDP穿透案例,目的只是为了给大家演示怎么实现UDP穿透,后续大家可以自行进行扩展,比如添加用户列表功能,可以不同的客户端之间进行通信
UDP穿透服务器代码
// 系统:Linux
#include <cstdio>
#include <iostream>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <list>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <memory>
int main() //UDPPenetrateServer
{
// 创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock == -1) {
printf("%s(%d): 套接字创建失败!erron:%d msg:%s\r\n", __FILE__, __LINE__, errno, strerror(errno));
return 0;
}
sockaddr_in addr = {}, clientAddr = {};
addr.sin_family = PF_INET;
addr.sin_addr.s_addr = inet_addr("172.18.189.229");
addr.sin_port = htons(16889);
if (-1 == bind(sock, (sockaddr*)&addr, sizeof(addr))) {
close(sock);
printf("%s(%d): 套接字绑定失败!erron:%d msg:%s\r\n", __FILE__, __LINE__, errno, strerror(errno));
return 0;
}
std::list<sockaddr_in> clientAddrs;
std::string buffer;
ssize_t ret = 0;
while (true) {
socklen_t len = sizeof(clientAddr);
memset(&clientAddr, 0, sizeof(clientAddr));
buffer.clear();
buffer.resize(1024);
ret = recvfrom(sock, (char*)buffer.c_str(), buffer.size(), 0, (sockaddr*)&clientAddr, &len);
if (ret < 0) {
close(sock);
printf("%s(%d): 数据接收出现错误!erron:%d msg:%s\r\n", __FILE__, __LINE__, errno, strerror(errno));
break;
}
printf("接收到数据 ip:%s port:%d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
if (clientAddrs.size() <= 0) {
// 第一个连接上来的客户端
buffer = "first client";
ret = sendto(sock, buffer.c_str(), buffer.size(), 0, (sockaddr*)&clientAddr, sizeof(clientAddr));
if (ret > 0) {
clientAddrs.emplace_back(clientAddr);
}
else {
printf("%s(%d): 数据发送失败!erron:%d msg:%s\r\n", __FILE__, __LINE__, errno, strerror(errno));
continue;
}
}
else {
buffer = "client connect ok";
ret = sendto(sock, buffer.c_str(), buffer.size(), 0, (sockaddr*)&clientAddr, sizeof(clientAddr));
if (ret > 0) {
clientAddrs.emplace_back(clientAddr);
}
else {
printf("%s(%d): 数据发送失败!erron:%d msg:%s\r\n", __FILE__, __LINE__, errno, strerror(errno));
continue;
}
// 将双方客户端地址信息,进行互相发送
buffer.clear();
buffer.resize(sizeof(clientAddr));
memcpy((void*)buffer.c_str(), &clientAddrs.front(), sizeof(clientAddr));
ret = sendto(sock, buffer.c_str(), buffer.size(), 0, (sockaddr*)&clientAddr, sizeof(clientAddr));
if (ret < 0) {
printf("%s(%d): 数据发送失败!erron:%d msg:%s\r\n", __FILE__, __LINE__, errno, strerror(errno));
continue;
}
memcpy((void*)buffer.c_str(), &clientAddr, sizeof(clientAddr));
ret = sendto(sock, buffer.c_str(), buffer.size(), 0, (sockaddr*)&clientAddrs.front(), sizeof(sockaddr_in));
if (ret < 0) {
printf("%s(%d): 数据发送失败!erron:%d msg:%s\r\n", __FILE__, __LINE__, errno, strerror(errno));
continue;
}
}
}
close(sock);
}
UDP穿透客户端代码
// 系统:Windows
#include <iostream>
#include <WinSock2.h>
#include <conio.h>
#pragma comment(lib, "ws2_32.lib")
int InitUDPSock()
{
WSADATA wsaData = {};
if (WSAStartup(MAKEWORD(1, 1), &wsaData)) {
printf("%s(%d): 网络初始化失败!erron:%d msg:%s\r\n", __FILE__, __LINE__, errno, strerror(errno));
return -1;
}
// 创建套接字
SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock == INVALID_SOCKET) {
printf("%s(%d): 套接字创建失败!erron:%d msg:%s\r\n", __FILE__, __LINE__, errno, strerror(errno));
return -1;
}
sockaddr_in addr = {};
addr.sin_family = PF_INET;
addr.sin_addr.s_addr = 0; // 这里设置为0.0.0.0表示可以接收任何地址发来的消息
addr.sin_port = htons(12345);// 12456
if (-1 == bind(sock, (sockaddr*)&addr, sizeof(addr))) {
closesocket(sock);
printf("%s(%d): 套接字绑定失败!erron:%d msg:%s\r\n", __FILE__, __LINE__, errno, strerror(errno));
return -1;
}
return sock;
}
#include <iterator>
int main()
{
SOCKET sock = InitUDPSock();
if (sock == -1) {
return 0;
}
sockaddr_in serverAddr = {};
serverAddr.sin_family = PF_INET;
serverAddr.sin_addr.s_addr = inet_addr("47.99.76.67");
serverAddr.sin_port = htons(16889);
std::string str = "hello";
int ret = sendto(sock, str.c_str(), str.size(), 0, (sockaddr*)&serverAddr, sizeof(serverAddr));
if (ret < 0) {
return 0;
}
str.clear();
str.resize(1024);
sockaddr_in recvAddr = {}, clientAddr = {};
int len = sizeof(recvAddr);
ret = recvfrom(sock, (char*)str.c_str(), str.size(), 0, (sockaddr*)&recvAddr, &len);
if (ret < 0) {
return 0;
}
if (!strcmp(str.c_str(), "first client")) {
printf("您是第一个登录的用户,请等待我们为您连接用户...\n");
}
else if (!strcmp(str.c_str(), "client connect ok")) {
printf("正在为您接入客户端,请稍等...\n");
}
str.clear();
str.resize(sizeof(clientAddr) + 10);
ret = recvfrom(sock, (char*)str.c_str(), str.size(), 0, (sockaddr*)&recvAddr, &len);
if (ret > 0) {
printf("UDP穿透建立成功,您可以发消息给他了!\n");
}
else {
closesocket(sock);
return 0;
}
// clientAddr = *(sockaddr_in*)str.c_str();
memcpy(&clientAddr, str.c_str(), sizeof(clientAddr));
printf("ip:%s port:%d\r\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
str.clear();
str.resize(1024);
struct timeval tv_out = {};
tv_out.tv_sec = 1;
tv_out.tv_usec = 0;
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv_out, sizeof(tv_out))) {
printf("非阻塞套接字设置失败\n");
return 0;
}
int ch = 0;
std::string SendBuffer;
while (true) {
if (_kbhit() != 0) {
ch = _getch();
if (ch == 13) {
if (SendBuffer.size() > 0) {
ret = sendto(sock, SendBuffer.c_str(), SendBuffer.size(), 0, (sockaddr*)&clientAddr, sizeof(clientAddr));
if (ret < 0) {
printf("%s(%d): 数据发送失败!erron:%d msg:%s\r\n", __FILE__, __LINE__, errno, strerror(errno));
break;
}
SendBuffer.clear();
printf("\n");
}
}
else {
printf("%c", (char)ch);
SendBuffer += (char)ch;
}
}
ret = recvfrom(sock, (char*)str.c_str(), str.size(), 0, (sockaddr*)&recvAddr, &len);
if (ret > 0) {
printf("消息来了:%s\n", str.c_str());
memset((void*)str.c_str(), 0, str.size());
}
else if (ret < 0) {
if (errno == 0) {
continue;
}
printf("%s(%d): 数据接收错误!erron:%d msg:%s\r\n", __FILE__, __LINE__, errno, strerror(errno));
break;
}
}
closesocket(sock);
}
需要注意的是,有些路由器NAT类型是不支持UDP穿透的,所以可能有些大佬们测试代码会不能成功运行
如果在使用套接字API函数,报出下面的错误,则表示没有引入套接字的静态库
解决方法:在代码中添加#pragma comment(lib, "ws2_32.lib")导入套接字的静态库

到这里,利用UDP穿透进行跨局域网通信就介绍到这里了
感谢观看学习,大佬们多多指点,愿明天的自己会感谢当下的努力!!!!