一、NAT网络原理

最近要做UDP通讯,网上一查资料还真不多,比如服务器如何保存客户端的地址,因为UDP不是长连接,内网穿透可能会有问题,就是数据发到了服务器上,服务器发不回去了的这种可能,这就需要具体来讲解一下现在的网络结构。

目前主流的网络IP地址还是IPV4,过度到IPV6是个非常漫长的过程,所以目前“节约”IP地址最常见的方式:NAT,NAT大家肯定不陌生,在家里、公司上网,一般都是通过路由器的,这么做的好处有如下几点:

1、节约IP地址:只需要给路由器分配公网IP,路由器内部的设备用内网网段的地址,不同路由器内网网段的地址能复用。 比如一个家用路由器,一般最多能支持几十个来个设备同时连接路由器,都通过路由器上,网运营商只需要给路由器分配一个公网IP即可。

2、内网的设备并不直接暴露在公网,只能让路由器对外直接通信,在一定程度上保障了内网设备收不到外界的各种扫描、探测类的数据包,保障内网安全。  路由器收到外界这些主动连接的数据包会直接丢弃(除非网关设置了反向代理,比如服务器前端的网关一般会转发80端口的数据包),这也是客户端之间通信需要“穿透,打洞”的根本原因!  

3、对于部分网关而言:只要内网发送数据的IP不变,那么自己对外映射转发的端口就不变,比如内部192.168.0.10对外发数据,网关的公网地址是222.111.111.111,此时随机选一个端口比如333,那么对外数据包的源信息就是222.111.111.111:333; 后续网关凡是收到目的是222.111.111.111:333的数据包,不管这个包来自哪(因为是UDP协议,源和目的没有建立连接),都认为是响应192.168.0.10这个节点的,一律转发到该节点,这就是所谓的完整的锥形NAT,这种方式NAT并不验证源IP和端口就直接转发到内网主机(包括内网主机并未访问过的IP和端口),不安全。 下面介绍的UDP打洞必须依赖这个特性!  

  UDP打洞实现NAT穿越是一种在处于使用了NAT的私有网络中的Internet主机之间建立双向UDP连接的方法。由于NAT的行为是非标准化的,因此它并不能应用于所有类型的NAT。

  其基本思想是这样的:让位于NAT后的两台主机都与处于公共地址空间的服务器相连,然后,一旦NAT设备建立好UDP状态信息就转为直接通信,这项技术需要一个圆锥型NAT设备才能够正常工作。对称型NAT不能使用这项技术。

UDP打洞的过程大体上如下:

主机A和主机B都是通过NAT设备访问互联网,主机S位于互联网上。

1. A和B都与S之间通过UDP进行心跳连接

2. A通知S,要与B通信

3. S把B的公网IP、port告诉A,同时把A的公网IP、port告诉B

4. A向B的公网IP、port发送数据(这个数据包应该会被丢弃,但是打开了B回来的窗户)

5. B向A的公网IP、port发送数据(这个数据包就会被A接受,之后A和B就建立起了连接)

二、网络类型(四种)

1、Full cone NAT(全锥形NAT)


所有从同一个内网的(IP,端口)发送出来的请求都会被映射到同一个外网(IP,端口),且任何一个外网主机都可以通过访问映射后的公网地址,实现访问位于内网的主机设备功能。

外网主机可以主动连接内网主机。


该类型NAT只与源IP和源端口相关,只要(源IP,源端口)相同则可以通过映射后的(公网IP,端口)访问任意网站,因此称之为全锥形NAT. 有点类似于静态NAT 

2、Restricted Cone NAT(地址受限锥形NAT)


所有从同一个内网的(IP,端口)发送出来的请求都会被映射到通过一个外网(IP,端口),但与全锥形不同点在于:生成的映射表项与目的IP有关,只有符合要求的目的IP(要访问的公网服务器IP)才可以通讯。此NAT还有个特点:不能主动连接内网中的主机地址,连接必须由内网地址发起。

限制比全锥形NAT多了:IP地址限制。


此类型NAT除了与源IP和源端口相关外,还与目的IP有关,只有内网主机主动连接的公网IP才可以与内网中的主机通讯。

3、Port Restricted Cone NAT(端口受限锥形NAT)


所有从同一个内网的(IP,端口)发送出来的请求都会被映射到通过一个外网(IP,端口),但是在地址受限锥形NAT基础上增加了端口的限制。

地址受限锥形NAT时,只有内网主机主动连接的公网主机才可与之进行通讯,而不用担心端口号是否与请求的端口相同。


但是端口受限锥形NAT除了IP限制外,增加了端口限制。意思是说:除了之前主动连接了主机的(IP,port1,)可以通讯,其他的(IP,port2)等都不可以与之通讯。此NAT映射与报文的三元组绑定。

4、Symetric NAT(对称NAT)


所有从同一个内网(IP,端口)发送到同一个目的IP和端口的请求都会被映射到同一个IP和端口。换句话说(SIP,Sport, DIP, Dport)只要有一个发生变化都会使用不同的映射条目,即此NAT映射与报文四元组绑定。


前三种nat有一个共同点:只要内网中的(IP,端口)相同的请求就会被NAT映射到同一个外网(IP,port)。 

NAT类型

说明

全锥形NAT

任何公网主机都可与之通讯。双方都可以主动发起

地址受限锥形NAT

只有内网主动连接的公网主机可与之通讯,必须内网主机发起。且此公网主机可通过任意端口与内网主机通讯。

端口受限锥形NAT

只有内网主动连接的公网主机的连接可与之通讯,必须内网主机发起。且此公网只能通过固定的端口与之进行通讯。

 最后一种对称NAT: 一个连接一条映射(网络上的连接通过四元组表示:[SIP,DIP,SPORT,DPORT] )

NAT类型

说明

对称NAT

根据四元组创建NAT映射,四元组中的任何一项发生变化均导致NAT映射的更换。此形状双方一对一映射,因此被称之为对称NAT

三、.Net Sock Udp代码

服务器会转发所有客户端发送的消息,执行效果

.Net UDP通讯示例_服务器

服务端代码

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace UDPServer
{
class Program
{
static void Main(string[] args)
{
string[] endPoints = new string[1000];
int endPointsCount = 0;
int recv;
byte[] data = new byte[1024];

//构建TCP 服务器

//得到本机IP,设置TCP端口号
IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 8889);
Socket newsock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

//绑定网络地址
newsock.Bind(ipep);

Console.WriteLine("This is a Server, host name is {0}", Dns.GetHostName());

//等待客户机连接
Console.WriteLine("Waiting for a client");


//客户机连接成功后,发送欢迎信息
string welcome = "Welcome ! ";

//字符串与字节数组相互转换
data = Encoding.ASCII.GetBytes(welcome);

//发送信息
//newsock.SendTo(data, data.Length, SocketFlags.None, Remote);
while (true)
{
data = new byte[1024];
//得到客户机IP
IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
EndPoint remote = (EndPoint)sender;
recv = newsock.ReceiveFrom(data, ref remote);
bool isSeek = false;
string ipport = remote.ToString();
Console.WriteLine(ipport);
for (int i = 0;i< endPointsCount; i++)
{
if (!string.IsNullOrEmpty(endPoints[i]) && (endPoints[i].Equals(ipport)))
{
isSeek = true;
}
}
if(isSeek == false)
{
if (endPointsCount < 999)
{
endPoints[endPointsCount++] = ipport;
//newsock.SendTo(data, data.Length, SocketFlags.None, remote);
Console.WriteLine("添加到列表中");
}
else
{
Console.WriteLine("设备太多了");
}
}
else
{
Console.WriteLine("已经在列表中");
}
Console.WriteLine(Encoding.ASCII.GetString(data, 0, recv));
for (int i = 0; i < endPointsCount; i++)
{
System.Net.IPAddress IPadr = System.Net.IPAddress.Parse(endPoints[i].Split(':')[0]);
System.Net.IPEndPoint EndPoint = new System.Net.IPEndPoint(IPadr, int.Parse(endPoints[i].Split(':')[1]));
newsock.SendTo(data, recv, SocketFlags.None, EndPoint);
}
}
}

}
}

客户端代码

using System.Net;
using System.Net.Sockets;
using System.Text;

namespace UDPClient
{
class Program
{
public static Socket _server = null;
public static bool _isClose = false;
static void Main(string[] args)
{
try
{
byte[] data = new byte[1024];
string input, stringData;

//构建TCP 服务器

Console.WriteLine("This is a Client, host name is {0}", Dns.GetHostName());

//设置服务IP,设置TCP端口号
IPEndPoint ipep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8889);

//定义网络类型,数据连接类型和网络协议UDP
Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
_server = server;
string welcome = "Hello! ";
data = Encoding.ASCII.GetBytes(welcome);
server.SendTo(data, data.Length, SocketFlags.None, ipep);

data = new byte[1024];
//对于不存在的IP地址,加入此行代码后,可以在指定时间内解除阻塞模式限制
//server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 100);

//创建一个线程接收远程主机发来的信息
Thread myThread = new Thread(ReceiveData);
myThread.IsBackground = true;
myThread.Start();
while (true)
{
input = Console.ReadLine();
if (input == "exit")
{
_isClose = true;
break;
}
server.SendTo(Encoding.ASCII.GetBytes(input), ipep);
}
Console.WriteLine("Stopping Client.");
server.Close();
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
}

public static void ReceiveData()
{
try
{
IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
EndPoint Remote = (EndPoint)sender;
byte[] data = new byte[1024];
while (true)
{
try
{
if (_isClose == true)
{
break;
}
int recv = _server.ReceiveFrom(data, ref Remote);
Console.WriteLine("Message received from {0}: ", Remote.ToString());
Console.WriteLine(Encoding.ASCII.GetString(data, 0, recv));
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
}
}