stun udp

1
https://cloud.tencent.com/developer/article/2215771

p2p server

需要通过stun穿透协议定时与穿透服务通信,即在定时器线程里面发送SStunLogin,再在epoll里面udp接收SStunLoginResponse表示登录成功,同时epoll里面udp需要解析SStunTransfer穿透命令,收到穿透命令后需要回复对方一个字节的包。

一个字节的穿透包不一定能发出去,比如当p2p client发起打洞标记时请求的p2p server port和p2p server外部端口对不上,那打的标记就没用,或者p2p client不是严格意义上的nat 3网络(外部端口会隔段时间变化),或者是nat 4对称型网络,基本打洞不成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// 定时器线程:每隔30秒发送STUN登录请求
std::thread timer_thread([&]()
{
while (true) {
static unsigned int s_seq = 0;
int size = sizeof(SStunLogin) + g_strDeviceId.size() - 1;
char* buf = new char[size];
SStunLogin* p = (SStunLogin*)buf;
p->head.len = htons(size);
p->head.cmd = htons(STUN_CMD_LOGIN);
p->head.seq = htonl(++s_seq);
p->nat = g_finalNatType;
memcpy_s(p->uid, size - sizeof(SStunLogin) + 1, g_strDeviceId.c_str(), g_strDeviceId.size());
sendto(sockfd, buf, size, 0, (const sockaddr*)&server_addr, sizeof(server_addr));
delete[] buf;
printf("udp stun alive seq = %u\n", s_seq);
sleep(30);
} });

// 创建epoll
int epoll_fd = epoll_create(1);
if (epoll_fd < 0)
{
std::cerr << "Failed to create epoll" << std::endl;
return 1;
}

// 添加UDP套接字到epoll
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = sockfd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &event) < 0)
{
std::cerr << "Failed to add socket to epoll" << std::endl;
return 1;
}

// epoll等待事件
const int MAX_EVENTS = 10;
struct epoll_event events[MAX_EVENTS];
while (true)
{
int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (num_events < 0)
{
std::cerr << "Error in epoll_wait" << std::endl;
return 1;
}

for (int i = 0; i < num_events; ++i)
{
if (events[i].data.fd == sockfd && events[i].events & EPOLLIN)
{
char buf[1500];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
ssize_t num_bytes = recvfrom(sockfd, &buf, sizeof(buf), 0,
(struct sockaddr *)&client_addr, &addr_len);
if (num_bytes < 0)
{
std::cerr << "Error in recvfrom" << std::endl;
continue;
}

printf("recvfrom %s_%d size = %ld\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), num_bytes);

if (sizeof(SStunLoginResponse) == num_bytes)
{
// 尝试解析成穿透响应
SStunLoginResponse *slr = (SStunLoginResponse *)buf;
if (ntohs(slr->head.cmd) == STUN_CMD_LOGIN && ntohs(slr->head.len) == num_bytes)
{
printf("recv stun alive result = %d,seq = %u,ip = %s,port = %d\n", (int)slr->result, ntohl(slr->head.seq),
IpInt2Str(slr->ip).c_str(), ntohs(slr->port));
}
}
if (sizeof(SStunTransfer) == num_bytes)
{
// 尝试解析成穿透命令
SStunTransfer *st = (SStunTransfer *)buf;
if (ntohs(st->head.cmd) == STUN_CMD_TRANSFER && ntohs(st->head.len) == num_bytes)
{
SStunResponse response;
response.head.cmd = st->head.cmd;
response.head.seq = st->head.seq;
response.head.len = htons(sizeof(response));
response.result = 0;
int tmpRet = sendto(sockfd, (const char *)&response, sizeof(response), 0, (sockaddr *)&client_addr, addr_len);
if (tmpRet != sizeof(response))
{
printf("sendto %s_%d ret = %d,error = %d\n", IpInt2Str(client_addr.sin_addr.s_addr).c_str(), ntohs(client_addr.sin_port), tmpRet,
errno);
}
client_addr.sin_addr.s_addr = st->ip;
client_addr.sin_port = st->port;
tmpRet = sendto(sockfd, "0", 1, 0, (sockaddr *)&client_addr, addr_len);
if (tmpRet != 1)
{
printf("sendto %s_%d ret = %d,error = %d\n", IpInt2Str(client_addr.sin_addr.s_addr).c_str(), ntohs(client_addr.sin_port), tmpRet,
errno);
}
printf("stun to %s_%d\n", IpInt2Str(client_addr.sin_addr.s_addr).c_str(), ntohs(client_addr.sin_port));
}
}
else
{
std::cout << "receive data " << string(buf, num_bytes) << std::endl;
}
}
}
}

p2p client

首先尝试发送一个字节与p2p节点通信(可能通信不上,但会在本地路由器留下打洞标记),再向穿透服务器stun发送穿透请求,最后接收udp的响应,如果是来自穿透服务器stun的响应,根据返回码判断是否穿透请求成功,如果是收到p2p节点的1字节打洞包,说明穿透打洞成功。穿透打洞成功后再和p2p节点进行正常的udp通信,例如kcp同步建连。

p2p client穿透请求成功,只能代表p2p server正常在线,需要收到p2p2 server一个字节打洞包后,才能真正说明是打洞成功了,收到打洞包后,可以根据对方的ip和端口修改本地记录,真正请求数据时肯定按这个ip和端口进行通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 向P2P节点发送一个字节的数据(打洞标记)
struct sockaddr_in p2p_node_addr;
p2p_node_addr.sin_family = AF_INET;
p2p_node_addr.sin_port = htons(p2pPort); // P2P节点的端口号
inet_pton(AF_INET, p2pIp.c_str(), &p2p_node_addr.sin_addr); // P2P节点的IP地址

int ret = 0;
ret = sendto(sockfd, "0", 1, 0,
(struct sockaddr *)&p2p_node_addr, sizeof(p2p_node_addr));
std::cout << "sendto p2p node " << ret << std::endl;

// 向STUN服务器发送穿透请求
struct sockaddr_in stun_server_addr;
stun_server_addr.sin_family = AF_INET;
stun_server_addr.sin_port = htons(STUN_SERVER_PORT);
inet_pton(AF_INET, p2pStunServer.c_str(), &stun_server_addr.sin_addr); // STUN服务器的IP地址

// 生成穿透请求数据
char buf[256];
SStunRequest *stunReq = (SStunRequest *)buf;
stunReq->head.cmd = htons(STUN_CMD_REQUEST);
stunReq->head.len = htons(sizeof(SStunRequest) - 1 + p2pUid.size());
stunReq->nat = 1;
memcpy(stunReq->uid, p2pUid.c_str(), p2pUid.size());
ret = sendto(sockfd, buf, ntohs(stunReq->head.len), 0,
(struct sockaddr *)&stun_server_addr, sizeof(stun_server_addr));
std::cout << "sendto stun server " << ret << std::endl;

// 判断响应是否来自STUN服务器
if (remote_addr.sin_addr.s_addr == stun_server_addr.sin_addr.s_addr &&
remote_addr.sin_port == stun_server_addr.sin_port)
{
// 穿透服务器的响应
SStunResponse *stun = (SStunResponse *)buffer;
printf("stun result %d\n", stun->result);
if (0 == stun->result)
{
// 穿透请求成功了
std::cout << "穿透请求成功!" << std::endl;
// 进行正常的UDP通信
// ...
}
else
{
std::cout << "穿透请求失败。" << std::endl;
}
}
else if (1 == num_bytes)
{
std::cout << "收到P2P节点的打洞包,穿透成功!" << std::endl;
// 穿透包,可能端口或ip不一致,改一下
if (remote_addr.sin_addr.s_addr != p2p_node_addr.sin_addr.s_addr)
{
string oldIp = inet_ntoa(p2p_node_addr.sin_addr);
string newIp = inet_ntoa(remote_addr.sin_addr);
printf("p2p_node ip change %s -> %s\n", oldIp.c_str(), newIp.c_str());
p2p_node_addr.sin_addr.s_addr = remote_addr.sin_addr.s_addr;
}
if (remote_addr.sin_port != p2p_node_addr.sin_port)
{
printf("p2p_node port change %d -> %d\n", ntohs(p2p_node_addr.sin_port), ntohs(remote_addr.sin_port));
p2p_node_addr.sin_port = remote_addr.sin_port;
}
// 进行正常的UDP通信
const char *sendData = "hello world";
int ret = sendto(sockfd, sendData, strlen(sendData), 0, (struct sockaddr *)&p2p_node_addr, sizeof(p2p_node_addr));
std::cout << "sendto p2p node " << ret << std::endl;
}

stun server

收到p2p server的SStunLogin后,根据uid记录对方的nat、addr和ts等信息放到map中,并回复对方SStunLoginResponse,带上p2p server的外部ip和端口,p2p server收到响应后,就知道自己的外部ip和端口了。

收到p2p client的STUN_CMD_REQUEST穿透请求后,查询当前map是否有该p2p server uid的心跳记录,如果没有,说明对方p2p server节点并未上线,如果有,转发STUN_CMD_TRANSFER穿透请求到该p2p server节点,stun收到该p2p server节点的响应后,再向p2p client响应穿透请求转发成功。

nephen wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!