使用Nodejs怎么实现内网穿透服务
这篇文章给大家介绍使用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链接了。
四个角色
通过上面的描述,我们引出四个角色。
公网客户端,我们取名叫client。
公网服务端,因为有代理的作用,我们取名叫proxyServe。
本地服务,取名localServe。
本地与服务端的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)
看一下数据监听里的代码逻辑:
把请求数据转换成字符串。
从请求里查找URL,找不到URL直接结束本次请求。
通过URL判断是不是bridge,如果是,注册这个bridge,否者,认为是一个client请求。
查看client请求有没有已经注册过的bridge -- 记住,这是一个代理服务,没有已经注册的bridge,就认为请求无效。
缓存这次请求。
接着再把请求发送给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')}
通过URL查找要注册的bridge的key。
把改socket连接缓存起来。
移除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怎么实现内网穿透服务就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341