NEKit中数据是如何流动的(一)

作者: 秦砖 | 来源:发表于2017-07-27 16:56 被阅读1456次

NEKit是iOS平台上一款开源的网络代理方案,可以用来实现使用私有协议的VPN,如ShadowSocks。这里主要分享下对其源码关于ShadowSocks部分的研究心得。

GCDAsyncSocket

NEKit中默认使用GCDAsyncSocket来抽象客户端与服务端的Socket通过过程。那么GCDAsyncSocketDelegate是必须要了解协议,它是数据流通的引擎。整个项目中数据的流动都是以它为核心来驱动的。这里给出协议中几个关键的接口定义:

//新的socket连接到服务器
public func socket(_ sock: GCDAsyncSocket, didAcceptNewSocket newSocket: GCDAsyncSocket)
//建立与指定host和端口主机的Socket连接
public func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16)
//从socket中读取数据成功后回调
func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int)
//向socket中写入数据成功后回调
func socket(_ sock: GCDAsyncSocket, didWriteDataWithTag tag: Int)

主体架构

NEKit开源项目ReadMe中有下面一张图,比较形象的描述了项目的基本框架。


NEKit框架图

从图中可以看出整个数据流动可以分成这样几个部分:本地代理服务器proxyServer的建立、应用将数据发往代理服务器、代理服务器将数据发往远程服务器,远程服务器将数据按原路径返回到应用。但该图并没有很好地体现从应用到远程服务器这一整个流程实际上是一个Tunnel。

proxyServer建立

let server = GCDHTTPProxyServer(address: IPv4Address(fromString: "127.0.0.1"), port: Port(port: 9090)
try! server.start()

开发者调用上面的代码就可以在本地启动一个IP为127.0.0.1端口为9090的代理服务器。

NEKit中本地代理服务器UML

ProxyServer为最底层的基类,它实现了TunnelDelegate与GCDAsyncSocketDelegate两个协议,实现了Socket协议中的两个方法。
ProxyServer中包含了address/port/tunnels等属性,address/port毫无疑问是初始化时传入的ip与端口值。tunnels是一个数组,它正是后面要说的通道对象的集合,即主体架构图中ProxyServer下方那一个又一个的Tunnel.
初始化用host与port将ProxyServer对象建立起来了,不过tunnels中什么都没有,Tunnel是如何建立的呢,接着看start流程。

通道建立

strart方法在NEKit的私有queue中新建了一个socket对象,并监听其它socket的连接请求。

override open func start() throws {
        try QueueFactory.executeOnQueueSynchronizedly {
            listenSocket = GCDAsyncSocket(delegate: self, delegateQueue: QueueFactory.getQueue(), socketQueue: QueueFactory.getQueue())
            try listenSocket.accept(onInterface: address?.presentation, port: port.value)
            try super.start()
        }
    }

新的Socket连接请求到来时,GCDAsyncSocketDelegate协议中的socket:didAcceptNewSocket:接口被调用。在新连接GCDAsyncSocket对象的基础之上,一个GCDTCPSocket对象被创建出来,然后在GCDHTTPProxyServer的handleNewGCDSocket接口中又被封装成了HTTPProxySocket对象,又进入到ProxyServer的didAcceptNewSocket接口中,在这里Tunnel对象被创建出来,将被填充到tunnels数组中。是不是有点晕,晕就来看代码吧。

 func socket(_ sock: GCDAsyncSocket, didAcceptNewSocket newSocket: GCDAsyncSocket) {
        let gcdTCPSocket = GCDTCPSocket(socket: newSocket)
        handleNewGCDSocket(gcdTCPSocket)
 }

override func handleNewGCDSocket(_ socket: GCDTCPSocket) {
        let proxySocket = HTTPProxySocket(socket: socket)
        didAcceptNewSocket(proxySocket)
}

func didAcceptNewSocket(_ socket: ProxySocket) {
        observer?.signal(.newSocketAccepted(socket, onServer: self))
        let tunnel = Tunnel(proxySocket: socket)
        tunnel.delegate = self
        tunnels.append(tunnel)
        tunnel.openTunnel()
}

到此,通道建立完毕了,当然openTunnel中存在非常非常多的细节。在深入细节之前,必须要搞清楚项目中一些对象间的关系。

ProxySocket

ProxySocket类系

ProxySocket作用于本地应用与本地代理服务器之间,用于二者间的通信。

在上一节中说到,GCDTCPSocket对象是由GCDAsyncSocket对象封装而成,HTTPProxySocket由GCDTCPSocket封装而成。

RawTCPSocketDelegate是GCDAsyncSocketDelegate的一个定制版,用于Socket连接/读写/取消连接后向外界发送通知。ProxySocket实现了RawTCPSocketDelegate协议,GCDTCPSocket实现了GCDAsyncSocketDelegate协议。

HTTPProxySocket是GCDTCPSocket的代理类,它代理了SocketProtocol协议,GCDTCPSocket是该协议的真正实现类。

HTTPProxySocket与GCDTCPSocket对象间相互引用,组成了一个环状结构,当然这里不存在内存泄露。

一个典型的数据流动从Tunnel中发起,经HTTPProxySocket与GCDTCPSocket中转,最终在GCDAsyncSocket中执行完毕后通过GCDAsyncSocketDelegate回调,沿原路返回,最终回到Tunnel中决定下一步的动作。


典型的数据流动

AdapterSocket

AdapterSocket作用于本地代理服务器与远程代理服务器之间,是二者通过的媒介。与ProxySocket有着相似的架构,不同的地方在于,AdapterSocket对象是通过工厂方法创建出来的。


AdapterSocket类系

数据流动也是同样沿着Tunnel-AdapterSocket-GCDTCPSocket-GCDAsyncSocket这条路径。这里列出一些关键性代码:


本地代理服务器与远程服务器连接

上面两个Socket体系弄清楚关系之后,继续上面Tunnel建立的话题,进入openTunnel接口中探寻更多的细节。

func openTunnel() {
        guard !self.isCancelled else {
            return
        }
        
        self.proxySocket.openSocket()
        self._status = .readingRequest
        self.observer?.signal(.opened(self))
    }

self.proxySocket对象即是上面所说的HTTPTCPSocket对象,按上节的分析,最终会从新连接的socket中读取数据并返回到prxySocket对象中的didRead接口中来。

override public func didRead(data: Data, from: RawTCPSocketProtocol) {
        ...
        
        switch (readStatus, result) {
        case (.readingFirstHeader, .header(let header)):
            currentHeader = header
            currentHeader.removeProxyHeader()
            currentHeader.rewriteToRelativePath()
            
            destinationHost = currentHeader.host
            destinationPort = currentHeader.port
            isConnectCommand = currentHeader.isConnect
            
            if !isConnectCommand {
                readStatus = .pendingFirstHeader
            } else {
                readStatus = .readingContent
            }
            
            session = ConnectSession(host: destinationHost!, port: destinationPort!)
            observer?.signal(.receivedRequest(session!, on: self))
            delegate?.didReceive(session: session!, from: self)
        ...
        }
    }

从数据包中获取到目标的主机地址与端口号,封装成ConnectSession对象,并回传给Tunnel对象的didReceive接口,整个建立的流程如下:


代理服务器与远程服务器建立连接

至此,整个通道便建立起来了。数据便可以通过本地应用-本地代理服务器-远程服务器建立起来了。

相关文章

  • NEKit中数据是如何流动的(一)

    NEKit是iOS平台上一款开源的网络代理方案,可以用来实现使用私有协议的VPN,如ShadowSocks。这里主...

  • 重磅发布:隐私计算知识图谱 | hellompc

    如何应用海量的数据,实现数据流动,同时能够保护数据隐私安全、防止敏感信息泄露是当前大数据应用中的重大挑战。隐私计算...

  • Flux&Redux

    Flux 解决传统MVC设计模式数据流动混乱问题,来源于Facebook。 在Flux中,数据是单向流动的 Flu...

  • React-redux

    Redux的出现是为了解决state里的数据问题。在React中,数据在组件中是单向流动,数据从一个方向(父组件)...

  • 业务流程

    在传统仓储企业中,物料的流动是企业重要的流动之一,为了更好的管理物料的流动,企业设定了很多必要的规章制度,包括如何...

  • <HTTP权威指南>读书笔记 ---- HTTP

    HTTP报文 报文是如何流动的 HTTP报文是在HTTP应用程序之间发送的数据块。这些数据块以一些文本形式的元信息...

  • Matic 数据流分析

    本文主要分析Matic数据流在 Ethereum <---> Heimdall <---> Bor 中的数据流动过...

  • Redux实现原理解析及应用

    1、为什么要用redux 在React中,数据在组件中是单向流动的,数据从一个方向父组件流向子组件(通过props...

  • cell tracking 入门

    一、如何label数据集以及数据集如何放入network中 1、网上已有的数据集是如何标注groundtruth的...

  • 常见vue面试题

    1. 解释单向数据流和双向数据绑定单向数据流: 数据流是单向的。数据流动方向可以跟踪,流动单一,追查问题的时候可以...

网友评论

  • b99fc8b4f30b:proxyServer建立 后 怎么调用到GCDAsyncSocketDelegate的代理里 求大神指点 方便的话能给点代码提示
  • 海边卡夫卡:请问一下大佬,我现在如果想要监听是否有数据在走这个通道,需要怎么才能做到呢??
    b99fc8b4f30b:@海边卡夫卡 :joy:
    海边卡夫卡:@駃樂o 没有。。。
    b99fc8b4f30b:有没头绪分享下谢谢
  • 952afa61a38f:楼主,请教一个问题,有没有办法知道在vpn服务不能试用的情况下,客户端断开vpn连接呢
  • tausha:请教下,try! self.proxyServer.start() 启动后,有没有被动式回调可以知道服务器,启动成功?
  • 灵界小灵:@秦砖 请问是哪个API呢,我看的苹果直接提供的API NEVPNManager只有ikev2和IPSec两种,使用networkextension是支持自定义的,但是L2TP VPN不知道怎么配置:sob::sob:
  • 灵界小灵:楼主,这个NEKit能实现访问的公司内网吗,L2TP类型的VPN
    秦砖:@灵界小灵 苹果不是提供有l2tp型vpn的api吗?

本文标题:NEKit中数据是如何流动的(一)

本文链接:https://www.haomeiwen.com/subject/xkznkxtx.html