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中本地代理服务器UMLProxyServer为最底层的基类,它实现了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接口,整个建立的流程如下:
代理服务器与远程服务器建立连接
至此,整个通道便建立起来了。数据便可以通过本地应用-本地代理服务器-远程服务器建立起来了。
网友评论