UDP打洞

提到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;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章