面向对象封装UDP穿透NAT技术实现

时间:2022-08-13 09:04:33

面向对象封装UDP穿透NAT技术实现

摘 要

本文介绍了NAT技术的主要特性以及其4种分类,并对每一种分类的穿透策略进行了分析。针对目前主流的锥NAT,本文使用面向对象的方式封装实现了UDP 穿透NAT,并用C++代码进行了具体实现。

【关键词】锥NAT UDP穿透 面向对象 C++

1 NAT概述

NAT(Network Address Translation,网络地址转换),提供了私有网络内部主机与Internet外网主机连接通信的方法。简单说,就是若干台主机组成的内部专有网络(局域网)与Internet外部主机建立联系的方法。装有NAT软件的路由器被称为NAT路由器,它将外部全球唯一的IP地址转换成多个内部私有IP地址。在合法网络IP地址日益稀少的今天,使更多私有主机访问Internet成为可能。

2 NAT特性

(1)NAT设备(NAT,一般被称为中间件)将内部私有网络与外部网络隔离开来,并且让内部主机独立使用一个IP地址,同时为每个连接动态翻译这些地址。除此之外,当内部主机和外部主机通信时,NAT设备必须要为它分配唯一的端口并连接到同样的地址和端口上(目标主机)。

(2)NAT只允许从内部(私有网络)发起的连接请求,它拒绝了所有不是由内部发起的连接请求。

3 NAT分类及穿透策略

要实现NAT的穿透,就必须全面了解NAT有哪几种类型,针对各自类型如何进行穿透。NAT从不同角度可以有不同的分类,我们讨论的是NAT的穿透,所以,从实现的技术角度,NAT总的分为两大类:锥NAT和对称NAT。锥NAT又可以细分为三个类型:全锥NAT、限制性锥NAT、端口限制性锥NAT。类型不同,穿透的情况有所不同,下面来分别探讨:

3.1 全锥NAT

全锥NAT是把来自相同内部IP地址和端口的请求映射到相同外部IP地址和端口。任意一个外部主机均可通过该映射发送信息到该内部主机。前面讲NAT有一特性,它拒绝所有不是由内部发起的通往外部的连接。全锥NAT是一例外,从外部主动发来的连接,它不会拒绝,所以,穿透这类型NAT最为容易,只需要获取内部主机对应的外部IP和端口号,便可以直接发送信息到该内部主机。

3.2 限制性锥NAT

限制性锥NAT是把所有来自相同内部IP地址和端口的请求映射到相同外部IP地址和端口。和全锥NAT不同的是,只有当内部主机先给外部主机发送信息后, 该外部主机才能向该内部主机发送信息。也就是说,不请自来的信息,该类型NAT会毫不犹豫的丢弃,所以我们假设两台都处在各自不同局域网内部的主机A和B,A单方面向B发送信息,就会被B局域网NAT丢弃,B单方面向A发送信息,就会被A局域网NAT丢弃,导致连接失败。

3.3 端口限制性锥NAT

端口限制性锥NAT与限制性锥NAT相似,只是多了个端口号的限制,即只有内部主机先向外部地址:端口发送信息,该外部主机才能使用特定的端口向内部主机发送信息。该类型NAT的穿透比限制性锥NAT的穿透多了个端口号的限制,所以穿透了该类型NAT,限制性锥NAT也自然会穿透。

3.4 对称NAT

对称NAT情况与上述3种类型都不同,当同一内部主机使用相同的端口和不同地址的外部主机进行通信时,对称NAT会重新创建一个Session,为这个Session分配不同的端口号,也就是说,当一个私网内主机和外部多个不同主机通信时,对称NAT并不会像锥(Cone,全锥,限制性锥,端口限制性锥)NAT那样分配同一个端口。而是重新建立一个Session,重新分配一个端口。因为端口号的不确定,所以穿透该类型NAT的机会很小。目前绝大多数NAT都是锥NAT,锥NAT的穿透也是本文讨论的重点。不过,仍有两种方法有可能穿透对称NAT:同时开放TCP、UDP端口猜测。这两种方法(包括TCP穿透NAT尝试),将在以后的论文中探讨。

4 面向对象封装UDP穿透NAT技术实现

面向对象是当今软件编程的主流技术,它为软件工程的开发提供了相当大的便捷,也给当今软件产品的丰富多彩奠定了基础。运用面向对象技术可以使你的代码异常的清晰明了,也给代码的移植和重用提供了相当大的便捷。本文使用面向对象的方式实现了锥NAT的穿透。因为是面向对象的实现方式,所以可以很容易的将代码嵌套到其它的软件工程中去,为点对点通信提供了便捷。

本文使用的编程语言是C++,该语言既兼容了C语言面向过程的特性,又涵盖了面向对象的所有特性,本文使用后者。传输协议使用的是UDP协议。实现点对点的NAT穿透,需要一台中间服务器来协助打洞,服务器端代码需实现的功能较为简单,所以,我们先从服务器端开始。

既然是面向对象,首先我们需要自定义一个类,将所有需实现的功能封装到该类中,以便以后的调用,假设类名为:NAT_UDP_SERVER,该类所有成员的声明放到名为NAT_UDP_SERVER.h的头文件中,所有成员函数的实现在NAT_UDP_SERVER.cpp文件中。以下是该类所有成员的声明:

class NAT_UDP_SERVER

{

public:

NAT_UDP_SERVER();

NAT_UDP_SERVER();

void InitWinSock();

void mksock();

void start_bind();

void startlisten();

UserListNode GetUser(char *username);

private:

UserList ClientList;

SOCKET socket_UDP;

sockaddr_in local;

sockaddr_in sender;

MESSAGE recvbuf;

};

这就是面向对象的好处,我们可以一览它的全貌,包括这个类有哪些数据成员,成员函数大概都做些什么(从名称上可以略知一二),简单的说,服务器所要做的一是监听各个客户端(内网或外网)发送的所有的连接请求,二是在必要时候,需要转达给某个客户端其它客户端的一些节点信息(外网IP和端口)。

声明中的Socket又称“套接字”,应用程序通常通过Socket向网络发出请求或者应答网络请求。这里不再详述。成员函数void InitWinSock()和void mksock()做了socket的初始化工作,函数void start_bind()将“套接字”绑定到固定端口上,为监听做好准备。

类NAT_UDP_SERVER最重要的函数是void startlisten(),该函数启动了监听程序,并使用switch语句中创建的子程序来处理接收到的数据包,关于数据包的格式,本文自定义了几种数据结构(NAT_UDP_SERVER.h头文件中),以方便信息的传递,这里列举一例:

struct MESSAGE

{

INT32 MessageType;

union _message

{

REGISTER_MESSAGE registermember;

LOGOUT_MESSAGE logoutmember;

P2P_BURROW burrowmessage;

}message;

};

结构体MESSAGE中包含了信息类型、注册成员信息、注销成员信息、打洞成员信息。嵌套的结构体的详细信息会在完整代码给出,这里不再一一列举。

服务器端需要拥有一个固定的外部IP及端口,以方便其它外网客户端的连接,需要实现的功能是,当多个外网客户端节点连接到该服务器时,服务器需将各个节点信息(外部IP及端口)记录下来,当节点A想直连节点B,服务器需将A节点信息发送给B节点,再将B节点信息发送给A节点,当A节点和B节点都拥有对方节点信息后,便具备打洞条件。前面介绍过,在锥NAT中,端口限制性锥NAT是最为严格的锥NAT,一切不请自来的信息均会被抛弃,只有内部主机主动连接外部主机后,外部主机发送过来的信息才会被放行。本文使用的NAT穿透技术,简单讲,就是打一个时间差,A节点发送数据包到B节点,会被B节点NAT丢弃,B节点此时虽然没有收到任何信息,但如果B节点人为的向A节点再发送一个数据包,那么A节点的NAT就会误认为这个数据包和刚才A节点发送出去的数据包属于同一个Session,而予以放行。同理,A节点再发送数据包到B节点时,也会被B节点NAT放行,这时,A节点与B节点的直连已经建立,穿透已经实现。

下面用代码来具体实现,服务器端的代码主要实现的是保存节点信息和转发节点信息的功能。重点讨论一下客户端代码:

客户端自定义的类名为:NAT_UDP_CLIENT,关于类的声明放在NAT_UDP_CLIENT.h头文件中,以下是类声明的全貌:

class NAT_UDP_CLIENT

{

public:

NAT_UDP_CLIENT();

~NAT_UDP_CLIENT();

void InitWinSock();

void mksock();

UserListNode GetUser(char *username);

void BindSock();

void ConnectToServer(char *username, char *serverip);

void OutputUsage();

bool SendMessageTo(char *UserName, char *Message);

void ParseCommand(char * CommandLine);

static DWORD WINAPI RecvThreadProc(LPVOID lpParameter);

private:

UserList ClientList;

SOCKET socket_UDP;

sockaddr_in sin;

};

该类自定义了一个命令处理函数void ParseCommand(char * CommandLine),它占据了客户端的主线程,而客户端需要时时接收来自外部的信息,所以需要再创建一个线程专门负责接收外部发来的信息。

直接创建线程函数很容易,但本文是面向对象的技术实现,一般来说,线程函数不能作为类的成员函数出现,我们需要将线程函数写入类中,这里需做几个特殊处理:

(1)将线程函数声明为static;

(2)在该成员函数的实现里,定义该类指针

NAT_UDP_CLIENT* p = (NAT_UDP_CLIENT*)lpParameter,通过该指针来访问类的其它成员;

(3)创建线程时,将该类定义的对象udp_client的地址作为参数传递给线程函数:

HANDLE threadhandle = CreateThread(NULL, 0,NAT_UDP_CLIENT::RecvThreadProc, (void*)&udp_client, NULL, NULL)。

此外,成员函数void ConnectToServer(char *username, char *serverip)实现了客户端节点连接服务器并注册的功能;而成员函数bool SendMessageTo(char *UserName, char *Message)是实现打洞的关键函数,具体步骤如下:

(1)在函数的开始,首先需要获得B客户端节点信息:

UserIP = (*UserIterator)->ip;

UserPort = (*UserIterator)->port;

(2) 因为全锥NAT是可以直接发送数据包被对方接收的,所以尝试直接向B节点发送数据包:

sendto(socket_UDP, (const char *)&realmessage, MessageHead.StringLen, 0, (const sockaddr*)&remote, sizeof(remote));

(3)如果没有接收到对方主机的任何回应,表明对方使用的NAT不是全锥NAT,此时就需要服务器的协助来完成打洞:首先向服务器发送一数据包sendto(socket_UDP, (const char*)&transMessage, sizeof(transMessage), 0, (const sockaddr*)&server, sizeof(server));数据包的内容指令是BURROW,目的是通过服务器告诉B客户端节点需先向A客户端节点(本地)发送一条信息;

(4) A客户端节点在向服务器发出请求后程序中断3秒钟Sleep(3000),等待对方先发送信息过来;虽然A客户端节点也不会收到任何信息,但3秒过后,A客户端节点(本机)再次调用发送程序向B客户端节点发送数据包:sendto(socket_UDP, (const char *)&realmessage, MessageHead.StringLen, 0, (const sockaddr*)&remote, sizeof(remote)),

(5)最终,B客户端节点会显示收到一个消息:printf("收到一个消息:%s\n",comemessage),表明A、B节点已成功建立直连,双方可互发信息,NAT穿透成功。

5 结束语

现实中使用的绝大多数NAT路由器均可使用本文提供的方法穿透成功,而面向对象封装所有模块功能,又为代码的移植与重用提供便利,最后引入多线程机制,又使代码实现的功能变的更加的灵活与丰富。

参考文献

[1](美)J.D.Wegner.(美)RobertRockell等著.赵英等.IP地址管理与子网划分[M].北京:机械工业出版社,2001.

[2] Boulton C.Rosenberg J.Camarillo G.NAT traversal practices for client-server SIP. RFC 6314,July 2011.

[3]朱文杰.吕锋.使用UDP穿越P2P网络中NAT的方法[J].微计算机应用,2007(12).

作者单位

长治学院计算机系 山西省长治市 046011

上一篇:物联网技术环境下食品安全监管途径 下一篇:光纤带宽对分布式光纤测温系统空间分辨率影响...