广告
返回顶部
首页 > 资讯 > 服务器 >内网穿透你真的了解吗?
  • 957
分享到

内网穿透你真的了解吗?

网络穿透NAT内网后端开发Linux服务器开发 2023-08-31 06:08:37 957人浏览 泡泡鱼
摘要

前言 内网穿透作为程序员常用的调试手段之一,我们可以通过在个人电脑上运行花生壳或者 frp 等方式,让他人访问我们本地启动的服务,而且这种访问可以不受局域网的限制,当我们使用ngrok,frp等开源框架时,你是否有好奇过它神奇的作用?明明没

前言

内网穿透作为程序员常用的调试手段之一,我们可以通过在个人电脑上运行花生壳或者 frp 等方式,让他人访问我们本地启动的服务,而且这种访问可以不受局域网的限制,当我们使用ngrok,frp开源框架时,你是否有好奇过它神奇的作用?明明没有将服务部署到服务器,程序员们究竟是怎么通过这种特殊方式让所有人访问自己的主机的?本文将以frp开源框架为例,介绍内网穿透的原理。

公网 IP 与内网 IP

能否在公网中访问服务器的决定性因素:公网 IP

IP 地址的作用

众所周知, IP 地址是每一位使用互联网的网民都会拥有的标识, IP 地址在互联网中起到的作用是定位,通过 IP 地址我们可以精确的定位到所需资源所在的服务器,这是对于一般用户来讲的,而对于程序员而言,我们需要的则是让用户通过 IP 地址定位到我们部署的资源,既然每个互联网用户都拥有 IP 地址,为什么用户无法直接访问部署在个人PC上的服务呢?

事实上, IP 地址分为两种:公网 IP 和内网 IP

内网 IP : 内网 IP 是用户在使用局域网时,由局域网的网关所分配的 IP 地址,每一个内网 IP 实际上都可以映射到当前所在局域网网关的某一端口( IPV4 地址通过 NAT 与端口映射方式实现,具体原理下文详解),拥有内网 IP 可以被同一局域网下的其他设备所访问到;

公网 IP : 内网的设备想要访问非同一局域网下的资源则必须通过公网 IP ,公网 IP 是没有经过 NAT 转换的由互联网供应商(ISP)提供的最原始的 IP 地址,每一个公网 IP 都可以直接在互联网中被直接定位到。

一个最简单的例子(以前端开发为例)

当我们使用 webpack-dev-server 来启动一个 node 项目时,我们除了通过localhost:[端口号]的方式以外,与我们的开发设备处于同一局域网下的设备可以通过内网 IP :[端口号]的方式对我们的项目进行访问,但当我们使用自己的流量或者连接其他非当前开发设备所在局域网的设备使用内网 IP :[端口号]的方式进行进行访问时,则无法访问。

原因:

内网 IP 地址仅在当前局域网下可以被定位并访问到,而当我们想要跨局域网访问时,我们的访问请求则需要先映射为公网 IP 然后访问到另一局域网的公网 IP ,最后由另一局域网的网关将其映射到相应的局域网设备,但我们访问的地址属于局域网中的内网 IP ,因此无法定位到其相应的公网 IP

综上所述,当我们想要让处于其他局域网下的设备访问到我们本地资源,必不可缺的就是公网 IP

公网 IP 的稀有程度

相较于内网 IP ,公网 IP 明显比内网 IP 更加有用,为什么不可以人手一个公网 IP 呢?

IPV4和 IPV6

尽管 IPV6 的概念在几年前已经被提出,但实际的普及程度并没有很高,现在大部分网络用户使用的依旧是 IPV4 的 IP 地址,这也是限制公网 IP 个数的最大原因。

** IPV4:** IPV4 由 32 位二进制数组成,一共有 2^32 个不同的 IPV4 地址

** IPV6**: IPV6 由 128 位二进制数组成,理论上共有 2^128 个不同的 IPV6 地址

由此可见, IPV4地址的个数并不足以满足当前全世界网络用户的人手一个 IP 地址的需求,那么当前的网络为什么可以让这么多用户同时在网络上冲浪呢?

NAT(网络地址转换)技术

网络地址转化技术的核心作用在于实现对公网 IP 地址的复用,即所有的内网主机共用同一个 IP 地址,NAT 的实现方式共有三种:

  • 静态转换:将内网 IP 直接转换为公网 IP 地址,形成一一对应的方式

  • 动态转换:将内网 IP 地址转换为公网 IP 地址,与静态转换不同的是动态转换会在 IP 池中选择空闲 IP 地址进行转换,即每次同一个内网 IP 对应的公网 IP 会发生改变

  • 端口多路复用(PAT 技术):将内网 IP 与公网 IP 的某一端口进行映射,通过公网 IP 的某一端口访问公网

可以看出以上三种形式中端口多路复用(PAT)技术可以最大程度上缓解 IPV4 地址紧张的现状,也是最为广泛使用的实现方式,三种 NAT 实现方式共同点在于:对于内网用户来说自己对应的公网 IP 是不可知的,就好像我们可以知道自己的门牌号但无法知道自己所在的小区,因此无法准确告诉别人我们的具体地址。

内网穿透

在已知了当前内外网工作方式后,我们再来看一看作为程序员常用的技术手段内网穿透

在此之前或许很多人都曾使用过如花生壳、ngrok、frp等方式在没有服务器的情况下将一些服务部署到网络上让别人使用

那么内网穿透的原理究竟是怎么样的呢?

内网穿透原理解析

目前市面上主流的内网穿透工具实现的原理如下:

可见,内网穿透的核心原理在于将外网 IP 地址与内网 IP 地址建立联系,市面上常用的如花生壳工具其核心原理就是依靠一台具有公网 IP 的服务器作为请求的中转站以此来达到从公网访问内网主机的目的。

当我们启动花生壳的服务时,花生壳会将本地配置好的端口和服务器上的端口进行映射,告知服务器请求转发的路径,花生壳的公网服务器则会监听相应端口的请求,当用户访问花生壳提供的 IP 地址时,花生壳的对应 IP 地址的公网主机将会根据访问的端口映射到相应的内网主机,并通过预先配置好的服务端口将请求转发,以达到访问内网主机相应服务的效果。

更多C++后台开发技术点知识内容包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,流媒体,音视频开发,Linux内核,TCP/IP,协程,DPDK多个高级知识点。

C/C++Linux服务器开发高级架构师/C++后台开发架构师​免费学习地址

【文章福利】另外还整理一些C++后台开发架构师 相关学习资料,面试题,教学视频,以及学习路线图,免费分享有需要的可以点击领取

实现内网穿透

花生壳作为一款商业产品,对于配置端口等一系列工作进行了封装,使得用户可以更快捷的使用内网穿透,但我们在了解原理后完全可以通过一些开源的框架以及一台公网服务器实现对应的内网穿透功能,我们以 frp 为例。

如何搭建最简单的 frp 服务

服务端设置(frps.ini):[common]bind_port = 7000       //此处填写客户端监听的服务端端口号vhost_Http_port = 8080 //此处填写用户访问的端口号客户端配置(frpc.ini):[common]server_addr = x.x.x.x //此处填写服务端 IP 地址server_port = 7000    //此处填写服务端配置的bind_port[WEB]type = http         //此处规定转发请求的协议类型local_port = 80     //此处规定本地服务启动的地址custom_domains = www.example.com   //此处可以填写自定义域名(需要在 IP 地址下配置域名解析)

当我们配置完上述的文件后,用户的访问请求将会经过如下的步骤:

用户的请求将会经过域名解析,公网端口的转发以及内网主机的监听三个步骤成功将请求发送到对应的内网服务,当然 frp 相较于花生壳提供了更多的自定义配置项,此处不做详细讲解,有兴趣的读者可以访问:frp中文文档

当我们使用 frp 去配置我们自己的内网穿透服务时,我们可以使用一台服务器为大量的内网主机提供公网访问的功能,以此来实现公网 IP 的复用,其原理与上文提到的 PAT 端口多路复用技术相类似,当我们临时需要使用服务器时,只需要向拥有公网服务器的朋友申请两个闲置端口即可。

frp 核心代码解析

本文以 http 请求为例解析当一个公网请求发送到frp服务器后究竟会经过哪些步骤

frps 初始化

func runServer(cfg config.ServerCommonConf) (err error) {  log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel, cfg.LogMaxDays, cfg.DisableLoGColor)  if cfgFile != "" {    log.Info("frps uses config file: %s", cfgFile)  } else {    log.Info("frps uses command line arguments for config")  }    // !important 核心代码1  svr, err := server.NewService(cfg)  if err != nil {    return err  }  log.Info("frps started successfully")  // !important 核心代码2  svr.Run()  return}

在frp/cmd/frps/root.Go

  • 核心代码1: server.NewService() 方法对我们在frps中的配置进行解析,初始化frp服务端
  • 核心代码2: serever.Run() 方法启动frp服务

frpc 初始化

for{      // !important 核心代码3conn, session, err := svr.login()    if err != nil {      xl.Warn("login to server failed: %v", err)      // if login_fail_exit is true, just exit this program      // otherwise sleep a while and try again to connect to server      if svr.cfg.LoginFailExit {        return err      }      util.RandomSleep(10*time.Second, 0.9, 1.1)    } else {      // login success      // !important 核心代码4      ctl := NewControl(svr.ctx, svr.runID, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter)      ctl.Run()      svr.ctlMu.Lock()      svr.ctl = ctl      svr.ctlMu.Unlock()      break    }}

在frp/cmd/client/service.go中

  • 核心代码3: for 循环不断去发起和服务端的连接,失败后会再次发起
  • 核心代码4: 连接成功后,客户端会使用连接的信息调用 NewControl()

frpc 和 frps 通信

frps 发起连接

func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, err error) {  xl := xlog.FromContextSafe(pxy.ctx)  // try all connections from the pool  for i := 0; i < pxy.poolCount+1; i++ {    // !important 核心代码5    if workConn, err = pxy.getWorkConnFn(); err != nil {      xl.Warn("failed to get work connection: %v", err)      return    }    xl.Debug("get a new work connection: [%s]", workConn.RemoteAddr().String())    xl.Spawn().AppendPrefix(pxy.GetName())    workConn = frpNet.NewContextConn(pxy.ctx, workConn)    ......    // !important 核心代码6    err := msg.WriteMsg(workConn, &msg.StartWorkConn{      ProxyName: pxy.GetName(),      SrcAddr:   srcAddr,      SrcPort:   uint16(srcPort),      DstAddr:   dstAddr,      DstPort:   uint16(dstPort),      Error:     "",    })  }}

在frp/server/proxy.go中

  • 核心代码5: frps从多个连接中通过依次遍历的方式来获取第一个成功获取到的连接
  • 核心代码6:frps通过获取到的连接向 frpc 发出 &msg.StartWorkConn 的消息,告诉frpc建立连接的相应信息

frpc 响应连接

func (pxy *tcpProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {  // !important 核心代码7  HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,    conn, []byte(pxy.clientCfg.Token), m)}

在frp/client/proxy/proxy.go中

  • 核心代码7:frpc接收到frps的信息后发起 TCP 连接

frps发送消息

func (ctl *Control) writer() {  xl := ctl.xl  defer func() {    if err := recover(); err != nil {      xl.Error("panic error: %v", err)      xl.Error(string(debug.Stack()))    }  }()  defer ctl.allShutdown.Start()  defer ctl.writerShutdown.Done()  encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.serverCfg.Token))  if err != nil {    xl.Error("crypto new writer error: %v", err)    ctl.allShutdown.Start()    return  }  for {    m, ok := <-ctl.sendCh    if !ok {      xl.Info("control writer is closing")      return    }    // !important 核心代码8    if err := msg.WriteMsg(encWriter, m); err != nil {      xl.Warn("write message to control connection error: %v", err)      return    }  }}

在frp/server/control.go中

  • 核心代码8: frps发送信息到 crypto.NewWriter() 创建的 writer 中

frpc 接收和响应

// !important 核心代码9func (ctl *Control) reader() {  xl := ctl.xl  defer func() {    if err := recover(); err != nil {      xl.Error("panic error: %v", err)      xl.Error(string(debug.Stack()))    }  }()  defer ctl.readerShutdown.Done()  defer close(ctl.closedCh)  encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Token))  for {    m, err := msg.ReadMsg(encReader)    if err != nil {      if err == io.EOF {        xl.Debug("read from control connection EOF")        return      }      xl.Warn("read error: %v", err)      ctl.conn.Close()      return    }    ctl.readCh <- m  }}
  • 核心代码9: frpc 读取 frps 转发的信息

到这里,我们的 frps 已经成功将公网中接收到的请求转发到 frpc 相应的端口了,这就是一个最简单的请求通过 frp 进行代理转发的流程。

总结

本文所介绍的内网穿透技术相关的实现方式其实在我们的日常开发生活中有更多的使用场景,当我们深入了解了当前 IP 地址以及内外网的实现方式后,我们不难发现,当我们将内网穿透的图片稍加修改后就成为了我们常用的另一种功能的实现方式(VPN实现原理):

原文作者:内网穿透你真的了解吗? - 掘金

来源地址:https://blog.csdn.net/a410974689/article/details/128469490

--结束END--

本文标题: 内网穿透你真的了解吗?

本文链接: https://www.lsjlt.com/news/384408.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 内网穿透你真的了解吗?
    前言 内网穿透作为程序员常用的调试手段之一,我们可以通过在个人电脑上运行花生壳或者 frp 等方式,让他人访问我们本地启动的服务,而且这种访问可以不受局域网的限制,当我们使用ngrok,frp等开源框架时,你是否有好奇过它神奇的作用?明明没...
    99+
    2023-08-31
    网络穿透 NAT 内网 后端开发 Linux服务器开发
  • 云服务器 vs. 内网穿透:了解它们的区别
    1. 云服务器:强大的远程计算资源 云服务器是一种基于云计算技术的虚拟服务器,它提供了强大的计算资源和存储能力,可以通过互联网远程访问和管理。云服务器通常由云服务提供商(如亚马逊AWS、微软Azure、谷歌云等)托管和管理,用户可以根据自...
    99+
    2023-10-27
    内网 区别 服务器
  • 云服务器 vs 内网穿透:了解两者的区别
    1. 云服务器 云服务器是一种基于云计算技术的虚拟服务器,它通过互联网连接到云服务提供商的数据中心。云服务器提供了弹性、可扩展和高可用性的计算资源,可以满足不同规模和需求的应用程序和服务。 云服务器的主要特点包括: 虚拟化:云服务器是在...
    99+
    2023-10-27
    内网 区别 服务器
  • C++的动态内存管理你真的了解吗
    目录前言用法上对内置类型对自定义类型new/delete底层原理重载类的专属operator new和 operator delete定位newnew/delete与malloc/f...
    99+
    2022-11-13
  • 你真的了解IP地址吗?
    本文已收录于专栏 ⭐️ 《计算机网络》⭐️ 学习指南: IP协议基本认识分类地址多播地址 无分类地址划分方式子网掩码 IP 分片与重组IPv6基本认识IPv4 首部与 ...
    99+
    2023-09-09
    tcp/ip 网络 服务器 网络协议 ip
  • 你真的需要了解Bash吗?
    Bash是一种强大的命令行解释器,它是Linux和macOS操作系统上的默认Shell。尽管它是如此重要,但很多人并不了解Bash的实际用途。在本文中,我们将探讨Bash的重要性以及它如何帮助我们更好地管理和控制计算机系统。 Bash是什么...
    99+
    2023-08-15
    path bash http
  • 关于Java Object你真的了解了吗
    导读: 在平时的coding中hashCode()和equals()的使用的场景有哪些?clone深复制怎么实现?wait()和notify()有什么作用?finalize()方法干嘛的?看似coding中使用的不多,不重要,但是有没有跟我...
    99+
    2023-05-31
    java object ava
  • java的泛型你真的了解吗
    目录泛型的概述和优势自定义泛型类自定义泛型方法自定义泛型接口泛型通配符、上下限总结泛型的概述和优势 泛型概述 泛型:是JDK5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检...
    99+
    2022-11-13
  • 关于@Autowierd && @Resource 你真的了解吗
    目录关于@Autowierd && @Resource 你真的了解吗1.@Autowierd2.@Resource2.1@Resource具体装配顺序,即指定的注入实...
    99+
    2022-11-12
  • Vue的过滤器你真了解吗
    目录1.过滤器1.1对过滤器的理解1.2全局过滤器:1.3局部过滤器:1.4过滤器的案例总结1. 过滤器 案例中使用到时间格式相关API 1.1 对过滤器的理解 定义:对要显示的数据...
    99+
    2022-11-13
  • JavaVolatile关键字你真的了解吗
    目录正文并发编程的三要素1.原子性2.可见性3.有序性VolatileVolatile 的内存模型Volatile 的实现原理(1) lock(2) unclock(3) read(...
    99+
    2022-11-12
  • Java 缓存:你真的了解它吗?
    在 Java 开发中,缓存是一个非常重要的概念。它可以大大提升应用程序的性能,减少对底层资源的占用,提高用户体验。但是,你真的了解 Java 缓存吗?本文将为大家介绍 Java 缓存的基本概念、分类、应用场景以及常见的缓存框架。 一、缓存...
    99+
    2023-10-06
    缓存 学习笔记 面试
  • 目前主流的内网穿透方式 你知道几个
    有时候,我们在外想要访问家里或公司主机的资料,要么由于主机处于内网下,要么就是是运营商随机分配的一个公网IP,都很难直接连上主机获取资料。那么,有什么办法可以解决这一难题?答案就是 内网穿透。当内网中的主机没有静态IP地址要被外网...
    99+
    2023-06-04
  • 内建质量,你真的了解么?
        内建质量定义     内建质量作用在开发过程中,要求软件生命周期之间参与的各个角色都需要实时的对软件的质量负责。确保软件在交付到下一环节前已经有了基础的质量保证。其核心目的就是减少因为质量问题导...
    99+
    2019-07-05
    内建质量,你真的了解么?
  • C++的运算符你真的了解吗
    目录前言1 算术运算符2 赋值运算符3 比较运算符4 逻辑运算符总结前言 运算符的作用:用于执行代码的运算 主要有: 1 算术运算符 用于处理四则运算 对于前置递增:将递增运算前...
    99+
    2022-11-13
  • Golang中的泛型你真的了解吗
    目录什么是泛型为什么需要泛型泛型语法类型参数类型集类型推断总结Golang 在 1.18 版本更新后引入了泛型,这是一个重要的更新,Gopher 万众瞩目,为 Golang 带来了更...
    99+
    2023-05-20
    Golang泛型用法 Golang泛型学习 Golang泛型 Go 泛型
  • Java和JavaScript的异同,你真的了解吗?
    Java和JavaScript是两种非常常见的编程语言,它们在很多方面有着相似的特性,但它们也有着很多不同之处。在本文中,我们将探讨Java和JavaScript的异同,并且演示一些代码示例来帮助你更好地理解它们。 Java和JavaS...
    99+
    2023-10-24
    javascript http 重定向
  • C++中的函数你真的理解了吗
    目录1 概述2 函数的定义及调用3 值传递4 函数的常见形式5 函数的声明6 函数的分文件编写作用:让代码结构更加清晰1.2.3.4.总结1 概述 作用:将一段经常使用的代码进行封装...
    99+
    2022-11-13
  • C++中的数组你真的理解了吗
    目录1 概述2 一维数组2.1 一维数组定义方式2.2 一维数组组名2.3 冒泡排序3 二维数组3.1 二维数组定义方式3.2 二维数组数组名3.3二维数组应用举例总结1 概述 所谓...
    99+
    2022-11-13
  • C++的数据类型你真的了解吗
    目录前言1 整型2 sizeof关键字3 实型(浮点型)4 字符型5 转义字符6 字符串型7 布尔类型 bool8 数据的输入总结前言 C++不像python,创建变量的时候必须指定...
    99+
    2022-11-13
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作