提到UDP打洞,先提一個概念叫做NAT。NAT就是把局域網內多個ip地址映射到公網上,從而能完成局域網到公網的訪問。但是NAT有一個特性,就是必須先從局域網到公網上建立映射,而後公網才能和局域網之間進行相互通信。如果兩個局域網之間的機器要藉助公網進行通信,這時就需要打洞了。UDP打洞要藉助一個打洞服務器,打洞服務器在公網上。
核心思路:
1.機器A,B,C等衆多需要打洞的服務向打洞服務器註冊。
2.A向打洞服務器發起向B打洞的請求。
3.B接收到向A的打洞請求,發送任意數據。(此時會被丟棄)
4.A在向B發起打洞請求,即發送任意數據。(此時打洞成功)
注意:3.4步驟順序可顛倒,但是第一個請求肯定都會被丟棄。
server端
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <errno.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <Iphlpapi.h>
#pragma comment (lib, "Ws2_32.lib")
#pragma comment(lib,"Iphlpapi.lib")
int ioctl(int fd, long cmd, u_long *ptr) {
return ioctlsocket(fd, cmd, ptr);
}
int close(int fd) {
return closesocket(fd);
}
#else
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <error.h>
#endif // !_WIN32
//中間樞紐獲得A客戶端的外網ip和port發送給客戶端B,獲得客戶端B的外網ip和port發送給A
//B通過A打的洞發數據給A,這時候A拒收B的消息,因爲A的nat映射中沒有B的信息,但是這次通
//信卻在B的網管中添加了映射可以接受A的
//消息,同理A通過B打的洞發數據給B,這時候由於B可以接受A的消息,所以數據接收成功且在A
//的映射中加入了B的信息,從而A與B可以跨服務器通信。實現p2p
/* 由於已知的外網服務器S可能並沒有AB客戶端的映射關係,所以要先建立A與S 還有 B與S之間的映射,這樣才能進行udp穿透。 */
#define ERR_EXIT(m)\
do{\
perror(m);\
exit(1);\
}while(0)
/* 用來記錄客戶端發送過來的外網ip+port */
typedef struct {
struct in_addr ip;
int port;
}clientInfo;
int main()
{
/* 一個客戶端信息結構體數組,分別存放兩個客戶端的外網ip+port */
clientInfo info[2];
/* 作爲心跳包需要接收的一個字節 */
/* char ch; */
char str[10] = { 0 };
#ifdef _WIN32
WORD wVersionRequested = MAKEWORD(2, 2);
WSADATA wsaData;
WSAStartup(wVersionRequested, &wsaData);
#endif //_WIN32
/* udp socket描述符 */
int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1)
ERR_EXIT("SOCKET");
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0");
serveraddr.sin_port = htons(8888);
serveraddr.sin_family = AF_INET;
int opt = 1;
int ret = setsockopt(sockfd, SOL_SOCKET,SO_REUSEADDR, (char *)&opt, static_cast<socklen_t>(sizeof(opt)));
ret = bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if (ret == -1)
ERR_EXIT("BIND");
/* 服務器接收客戶端發來的消息並轉發 */
while (1)
{
memset(info,0, sizeof(clientInfo) * 2);
/* 接收兩個心跳包並記錄其與此鏈接的ip+port */
socklen_t addrlen = sizeof(struct sockaddr_in);
/* recvfrom(sockfd, &ch, sizeof(ch), 0, (struct sockaddr *)&serveraddr, &addrlen); */
recvfrom(sockfd, str, sizeof(str), 0, (struct sockaddr *)&serveraddr, &addrlen);
memcpy(&info[0].ip, &serveraddr.sin_addr, sizeof(struct in_addr));
info[0].port = serveraddr.sin_port;
printf("A client IP:%s \tPort:%d creat link OK!\n", inet_ntoa(info[0].ip), ntohs(info[0].port));
/* recvfrom(sockfd, &ch, sizeof(ch), 0, (struct sockaddr *)&serveraddr, &addrlen); */
recvfrom(sockfd, str, sizeof(str), 0, (struct sockaddr *)&serveraddr, &addrlen);
memcpy(&info[1].ip, &serveraddr.sin_addr, sizeof(struct in_addr));
info[1].port = serveraddr.sin_port;
printf("B client IP:%s \tPort:%d creat link OK!\n", inet_ntoa(info[1].ip), ntohs(info[1].port));
/* 分別向兩個客戶端發送對方的外網ip+port */
printf("start informations translation...\n");
serveraddr.sin_addr = info[0].ip;
serveraddr.sin_port = info[0].port;
sendto(sockfd, (char *)&info[1], sizeof(clientInfo), 0, (struct sockaddr *)&serveraddr, addrlen);
serveraddr.sin_addr = info[1].ip;
serveraddr.sin_port = info[1].port;
sendto(sockfd, (char *)&info[0], sizeof(clientInfo), 0, (struct sockaddr *)&serveraddr, addrlen);
printf("send informations successful!\n");
}
#ifdef _WIN32
WSACleanup();
#endif //_WIN32
return 0;
}
客戶端A
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <errno.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <Iphlpapi.h>
#pragma comment (lib, "Ws2_32.lib")
#pragma comment(lib,"Iphlpapi.lib")
int ioctl(int fd, long cmd, u_long *ptr) {
return ioctlsocket(fd, cmd, ptr);
}
int close(int fd) {
return closesocket(fd);
}
#else
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <error.h>
#endif // !_WIN32
/* 原理見服務器源程序 */
#define ERR_EXIT(m)\
do{\
perror(m); \
exit(1);\
}while(0)
typedef struct {
struct in_addr ip;
int port;
}clientInfo;
/* 用於udp打洞成功後兩個客戶端跨服務器通信 */
void echo_ser(int sockfd, struct sockaddr* addr, socklen_t *len)
{
printf("start recv B data...\n");
char buf[1024];
while (1)
{
memset(buf, 0, sizeof(buf));
//接收B發來的數據
recvfrom(sockfd, buf, sizeof(buf) - 1, 0, addr, len);
printf("%s \n", buf);
//向B發送數據
printf("send data to B ...\n");
sendto(sockfd, buf, sizeof(buf) - 1, 0, addr, sizeof(struct sockaddr_in));
buf[strlen(buf)] = '\0';
if (strcmp(buf, "exit") == 0)
break;
}
}
int main()
{
#ifdef _WIN32
WORD wVersionRequested = MAKEWORD(2, 2);
WSADATA wsaData;
WSAStartup(wVersionRequested, &wsaData);
#endif //_WIN32
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1)
ERR_EXIT("SOCKET");
//向服務器發送心跳包的一個字節的數據
char ch = 'a';
clientInfo info;
socklen_t addrlen = sizeof(struct sockaddr_in);
memset(&info, 0, sizeof(info));
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
//實際情況下這裏用一個已知的外網的服務器的端口號
clientaddr.sin_port = htons(8888);
//實際情況下這裏用一個已知的外網的服務器的ip地址,這裏保護我的雲服務器ip所以沒有寫出來,自己換一下ip地址。
clientaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
clientaddr.sin_family = AF_INET;
/* 向服務器S發送數據包 */
sendto(sockfd, &ch, sizeof(ch), 0, (struct sockaddr *)&clientaddr, sizeof(struct sockaddr_in));
/* 接收B的ip+port */
printf("send success\n");
recvfrom(sockfd, (char*)&info, sizeof(clientInfo), 0, (struct sockaddr *)&clientaddr, &addrlen);
printf("IP: %s\tPort: %d\n", inet_ntoa(info.ip), ntohs(info.port));
clientaddr.sin_addr = info.ip;
clientaddr.sin_port = info.port;
sendto(sockfd, &ch, sizeof(ch), 0, (struct sockaddr *)&clientaddr, sizeof(struct sockaddr_in));//注意:最後兩個客戶端相互sendto中'a'時,有一個會被捨棄,有一個會被接收到
echo_ser(sockfd, (struct sockaddr *)&clientaddr, &addrlen);
close(sockfd);
#ifdef _WIN32
WSACleanup();
#endif //_WIN32
return 0;
}
客戶端B
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <errno.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <Iphlpapi.h>
#pragma comment (lib, "Ws2_32.lib")
#pragma comment(lib,"Iphlpapi.lib")
int ioctl(int fd, long cmd, u_long *ptr) {
return ioctlsocket(fd, cmd, ptr);
}
int close(int fd) {
return closesocket(fd);
}
#else
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <error.h>
#endif // !_WIN32
/* 原理見服務器源程序 */
#define ERR_EXIT(m)\
do{\
perror(m); \
exit(1);\
}while(0)
typedef struct {
struct in_addr ip;
int port;
}clientInfo;
/* 用於udp打洞成功後兩個客戶端跨服務器通信 */
void echo_ser(int sockfd, struct sockaddr* addr, socklen_t *len)
{
char buf[1024];
while (1)
{
memset(buf, 0, sizeof(buf));
printf(">> ");
fflush(stdout);
fgets(buf, sizeof(buf) - 1, stdin);
//向A發送數據
sendto(sockfd, buf, strlen(buf), 0, addr, sizeof(struct sockaddr_in));
//接收A發來的數據
memset(buf, 0, sizeof(buf));
printf("start recv A data...\n");
recvfrom(sockfd, buf, sizeof(buf) - 1, 0, addr, len);
printf("%s \n", buf);
buf[strlen(buf)] = '\0';
if (strcmp(buf, "exit") == 0)
break;
}
}
int main()
{
#ifdef _WIN32
WORD wVersionRequested = MAKEWORD(2, 2);
WSADATA wsaData;
WSAStartup(wVersionRequested, &wsaData);
#endif //_WIN32
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1)
ERR_EXIT("SOCKET");
//向服務器發送心跳包的一個字節的數據
char ch = 'a';
/* char str[] = "abcdefgh"; */
clientInfo info;
socklen_t addrlen = sizeof(struct sockaddr_in);
memset(&info, 0, sizeof(info));
struct sockaddr_in clientaddr, serveraddr;
/* 客戶端自身的ip+port */
/* memset(&clientaddr, 0, sizeof(clientaddr)); */
/* clientaddr.sin_port = htons(8888); */
/* clientaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */
/* clientaddr.sin_family = AF_INET; */
/* 服務器的信息 */
memset(&clientaddr, 0, sizeof(clientaddr));
//實際情況下爲一個已知的外網的服務器port
serveraddr.sin_port = htons(8888);
//實際情況下爲一個已知的外網的服務器ip,這裏僅用本地ip填充,下面這行的ip自己換成已知的外網服務器的ip
serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
/* clientaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */
serveraddr.sin_family = AF_INET;
/* 向服務器S發送數據包 */
sendto(sockfd, &ch, sizeof(ch), 0, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr_in));
/* sendto(sockfd, str, sizeof(str), 0, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr_in)); */
/* 接收B的ip+port */
printf("send success\n");
recvfrom(sockfd, (char*)&info, sizeof(clientInfo), 0, (struct sockaddr *)&serveraddr, &addrlen);
printf("IP: %s\tPort: %d\n", inet_ntoa(info.ip), ntohs(info.port));
serveraddr.sin_addr = info.ip;
serveraddr.sin_port = info.port;
sendto(sockfd, &ch, sizeof(ch), 0, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr_in));//注意:最後兩個客戶端相互sendto中'a'時,有一個會被捨棄,有一個會被接收到
echo_ser(sockfd, (struct sockaddr *)&serveraddr, &addrlen);
close(sockfd);
#ifdef _WIN32
WSACleanup();
#endif //_WIN32
return 0;
}