最近在一个 iOS 项目中要用到 Socket 编程. Foundation 没有 Socket 模块. CoreFoundation 倒是有,但是我一直挺讨厌 CoreFoundation 的.看了一眼 CFSocket 相关 API,有种想吐的感觉. 作罢.
Swift 具有跟 C 语言互操作的特性,可以直接调用 C 库函数 . Socket C 库的 API 看起来挺简洁的,决定试着封装一下. 理想中的结果应该像下面的代码一样,简洁明了.
let addr = InternetAddress(hostname: "httpbin.org",
port: 80)
let socket = try TCPInternetSocket(address: addr)
try socket.connect()
try socket.send(data: "GET /\r\n\r\n".toBytes())
//receiving data
let received = try socket.recv()
//converting data to a string
let str = try received.toString()
//yay!
XCTAssertTrue(received.starts(with: "<!DOCTYPE html>".toBytes()), "Instead received: \(str)")
try! socket.close()
socket
在 Swift 中只要 import Darwin
就可以使用系统的 C 库了.
创建一个 macOS 命令行项目, 创建一个 Swift 源文件,直接就写.
输入 socket
几个字,就有补全出来了,但是除了类型提示信息,其他都是空. 点进头声明看看. Swift 翻译过来的声明是这样的.
public func socket(_: Int32, _: Int32, _: Int32) -> Int32
Swift 把 C 函数声明的参数名给丢了,而且也没有在前面加上注释.
按以往,对于不熟悉的函数一般是直接看下声明及其中的文档就基本可以用了. 不过对于导入到 Swift 的 C 头文件很多声明信息丢失了. 看来只有打开文档了.
直接在 man 手册页就可以查到 socket
函数的文档. 其实其声明是:
int socket(int domain, int type, int protocol);
这个函数声明看起来是挺简单的,但是选择合适的参数,也是要费一些脑筋的.
domain
似乎有点奇怪, 还有一系列 AF_XX
和 PF_XX
参数.
我知道 TCP 和 UDP, 但是并没有直接指定 TCP 或 UDP 的参数.
connect
与 struct sockaddr
相遇: 痛苦的源泉.初看之下 connect
函数好像挺简单的.
int connect(int socket, const struct sockaddr *address,socklen_t address_len);
但是马上就有了困惑,这个 address_len
是什么? 看起来像是说地址的长度, 为什么需要提供这个参数? 这个参数到底是什么呢? 文档也没有说. 先不管, 先搞定第二个参数 struct sockaddr
再说.
此时我沉浸在对于简单的结构的想像中,如 struct point
直接 struct point p = {3,4}
就可以构造一个了.然而不幸的是, man 手册没有struct sockaddr
这个结构的说明.
直接打开 Swift 中导入的 struct sockaddr
声明.
/*
* [XSI] Structure used by kernel to store most addresses.
*/
public struct sockaddr {
public var sa_len: __uint8_t /* total length */
public var sa_family: sa_family_t /* [XSI] address family */
public var sa_data: (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8) /* [XSI] addr value (actually larger) */
public init()
public init(sa_len: __uint8_t, sa_family: sa_family_t, sa_data: (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8))
}
毫不夸张的说,看着有点傻眼.特别是这个 sa_data
到底要传什么? 还有为什么这里 sa_len
又要传什么长度? sa_family
是指协议簇吗? 可以我创建 socket
的时候已经声明过了啊?
此时,我觉得光看文档, 看头文件声明,已经不能继续把代码写下去了.
后来经过 Google,我发现 Socket 编程的这个地址处理的水很深.
涉及到的结构有这么一些:
- struct sockaddr
- struct sockaddr_in
- struct sockaddr_in6
- struct in_addr
- struct addrinfo
- struct sockaddr_storage
每一个结构都是让人头晕的那种, 更何况我开始看的是 Swift 中翻译过来的声明. 我想静静.
后来我找到了 vapor/socks 这个 封装了 Socket 库的 Swift 库,代码和结构都非常 Nice,要使用 Swift 进行 TCP/UDP 编程的话,推荐使用这个库. 事实上,我刚开始贴的理想中的代码的样子,就是来自 socks 的一段测试代码.
如果想要搞清楚上面的系列 Socket 的地址结构. 不用多想, 直接找到 W. Richard Stevens 的 UNIX 网络编程 卷 1:套接字联网 API 一书. 翻到第三章: 套接字简介 (Sockets Introduction),然后就会找到答案了.
原来是想直接凭文档和声明接口就可以进行 Socket 编程的.
实践下来这是根本不可行的. Socket 库 从 上个世纪 80 年代到现在已经有 30 多年的历史了. 随着时代的发展,其 API 自然会发生这样那样的变化, 如果没有老司机带路, 这路不好走.
老实说, Socket 库从 80 年代到现在,API 基本没什么变化. 除了 sockaddr
这一块比较复杂之外, 其他的都比较简洁明了. 另外一个明显的复杂度主要来源于期望的偏差, Socket 编程不仅仅是用来进行 TCP/UDP 编程,它具有更强大,更通用的能力.
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.