Administrator
Published on 2025-05-08 / 19 Visits
0
0

NAT

概述

NAT(网络地址转换)本质上就是描述路由器等中转设备在处理数据包时,对IP地址和端口的转发映射行为。

NAT的核心是中转设备对IP和端口的动态或静态转换,以实现内网与外网通信的隔离与共享。其行为包括地址隐藏、端口映射、会话管理等,是现代网络中不可或缺的基础技术。

NAT通过五元组(源IP、源端口、协议、目标IP、目标端口)建立映射表。若五元组相同,则复用映射;否则分配新端口。

详细看:什么是STUN?STUN的作用是什么? - 华为

具体可分为以下几点:

1. 核心功能:地址转换

NAT的主要作用是将私有网络(如家庭或企业内网)中的设备通过一个或多个公共IP地址与外部网络(如互联网)通信。

例如,内网设备(如192.168.1.100)通过NAT设备(如路由器)访问互联网时,NAT会将数据包的源IP地址(私有地址)替换为路由器的公网IP地址,同时可能修改端口号以区分不同内网设备或会话。

2. 端口映射与转发

端口映射(Port Mapping):将内网设备的特定端口映射到公网IP的某个端口,使外部用户可通过访问公网IP的该端口直接访问内网服务(如Web服务器)。

端口转发(Port Forwarding):类似端口映射,但更强调规则驱动,即当外部请求到达公网IP的指定端口时,NAT自动将请求转发给内网对应的设备或端口。

3. 隐藏内网结构

NAT通过隐藏内网私有IP地址,提高了安全性,因为外部网络无法直接获取内网拓扑信息。

例如,外部设备只能看到NAT设备的公网IP,而无法直接访问内网设备,除非通过端口映射或穿透技术。

4. 缓解IPv4地址短缺

通过共享少量公网IP服务大量内网设备,NAT有效缓解了IPv4地址耗尽的问题。

5. 会话管理与状态跟踪

NAT设备会维护一个状态表(NAT表),记录内网设备与外部通信的会话信息(如源/目的IP、端口等),确保返回的数据包能正确路由回内网设备。

例如,内网设备A通过NAT访问外部服务器时,NAT会记录A的私有IP和端口与公网IP和分配端口的映射关系,后续返回数据包会依据此映射转发。

类型

NAT根据对NAT内部主机的对外IP和端口的映射方式,可分为全锥型NAT,受限锥型NAT,对称NAT。

检测某个主机的所在网络的NAT方式工具:高精度 NAT 类型检测工具NAT 在线检测工具NAT类型在线检测工具,查看我的NAT类型 – 猫点饭NAT测试工具NatTypeTester - 米多贝克&米多网络工程

全锥型NAT

全锥型NAT最宽松的NAT类型,与地址受限锥型NAT和端口受限锥型NAT不同,其对IP地址和端口都没有限制。一旦内部主机将内部IP地址和端口映射到外部IP地址和端口,外部任何主机都可以通过这个外部IP地址和端口与内部主机通信。

地址受限锥型NAT

跟端口受限锥型NAT规则类型,核心区别是外部主机回传数据时所使用的源端口,不用是要NAT内部主机发送的目标端口,但是所使用的源IP还是要一致才行。

端口受限锥型NAT

端口受限锥型NAT通常为NAT内部主机的相同内部(源)端口到不同外部目标的连接重用外部端口(使用同个映射的外网端口)。

对于端口受限锥型NAT(Port Restricted Cone NAT),外部主机能够回传数据到NAT内部的主机的前提是,NAT内部的主机必须先发送数据到外部主机。这是因为在端口受限锥型NAT中,NAT设备会记录内部主机发起的出站连接的详细信息,包括内部(源)IP地址、内部(源)端口、外部(目标)IP地址和外部(目标)端口。只有当外部主机的回传数据包符合这些记录的详细信息时,NAT设备才会允许数据包通过并转发给内部主机,不然数据就会被丢弃不转发。

端口受限锥型NAT对外部主机回传数据给到NAT内的主机时,外部主机发送数据的源IP和源端口有严格要求:

  1. 目标IP:必须是NAT的外部IP地址 (203.0.113.5)。

  2. 目标端口:必须是NAT为该特定出站连接分配的外部端口。

  3. 源IP:必须是该外部主机自身的IP地址 (198.51.100.1)。

  4. 源端口:必须是该外部主机在原始出站连接请求中使用的那个端口。

对称NAT

对称NAT除了含有端口受限锥型NAT的限制外,核心区别是对称NAT为NAT内部主机的相同内部(源)端口到不同外部目标的连接分配不同的外部端口(使用不同映射的外网端口)。外部设备必须知道客户端针对特定目标的映射端口才能通信,且仅允许之前与该端口交互过的设备回连。

每次客户端连接不同目标时,NAT都会分配新的公网端口,即使源端口不变。比如客户端向服务器A和服务器B发送数据时,可能分别映射为54321和54322。

例如:内网客户端连接8.8.8.8:53时,NAT分配公网端口6000;若它再连接4.4.4.4:53,NAT可能分配6001,外部设备需使用6001才能通信。

端口受限锥型NAT详情说明

端口受限锥型NAT要求外部IP与之前通信的IP一致,下面详情举例说明:

当内网主机(如192.168.1.10:5000)首次向外部主机(如203.0.113.5:80)发送数据时,NAT会记录该外部主机的IP和端口,并分配给内外主机一个公网端口(如203.0.113.1:60000)。

只有该外部主机(203.0.113.5:80)后续发送的数据包,才能通过NAT的公网端口(203.0.113.1:60000)转发到内网主机。

如果该外部主机其他端口(203.0.113.5:81)或另一个外部主机(如203.0.113.6:80)尝试向分配的公网端口(203.0.113.1:60000)发送数据,NAT会直接丢弃该包,因为内网主机未与该IP/端口建立过通信。

与地址受限锥型NAT的区别是,对于地址受限锥型NAT,该外部主机其他端口也能发送数据,其他外部主机则同样不允许。

受限锥型NAT与对称NAT区别

受限锥型NAT:

NAT内的主机访问不同IP或不同端口的外部目标,NAT设备映射的端口不一样会变,可能会是同一个,但是也不一定是同一个。

NAT内的主机源IP或源端口变了,只要是之前有访问过外部主机的IP和端口,外部主机的IP和端口还是能向NAT内的主机进行发送数据,不会被舍弃。

对称NAT:

NAT内的主机访问不同IP或不同端口的外部目标,NAT设备映射的端口一定会变。

NAT内的主机源IP或源端口变了,之前有访问过外部主机的IP和端口,外部主机的IP和端口不能向NAT内的主机进行发送数据,一定会舍弃。

受限锥型NAT(Port Restricted Cone NAT):

映射端口变化规则:

当NAT内的主机访问不同IP或不同端口的外部目标时,NAT设备可能会分配不同的公共端口,但不保证一定变化(取决于NAT实现)。

如果访问相同的IP和端口,且源端口不变,通常复用相同的公共端口。

外部访问规则:

外部主机(eAddr:ePort)要向NAT内的主机(iAddr:iPort)发送数据,必须满足:

eAddr:ePort 必须是NAT内主机之前主动访问过的外部IP和端口。

NAT内主机的源IP和源端口可以变化,只要外部主机的IP和端口不变,通信仍然允许。

例如:如果内网主机从192.168.1.10:5000切换到192.168.1.10:6000,但之前访问过203.0.113.5:80,外部主机仍可向203.0.113.5:80发送数据。

对称NAT(Symmetric NAT):

映射端口变化规则:

当NAT内的主机访问不同IP或不同端口的外部目标时,NAT设备一定会分配新的公共端口(严格绑定通信对)。

即使访问相同的IP和端口,但源端口或源IP变化,也会分配新的公共端口。

外部访问规则:

外部主机(eAddr:ePort)要向NAT内的主机(iAddr:iPort)发送数据,必须满足:

eAddr:ePort 必须是NAT内主机最后一次主动访问过的外部IP和端口。

NAT内主机的源IP或源端口变化会导致之前的映射失效,外部主机无法再访问。

例如:如果内网主机从192.168.1.10:5000切换到192.168.1.10:6000,即使之前访问过203.0.113.5:80,外部主机也无法再通过203.0.113.5:80发送数据。

关键区别:

受限锥型NAT允许外部主机基于历史访问记录(IP和端口)反向连接,且不严格绑定源端口。

对称NAT要求外部主机的IP和端口与最后一次通信完全一致,且源端口变化会阻断反向连接。

您的描述基本准确,但对称NAT的规则更严格:外部主机的IP和端口必须与NAT内主机最后一次主动访问时记录的完全一致,且源端口变化会立即失效映射。

实际测验

找一个电脑,然后使用java编写一个stun客户端,进行一下试验:

  1. 第一次:

源端口使用12345,请求公共的stun服务器(比如stun.freeswitch.org:3478),获取到信息:

publicIp: 120.239.58.117

publicPort: 4818

  1. 第二次:

源端口使用12345,请求公共的stun服务器(stun.freeswitch.org:3478),获取到信息:

publicIp: 120.239.58.117

publicPort: 4818

  1. 第三次:

源端口使用12346,请求公共的stun服务器(stun.freeswitch.org:3478),获取到信息:

publicIp: 120.239.58.117

publicPort: 4841

  1. 第四次:

源端口使用12345,请求公共的stun服务器(stun.l.google.com:19302),获取到信息:

publicIp: 120.239.58.117

publicPort: 4761

根据您提供的STUN测试结果,我们可以分析出以下几点:

第一次和第二次测试:

源端口相同(12345),请求相同的STUN服务器(stun.freeswitch.org:3478)。

获取到的公共IP和端口相同(publicIp: 120.239.58.117, publicPort: 4818)。

这表明NAT设备在相同的内部源端口和目标地址条件下,会复用相同的公共端口。

第三次测试:

源端口改变(12346),请求相同的STUN服务器(stun.freeswitch.org:3478)。

获取到的公共IP相同,但端口不同(publicPort: 4841)。

这表明NAT设备为不同的内部源端口分配了不同的公共端口。

第四次测试:

源端口与第一次和第二次相同(12345),但请求不同的STUN服务器(stun.l.google.com:19302)。

获取到的公共IP相同,但端口不同(publicPort: 4761)。

这表明NAT设备为相同的内部源端口但不同的目标地址分配了不同的公共端口。

根据这些观察结果,我们可以得出以下结论:

NAT设备为每个内部源端口和目标地址的组合分配了一个唯一的公共端口。

当内部源端口或目标地址改变时,NAT设备会分配一个新的公共端口。

这种行为符合对称NAT(Symmetric NAT)的特点。

对称NAT的特点是对于每个内部主机发出的连接,都会根据源IP、源端口、目标IP和目标端口这四个值来分配一个唯一的公共IP和端口。这意味着即使内部主机的源端口相同,只要目标IP或端口不同,NAT设备也会分配不同的公共端口。

因此,根据您提供的测试结果,可以判断您所使用的NAT类型为对称NAT。

NatTypeTester工具检测也是对称NAT。

如果是端口限制型NAT,那么第二次和第三次获取到的publicPort应该是一致。

在另外一个电脑环境下测试:

  1. 第一次:

源端口使用12345,请求公共的stun服务器(比如stun.freeswitch.org:3478),获取到信息:

publicIp: 183.26.115.67

publicPort: 22989

  1. 第二次:

源端口使用12345,请求公共的stun服务器(stun.freeswitch.org:3478),获取到信息:

publicIp: 183.26.115.67

publicPort: 22989

  1. 第三次:

源端口使用12346,请求公共的stun服务器(stun.freeswitch.org:3478),获取到信息:

publicIp: 183.26.115.67

publicPort: 22878

  1. 第四次:

源端口使用12345,请求公共的stun服务器(stun.l.google.com:19302),获取到信息:

publicIp: 183.26.115.67

publicPort: 22989

其中第一次和第四次对比,变更了目标IP和端口,映射的端口无发送变化,说明其为端口限制型NAT。

通过这样的测试,我们就可以知道端口限制型NAT为什么可以点对点UDP,可以设计客户端访问STUN,获取外网IP和外网端口。然后用另外一个UDP客户端使用相同的源端口,然后让两个NAT内的主机通过消息交互获取双方的外网IP和外网端口,从而都主动发一包打通NAT,然后双方就可以通信了。

NatTypeTester工具检测也是端口限制型NAT。

限制

路由器等在进行NAT时,会存在超时问题,若客户端长时间无数据发送,NAT可能释放端口,需NAT内部主机定期发送心跳包保持映射。

在进行NAT时,如果路由器等设备的端口耗尽时,NAT可能拒绝新连接或触发超时回收旧映射。

获取NAT内部主机的映射端口

当UDP客户端通过NAT设备发送数据时,NAT会将其私有IP和源端口映射为公网IP和端口。外部设备需要知道这个公网端口才能向客户端发送数据。有下面几种方式:

  1. 使用STUN协议

STUN(Session Traversal Utilities for NAT)服务器可帮助客户端查询其公网IP和映射端口。客户端向STUN服务器发送请求,服务器返回NAT后的公网地址和端口。

  1. 手动配置端口映射

在NAT设备(如路由器)上手动设置端口映射规则,将公网端口映射到客户端的私有IP和端口。

  1. 服务器中转

若客户端A和客户端B都在NAT后,可通过服务器维护客户端的连接信息,然后进行数据转发。

  1. 利用对称NAT的穿透技术

UDP打洞:若客户端A和客户端B都在NAT后,可通过服务器协调双方交换各自的公网地址和端口,然后直接向对方发送数据。

ICE框架:结合STUN、TURN(中继服务器)和本地候选端口,动态选择最佳通信路径。

借助STUN服务器能让两个NAT内部主机成功单对单通信的解释

在双方都是地址受限锥型NAT环境中,使用STUN协议,两个NAT内部的主机通过访问STUN服务器分别拿到自己对外IP和映射端口,然后通过信息交互手段,比如消息服务器(MQTT/TCP等),让两个NAT内部的主机分别知道对方的对外IP和映射端口,然后各自发送一个UDP信息到对方的对外IP和端口,根据地址受限锥型NAT的规则,主机A现在允许主机B任意端口对本机端口发送数据,主机B现在允许主机A任意端口对本机端口发送数据,这样就是建立通信了。

各自主机如果不主动发送UDP信息到对方,根据地址受限锥型NAT的规则,那么对方无权发送数据到本机端口,会被NAT设备直接丢弃不转发。

UDP信息需要注意定时发送,避免映射端口超时被回收。

另外,仅仅依赖STUN获取地址,对于地址受限锥型NAT,也需要双方都主动向对方的公网地址发送数据包(打洞),才能大概率建立P2P连接。而对于端口受限或对称NAT,情况会更复杂,成功率更低,因此需要借助TURN等来处理NAT下的通信。

对称NAT之所以很难实现点对点UDP数据传输,是因为在一开时通过STUN拿到的对外端口,不是传输数据所对应使用的源端口,如果NAT主机内部的源端口变了,或者因为UDP传输的IP目标从STUN变成对方实际要传数据的主机,从而导致NAT分配新的对外映射端口,从而对方NAT认为该数据不合法而被丢弃。

打洞和穿透

打洞技术(NAT打洞)

打洞技术是指NAT(网络地址转换)穿透技术,主要用于解决在NAT环境下实现P2P(点对点)通信的问题。其核心原理是:

建立映射关系:NAT内的节点需要在其NAT设备上建立一条转发映射关系,这就是所谓的"在NAT上打下一个洞"。

外部节点通信:外网的节点就可以通过这个预先建立的映射关系直接与内网节点通信。

UDP打洞:这是一种在NAT环境下实现P2P通信的方法,尤其在处理子网间通信时非常有用。

打洞技术主要解决的是NAT设备阻碍了内网设备与外网设备直接通信的问题,通过在NAT上"开一个洞"来建立直接连接路径。

穿透技术(内网穿透)

内网穿透是一种网络技术,它允许将位于局域网(内网)中的计算机或设备暴露给外部网络,以便可以通过互联网访问这些内部资源。具体来说:

定义:内网穿透是指通过一种技术手段,将内部网络或者本地计算机服务暴露在公网上,从而实现公网用户可以访问内部网络或者本地计算机服务的目的。

多层NAT穿透:在存在多个网络地址转换(NAT)设备的网络环境中,实现内网设备与外部网络的通信。

应用场景:主要用于解决IPv4地址短缺问题,同时保持内网设备的安全性。

打洞与穿透的关系

打洞技术是内网穿透的一种具体实现方式,特别是针对NAT环境下的P2P通信。两者关系可以概括为:

打洞:主要解决NAT设备阻碍P2P通信的问题,通过在NAT上建立特定映射来实现直接连接。

穿透:更广泛的概念,包括打洞技术以及其他将内网服务暴露到公网的技术手段。

技术优势

提高效率:在点对点(P2P)网络中,NAT打洞可以提高数据传输效率,减少延迟。

降低成本:通过直接通信,可以避免使用昂贵的中继服务,从而降低成本。

增强安全性:内网设备并不直接暴露在公网,减少了被扫描、探测的风险

UDP打洞

UDP打洞是最常见的NAT穿透技术,需要中间服务器协助完成:

  1. 服务器协调阶段:

两个客户端(如A和B)首先通过一个位于公网的STUN/TURN服务器获取各自的公网IP和端口信息

服务器将A和B的公网信息互相告知对方

  1. 打洞操作:

客户端A向客户端B的公网地址发送UDP数据包

客户端B向客户端A的公网地址发送UDP数据包

这两个操作会在各自的NAT设备上创建映射表项,使后续直接通信成为可能

  1. 直接通信建立:

映射表项创建后,客户端A和B可以直接发送UDP数据包到对方的公网地址,NAT设备会正确转发这些数据包。

STUN/TURN服务器是怎么实现维持映射端口的?

1. STUN服务器的机制

Binding请求响应:STUN客户端向STUN服务器发送Binding请求,服务器返回响应时,会将客户端的源IP和端口(即NAT映射后的公网地址)填入XOR-MAPPED-ADDRESS属性中。这一过程本身不会直接维持映射,但通过周期性发送Binding请求,可以刷新NAT设备的映射表,防止端口超时回收。

心跳保活:STUN协议本身不强制要求心跳,但客户端可以通过定期发送Binding请求(如每30秒)来模拟心跳,保持NAT映射活跃。

2. TURN服务器的机制

中继转发:TURN服务器作为数据中继,始终与客户端保持连接。由于TURN会持续转发数据包,NAT设备会认为映射处于活跃状态,从而避免超时。

保活机制:TURN协议内置了保活机制,服务器会定期发送Keepalive消息(如空UDP包)维持连接,确保NAT映射不失效。

授权管理:TURN服务器为客户端分配中继地址后,会通过Allocate和Refresh请求管理映射有效期,客户端需在超时前续期。

3. 关键区别

STUN:轻量级工具,依赖客户端主动发送请求刷新映射,无内置保活功能。

TURN:通过持续数据转发和服务器端保活,强制维持映射,适合长连接场景。

4. 总结

STUN通过客户端周期性请求间接维持映射,而TURN通过服务器主动中继和保活机制直接管理映射。选择哪种方案取决于是否需要直接P2P通信(STUN)或必须通过中继(TURN)

STUN与TURN的应用场景与区别

STUN不适用的情况

STUN(实时传输协议)在以下情况下无法满足需求,需要使用TURN:

对称型NAT环境:当客户端位于对称型NAT后时,STUN打洞通常失败,因为每次连接目标变化都会导致NAT分配新端口。

对称NAT的映射与目标地址绑定。STUN获取的端口仅对STUN服务器有效,对其他目标无效。

即使双方交换了从STUN获取到的外网IP地址和端口,由于实际通信目标变化,NAT会生成新端口,导致连接失败。

通过STUN,对称NAT之所以不能处理A与B的视频P2P,就是因为从STUN获取到的映射端口,只有STUN服务器有效,其他IP地址,发向该映射端口的数据不会通过NAT进入到内网设备。

防火墙限制:某些严格防火墙会阻止UDP通信,STUN无法穿透这类限制。

移动网络切换:当设备频繁切换网络(如Wi-Fi到4G)时,STUN建立的映射会失效。

高延迟或低带宽场景:STUN依赖直接P2P,在质量差的网络中可能无法建立稳定连接。

需要可靠性保障:STUN不保证数据传输的可靠性,而某些应用需要100%的交付保证。

TURN的数据转发机制

是的,TURN(Traversal Using Relays around NAT)会进行数据转发,其工作原理如下:

中继传输:TURN服务器作为中介,接收客户端A的数据,再转发给客户端B,反之亦然。

协议支持:TURN支持多种协议,包括UDP、TCP、TLS等,适用于不同应用场景。

授权机制:客户端需通过Allocate请求获取中继地址,并定期通过Refresh请求续期。

保活功能:TURN服务器会自动发送Keepalive消息维持NAT映射,避免超时。

负载均衡:高级TURN实现支持负载均衡和故障转移,确保服务连续性。

总结

STUN适合大多数简单NAT环境下的P2P通信,而TURN作为后备方案,在直接连接不可行时提供可靠的中继服务。实际应用中,通常先尝试STUN,失败后回退到TURN。

java代码实验

在两个不同NAT内的主机中(皆为端口限制型NAT),运行下面客户端A和客户端B(双方代码中发送数据的目标,需更改为获取到的对方目标IP和端口)。

通过查看打印,双方都能接收到对方的数据,证明打洞成。

实际测试在同个电脑测试,在不同NAT内的主机测试应该结果也一致。

StunClientA.java:

package com.heawill;

import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.SocketException;
import java.io.IOException;
import java.util.Arrays;
import java.util.ArrayList;

class StunClientA {
    private static final String STUN_SERVER_ADDRESS = "stun.freeswitch.org"; //stun.freeswitch.org stun.l.google.com
    private static final int STUN_SERVER_PORT = 3478 ; //3478 19302
    private static final int RECEIVE_TIMEOUT = 3000;

    public enum NatType {
        BLOCKED, OPEN_INTERNET, FULL_CONE, SYMMETRIC_FIREWALL, RESTRICTED_CONE_NAT, RESTRICTED_PORT_NAT, SYMMETRIC_NAT, UNKNOWN;
    }
    public static class StunResult {
        public NatType natType;
        public String publicIp;
        public int publicPort;
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("natType:\t").append(natType).
                    append("\npublicIp:\t").append(publicIp).
                    append("\npublicPort:\t").append(publicPort);
            return sb.toString();
        }
    }

    public static StunResult makeStun(DatagramSocket socket) {
        if(!socket.isBound()) throw new RuntimeException("can not process a unbound datagram socket");
        StunResult result= null;
        int oldReceiveTimeout = 0;
        try {
            oldReceiveTimeout = socket.getSoTimeout();
            socket.setSoTimeout(RECEIVE_TIMEOUT);
        } catch (SocketException e) {
            e.printStackTrace();
        }

        //do test1
        ResponseResult stunResponse = stunTest(socket, null);
        //System.out.println(stunResponse);

        //NOTE:
        //It is no need to understand the nat type for my desire,
        //so ignore subsequent stun test

        try {
            socket.setSoTimeout(oldReceiveTimeout);
        } catch( SocketException e) {
            e.printStackTrace();
        }

        if(stunResponse != null && stunResponse.responsed) {
            result = new StunResult();
            result.natType = NatType.UNKNOWN;
            result.publicIp = stunResponse.externalIp;
            result.publicPort = stunResponse.externalPort;
        }

        return result;
    }

    private static class ResponseResult {
        public boolean responsed;
        public String externalIp;
        public int externalPort;
        public String sourceIp;
        public int sourcePort;
        public String changedIp;
        public int changedPort;
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("responsed:\t").append(responsed).
                    append("\nexternalIp:\t").append(externalIp).
                    append("\nexternalPort:\t").append(externalPort).
                    append("\nsourceIp:\t").append(sourceIp).
                    append("\nsourcePort:\t").append(sourcePort).
                    append("\nchangedIp:\t").append(changedIp).
                    append("\nchangedPort:\t").append(changedPort);
            return sb.toString();
        }
    }

    private static ResponseResult stunTest(DatagramSocket socket, byte[] msgData) {
        ResponseResult result = new ResponseResult();
        int msgLength = msgData==null? 0:msgData.length;
        MessageHeader bindRequestHeader = new MessageHeader();
        bindRequestHeader.generateTransactionID();
        bindRequestHeader.setMessageLength(msgLength);
        bindRequestHeader.setStunType(MessageHeader.StunType.BIND_REQUEST_MSG);
        byte[] headerData = bindRequestHeader.encode();
        byte[] sendData = new byte[headerData.length + msgLength];
        System.arraycopy(headerData, 0, sendData, 0, headerData.length);
        if(msgLength > 0) System.arraycopy(msgData, 0, sendData, headerData.length, msgLength);

        int tryForGettingCorrectPacketCount = 3;
        while(tryForGettingCorrectPacketCount > 0) {
            int tryForGettingDataCount = 3;
            byte[] receivedData = null;
            //System.out.println("###############################################################");
            while(receivedData == null) {
                try{
                    DatagramPacket  sendPacket = new DatagramPacket(
                            sendData,
                            sendData.length,
                            InetAddress.getByName(STUN_SERVER_ADDRESS),
                            STUN_SERVER_PORT);
                    socket.send(sendPacket);

                    DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024);
                    socket.receive(receivePacket);

                    receivedData = Arrays.copyOfRange(receivePacket.getData(), 0,  receivePacket.getLength());
                    //System.out.println("got data! -------------------------------------------------------##" + receivedData.length);
                } catch (Exception e) {
                    e.printStackTrace();
                    //System.out.println("tryForGettingDataCount is : " + tryForGettingDataCount);
                    if(tryForGettingDataCount > 0) {
                        tryForGettingDataCount--;
                    } else {
                        result.responsed = false;
                        return result;
                    }
                }
            }

            Message receivedMessage = Message.parseData(receivedData);
            if(     receivedMessage != null &&
                    receivedMessage.getStunType() == MessageHeader.StunType.BIND_RESPONSE_MSG &&
                    Arrays.equals(receivedMessage.getTransactionId(), bindRequestHeader.getTransactionId()) ) {
                MessageAttribute[] attributes = receivedMessage.getAttributes();
                //System.out.println("message data was received , attributes list below:");
                result.responsed = true;
                for(MessageAttribute attr : attributes) {
                    //System.out.println(attr.toString());
                    if(attr instanceof MappedAddress) {
                        MappedAddress ma = (MappedAddress)attr;
                        result.externalIp = ma.getAddress();
                        result.externalPort = ma.getPort();
                    } else if(attr instanceof SourceAddress) {
                        SourceAddress sa = (SourceAddress)attr;
                        result.sourceIp = sa.getAddress();
                        result.sourcePort = sa.getPort();
                    } else if(attr instanceof ChangedAddress) {
                        ChangedAddress ca = (ChangedAddress)attr;
                        result.changedIp = ca.getAddress();
                        result.changedPort = ca.getPort();
                    }
                }
                return result;
            }

            tryForGettingCorrectPacketCount--;
        }
        return null;
    }

    private static class UtilityException extends Exception {
        //private static final long serialVersionUID = 3545800974716581680L;
        UtilityException(String mesg) { super(mesg); }
    }

    private static class Message {
        private MessageHeader header;
        /*
        public MessageHeader getHeader() {
            return header;
        }
        */
        private MessageAttribute[] attributes;
        public MessageAttribute[] getAttributes() {return attributes;}


        public MessageHeader.StunType getStunType() {
            if(header == null) return null;
            return header.getStunType();
        }

        public byte[] getTransactionId() {
            if(header == null) return null;
            return header.getTransactionId();
        }


        public static Message parseData(byte[] messageData) {
            try {
                MessageHeader header = new MessageHeader();

                int msgLength = Utility.twoBytesToInteger(messageData, 2);
                if(messageData.length != msgLength +MessageHeader.HEAD_LENGTH) return null;
                header.setMessageLength(msgLength);

                int stunType = Utility.twoBytesToInteger(messageData, 0);
                MessageHeader.StunType[] types = MessageHeader.StunType.values();
                for(MessageHeader.StunType type : types) {
                    if(type.getValue() == stunType) {
                        header.setStunType(type);
                        break;
                    }
                }
                if(header.getStunType() == null) return null;

                byte[] tranId = new byte[16];
                System.arraycopy(messageData, 4, tranId, 0, 16);
                header.setTransactionId(tranId);

                //MessageHead parsing is finished
                MessageAttribute[] attributes = MessageAttribute.parseData(messageData);
                if(attributes != null && attributes.length > 0) {
                    Message msg = new Message();
                    msg.header = header;
                    msg.attributes = attributes;
                    return msg;
                }

            } catch(Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }

    /*
     *  0                   1                   2                   3
     *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |         Type                  |            Length             |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |                             Value                             ....
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * */
    private static abstract class MessageAttribute {
        private static final int MAPPED_ADDRESS = 0x0001;
        private static final int RESPONSE_ADDRESS = 0X0002;
        private static final int CHANGE_REQUEST = 0X0003;
        private static final int SOURCE_ADDRESS = 0X0004;
        private static final int CHANGED_ADDRESS = 0X0005;

        public static MessageAttribute[] parseData(byte[] messageData) {
            try {
                ArrayList<MessageAttribute> attributeList = new ArrayList<MessageAttribute>();
                int offset = MessageHeader.HEAD_LENGTH;
                //int lengthRemain =  Utility.twoBytesToInteger(messageData, 2);

                while( offset < messageData.length ) {
                    int attrType = Utility.twoBytesToInteger(messageData, offset);
                    int attrLength = Utility.twoBytesToInteger(messageData, offset + 2);

                    MessageAttribute attr = null;
                    switch(attrType) {
                        case MAPPED_ADDRESS:
                            attr = new MappedAddress();
                            break;
                        case SOURCE_ADDRESS:
                            attr = new SourceAddress();
                            break;
                        case CHANGED_ADDRESS:
                            attr = new ChangedAddress();
                            break;
                        default:
                            attr = new UnknownAttribute(attrType, attrLength);
                    }
                    if(messageData.length >= attr.getLength() + offset + 4) {
                        attr.parse(messageData, offset + 4);
                        attributeList.add(attr);
                    } else {
                        //messageData is incorrect
                        throw new Exception("messageData is incorrect");
                    }
                    offset += attrLength + 4;
                }

                int size = attributeList.size();
                if(size > 0) {
                    MessageAttribute[] attrs = new MessageAttribute[size];
                    return attributeList.toArray(attrs);
                }
            } catch (Exception e ) {
                e.printStackTrace();
            }
            return null;
        }

        public abstract int getTypeCode();
        public abstract int getLength();
        public abstract void parse(byte[] messageData, int offset);
        public abstract String toString();
    }

    /*
     * 0                   1                   2                   3
     * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     *+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *|x x x x x x x x|    Family     |           Port                |
     *+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *|                             Address                           |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     */
    private static abstract class AddressAttribute extends  MessageAttribute {

        @Override
        public int getLength() { return 8; }

        @Override
        public String toString() {
            return    new StringBuilder().
                    append("type:").append(getTypeCode()).
                    append("\tlength:").append(getLength()).
                    append("\tip:").append(mAddress).
                    append("\tport:").append(mPort).toString();
        }

        @Override
        public void parse(byte[] messageData, int offset) {
            try {
                mPort = Utility.twoBytesToInteger(messageData, offset + 2);
                StringBuilder sb = new StringBuilder(15);
                sb.append(Utility.oneByteToInteger(messageData, offset + 4)).
                        append(".").
                        append(Utility.oneByteToInteger(messageData, offset + 5)).
                        append(".").
                        append(Utility.oneByteToInteger(messageData, offset + 6)).
                        append(".").
                        append(Utility.oneByteToInteger(messageData, offset + 7));
                mAddress = sb.toString();
            } catch (Exception e ) {
                e.printStackTrace();
            }
        }

        private int mPort;
        public int getPort() { return mPort; }
        private String mAddress;
        public String getAddress() { return mAddress; }
    }

    private static class MappedAddress extends AddressAttribute {
        @Override
        public int getTypeCode() { return 0x0001; }
    }

    /* ignore all attributes present in request
    private static class ResponseAddress extends AddressAttribute {
        @Override
        public int getTypeCode() { return 0x0002; }
    }
    */

    private static class SourceAddress extends AddressAttribute {
        @Override
        public int getTypeCode() { return 0x0004; }
    }

    private static class ChangedAddress extends AddressAttribute {
        @Override
        public int getTypeCode() { return 0x0005; }
    }

    /*
     * 0                   1                   2                   3
     * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 A B 0|
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     */
    /*
    private static class ChangeRequest extends MessageAttribute {
       @Override
       public int getTypeCode() { return 0x0003; }

       @Override
       public int getLength() { return 4; }

       boolean mChangeIp;
       public boolean isChangeIp() {return mChangeIp;}
       public void setChangeIp(boolean changeIp) { mChangeIp = changeIp; }

       boolean mChangePort;
       public boolean isChangePort() {return mChangePort;}
       public void setChangePort(boolean changePort) { mChangePort = changePort; }
    }
    */

    private static class UnknownAttribute extends MessageAttribute {
        int mTypeCode;
        int mLength;
        byte[] mValue;

        public UnknownAttribute(int typeCode, int length) {
            mTypeCode = typeCode;
            mLength = length;
        }

        @Override
        public int getTypeCode() {return mTypeCode; }

        @Override
        public int getLength() { return mLength; }

        @Override
        public void parse(byte[] messageData, int offset) {
            mValue = new byte[mLength];
            System.arraycopy(messageData, offset, mValue, 0, mLength);
        }

        @Override
        public String toString() {
            return new StringBuilder().append("UnknownAttribute\t").
                    append("type:").append(mTypeCode).
                    append("\tlength:").append(mLength).toString();
        }
    }

    /*
     *  0                   1                   2                   3
     *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |      STUN Message Type        |         Message Length        |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *                          Transaction ID
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *                                                                 |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     */
    private static class MessageHeader {
        public static final int HEAD_LENGTH = 20;
        private byte[] mStunType;// = new byte[2];
        private byte[] mMessageLength;// = new byte[2];
        private byte[] mTranId;// = new byte[16];

        public enum StunType {
            BIND_REQUEST_MSG(0x0001),
            BIND_RESPONSE_MSG(0X0101),
            BIND_ERROR_RESPONSE_MSG(0x0111),
            SHARED_SECRET_REQUEST_MSG(0x0002),
            SHARED_SECRET_RESPONSE_MSG(0X0102),
            SHARED_SECRETERROR_RESPONSE_MSG(0x0112);
            private final int value;
            private StunType(int value) {this.value = value;}
            public String toString() {return super.toString() + "value:" + value;}
            public int getValue() {return value;}
        }

        public  StunType getStunType(){
            if(mStunType == null) return null;
            try {
                int intType = Utility.twoBytesToInteger(mStunType);
                StunType[] types = StunType.values();
                for(StunType type : types) {
                    if(type.getValue() == intType) {
                        return type;
                    }
                }
            } catch (UtilityException e) {
                e.printStackTrace();
            }
            return null;
        }

        public void setStunType(StunType type) {
            try {
                mStunType = Utility.integerToTwoBytes(type.getValue());
            } catch (UtilityException e) {
                e.printStackTrace();
            }
        }

        public int getMessageLength() {
            try {
                return Utility.twoBytesToInteger(mMessageLength);
            } catch(UtilityException e) {
                e.printStackTrace();
            }
            return -1;
        }

        public void setMessageLength(int length) {
            try {
                mMessageLength = Utility.integerToTwoBytes(length);
            } catch(UtilityException e) {
                e.printStackTrace();
            }
        }

        public void generateTransactionID() {
            mTranId = new byte[16];
            try  {
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 0, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 2, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 4, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 6, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 8, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 10, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 12, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 14, 2);
            } catch (UtilityException e) {
                e.printStackTrace();
            }
        }

        public byte[] getTransactionId() {
            return mTranId;
        }

        public boolean setTransactionId(byte[] tranId) {
            if(tranId.length != 16) return false;
            mTranId = tranId;
            return true;
        }

        public byte[] encode() {
            if(mStunType==null || mStunType.length!=2) throw new RuntimeException("Stuntype is not correct");
            if(mMessageLength ==null || mMessageLength.length!=2) throw new RuntimeException("mMessageLength is not correct");
            if(mTranId==null || mTranId.length!=16) throw new RuntimeException(" mTranId is not correct");

            byte[] result = new byte[HEAD_LENGTH];
            System.arraycopy(mStunType, 0, result, 0, 2);
            System.arraycopy(mMessageLength, 0, result, 2, 2);
            System.arraycopy(mTranId, 0, result, 4, 16);
            return result;
        }
    }

    private static class Utility {
        public static final byte integerToOneByte(int value) throws UtilityException {
            if ((value > Math.pow(2,15)) || (value < 0)) {
                throw new UtilityException("Integer value " + value + " is larger than 2^15");
            }
            return (byte)(value & 0xFF);
        }

        public static final byte[] integerToTwoBytes(int value) throws UtilityException {
            byte[] result = new byte[2];
            if ((value > Math.pow(2,31)) || (value < 0)) {
                throw new UtilityException("Integer value " + value + " is larger than 2^31");
            }
            result[0] = (byte)((value >>> 8) & 0xFF);
            result[1] = (byte)(value & 0xFF);
            return result;
        }

        public static final byte[] integerToFourBytes(int value) throws UtilityException {
            byte[] result = new byte[4];
            if ((value > Math.pow(2,63)) || (value < 0)) {
                throw new UtilityException("Integer value " + value + " is larger than 2^63");
            }
            result[0] = (byte)((value >>> 24) & 0xFF);
            result[1] = (byte)((value >>> 16) & 0xFF);
            result[2] = (byte)((value >>> 8) & 0xFF);
            result[3] = (byte)(value & 0xFF);
            return result;
        }

        public static final int oneByteToInteger(byte value) throws UtilityException {
            byte[] val  = new byte[1];
            val[0] = value;
            return oneByteToInteger(val, 0);
        }

        public static final int oneByteToInteger(byte[] value, int offset) throws UtilityException {
            if (value.length < 1+offset) {
                throw new UtilityException("Byte array too short!");
            }

            return (int)value[offset] & 0xFF;
        }

        public static final int twoBytesToInteger(byte[] value) throws UtilityException {
            return twoBytesToInteger(value, 0);
        }

        public static final int twoBytesToInteger(byte[] value, int offset) throws UtilityException {
            if (value.length < 2+offset) {
                throw new UtilityException("Byte array too short!");
            }
            int temp0 = value[offset] & 0xFF;
            int temp1 = value[1+offset] & 0xFF;
            return ((temp0 << 8) + temp1);
        }

        public static final long fourBytesToLong(byte[] value) throws UtilityException {
            return fourBytesToLong(value, 0);
        }

        public static final long fourBytesToLong(byte[] value, int offset) throws UtilityException {
            if (value.length < 4+offset) {
                throw new UtilityException("Byte array too short!");
            }
            int temp0 = value[offset] & 0xFF;
            int temp1 = value[1 + offset] & 0xFF;
            int temp2 = value[2 + offset] & 0xFF;
            int temp3 = value[3 + offset] & 0xFF;
            return (((long)temp0 << 24) + (temp1 << 16) + (temp2 << 8) + temp3);
        }
    }

    public static void send(DatagramSocket socket,String message) throws IOException {
        // 将字符串消息转换为字节
        byte[] msg = message.getBytes();
        // 创建数据报包,包含要发送的数据、数据长度、服务器地址和端口
        DatagramPacket packet = new DatagramPacket(msg, msg.length, InetAddress.getByName("120.239.58.117"), 4810);
        // 发送数据报包
        socket.send(packet);
    }

    private static byte[] buf = new byte[256];
    public static void receive(DatagramSocket socket) throws IOException {
        // 创建数据报包,用于接收数据
        DatagramPacket packet = new DatagramPacket(buf, buf.length);
        // 接收数据报包
        socket.receive(packet);
        // 将接收到的数据转换为字符串
        String received = new String(packet.getData(), 0, packet.getLength());
        // 打印接收到的数据
        System.out.println("Received: " + received);
    }

    public static void main(String[] args) {
        DatagramSocket socket = null;
        try{
            socket = new DatagramSocket(12345);
            StunResult stunResult = makeStun(socket);
            System.out.println(stunResult);

            InetAddress addressB = InetAddress.getByName("183.26.115.67");

            // 启动发送线程
            new Thread(new Sender(socket, addressB, 21621)).start();

            // 启动接收线程
            new Thread(new Receiver(socket)).start();
        } catch (Exception e ) {
            e.printStackTrace();
        }
    }

    // 发送线程类
    static class Sender implements Runnable {
        private DatagramSocket socket;
        private InetAddress address;
        private int port;

        public Sender(DatagramSocket socket, InetAddress address, int port) {
            this.socket = socket;
            this.address = address;
            this.port = port;
        }

        @Override
        public void run() {
            try {
                byte[] buffer = "Hello By A".getBytes();
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, port);
                while (true) {
                    socket.send(packet);
                    System.out.println("Client A sent: Hello");
                    Thread.sleep(1000);
                }
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // 接收线程类
    static class Receiver implements Runnable {
        private DatagramSocket socket;

        public Receiver(DatagramSocket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try {
                byte[] buffer = new byte[1024];
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                while (true) {
                    socket.receive(packet);
                    String message = new String(packet.getData(), 0, packet.getLength());
                    System.out.println("Client A received: " + message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

StunClientB.java:

package org.example;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;

class StunClientB {
    private static final String STUN_SERVER_ADDRESS = "stun.freeswitch.org"; //stun.freeswitch.org stun.l.google.com
    private static final int STUN_SERVER_PORT = 3478 ; //3478 19302
    private static final int RECEIVE_TIMEOUT = 3000;

    public enum NatType {
        BLOCKED, OPEN_INTERNET, FULL_CONE, SYMMETRIC_FIREWALL, RESTRICTED_CONE_NAT, RESTRICTED_PORT_NAT, SYMMETRIC_NAT, UNKNOWN;
    }
    public static class StunResult {
        public NatType natType;
        public String publicIp;
        public int publicPort;
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("natType:\t").append(natType).
                    append("\npublicIp:\t").append(publicIp).
                    append("\npublicPort:\t").append(publicPort);
            return sb.toString();
        }
    }

    public static StunResult makeStun(DatagramSocket socket) {
        if(!socket.isBound()) throw new RuntimeException("can not process a unbound datagram socket");
        StunResult result= null;
        int oldReceiveTimeout = 0;
        try {
            oldReceiveTimeout = socket.getSoTimeout();
            socket.setSoTimeout(RECEIVE_TIMEOUT);
        } catch (SocketException e) {
            e.printStackTrace();
        }

        //do test1
        ResponseResult stunResponse = stunTest(socket, null);
        //System.out.println(stunResponse);

        //NOTE:
        //It is no need to understand the nat type for my desire,
        //so ignore subsequent stun test

        try {
            socket.setSoTimeout(oldReceiveTimeout);
        } catch( SocketException e) {
            e.printStackTrace();
        }

        if(stunResponse != null && stunResponse.responsed) {
            result = new StunResult();
            result.natType = NatType.UNKNOWN;
            result.publicIp = stunResponse.externalIp;
            result.publicPort = stunResponse.externalPort;
        }

        return result;
    }

    private static class ResponseResult {
        public boolean responsed;
        public String externalIp;
        public int externalPort;
        public String sourceIp;
        public int sourcePort;
        public String changedIp;
        public int changedPort;
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("responsed:\t").append(responsed).
                    append("\nexternalIp:\t").append(externalIp).
                    append("\nexternalPort:\t").append(externalPort).
                    append("\nsourceIp:\t").append(sourceIp).
                    append("\nsourcePort:\t").append(sourcePort).
                    append("\nchangedIp:\t").append(changedIp).
                    append("\nchangedPort:\t").append(changedPort);
            return sb.toString();
        }
    }

    private static ResponseResult stunTest(DatagramSocket socket, byte[] msgData) {
        ResponseResult result = new ResponseResult();
        int msgLength = msgData==null? 0:msgData.length;
        MessageHeader bindRequestHeader = new MessageHeader();
        bindRequestHeader.generateTransactionID();
        bindRequestHeader.setMessageLength(msgLength);
        bindRequestHeader.setStunType(MessageHeader.StunType.BIND_REQUEST_MSG);
        byte[] headerData = bindRequestHeader.encode();
        byte[] sendData = new byte[headerData.length + msgLength];
        System.arraycopy(headerData, 0, sendData, 0, headerData.length);
        if(msgLength > 0) System.arraycopy(msgData, 0, sendData, headerData.length, msgLength);

        int tryForGettingCorrectPacketCount = 3;
        while(tryForGettingCorrectPacketCount > 0) {
            int tryForGettingDataCount = 3;
            byte[] receivedData = null;
            //System.out.println("###############################################################");
            while(receivedData == null) {
                try{
                    DatagramPacket  sendPacket = new DatagramPacket(
                            sendData,
                            sendData.length,
                            InetAddress.getByName(STUN_SERVER_ADDRESS),
                            STUN_SERVER_PORT);
                    socket.send(sendPacket);

                    DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024);
                    socket.receive(receivePacket);

                    receivedData = Arrays.copyOfRange(receivePacket.getData(), 0,  receivePacket.getLength());
                    //System.out.println("got data! -------------------------------------------------------##" + receivedData.length);
                } catch (Exception e) {
                    e.printStackTrace();
                    //System.out.println("tryForGettingDataCount is : " + tryForGettingDataCount);
                    if(tryForGettingDataCount > 0) {
                        tryForGettingDataCount--;
                    } else {
                        result.responsed = false;
                        return result;
                    }
                }
            }

            Message receivedMessage = Message.parseData(receivedData);
            if(     receivedMessage != null &&
                    receivedMessage.getStunType() == MessageHeader.StunType.BIND_RESPONSE_MSG &&
                    Arrays.equals(receivedMessage.getTransactionId(), bindRequestHeader.getTransactionId()) ) {
                MessageAttribute[] attributes = receivedMessage.getAttributes();
                //System.out.println("message data was received , attributes list below:");
                result.responsed = true;
                for(MessageAttribute attr : attributes) {
                    //System.out.println(attr.toString());
                    if(attr instanceof MappedAddress) {
                        MappedAddress ma = (MappedAddress)attr;
                        result.externalIp = ma.getAddress();
                        result.externalPort = ma.getPort();
                    } else if(attr instanceof SourceAddress) {
                        SourceAddress sa = (SourceAddress)attr;
                        result.sourceIp = sa.getAddress();
                        result.sourcePort = sa.getPort();
                    } else if(attr instanceof ChangedAddress) {
                        ChangedAddress ca = (ChangedAddress)attr;
                        result.changedIp = ca.getAddress();
                        result.changedPort = ca.getPort();
                    }
                }
                return result;
            }

            tryForGettingCorrectPacketCount--;
        }
        return null;
    }

    private static class UtilityException extends Exception {
        //private static final long serialVersionUID = 3545800974716581680L;
        UtilityException(String mesg) { super(mesg); }
    }

    private static class Message {
        private MessageHeader header;
        /*
        public MessageHeader getHeader() {
            return header;
        }
        */
        private MessageAttribute[] attributes;
        public MessageAttribute[] getAttributes() {return attributes;}


        public MessageHeader.StunType getStunType() {
            if(header == null) return null;
            return header.getStunType();
        }

        public byte[] getTransactionId() {
            if(header == null) return null;
            return header.getTransactionId();
        }


        public static Message parseData(byte[] messageData) {
            try {
                MessageHeader header = new MessageHeader();

                int msgLength = Utility.twoBytesToInteger(messageData, 2);
                if(messageData.length != msgLength + MessageHeader.HEAD_LENGTH) return null;
                header.setMessageLength(msgLength);

                int stunType = Utility.twoBytesToInteger(messageData, 0);
                MessageHeader.StunType[] types = MessageHeader.StunType.values();
                for(MessageHeader.StunType type : types) {
                    if(type.getValue() == stunType) {
                        header.setStunType(type);
                        break;
                    }
                }
                if(header.getStunType() == null) return null;

                byte[] tranId = new byte[16];
                System.arraycopy(messageData, 4, tranId, 0, 16);
                header.setTransactionId(tranId);

                //MessageHead parsing is finished
                MessageAttribute[] attributes = MessageAttribute.parseData(messageData);
                if(attributes != null && attributes.length > 0) {
                    Message msg = new Message();
                    msg.header = header;
                    msg.attributes = attributes;
                    return msg;
                }

            } catch(Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }

    /*
     *  0                   1                   2                   3
     *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |         Type                  |            Length             |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |                             Value                             ....
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * */
    private static abstract class MessageAttribute {
        private static final int MAPPED_ADDRESS = 0x0001;
        private static final int RESPONSE_ADDRESS = 0X0002;
        private static final int CHANGE_REQUEST = 0X0003;
        private static final int SOURCE_ADDRESS = 0X0004;
        private static final int CHANGED_ADDRESS = 0X0005;

        public static MessageAttribute[] parseData(byte[] messageData) {
            try {
                ArrayList<MessageAttribute> attributeList = new ArrayList<MessageAttribute>();
                int offset = MessageHeader.HEAD_LENGTH;
                //int lengthRemain =  Utility.twoBytesToInteger(messageData, 2);

                while( offset < messageData.length ) {
                    int attrType = Utility.twoBytesToInteger(messageData, offset);
                    int attrLength = Utility.twoBytesToInteger(messageData, offset + 2);

                    MessageAttribute attr = null;
                    switch(attrType) {
                        case MAPPED_ADDRESS:
                            attr = new MappedAddress();
                            break;
                        case SOURCE_ADDRESS:
                            attr = new SourceAddress();
                            break;
                        case CHANGED_ADDRESS:
                            attr = new ChangedAddress();
                            break;
                        default:
                            attr = new UnknownAttribute(attrType, attrLength);
                    }
                    if(messageData.length >= attr.getLength() + offset + 4) {
                        attr.parse(messageData, offset + 4);
                        attributeList.add(attr);
                    } else {
                        //messageData is incorrect
                        throw new Exception("messageData is incorrect");
                    }
                    offset += attrLength + 4;
                }

                int size = attributeList.size();
                if(size > 0) {
                    MessageAttribute[] attrs = new MessageAttribute[size];
                    return attributeList.toArray(attrs);
                }
            } catch (Exception e ) {
                e.printStackTrace();
            }
            return null;
        }

        public abstract int getTypeCode();
        public abstract int getLength();
        public abstract void parse(byte[] messageData, int offset);
        public abstract String toString();
    }

    /*
     * 0                   1                   2                   3
     * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     *+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *|x x x x x x x x|    Family     |           Port                |
     *+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *|                             Address                           |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     */
    private static abstract class AddressAttribute extends  MessageAttribute {

        @Override
        public int getLength() { return 8; }

        @Override
        public String toString() {
            return    new StringBuilder().
                    append("type:").append(getTypeCode()).
                    append("\tlength:").append(getLength()).
                    append("\tip:").append(mAddress).
                    append("\tport:").append(mPort).toString();
        }

        @Override
        public void parse(byte[] messageData, int offset) {
            try {
                mPort = Utility.twoBytesToInteger(messageData, offset + 2);
                StringBuilder sb = new StringBuilder(15);
                sb.append(Utility.oneByteToInteger(messageData, offset + 4)).
                        append(".").
                        append(Utility.oneByteToInteger(messageData, offset + 5)).
                        append(".").
                        append(Utility.oneByteToInteger(messageData, offset + 6)).
                        append(".").
                        append(Utility.oneByteToInteger(messageData, offset + 7));
                mAddress = sb.toString();
            } catch (Exception e ) {
                e.printStackTrace();
            }
        }

        private int mPort;
        public int getPort() { return mPort; }
        private String mAddress;
        public String getAddress() { return mAddress; }
    }

    private static class MappedAddress extends AddressAttribute {
        @Override
        public int getTypeCode() { return 0x0001; }
    }

    /* ignore all attributes present in request
    private static class ResponseAddress extends AddressAttribute {
        @Override
        public int getTypeCode() { return 0x0002; }
    }
    */

    private static class SourceAddress extends AddressAttribute {
        @Override
        public int getTypeCode() { return 0x0004; }
    }

    private static class ChangedAddress extends AddressAttribute {
        @Override
        public int getTypeCode() { return 0x0005; }
    }

    /*
     * 0                   1                   2                   3
     * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 A B 0|
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     */
    /*
    private static class ChangeRequest extends MessageAttribute {
       @Override
       public int getTypeCode() { return 0x0003; }

       @Override
       public int getLength() { return 4; }

       boolean mChangeIp;
       public boolean isChangeIp() {return mChangeIp;}
       public void setChangeIp(boolean changeIp) { mChangeIp = changeIp; }

       boolean mChangePort;
       public boolean isChangePort() {return mChangePort;}
       public void setChangePort(boolean changePort) { mChangePort = changePort; }
    }
    */

    private static class UnknownAttribute extends MessageAttribute {
        int mTypeCode;
        int mLength;
        byte[] mValue;

        public UnknownAttribute(int typeCode, int length) {
            mTypeCode = typeCode;
            mLength = length;
        }

        @Override
        public int getTypeCode() {return mTypeCode; }

        @Override
        public int getLength() { return mLength; }

        @Override
        public void parse(byte[] messageData, int offset) {
            mValue = new byte[mLength];
            System.arraycopy(messageData, offset, mValue, 0, mLength);
        }

        @Override
        public String toString() {
            return new StringBuilder().append("UnknownAttribute\t").
                    append("type:").append(mTypeCode).
                    append("\tlength:").append(mLength).toString();
        }
    }

    /*
     *  0                   1                   2                   3
     *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |      STUN Message Type        |         Message Length        |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     * |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *                          Transaction ID
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *                                                                 |
     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     */
    private static class MessageHeader {
        public static final int HEAD_LENGTH = 20;
        private byte[] mStunType;// = new byte[2];
        private byte[] mMessageLength;// = new byte[2];
        private byte[] mTranId;// = new byte[16];

        public enum StunType {
            BIND_REQUEST_MSG(0x0001),
            BIND_RESPONSE_MSG(0X0101),
            BIND_ERROR_RESPONSE_MSG(0x0111),
            SHARED_SECRET_REQUEST_MSG(0x0002),
            SHARED_SECRET_RESPONSE_MSG(0X0102),
            SHARED_SECRETERROR_RESPONSE_MSG(0x0112);
            private final int value;
            private StunType(int value) {this.value = value;}
            public String toString() {return super.toString() + "value:" + value;}
            public int getValue() {return value;}
        }

        public  StunType getStunType(){
            if(mStunType == null) return null;
            try {
                int intType = Utility.twoBytesToInteger(mStunType);
                StunType[] types = StunType.values();
                for(StunType type : types) {
                    if(type.getValue() == intType) {
                        return type;
                    }
                }
            } catch (UtilityException e) {
                e.printStackTrace();
            }
            return null;
        }

        public void setStunType(StunType type) {
            try {
                mStunType = Utility.integerToTwoBytes(type.getValue());
            } catch (UtilityException e) {
                e.printStackTrace();
            }
        }

        public int getMessageLength() {
            try {
                return Utility.twoBytesToInteger(mMessageLength);
            } catch(UtilityException e) {
                e.printStackTrace();
            }
            return -1;
        }

        public void setMessageLength(int length) {
            try {
                mMessageLength = Utility.integerToTwoBytes(length);
            } catch(UtilityException e) {
                e.printStackTrace();
            }
        }

        public void generateTransactionID() {
            mTranId = new byte[16];
            try  {
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 0, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 2, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 4, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 6, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 8, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 10, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 12, 2);
                System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, mTranId, 14, 2);
            } catch (UtilityException e) {
                e.printStackTrace();
            }
        }

        public byte[] getTransactionId() {
            return mTranId;
        }

        public boolean setTransactionId(byte[] tranId) {
            if(tranId.length != 16) return false;
            mTranId = tranId;
            return true;
        }

        public byte[] encode() {
            if(mStunType==null || mStunType.length!=2) throw new RuntimeException("Stuntype is not correct");
            if(mMessageLength ==null || mMessageLength.length!=2) throw new RuntimeException("mMessageLength is not correct");
            if(mTranId==null || mTranId.length!=16) throw new RuntimeException(" mTranId is not correct");

            byte[] result = new byte[HEAD_LENGTH];
            System.arraycopy(mStunType, 0, result, 0, 2);
            System.arraycopy(mMessageLength, 0, result, 2, 2);
            System.arraycopy(mTranId, 0, result, 4, 16);
            return result;
        }
    }

    private static class Utility {
        public static final byte integerToOneByte(int value) throws UtilityException {
            if ((value > Math.pow(2,15)) || (value < 0)) {
                throw new UtilityException("Integer value " + value + " is larger than 2^15");
            }
            return (byte)(value & 0xFF);
        }

        public static final byte[] integerToTwoBytes(int value) throws UtilityException {
            byte[] result = new byte[2];
            if ((value > Math.pow(2,31)) || (value < 0)) {
                throw new UtilityException("Integer value " + value + " is larger than 2^31");
            }
            result[0] = (byte)((value >>> 8) & 0xFF);
            result[1] = (byte)(value & 0xFF);
            return result;
        }

        public static final byte[] integerToFourBytes(int value) throws UtilityException {
            byte[] result = new byte[4];
            if ((value > Math.pow(2,63)) || (value < 0)) {
                throw new UtilityException("Integer value " + value + " is larger than 2^63");
            }
            result[0] = (byte)((value >>> 24) & 0xFF);
            result[1] = (byte)((value >>> 16) & 0xFF);
            result[2] = (byte)((value >>> 8) & 0xFF);
            result[3] = (byte)(value & 0xFF);
            return result;
        }

        public static final int oneByteToInteger(byte value) throws UtilityException {
            byte[] val  = new byte[1];
            val[0] = value;
            return oneByteToInteger(val, 0);
        }

        public static final int oneByteToInteger(byte[] value, int offset) throws UtilityException {
            if (value.length < 1+offset) {
                throw new UtilityException("Byte array too short!");
            }

            return (int)value[offset] & 0xFF;
        }

        public static final int twoBytesToInteger(byte[] value) throws UtilityException {
            return twoBytesToInteger(value, 0);
        }

        public static final int twoBytesToInteger(byte[] value, int offset) throws UtilityException {
            if (value.length < 2+offset) {
                throw new UtilityException("Byte array too short!");
            }
            int temp0 = value[offset] & 0xFF;
            int temp1 = value[1+offset] & 0xFF;
            return ((temp0 << 8) + temp1);
        }

        public static final long fourBytesToLong(byte[] value) throws UtilityException {
            return fourBytesToLong(value, 0);
        }

        public static final long fourBytesToLong(byte[] value, int offset) throws UtilityException {
            if (value.length < 4+offset) {
                throw new UtilityException("Byte array too short!");
            }
            int temp0 = value[offset] & 0xFF;
            int temp1 = value[1 + offset] & 0xFF;
            int temp2 = value[2 + offset] & 0xFF;
            int temp3 = value[3 + offset] & 0xFF;
            return (((long)temp0 << 24) + (temp1 << 16) + (temp2 << 8) + temp3);
        }
    }

    public static void send(DatagramSocket socket,String message) throws IOException {
        // 将字符串消息转换为字节
        byte[] msg = message.getBytes();
        // 创建数据报包,包含要发送的数据、数据长度、服务器地址和端口
        DatagramPacket packet = new DatagramPacket(msg, msg.length, InetAddress.getByName("120.239.58.117"), 4810);
        // 发送数据报包
        socket.send(packet);
    }

    private static byte[] buf = new byte[256];
    public static void receive(DatagramSocket socket) throws IOException {
        // 创建数据报包,用于接收数据
        DatagramPacket packet = new DatagramPacket(buf, buf.length);
        // 接收数据报包
        socket.receive(packet);
        // 将接收到的数据转换为字符串
        String received = new String(packet.getData(), 0, packet.getLength());
        // 打印接收到的数据
        System.out.println("Received: " + received);
    }

    public static void main(String[] args) {
        DatagramSocket socket = null;
        try{
            socket = new DatagramSocket(12346);
            StunResult stunResult = makeStun(socket);
            System.out.println(stunResult);

            InetAddress addressB = InetAddress.getByName("183.26.115.67");

            // 启动发送线程
            new Thread(new Sender(socket, addressB, 25477)).start();

            // 启动接收线程
            new Thread(new Receiver(socket)).start();
        } catch (Exception e ) {
            e.printStackTrace();
        }
    }

    // 发送线程类
    static class Sender implements Runnable {
        private DatagramSocket socket;
        private InetAddress address;
        private int port;

        public Sender(DatagramSocket socket, InetAddress address, int port) {
            this.socket = socket;
            this.address = address;
            this.port = port;
        }

        @Override
        public void run() {
            try {
                byte[] buffer = "Hello By B".getBytes();
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, port);
                while (true) {
                    socket.send(packet);
                    System.out.println("Client B sent: Hello");
                    Thread.sleep(1000);
                }
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // 接收线程类
    static class Receiver implements Runnable {
        private DatagramSocket socket;

        public Receiver(DatagramSocket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try {
                byte[] buffer = new byte[1024];
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                while (true) {
                    socket.receive(packet);
                    String message = new String(packet.getData(), 0, packet.getLength());
                    System.out.println("Client B received: " + message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}


Comment