您的位置:首页 > 理论基础 > 计算机网络

利用ZoomPipeline迅速实现基于线程池的全异步TCP点对点代理

2017-02-21 21:16 387 查看
在博文《一种基于Qt的可伸缩的全异步C/S架构服务器实现》中提到的高度模块化的类可以进行任意拆解,实现非常灵活的功能。今天,我们来看一看一个公司局域网访问英特网云服务器的点对点代理例子。代理服务器代码在我的Github仓库下载

1.应用场景

餐饮管理公司的业务员经常偷偷上班网购,老板决定实施断网。然而,原有餐饮系统的服务器在一家知名云虚拟机上,技术经理希望通过一个受控节点访问Internet 。由于合同尾款已经结清,甲乙方都不想为了这个需求再折腾。

解决方案:征用旧计算机一台,IP地址为192.168.100.100,双网卡。使用ZoomPipeline的网络模块搭建代理程序,仅对需要的端口进行透明转发。系统连接图:



1、代理程序会在内部网络的15668端口,13987端口监听。

2、内部PC发起连接,代理程序接受局域网连接。

3、代理程序立刻为该内部PC主动发起外部连接,分别到云虚拟机的5668或者3987端口。

4、代理程序成功连接云虚拟机,建立一个透明的隧道,实现双向通信。

实现这个功能的配置文件如下:

[PROXY]
#有多少对链路
Ports=2
#链路1
[PORT0]
#局域网的端口、地址(局域网PC全连这个地址)
InnerPort=15668
InnerAddress=192.168.100.100
#英特网服务器地址、端口(局域网PC应该最终连接这个地址)
OuterPort=5668
OuterAddress=XXXX.XXXX.com

#链路2
[PORT1]
InnerPort=13987
InnerAddress=192.168.100.100
OuterPort=3987
OuterAddress=YYYY.YYYY.com


2.实现原理

这种点对点传话器的工作思路非常清晰,借助Qt以及现有资源,代码量很小。使用zoomPipleline工程中的network, log 两个模块即可,这两个模块的原理不再赘述,可参见系列原文《一种基于Qt的可伸缩的全异步C/S架构服务器实现》。

2.1 代理类proxyObject

proxyObject是QObject的子类。之所以使用QOBject派生,为了后面方便把它放入独立的线程中去。

类声明:

//...
#include "network/zp_net_threadpool.h"
class ProxyObject : public QObject
{
//...
void initEngine();
private:
//!连接参数,内部端口号-->英特网端口号的映射
QMap<int, int> m_para_OuterPort;
//!连接参数,内部端口号-->英特网DNS解析IP地址的映射
QMap<int, QString> m_para_OuterAddress;
//!连接参数,英特网DNS解析IP地址-->内部端口号的映射
QMap<QString,int> m_para_IPLocalPort;
private:
//!Zoompipeline网络引擎
ZPNetwork::zp_net_Engine * engine;
//!客户端表,内部PC->外部服务器
QHash<QObject *, QObject *> m_hash_Inner2Outer;
//!客户端表,外部服务器->内部PC
QHash<QObject *, QObject *> m_hash_Outer2Inner;
//!临时数据缓存,在主对方尚未全部在线时,缓存双方的数据
QHash<QObject *, QList< QByteArray > > penging_data;
public slots:
//...
void slot_NewClientConnected(QObject * /*clientHandle*/,quint64);
//this event indicates a client disconnected.
void slot_ClientDisconnected(QObject * /*clientHandle*/,quint64);
//some data arrival
void slot_Data_recieved(QObject *  /*clientHandle*/,QByteArray  /*datablock*/ ,quint64);
};
//...


解释:

1. 为了在局域网、英特网搭建一个桥梁,需要记录每个桥梁的参数,如内部端口、地址、外部端口、地址。这些参数使用QMap存放,以方便查找。由于DNS解析是很耗时的工作,会为每个外部地址存储一个当前的IP地址,在变量m_para_OuterAddress、m_para_IPLocalPort中,这两个表互相反查对方。

2. 主对方套接字存在m_hash_Outer2Inner、m_hash_Inner2Outer中,可以在接收数据时,方便的检索到对方的套接字。

2.2 读取INI文件

我们来看看读取INI文件的代码。在这部分代码中,将逐一读取配置。

void ProxyObject::initEngine()
{
QString inidfile = QCoreApplication::applicationFilePath()+".ini";
QSettings settings(inidfile,QSettings::IniFormat);
//读取链接数
int nPorts = settings.value("PROXY/Ports",0).toInt();
for (int i=0;i<nPorts;++i)//读取每个链接
{
QString keyPrefix = QString().sprintf("PORT%d",i);
QString sk = keyPrefix + "/InnerPort";
//内部端口
int nInnerPort = settings.value(sk,0).toInt();
sk = keyPrefix + "/InnerAddress";
//内部地址
QString strInnerAddress = settings.value(sk,"").toString();
sk = keyPrefix + "/OuterPort";
//外部端口
int nOuterPort = settings.value(sk,0).toInt();
sk = keyPrefix + "/OuterAddress";
//外部域名
QString strOuterAddress = settings.value(sk,"").toString();
//开启一个监听线程
engine->AddListeningAddress( keyPrefix, QHostAddress( strInnerAddress ), nInnerPort,false);
//查询DNS
QHostInfo info = QHostInfo::fromName(strOuterAddress);
QList<QHostAddress> lstaddr = info.addresses();
if (lstaddr.size())
{
QString outerIP = lstaddr.first().toString();
m_para_IPLocalPort[outerIP] = nInnerPort;
m_para_OuterAddress[nInnerPort] = outerIP;
}
m_para_OuterPort[nInnerPort] = nOuterPort;
}
//开启4个收发线程
engine->AddClientTransThreads(4,false);
}


2.3 响应主对方的连接事件

Engine 在成功接受连接后,会发射信号,我们在下面这个槽中响应,并作出动作:

1. 如果连接来自内部局域网,则记录内部套接字,并立刻发起向云虚机的连接。

2. 如果连接来自外部,则与缓存的内部发起套接字形成参照,开始透明转发。

/*!
* \brief ProxyObject::slot_NewClientConnected
* \param clientHandle 当前接受连接的套接字
* \param extraData engine主动发起连接时,调用者给的便条,这里用于记录内部套接字的指针.
*/
void ProxyObject::slot_NewClientConnected(QObject * clientHandle,quint64 extraData)
{
QTcpSocket * sock = qobject_cast<QTcpSocket *> (clientHandle);
if (sock)
{
QString pn = sock->peerName();//获得对端名称
if (extraData)//如果存在便条,说明本套接字为主动连接外部网络返回的套接字
{
if (m_para_IPLocalPort.contains(pn))//查找对应的内部端口
{
int nLocalPort = m_para_IPLocalPort[pn];
QObject * innerClient = reinterpret_cast<QObject *> (extraData);//得到内部套接字
if (innerClient)
{
//记录主对方
m_hash_Inner2Outer[innerClient] = clientHandle;
m_hash_Outer2Inner[clientHandle] = innerClient;
//首先转发双方的缓存数据。
if (penging_data.contains(innerClient))
{
while (penging_data [innerClient].empty() == false)
{
engine->SendDataToClient( clientHandle, penging_data[innerClient].first() );
penging_data[innerClient].pop_front();
}
penging_data.remove(innerClient);
}
if (penging_data.contains(clientHandle))
{
while (penging_data[clientHandle].empty()==false)
{
engine->SendDataToClient(innerClient,penging_data[clientHandle].first());
penging_data[clientHandle].pop_front();
}
penging_data.remove(clientHandle);
}
}
else
qWarning()<<"Incomming Out connection has no pending local peer. ";
}
else
{
qWarning()<<"Incomming Out connection "<<pn<<"has no local Port";
//engine->KickClients(clientHandle);
}
}
else//本套接字是内部PC连接代理的套接字
{
int localPort = sock->localPort();
if (m_para_OuterPort.contains(localPort))
{
//主动连接云虚机
engine-> connectTo(
QHostAddress(m_para_OuterAddress[localPort]),//地址
m_para_OuterPort[localPort],//端口
false, //Plain TCP,不是SSL
//把发起方的套接字作为extraData缓存。
reinterpret_cast<quint64>(sock)
);
}
else
{
qWarning()<<"Local port "<<localPort<<" Is not valid.";
engine->KickClients(clientHandle);
}
}

}
}


这里第一个值得注意的是变量“extraData”。由于 engine 是全异步的,导致:

1. 内部PC连接代理;

2. 代理主动连接云虚机,调用 connectTo 后,会立刻返回消息循环。

3. 当云虚机接受连接,成功建立时,系统再次进入 ProxyObject::slot_NewClientConnected

4. 此刻,必须明确知道,是谁(哪台PC)向代理发起了连接,从而触发亮起1、2、3、4

为了知道4,我们在2中,把发起方的套接字作为extraData交给引擎记忆。如果不这样做,当多个内部PC同时连接时,会发生混淆。

第二个值得说一说的是队列penging_data。在上面的步骤1中,其实内部PC到代理的链路已经建立。此时,PC上的程序会发送一些请求(如登录、SSL握手之类的)。但,代理与外部云虚机的链路尚未打通。penging_data 就是用来缓存这种数据的。

2.4 透明转发

一旦链路建立,剩下的就简单了。在收到数据后,直接透明转发。

//some data arrival
void ProxyObject::slot_Data_recieved(QObject *  clientHandle,QByteArray  datablock,quint64 )
{
if (m_hash_Inner2Outer.contains(clientHandle))
engine->SendDataToClient(m_hash_Inner2Outer[clientHandle],datablock);
else if (m_hash_Outer2Inner.contains(clientHandle))
engine->SendDataToClient(m_hash_Outer2Inner[clientHandle],datablock);
else
penging_data[clientHandle].push_back(datablock);
}


当收到数据时,直接查找对方套接字,并回传。

3 部署-树莓派也能做代理

我们在树莓派上直接下载并编译,插上一块无线网卡连接到局域网192.168.1.86,树莓派拨号连接到英特网。

pi@raspberrypi:~/projects $ git clone https://github.com/goldenhawking/dataProxy.git pi@raspberrypi:~/projects $ cd dataProxy
pi@raspberrypi:~/projects/dataProxy $ qmake
pi@raspberrypi:~/projects/dataProxy $ make


而后设置好ini文件测试(windows 为 DataProxy.exe.ini, Linux为 DataProxy.ini):

pi@raspberrypi:~/projects $ nano DataProxy.ini


内容:

[PROXY]
Ports=1

#p2p trans 0, a SQL-Server database.
[PORT0]
#inner port is the port to which your clients in local area network will connect to
InnerPort=12345
#inner address is the subnet address your local area network use.
InnerAddress=192.168.1.86
#outer port is the internet server's port
OuterPort=80
#ourer address is the internet server's address.
OuterAddress=tile.openstreetmap.org


而后启动程序:

pi@raspberrypi:~/projects $ ./DataProxy
Reading config from : /home/pi/projects/dataProxy/DataProxy.ini
PROXY/Ports = 1
PORTS0:
PORT0/InnerPort=12345
PORT0/InnerAddress=192.168.1.86
PORT0/OuterPort=80
PORT0/OuterAddress=tile.openstreetmap.org
tile.openstreetmap.org IP =140.110.240.7


测试下载:

wget http://192.168.1.86:12345/0/0/0.png --2017-02-21 20:59:37--  http://192.168.1.86:12345/0/0/0.png 0.png               100%[=====================>]   6.66K  3.69KB/s ?? 1.8s




4 一些特点、局限

该代理程序仅能实现点对点连接。这类连接一般用于专用的服务器,比如网络数据库SQLServer、地图瓦片Web服务器等。如果想允许一个网站,一般是不够的。

该程序不但可以转接Plain TCP,也支持 SSL TCP。SSL其实在TCP协议栈上层,对于代理来说,SSL也相当于是转发的字节流而已。一旦TCP连接建立,其余的逻辑行为就不重要了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: