当前位置: 首页 > article >正文

NAT及P2P通信

文章目录

  • 什么是NAT
  • 私有IP地址
  • 什么是P2P通信
  • NAT类型及判定

什么是NAT

NAT(网络地址转换,Network Address Translation)是一种网络技术,
用于将私有网络中的IP地址转换为公共IP地址,以便与外部网络(如互联网)通信。
其主要目的是解决IPv4地址不足的问题,并增强网络安全性。

我们可以用一个非常长的网络地址,比如128 位地址,可以表示$2^{128} = 340282366920938463463374607431768211456$个机器。
但世界没有如此能力的路由器来直接制表。
参考人类的地址表示,银河系-太阳第-地球-亚洲-中国-广东省- 。。。
NAT也就是如此。

NAT的工作原理

  1. 私有IP地址与公共IP地址的转换:
    • 私有网络中的设备使用私有IP地址(如192.168.x.x),这些地址无法直接在互联网上使用。
    • NAT设备(如路由器)将私有IP地址转换为公共IP地址,使设备能够访问外部网络。
  2. NAT表:
    • NAT设备维护一个转换表,记录私有IP地址和端口与公共IP地址和端口的映射关系。
    • 当数据包返回时,NAT设备根据表中的信息将其转发到正确的内部设备。

私有IP地址

私有IP地址(Private IP Address)是在局域网(LAN)内部使用的IP地址,不直接在互联网上路由。
在互联网中可以直接路由的地址是公有地址。
私有IP地址由RFC 1918定义,用于在私有网络内分配,避免与公共IP地址冲突。

私有IP有以下三类

  1. A类IP
    • 范围:10.0.0.0 到 10.255.255.255
    • 子网掩码:255.0.0.0(/8)
    • 可用地址数:约1677万个
    • 适用于大型网络。
  2. B类IP
    • 范围:172.16.0.0 到 172.31.255.255
    • 子网掩码:255.240.0.0(/12)
    • 可用地址数:约104万个
    • 适用于中型网络。
  3. 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类型分为四类:

  1. ​全锥型(Full Cone)​
    ​特征:允许任何外部主机通过映射的公网IP和端口访问内网设备。
    ​应用场景:P2P下载、远程监控等需直接通信的场景。

  2. ​受限锥型(Restricted Cone)​
    ​特征:仅允许内网设备主动通信过的外部IP访问,端口不限。
    ​典型配置:企业防火墙限制特定IP访问。

  3. ​端口受限锥型(Port-Restricted Cone)​
    ​特征:要求外部主机的IP和端口均与内网设备此前通信过的匹配。
    ​常见环境:家庭宽带默认类型,安全性较高。

  4. ​对称型(Symmetric)​
    ​特征:为每个外部目标分配不同的映射端口,仅允许响应式通信。
    ​影响:联机游戏、视频通话易出现连接失败。

通过STUN(Session Traversal Utilities for NAT)协议工具与服务器交互,根据响应结果判断NAT类型。这是最权威的检测方式。

  1. 常用工具
    • ​stunclient​(命令行工具)
# Debian/Ubuntu安装
sudo apt-get install stun-client
# 执行检测(以Google STUN服务器为例)
stun stun.l.google.com 3478
  • ​NatTypeTester​(图形化工具):
  1. Python脚本检测
# 安装库
pip install pystun3
# 执行检测
pystun3
  1. 在线检测平台
  • ​Trickle ICE:
    访问网页后点击“Gather candidates”,通过浏览器WebRTC接口自动分析NAT类型。
  • ​网心云小程序:
    需连接WiFi后登录账号,一键检测并显示NAT类型(如“全锥形”)。
    ​- 自定义工具(如https://mao.fan/mynat)​:
    提供在线即时检测,结果包含NAT类型和网络通透性评分。

http://www.kler.cn/a/592800.html

相关文章:

  • 比较常见的几种排序算法
  • 利用knn算法实现手写数字分类
  • Kafka-QA
  • 前端字段名和后端不一致?解锁 JSON 映射的“隐藏规则” !!!
  • 批量删除 PPT 中的所有图片、某张指定图片或者所有二维码图片
  • 链式二叉树概念和结构
  • GPU视频编解码:X86 DeepStream 视频编解码入门(三)
  • PostgreSQL逻辑复制槽功能
  • 华为全流程全要素研发项目管理(81页PPT)(文末有下载方式)
  • 【从零开始学习计算机科学与技术】计算机网络(六)传输层
  • java后端怎么写好根据角色控制查询不同数据,
  • c++图论(三)之图的遍历
  • 【图论】FLOYD弗洛伊德算法-最短路径
  • js给后端发送请求的方式有哪些
  • 【C++】动态规划从入门到精通
  • C++20 中的同步输出流:`std::basic_osyncstream` 深入解析与应用实践
  • IMX335摄像头驱动注册分析
  • AI学习——卷积神经网络(CNN)入门
  • uniapp笔记-底部和首部标签页菜单生成
  • Java基础编程练习第34题-正则表达式