NAT及P2P通信
文章目录
- 什么是NAT
- 私有IP地址
- 什么是P2P通信
- NAT类型及判定
什么是NAT
NAT(网络地址转换,Network Address Translation)是一种网络技术,
用于将私有网络中的IP地址转换为公共IP地址,以便与外部网络(如互联网)通信。
其主要目的是解决IPv4地址不足的问题,并增强网络安全性。
我们可以用一个非常长的网络地址,比如128 位地址,可以表示$2^{128} = 340282366920938463463374607431768211456$个机器。
但世界没有如此能力的路由器来直接制表。
参考人类的地址表示,银河系-太阳第-地球-亚洲-中国-广东省- 。。。
NAT也就是如此。
NAT的工作原理
- 私有IP地址与公共IP地址的转换:
- 私有网络中的设备使用私有IP地址(如192.168.x.x),这些地址无法直接在互联网上使用。
- NAT设备(如路由器)将私有IP地址转换为公共IP地址,使设备能够访问外部网络。
- NAT表:
- NAT设备维护一个转换表,记录私有IP地址和端口与公共IP地址和端口的映射关系。
- 当数据包返回时,NAT设备根据表中的信息将其转发到正确的内部设备。
私有IP地址
私有IP地址(Private IP Address)是在局域网(LAN)内部使用的IP地址,不直接在互联网上路由。
在互联网中可以直接路由的地址是公有地址。
私有IP地址由RFC 1918定义,用于在私有网络内分配,避免与公共IP地址冲突。
私有IP有以下三类
- A类IP
- 范围:10.0.0.0 到 10.255.255.255
- 子网掩码:255.0.0.0(/8)
- 可用地址数:约1677万个
- 适用于大型网络。
- B类IP
- 范围:172.16.0.0 到 172.31.255.255
- 子网掩码:255.240.0.0(/12)
- 可用地址数:约104万个
- 适用于中型网络。
- C类IP
- 范围:192.168.0.0 到 192.168.255.255
- 子网掩码:255.255.0.0(/16)
- 可用地址数:约6.5万个
- 适用于小型网络(如家庭或办公室)。
公网IP在互联网上是唯一的,私有IP可能重复出现,就像房号 1107,可能在很多居民楼都会出现,但大的地址只能有一个,比如全球只有一个亚洲。
局域网是可以嵌套的,如些一来,互联网可以容下无数的机器通信。
什么是P2P通信
P2P通信(Peer-to-Peer Communication)是一种去中心化的网络通信模式,其中所有参与节点(Peer)平等地共享资源和服务,而不依赖中央服务器。
每个节点既可以是客户端,也可以是服务器,能够直接与其他节点通信和交换数据。
在互联网上,各机器可以通过公网上的机器来取到信息,但对于以下业务:
- 文件共享
- 流媒体
- 分布式计算
- 区块链和加密货币
- 即时通讯
- 内容分发网络(CDN)
全部依赖中心服务器来通信是不现实的。
在局域网络中很容易实现一个P2P通信
例如下面代码片段
void communicator::onListenClicked()
{
if (!m_bListening)
{
this->m_bListening = this->m_server->listen(QHostAddress::Any, GetPort());
}
else {
this->m_server->close();
this->m_bListening = this->m_server->isListening();
}
SetBtnListen();
}
void communicator::onNewConnection()
{
m_socketTarget = this->m_server->nextPendingConnection();
connect(m_socketTarget, SIGNAL(readyRead()), this, SLOT(onTargetReadyRead()));
connect(m_socketTarget, &QTcpSocket::disconnected, m_socketTarget, &QTcpSocket::deleteLater);
}
void communicator::onTargetReadyRead()
{
auto* socket = qobject_cast<QTcpSocket*>(sender());
// auto* socket = m_socketTarget;
QString pref = QString("[%1]%2:%3").arg(socket->peerName()).arg(socket->peerAddress().toString()).arg(socket->peerPort());
QByteArray bytes = socket->readAll();
this->ui.txtBrow_Shower->append(pref + "> " + bytes);
}
void communicator::onSendClicked()
{
const QString text = this->ui.edit_message->text().trimmed();
if (this->m_socketTarget) this->m_socketTarget->write(text.toLocal8Bit());
if (this->m_socketSelf) this->m_socketSelf->write(text.toLocal8Bit());
this->ui.txtBrow_Shower->append("Self:< " + text);
}
void communicator::onConnectClicked()
{
QString target_ip = this->ui.edit_TargetIp->text().trimmed();
QString target_port = this->ui.edit_TargetPort->text().trimmed();
quint16 t_port = static_cast<quint16>(target_port.toUShort());
this->m_socketSelf->connectToHost(target_ip, t_port);
connect(m_socketSelf, SIGNAL(readyRead()), this, SLOT(onTargetReadyRead()));
}
ui.txtBrow_Shower 会回显对方的host与ip
1234是服务端监听的端口,15001是机器为客户端与服务端的连接开的一个临时端口。
如果是在互联网上实现P2P通信,简单的一点的方法是需要一个信令服务器,做UDP打洞。
如下
#include <QObject>
#include <QUdpSocket>
#include <QMap>
#include <QDebug>
class Server : public QObject
{
Q_OBJECT
public:
explicit Server(quint16 port = 30011, QObject *parent = nullptr);
virtual ~Server();
protected slots:
virtual void onReadyRead();
protected:
QUdpSocket* serverSocket;
QMap<QString, QPair<QHostAddress, quint16>> regMap;
};
#include "Server.h"
Server::Server(quint16 port, QObject *parent)
: QObject(parent)
, serverSocket(new QUdpSocket(this))
{
serverSocket->bind(QHostAddress::Any, port);
connect(serverSocket, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
qDebug() << "bind in " << serverSocket->localAddress().toString() << " : " << port;
}
Server::~Server()
{
}
void Server::onReadyRead()
{
while (serverSocket->hasPendingDatagrams()) {
QByteArray data;
data.resize(serverSocket->pendingDatagramSize());
QHostAddress clientAddr;
quint16 clientPort;
serverSocket->readDatagram(data.data(), data.size(), &clientAddr, &clientPort);
qDebug() << "readdata: " << QString(data) << " from " << clientAddr.toString() << " : " << clientPort;
if (data.startsWith("register ")) {
QByteArrayList list = data.split(' ');
QString regName(list[1]);
regMap.insert(regName, qMakePair(clientAddr, clientPort));
qDebug() << "register [" << regName << "] -> " << clientAddr << " : " << clientPort;
}
else if (data.startsWith("get_peer ")) {
QByteArrayList list = data.split(' ');
QString peerName(list[1]);
auto it = regMap.find(peerName);
if (it == regMap.end()) {
serverSocket->writeDatagram("null", 5, clientAddr, clientPort);
}
else {
QByteArray sendData;
sendData.append("ack_peer ");
sendData.append(it.value().first.toString());
sendData.append(" ");
sendData.append(QString::number(it.value().second));
serverSocket->writeDatagram(sendData, clientAddr, clientPort);
}
}
}
}
客户端如下
#if !defined(__COMMUNICATOR_H__)
#define __COMMUNICATOR_H__
#include <QtWidgets/QMainWindow>
#include <QUdpSocket>
#include "ui_Communicator.h"
class Communicator : public QMainWindow
{
Q_OBJECT
public:
explicit Communicator(QWidget *parent = Q_NULLPTR);
QHostAddress getServerAddr() const;
quint16 getServerPort() const;
QString getRegName() const;
QString getPeerName() const;
void setTargetAddr(QHostAddress addr);
void setTargetPort(quint16 port);
quint16 getTargetPort() const;
protected slots:
virtual void onReadyRead();
virtual void onRegister();
virtual void onFetch();
virtual void onSendMessage();
virtual void appendMessage(QString name,QString message);
private:
Ui::CommunicatorClass ui;
QUdpSocket *clientSocket;
QHostAddress targetHostAddr;
};
#endif
#include "Communicator.h"
#include <QDebug>
#include <QDateTime>
Communicator::Communicator(QWidget *parent)
: QMainWindow(parent)
, clientSocket(new QUdpSocket(this))
{
ui.setupUi(this);
clientSocket->bind(QHostAddress::Any);
connect(clientSocket, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
connect(ui.btn_Register, SIGNAL(clicked()), this, SLOT(onRegister()));
connect(ui.btn_Fetch, SIGNAL(clicked()), this, SLOT(onFetch()));
connect(ui.btn_Send, SIGNAL(clicked()), this, SLOT(onSendMessage()));
}
QHostAddress Communicator::getServerAddr() const
{
QString addr = ui.lnEdit_ServerAddr->text().trimmed();
QHostAddress hostAddr(addr);
return hostAddr;
}
quint16 Communicator::getServerPort() const
{
return ui.spin_serverPort->value();
}
QString Communicator::getRegName() const
{
return ui.lnEdit_RegName->text().trimmed();
}
QString Communicator::getPeerName() const
{
return ui.lnEdit_PeerName->text().trimmed();
}
void Communicator::setTargetAddr(QHostAddress addr)
{
this->targetHostAddr = addr;
ui.lnEdit_TargetAddr->setText(addr.toString());
}
void Communicator::setTargetPort(quint16 port)
{
ui.spin_Port->setValue(port);
}
quint16 Communicator::getTargetPort() const
{
return ui.spin_Port->value();
}
void Communicator::onReadyRead()
{
while (clientSocket->hasPendingDatagrams()) {
QByteArray data;
qint64 sz = clientSocket->pendingDatagramSize();
data.resize(sz);
QHostAddress addr;
quint16 port;
clientSocket->readDatagram(data.data(), sz, &addr, &port);
if (data.startsWith("null")) {
setTargetAddr(QHostAddress::Any);
}
else if (data.startsWith("ack_peer ")) {
QByteArrayList list = data.split(' ');
this->targetHostAddr = QHostAddress(QString(list[1]));
setTargetAddr(this->targetHostAddr);
setTargetPort(QString(list[2]).toUInt());
}
else if (data.startsWith("send\n")) {
QByteArrayList list = data.split('\n');
appendMessage(list[1], list[2]);
}
}
}
void Communicator::onRegister()
{
QByteArray data;
data.append("register ");
data.append(getRegName());
QHostAddress serverAddr = getServerAddr();
clientSocket->writeDatagram(data, serverAddr, getServerPort());
qDebug() << data << " to " << serverAddr.toString() << " : " << getServerPort();
}
void Communicator::onFetch()
{
QByteArray data;
data.append("get_peer ");
data.append(getPeerName());
clientSocket->writeDatagram(data, getServerAddr(), getServerPort());
}
void Communicator::onSendMessage()
{
QString message = ui.lnEdit_msg->text().trimmed();
QByteArray data;
data.append("send\n");
data.append(getRegName());
data.append("\n");
data.append(message);
clientSocket->writeDatagram(data, targetHostAddr, getTargetPort());
appendMessage(getRegName(), message);
}
void Communicator::appendMessage(QString name, QString message)
{
QString msg = QString("%1[%2]:> %3").arg(name).arg(
QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss")).arg(message);
ui.txtEdit_Message->append(msg);
}
打开服务器,打开两个客户端分别注册与获取,
之后即使关闭信令服务器也能正常通信,
关于通信更多的保障,就需要用这个UDP协议来模拟有连接的通信方式了,比如心跳测试对方在线。
NAT类型及判定
根据STUN协议的定义,NAT类型分为四类:
-
全锥型(Full Cone)
特征:允许任何外部主机通过映射的公网IP和端口访问内网设备。
应用场景:P2P下载、远程监控等需直接通信的场景。 -
受限锥型(Restricted Cone)
特征:仅允许内网设备主动通信过的外部IP访问,端口不限。
典型配置:企业防火墙限制特定IP访问。 -
端口受限锥型(Port-Restricted Cone)
特征:要求外部主机的IP和端口均与内网设备此前通信过的匹配。
常见环境:家庭宽带默认类型,安全性较高。 -
对称型(Symmetric)
特征:为每个外部目标分配不同的映射端口,仅允许响应式通信。
影响:联机游戏、视频通话易出现连接失败。
通过STUN(Session Traversal Utilities for NAT)协议工具与服务器交互,根据响应结果判断NAT类型。这是最权威的检测方式。
- 常用工具
- stunclient(命令行工具)
# Debian/Ubuntu安装
sudo apt-get install stun-client
# 执行检测(以Google STUN服务器为例)
stun stun.l.google.com 3478
- NatTypeTester(图形化工具):
- Python脚本检测
# 安装库
pip install pystun3
# 执行检测
pystun3
- 在线检测平台
- Trickle ICE:
访问网页后点击“Gather candidates”,通过浏览器WebRTC接口自动分析NAT类型。 - 网心云小程序:
需连接WiFi后登录账号,一键检测并显示NAT类型(如“全锥形”)。
- 自定义工具(如https://mao.fan/mynat):
提供在线即时检测,结果包含NAT类型和网络通透性评分。