Nodejs实现内网穿透服务

也许你很难从网上找到一篇从代码层面讲解内网穿透的文章,我曾搜过,未果,遂成此文。

1. 局域网内代理

我们先来回顾上篇,如何实现一个局域网内的服务代理?因为这个非常简单,所以,直接上代码。

const net = require('net')

const proxy = net.createServer(socket => {
  const localServe = new net.Socket()
  localServe.connect(5502, '192.168.31.130') // 局域网内的服务端口及ip。

  socket.pipe(localServe).pipe(socket)
})

proxy.listen(80)

这就是一个非常简单的服务端代理,代码简单清晰明了,如果有疑问的话,估计就是管道(pipe)这里,简单说下。socket是一个全双工流,也就是既可读又可写的数据流。代码中,当socket接收到客户端数据的时候,它会把数据写入localSever,当localSever有数据的时候,它会把数据写入socket,socket再把数据发送给客户端。

2. 内网穿透

局域网代理简单,内网穿透就没这么简单了,但是,它却是核心的代码,需要在其上做相当的逻辑处理。具体实现之前,我们先梳理一下内网穿透。

什么是内网穿透?

简单来说,就是公网客户端,可以访问局域网内的服务。比如,本地启动的服务。公网客户端怎么会知道本地启的serve呢?这里必然要借助公网服务端。那么公网服务端又怎么知道本地服务呢?这就需要本地和服务端建立socket链接了。

四个角色

通过上面的描述,我们引出四个角色。

  1. 公网客户端,我们取名叫client。
  2. 公网服务端,因为有代理的作用,我们取名叫proxyServe。
  3. 本地服务,取名localServe。
  4. 本地与服务端的socket长连接,它是proxyServe与localServe之前的桥梁,负责数据的中转,我们取名叫bridge。

其中,client和localServe不需要我们关心,因为client可以是浏览器或者其它,localServe就是一个普通的本地服务。我们只需要关心proxyServe和bridge就可以了。我们这里介绍的依然是最简单的实现方式,提供一种思路与思考,那我们先从最简单的开始。

bridge

我们从四个角色一节知道, bridge是一个与proxyServe之间socket连接,且是数据的中转,上代码捋捋思路。

const net = require('net')

const proxyServe = '10.253.107.245'

const bridge = new net.Socket()
bridge.connect(80, proxyServe, _ => {
  bridge.write('GET /regester?key=sq HTTP/1.1\r\n\r\n')
})

bridge.on('data', data => {
  const localServer = new net.Socket()
  localServer.connect(8088, 'localhost', _ => {
    localServer.write(data)
    localServer.on('data', res => bridge.write(res))
  })
})

代码清晰可读,甚至朗朗上口。引入net库,声明公网地址,创建bridge,使bridge连接proxyServe,成功之后,向proxyServe注册本地服务,接着,bridge监听数据,有请求到达时,创建与本地服务的连接,成功之后,把请求数据发送给localServe,同时监听响应数据,把响应流写入到bridge。

其余没什么好解释的了,毕竟这只是示例代码。不过示例代码中有段/regester?key=sq,这个key可是有大作用的,在这里key=sq。那么角色client通过代理服务访问本地服务的是,需要在路径上加上这个key,proxyServe才能对应的上bridge,从而对应上localServe。

例如:lcoalServe是:http://localhost:8088 ,rpoxyServe是example.com ,注册的key是sq。那么要想通过prxoyServe访问到localServe,需要如下写法:example.com/sq 。为什么要这样写?当然只是一个定义而已,你读懂这篇文章的代码之后,可以修改这样的约定。

那么,且看以下关键代码:

proxyServe

这里的proxyServe虽然是一个简化后的示例代码,讲起来依然有些复杂,要想彻底弄懂,并结合自己的业务做成可用代码,是要下一番功夫的。这里我把代码拆分成一块一块,试着把它讲明白,我们给代码块取个名字,方便讲解。
代码块一:createServe

该块的主要功能是创建代理服务,与client和bridge建立socket链接,socket监听数据请求,在回调函数里做逻辑处理,具体代码如下:

const net = require('net')

const bridges = {} // 当有bridge建立socket连接时,缓存在这里
const clients = {} // 当有client建立socket连接时,缓存在这里,具体数据结构看源代码

net.createServer(socket => {
  socket.on('data', data => {
    const request = data.toString()
    const url = request.match(/.+ (?<url>.+) /)?.groups?.url

    if (!url) return

    if (isBridge(url)) {
      regesterBridge(socket, url)
      return
    }

    const { bridge, key } = findBridge(request, url)
    if (!bridge) return

    cacheClientRequest(bridge, key, socket, request, url)

    sendRequestToBridgeByKey(key)
  })
}).listen(80)

看一下数据监听里的代码逻辑:

  1. 把请求数据转换成字符串。
  2. 从请求里查找URL,找不到URL直接结束本次请求。
  3. 通过URL判断是不是bridge,如果是,注册这个bridge,否者,认为是一个client请求。
  4. 查看client请求有没有已经注册过的bridge -- 记住,这是一个代理服务,没有已经注册的bridge,就认为请求无效。
  5. 缓存这次请求。
  6. 接着再把请求发送给bridge。

结合代码及逻辑梳理,应该能看得懂,但是,对5或许有疑问,接下来一一梳理。

代码块二:isBridge

判断是不是一个bridge的注册请求,这里写的很简单,不过,真实业务,或许可以定义更加确切的数据。

function isBridge (url) {
  return url.startsWith('/regester?')
}

代码块三:regesterBridge
简单,看代码再说明:

function regesterBridge (socket, url) {
  const key = url.match(/(^|&|\?)key=(?<key>[^&]*)(&|$)/)?.groups?.key
  bridges[key] = socket
  socket.removeAllListeners('data')
}
  1. 通过URL查找要注册的bridge的key。
  2. 把改socket连接缓存起来。
  3. 移除bridge的数据监听 -- 代码块一里每个socket都有默认的数据监听回调函说,如果不移除,会导致后续数据混乱。

代码块四:findBridge

逻辑走到代码块4的时候,说明这已经是一个client请求了,那么,需要先找到它对应的bridge,没有bridge,就需要先注册bridge,然后需要用户稍后再发起client请求。代码如下:

function findBridge (request, url) {
  let key = url.match(/\/(?<key>[^\/\?]*)(\/|\?|$)/)?.groups?.key
  let bridge = bridges[key]
  if (bridge) return { bridge, key }

  const referer = request.match(/\r\nReferer: (?<referer>.+)\r\n/)?.groups?.referer
  if (!referer) return {}

  key = referer.split('//')[1].split('/')[1]
  bridge = bridges[key]
  if (bridge) return { bridge, key }

  return {}
}
  • 从URL中匹配出要代理的bridge的key,找到就返回对应的bridge及key。
  • 找不到再从请求头里的referer里找,找到就返回bridge及key。
  • 都找不到,我们知道在代码块一里会结束掉本次请求。

代码块五:cacheClientRequest

代码执行到这里,说明已经是一个client请求了,我们先把这个请求缓存起来,缓存的时候,我们一并把请求对应的bridge、key绑定一起缓存,方便后续操作。

为什么要缓存client请求?

在目前的方案里,我们希望请求和响应都是成对有序的。我们知道网络传输都是分片传输的,目前来看,如果我们不在应用层控制请求和响应成对且有序,会导致数据包之间的混乱现象。暂且这样,后续如果有更好方案,可以不在应用层强制控制数据的请求响应有序,可以信赖tcp/ip层。
讲完原因,我们先来看缓存代码,这里比较简单,复杂的在于逐个取出请求并有序返回整个响应。

function cacheClientRequest (bridge, key, socket, request, url) {
  if (clients[key]) {
    clients[key].requests.push({bridge, key, socket, request, url})
  } else {
    clients[key] = {}
    clients[key].requests = [{bridge, key, socket, request, url}]
  }
}

我们先判断该bridge对应的key下是不是已经有client的请求缓存了,如果有,就push进去。

如果没有,我们就创建一个对象,把本次请求初始化进去。

接下来就是最复杂的,取出请求缓存,发送给bridge,监听bridge的响应,直到本次响应结束,在删除bridge的数据监听,再试着取出下一个请求,重复上面的动作,直到处理完client的所有请求。

代码块六:sendRequestToBridgeByKey

在代码块五的最后,对该块做了概括性的说明。可以先稍作理解,在看下面代码,因为代码里会有一些响应完整性的判断,去除这一些,代码就好理解一些。整个方案,我们没有对请求完整性进行处理,原因是,一个请求的基本都在一份数据包大小内,除非是文件上传接口,我们暂不处理,不然,代码又会复杂一些。

function sendRequestToBridgeByKey (key) {
  const client = clients[key]
  if (client.isSending) return

  const requests = client.requests
  if (requests.length <= 0) return

  client.isSending = true
  client.contentLength = 0
  client.received = 0

  const {bridge, socket, request, url} = requests.shift()

  const newUrl = url.replace(key, '')
  const newRequest = request.replace(url, newUrl)

  bridge.write(newRequest)
  bridge.on('data', data => {
    const response = data.toString()

    let code = response.match(/^HTTP[S]*\/[1-9].[0-9] (?<code>[0-9]{3}).*\r\n/)?.groups?.code
    if (code) {
      code = parseInt(code)
      if (code === 200) {
        let contentLength = response.match(/\r\nContent-Length: (?<contentLength>.+)\r\n/)?.groups?.contentLength
        if (contentLength) {
          contentLength = parseInt(contentLength)
          client.contentLength = contentLength
          client.received = Buffer.from(response.split('\r\n\r\n')[1]).length
        }
      } else {
        socket.write(data)
        client.isSending = false
        bridge.removeAllListeners('data')
        sendRequestToBridgeByKey(key)
        return
      }
    } else {
      client.received += data.length
    }

    socket.write(data)

    if (client.contentLength <= client.received) {
      client.isSending = false
      bridge.removeAllListeners('data')
      sendRequestToBridgeByKey(key)
    }
  })
}

从clients里取出bridge key对应的client。
判断该client是不是有请求正在发送,如果有,结束执行。如果没有,继续。
判断该client下是否有请求,如果有,继续,没有,结束执行。
从队列中取出第一个,它包含请求的socket及缓存的bridge。
替换掉约定的数据,把最终的请求数据发送给bridge。
监听bridge的数据响应。

  • 获取响应code

    • 如果响应是200,我们从中获取content length,如果有,我们对本次请求做一些初始化的操作。设置请求长度,设置已经发送的请求长度。
    • 如果不是200,我们把数据发送给client,并且结束本次请求,移除本次数据监听,递归调用sendRequestToBridgeByKey
  • 如果没有获取的code,我们认为本次响应非第一次,于是,把其长度累加到已发送字段上。
  • 我们接着发送该数据到client。
  • 再判断响应的长度是否和已经发送的过的数据长度一致,如果一致,设置client的数据发送状态为false,移除数据监听,递归调用递归调用sendRequestToBridgeByKey。

至此,核心代码逻辑已经全部结束。

总结

理解这套代码之后,就可以在其上做扩展,丰富代码,为你所用。理解完这套代码,你能想到,它还有哪些使用场景吗?是不是这个思路也可以用在远程控制上,如果你要控制客户端时,从这段代码找找,是不是会有灵感。
这套代码或许会有难点,可能要对tcp/ip所有了解,也需要对http有所了解,并且知道一些关键的请求头,知道一些关键的响应信息,当然,对于http了解的越多越好。
如果有什么需要交流,欢迎留言。

proxyServe源码

const net = require('net')

const bridges = {}
const clients = {}

net.createServer(socket => {
  socket.on('data', data => {
    const request = data.toString()
    const url = request.match(/.+ (?<url>.+) /)?.groups?.url

    if (!url) return

    if (isBridge(url)) {
      regesterBridge(socket, url)
      return
    }

    const { bridge, key } = findBridge(request, url)
    if (!bridge) return

    cacheClientRequest(bridge, key, socket, request, url)

    sendRequestToBridgeByKey(key)
  })
}).listen(80)

function isBridge (url) {
  return url.startsWith('/regester?')
}

function regesterBridge (socket, url) {
  const key = url.match(/(^|&|\?)key=(?<key>[^&]*)(&|$)/)?.groups?.key
  bridges[key] = socket
  socket.removeAllListeners('data')
}

function findBridge (request, url) {
  let key = url.match(/\/(?<key>[^\/\?]*)(\/|\?|$)/)?.groups?.key
  let bridge = bridges[key]
  if (bridge) return { bridge, key }

  const referer = request.match(/\r\nReferer: (?<referer>.+)\r\n/)?.groups?.referer
  if (!referer) return {}

  key = referer.split('//')[1].split('/')[1]
  bridge = bridges[key]
  if (bridge) return { bridge, key }

  return {}
}

function cacheClientRequest (bridge, key, socket, request, url) {
  if (clients[key]) {
    clients[key].requests.push({bridge, key, socket, request, url})
  } else {
    clients[key] = {}
    clients[key].requests = [{bridge, key, socket, request, url}]
  }
}

function sendRequestToBridgeByKey (key) {
  const client = clients[key]
  if (client.isSending) return

  const requests = client.requests
  if (requests.length <= 0) return

  client.isSending = true
  client.contentLength = 0
  client.received = 0

  const {bridge, socket, request, url} = requests.shift()

  const newUrl = url.replace(key, '')
  const newRequest = request.replace(url, newUrl)

  bridge.write(newRequest)
  bridge.on('data', data => {
    const response = data.toString()

    let code = response.match(/^HTTP[S]*\/[1-9].[0-9] (?<code>[0-9]{3}).*\r\n/)?.groups?.code
    if (code) {
      code = parseInt(code)
      if (code === 200) {
        let contentLength = response.match(/\r\nContent-Length: (?<contentLength>.+)\r\n/)?.groups?.contentLength
        if (contentLength) {
          contentLength = parseInt(contentLength)
          client.contentLength = contentLength
          client.received = Buffer.from(response.split('\r\n\r\n')[1]).length
        }
      } else {
        socket.write(data)
        client.isSending = false
        bridge.removeAllListeners('data')
        sendRequestToBridgeByKey(key)
        return
      }
    } else {
      client.received += data.length
    }

    socket.write(data)

    if (client.contentLength <= client.received) {
      client.isSending = false
      bridge.removeAllListeners('data')
      sendRequestToBridgeByKey(key)
    }
  })
}

到此这篇关于Nodejs实现内网穿透服务的文章就介绍到这了,更多相关Node 内网穿透内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SSH端口转发实现内网穿透的实现

    我们局域网的机器能够访问外网,但是外网不能访问内网.因为内网访问互联网时候能确定外网的地址,外网却不能确定我们局域网内的具体地址.(ip地址有限)如果 我们在访问外网的时候,这个链接让他保持.不断,那么这个链接就相当于我们建了一条路,内网数据能出去,外网数据能进来,ssh也是这个方法. 使用ssh命令链接公网服务器 1.首先在外网服务器上编辑sshd的配置文件 vim /etc/ssh/sshd_config #将GatewayPorts 开关打开 GatewayPorts yes 重启sshd

  • 详解基于docker搭建lanproxy内网穿透服务

    文档更新说明 2018年04月06日 v1.0 内网穿透相信是后端开发者经常遇到的需求,可是怎么实现呢?其实有现成的服务:花生壳.ngrok等,但是,最近花生壳宣布,免费版的内网穿透将不支持80端口映射了,而免费版的ngrok也不够稳定,于是乎,我就开始需找新的解决方案了 本文使用了docker.nginx,要全部搞懂的话需要一定的后端基础(当然,基本上入个门就可以了),个人认为还是有一定阅读门槛的,但是你如果只是想把服务搭建起来,按照步骤来做是不难的 1.概述 内网穿透其实就是用服务器做一个中

  • Docker内网穿透frp部署实现过程解析

    1.创建配置文件目录 cd /home目录 mkdir frp 最后的目录结构: 2.创建服务端配置文件 vi frps.ini [common] bind_port = 17000 token = myToken vhost_http_port = 10080 vhost_https_port = 10443 dashboard_port = 17500 dashboard_user = admin dashboard_pwd = admin tcp_mux = true max_pool_c

  • Nodejs实现内网穿透服务

    也许你很难从网上找到一篇从代码层面讲解内网穿透的文章,我曾搜过,未果,遂成此文. 1. 局域网内代理 我们先来回顾上篇,如何实现一个局域网内的服务代理?因为这个非常简单,所以,直接上代码. const net = require('net') const proxy = net.createServer(socket => { const localServe = new net.Socket() localServe.connect(5502, '192.168.31.130') // 局域网

  • 详解使用内网穿透工具Ngrok代理本地服务

    目录 1.官网地址 2.下载 3.添加白名单 4.使用 5.访问 我们在微信开发过程中,需要不断调试,微信服务器无法访问我们本地的接口,那么我们就需要一个内网穿透的工具,将我们本地的服务暴露出去使外部能够访问,内网穿透成功后,我们自己开发的电脑就相当于一台云服务器.常用的内网穿透工具有:ngrok.花生壳.Natapp.小米球等等,我平常使用的就是 ngrok 了. 1.官网地址 https://ngrok.com/ 在官网可以下载 ngrok 的安装包,和我使用的版本不一样,推荐使用我的资源文

  • 使用Python+Flask开发博客项目并实现内网穿透

    目录 前言 1.个人的注册与登录模块 2.首页文章展示模块 3.文章详情展示模块 4.文章发布模块 5.文章添加分类模块 6.文章分类管理模块 7.文章管理模块 8.用户个人信息注销模块 9.信息管理模块 10.程序启动模块 11.内网穿透模块 12.总结 前言 Flask是一个使用python编写的轻量级Web框架,对比其他相同类型的框架而言,这个框架更加的灵活轻便.并且具有很强的定制性,用户可以根据自己的需求添加功能,有强大的插件库,这也是为什么这个框架在python领域一直火热的原因.这篇

  • 超好用的免费内网穿透工具【永久免费不限制流量】

    目录 1. 安装cpolar内网穿透工具 1.1 Windows系统 1.2 Linux系统 1.2.1 安装 国内使用 1.2.2 向系统添加服务 1.2.3 启动服务 1.2.4 查看服务状态 2. 创建隧道映射内网端口 对于开发人员来讲,演示内网web站点.本地开发微信公众号.小程序开发.调试第三方支付系统对接等开发环境,往往需要一个环境可以进行调试. 而解决办法很简单,用内网穿透工具就可以,不需要自己搭建服务,也不需要公网IP.市面上内网穿透工具有很多,这里推荐一款简单又好用的————c

  • 高性能的内网穿透工具frp使用场景

    目录 什么是 frp 为什么使用 frp 安装 使用 使用场景 统一的服务端配置 SSH 连接内网服务器 暴露内网 HTTP 服务 TCP 类型 HTTP 类型 静态文件下载服务 本文将分享一个很好用的内网穿透工具 frp,以及一些具体的使用场景. 什么是 frp frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP.UDP.HTTP.HTTPS 等多种协议.可以将内网服务以安全.便捷的方式通过具有公网 IP 节点的中转暴露到公网. 为什么使用 frp 通过在具有公网 IP 的节点上

  • vscode远程开发使用SSH远程连接服务器的方法「内网穿透」

    目录 1.安装OpenSSH 2.vscode配置ssh 3. 局域网测试连接远程服务器 4. 公网远程连接 4.1 ubuntu安装cpolar 4.2 创建隧道映射 4.3 测试公网远程连接 5. 配置固定TCP端口地址 5.1 保留一个固定TCP端口地址 5.2 配置固定TCP端口地址 5.3 测试固定公网地址远程 远程连接服务器工具有很多,比如XShell.putty等,可以通过ssh来远程连接服务器,但这用于写代码并不方便,可能需要现在本地写好代码后再将源代码传送到服务器运行.服务器上

  • 公网使用SSH远程登录macOS服务器的过程(内网穿透)

    目录 前言 1. macOS打开远程登录 2. 局域网内测试ssh远程 3. 公网ssh远程连接macOS 3.1 macOS安装配置cpolar 3.2 获取ssh隧道公网地址 3.3 测试公网ssh远程连接macOS 4. 配置公网固定TCP地址 4.1 保留一个固定TCP端口地址 4.2 配置固定TCP端口地址 5. 使用固定TCP端口地址ssh远程 前言 macOS系统自带有Secure Shell 客户端,它可让您登录到侦听传入SSH连接的远程服务器和台式机.我们可以用ssh user

  • 使用PLSQL远程连接Oracle数据库的方法(内网穿透)

    目录 1.前言 2.Oracle数据库的安装 2.1 Oracle下载安装 2.2 Oracle连接测试 2.3 Cpolar下载安装 3.Cpolar端口设置 3.1 Cpolar云端设置 3.2.Cpolar本地设置 4.公网访问测试 5.结语 1.前言 数据库作为十分重要的资料管理和存储库,一致都受到各软件大厂关注:而为了更好的管理和组织越来越庞大的数据库里的数据,又衍生出不同派系的数据模型(如关系和非关系.层次和网状模型.面向对象和半结构化等等),令人眼花缭乱.随着数据库在现代社会中的作

随机推荐