深入解析 PocketSocket:用 Objective-C 打造符合 RFC6455 的 WebSocket 客户端与服务端
【免费下载链接】hammerspoonStaggeringly powerful macOS desktop automation with Lua项目地址: https://gitcode.com/gh_mirrors/ha/hammerspoon
导读
PocketSocket 是一个用 Objective-C 编写、面向 iOS 与 macOS 的 WebSocket 库,目标是让开发者用原生的 NSStream / CFSocket 技术栈构建实时通信应用。本篇文章围绕仓库内 Pods/PocketSocket/README.md 展开,完整讲解它的核心特性、三大组件(PSWebSocketDriver、PSWebSocket、PSWebSocketServer)、客户端与服务端实战用法、permessage-deflate 压缩扩展的实现原理,以及它作为 Hammerspoon 项目依赖被集成的方式。读完本文,你将掌握在 iOS/OS X 应用中以纯 Objective-C 代码完成 WebSocket 握手、收发消息、关闭连接和自定义 TLS 证书校验的完整方案,并理解这套“网络层与协议层解耦”的架构为什么比传统单文件实现更容易扩展。
项目概览:PocketSocket 是什么
PocketSocket 由 Zwopple Limited 开发,作者为 Robert Payne,是一个完全遵循 RFC6455 规范的 Objective-C WebSocket 库。它的设计目标非常明确:在 iOS 和 OS X 上,用系统自带的网络基础设施(CFNetwork、Foundation、Security、libz)构建一个同时覆盖客户端与服务端、并且支持压缩扩展的实时通信工具箱。
在 Hammerspoon 仓库中,PocketSocket 通过 CocoaPods 以pod 'PocketSocket/Client', '1.0.1'的形式被引入(见 Podfile),并且和 SocketRocket、CocoaAsyncSocket 等一起为 Hammerspoon 的自动化能力提供底层网络支持。也就是说,这篇文档所讲解的组件,实际已经作为依赖被打包进了这个桌面自动化工具的实时通信链路中。
核心特性
根据 README 的原始描述,PocketSocket 具备以下能力:
- 完全符合 RFC6455 WebSocket 协议,从握手到帧解析都遵循规范;
- 支持 permessage-deflate 压缩扩展(对应 RFC 7692 草案),可以在单个消息级别压缩 payload;
- 通过 Autobahn 测试套件约 519 个客户端与服务端用例,声明 100% 合规(其中少量服务端测试因收到畸形 payload 会提前断连,属于非严格模式下的正常表现);
- 同时提供客户端与服务端两种模式;
- TLS/SSL 支持,客户端可自动协商证书链,也支持自定义证书校验与证书锁定;
- 异步 IO,基于 NSInputStream / NSOutputStream 与 CFSocket 的非阻塞读写;
- 提供独立的
PSWebSocketDriver,让开发者可以“自带网络 IO”,把协议处理嵌入任何现有的 IO 架构中。
依赖框架
库的运行依赖以下系统框架(见 README 的 Dependencies 一节):
- CFNetwork.framework —— 服务端 CFSocket、CFStream 等底层网络设施;
- Foundation.framework —— NSURLRequest、NSStream 等基础类型;
- Security.framework —— TLS/SSL 证书校验(SecTrustRef);
- libSystem.dylib —— 系统 C 运行时与 socket 接口;
- libz.dylib —— zlib 压缩,供 permessage-deflate 的 deflate/inflate 使用。
三大组件架构:协议与网络彻底解耦
PocketSocket 最核心的架构思想,是把“协议处理”和“网络 IO”分离成两个独立的层。README 明确列出了三个主要组件:
| 组件 | 定位 | 职责 |
|---|---|---|
PSWebSocketDriver | 无网络依赖的协议引擎 | 把原始字节解析成事件,把发送事件编码成原始字节;负责握手请求/响应的生成与校验 |
PSWebSocket | 网络化 Socket 封装 | 基于 NSInputStream / NSOutputStream 维持连接,内部通过 Driver 处理输入输出 |
PSWebSocketServer | 基于 CFSocket 的服务器 | 绑定地址与端口接受连接,每个入站请求创建一个 PSWebSocket 实例 |
这种分层设计带来的直接好处是:如果你已经有了一套自有的网络栈(比如自定义的异步 socket 层、或者要嵌进游戏引擎),可以不使用PSWebSocket,直接把PSWebSocketDriver接在自己的 IO 上——README 称之为 “Bring your own networking IO”。
从源码看,PSWebSocket内部确实持有PSWebSocketDriver *_driver、PSWebSocketBuffer *_inputBuffer、_outputBuffer以及输入输出流(见 PSWebSocket.m),它只是负责把流上读到的字节喂给 Driver、把 Driver 写出的字节交给输出流,业务逻辑全部收敛在 Driver 层。
PSWebSocketDriver:协议引擎的源码级解析
PSWebSocketDriver是整套库的心脏。根据 PSWebSocketDriver.h 和 PSWebSocketDriver.m,它处理以下完整生命周期:
- 握手请求/响应(client 模式写出 GET + Upgrade 头,server 模式校验并写出响应);
- 把消息打包成 WebSocket 帧(含 FIN/RSV/opcode/mask/payload 各字段)写往对端;
- 把收到的帧逐字节解析并还原成消息;
- 通过内部状态机在
PSWebSocketDriverStateHandshakeRequest / HandshakeResponse / FrameHeader / FrameHeaderExtra / FramePayload之间迁移(见 PSWebSocketDriver.m)。
创建 Driver:客户端与服务端两个工厂方法
+ (instancetype)clientDriverWithRequest:(NSURLRequest *)request; + (instancetype)serverDriverWithRequest:(NSURLRequest *)request;- 客户端模式:传入一个
NSURLRequest,start后 Driver 会把它作为握手请求发出(源码中start在 client 模式调用writeHandshakeRequest,见 PSWebSocketDriver.m); - 服务端模式:传入对端发来的握手
NSURLRequest,Driver 负责校验请求头(如 Upgrade、Sec-WebSocket-Key 等)并生成规范的握手响应。
两种模式共享完全一致的 API,这正是 README 强调的“identical API for each mode”。
Driver 的对外动作 API
Driver 暴露了极简的操作接口:
- (void)start; - (void)sendText:(NSString *)text; - (void)sendBinary:(NSData *)binary; - (void)sendCloseCode:(NSInteger)code reason:(NSString *)reason; - (void)sendPing:(NSData *)data; - (void)sendPong:(NSData *)data; - (NSUInteger)execute:(void *)bytes maxLength:(NSUInteger)maxLength;其中execute:maxLength:是网络层喂入原始字节的入口:每收到一段字节就调用一次,Driver 内部返回本次消费的字节数。同时通过PSWebSocketDriverDelegate的driver:write:回调,把需要发出的字节交还给调用方。这个双向字节通道就是“自带网络 IO”的全部接口。
消息类型与状态码
与协议相关的枚举定义在 PSWebSocketTypes.h:
- 错误码(
PSWebSocketErrorCodes):Unknown、TimedOut、HandshakeFailed、ConnectionFailed; - 关闭状态码(
PSWebSocketStatusCode):Normal = 1000、GoingAway = 1001、ProtocolError = 1002、UnhandledType = 1003、NoStatusReceived = 1005、InvalidUTF8 = 1007、PolicyViolated = 1008、MessageTooBig = 1009; - 常量
PSWebSocketGUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)是 RFC6455 规定的 Sec-WebSocket-Accept 计算密钥,PSWebSocketErrorDomain是统一错误域。
另外,握手失败产生的NSError会在 userInfo 中携带PSHTTPStatusErrorKey(HTTP 状态码)和PSHTTPResponseErrorKey(完整的 CFHTTPMessageRef 响应),方便服务端排查 404 之类的握手拒绝原因。
客户端实战:PSWebSocket 的使用
README 给出了一个完整的 iOS AppDelegate 示例。客户端支持ws://与wss://两种协议,核心流程分三步:创建请求 → 创建 socket → open。
#import <PSWebSocket/PSWebSocket.h> @interface AppDelegate() <PSWebSocketDelegate> @property (nonatomic, strong) PSWebSocket *socket; @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; // create the NSURLRequest that will be sent as the handshake NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"wss://example.com"]]; // create the socket and assign delegate self.socket = [PSWebSocket clientSocketWithRequest:request]; self.socket.delegate = self; // open socket [self.socket open]; return YES; } #pragma mark - PSWebSocketDelegate - (void)webSocketDidOpen:(PSWebSocket *)webSocket { NSLog(@"The websocket handshake completed and is now open!"); [webSocket send:@"Hello world!"]; } - (void)webSocket:(PSWebSocket *)webSocket didReceiveMessage:(id)message { NSLog(@"The websocket received a message: %@", message); } - (void)webSocket:(PSWebSocket *)webSocket didFailWithError:(NSError *)error { NSLog(@"The websocket handshake/connection failed with an error: %@", error); } - (void)webSocket:(PSWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean { NSLog(@"The websocket closed with code: %@, reason: %@, wasClean: %@", @(code), reason, (wasClean) ? @"YES" : @"NO"); } @end客户端行为细节
README 对客户端行为做了三点补充,这些细节直接决定了生产环境的可靠性:
- 自动证书协商:
wss连接默认会从设备上的证书链自动协商证书;需要自定义 SSL 证书或做证书锁定(pinning)时,实现PSWebSocketDelegate的可选方法webSocket:evaluateServerTrust:即可,见 PSWebSocket.h。源码中在 TLS 握手完成后会取出kCFStreamPropertySSLPeerTrust对应的SecTrustRef,先走系统默认评估,若 delegate 实现了该方法则把决策权交给 delegate(见 PSWebSocket.m)。 - 默认请求压缩:客户端发出的握手请求总会携带 permessage-deflate 扩展声明;若服务端接受,整个连接期间所有消息都启用压缩。
- 超时语义:若初始
NSURLRequest设置了大于 0 的 timeoutInterval,则连接在该时间内未能建立就会被判定超时失败(对应错误码PSWebSocketErrorCodeTimedOut);若未设置,连接可能因系统行为一直等待。
PSWebSocket 完整 API 一览
结合 PSWebSocket.h,客户端可用能力还包括:
readyState:只读,反映Connecting(0) / Open(1) / Closing(2) / Closed(3)四态;delegateQueue:delegate 回调所在的 dispatch queue;inputPaused / outputPaused:暂停/恢复输入输出流,配合webSocketDidFlushInput:与webSocketDidFlushOutput:可选回调可实现背压控制;send::发送消息,接受 NSString 或 NSData 实例;ping:handler::发送 Ping 帧并注册收到对应 Pong 时的回调;close/closeWithCode:reason::默认以 1000 关闭,也可指定关闭码与原因;serverSocketWithRequest:inputStream:outputStream::服务端模式初始化,直接接管两条已打开的流;copyStreamPropertyForKey:/setStreamProperty:forKey::读写底层流的属性(如 kCFStreamProperty 常量),用于在打开前配置 TLS 等;打开后再调用 setter 会抛异常;remoteAddress/remoteHost:读取对端地址与主机名(见 PSWebSocket.m)。
服务端实战:PSWebSocketServer
服务端目前只支持ws://协议。它绑定到指定的 host 与端口,接受入站连接,解析每个连接中的首个 HTTP 请求,然后通过 delegate 询问是否接受并完成 WebSocket 握手。README 特别提醒:Server 必须是它创建的每个 PSWebSocket 实例的 delegate,不要自己接管或把这些 socket 从 Server 上摘除,否则会破坏内部状态管理。
完整示例:
#import <PSWebSocket/PSWebSocketServer.h> @interface AppDelegate() <PSWebSocketServerDelegate> @property (nonatomic, strong) PSWebSocketServer *server; @end @implementation AppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)notification { _server = [PSWebSocketServer serverWithHost:nil port:9001]; _server.delegate = self; [_server start]; } #pragma mark - PSWebSocketServerDelegate - (void)serverDidStart:(PSWebSocketServer *)server { NSLog(@"Server did start…"); } - (void)serverDidStop:(PSWebSocketServer *)server { NSLog(@"Server did stop…"); } - (BOOL)server:(PSWebSocketServer *)server acceptWebSocketWithRequest:(NSURLRequest *)request { NSLog(@"Server should accept request: %@", request); return YES; } - (void)server:(PSWebSocketServer *)server webSocket:(PSWebSocket *)webSocket didReceiveMessage:(id)message { NSLog(@"Server websocket did receive message: %@", message); } - (void)server:(PSWebSocketServer *)server webSocketDidOpen:(PSWebSocket *)webSocket { NSLog(@"Server websocket did open"); } - (void)server:(PSWebSocketServer *)server webSocket:(PSWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean { NSLog(@"Server websocket did close with code: %@, reason: %@, wasClean: %@", @(code), reason, @(wasClean)); } - (void)server:(PSWebSocketServer *)server webSocket:(PSWebSocket *)webSocket didFailWithError:(NSError *)error { NSLog(@"Server websocket did fail with error: %@", error); } @end要点拆解:
serverWithHost:nil port:9001:host 传 nil 表示绑定到本机所有可用地址,端口 9001 为监听端口;acceptWebSocketWithRequest:是接入控制点,返回 YES 才完成握手,可以在这一层做鉴权、路径白名单等策略;- 连接生命周期事件(open / message / close / fail)通过 delegate 回调暴露,和客户端侧的回调语义一一对应。
permessage-deflate:消息级压缩的实现内幕
这是 PocketSocket 相对大多数早期 Objective-C WebSocket 库的最大差异化能力。源码中PSWebSocketDeflater与PSWebSocketInflater分别封装 zlib 的 deflate / inflate,并挂在 PSWebSocketDriver.m 的_deflater/_inflater上。
关键实现细节(从源码可以确认):
- Driver 默认启用压缩(
_pmdEnabled = YES),客户端与服务端的滑动窗口位数初始值均为-11(对应 2KB 窗口,见 PSWebSocketDriver.m); - 发送时:对非控制帧且 payload 非空的消息,先执行 deflate,并根据是否协商了
no_context_takeover决定是否在每条消息前重置压缩上下文(见 PSWebSocketDriver.m); - 接收时:只有
rsv1位被置位(且压缩已启用)的帧才做 inflate;若压缩未协商却出现 rsv1 数据帧,会直接以PSWebSocketStatusCodeProtocolError拒绝(见 PSWebSocketDriver.m); - 握手阶段若 permessage-deflate 扩展参数协商失败(参数非法),会以
PSWebSocketErrorCodeHandshakeFailed失败并注明原因,相关错误字符串为 “invalid permessage-deflate extension parameters”(见 PSWebSocketDriver.m)。
对开发者而言,使用该特性是零成本的:客户端默认在握手时声明支持压缩,只要服务端(无论是否 PocketSocket 实现)在响应中确认该扩展,整条连接就自动启用;无需额外的 API 调用。
安装、测试与运行
通过 CocoaPods 安装
README 推荐的安装方式是 CocoaPods。在 Podfile 中加入依赖并执行安装:
pod 'PocketSocket' pod installHammerspoon 项目实际使用的是子系统化写法pod 'PocketSocket/Client', '1.0.1'(见 Podfile),对应 Podfile.lock 中的PocketSocket/Client+PocketSocket/Core两个子模块。仓库内 PocketSocket 相关源码位于 Pods/PocketSocket/PocketSocket/,共包含PSWebSocket.m、PSWebSocketDriver.m、PSWebSocketBuffer.m、PSWebSocketDeflater.m、PSWebSocketInflater.m、PSWebSocketNetworkThread.m、PSWebSocketUTF8Decoder.m以及各自的头文件。
运行 Autobahn 测试
Autobahn Test Suite 是 WebSocket 实现的行业标准合规测试。README 给出的步骤:
- 安装测试套件:
sudo pip install autobahntestsuite - 启动 Autobahn 模糊测试服务端:
wstest -m fuzzingserver - 在 Xcode 中运行测试用例
由于 README 声明通过了约 519 个客户端与服务端测试用例(其中部分服务端测试为非严格模式,遇到畸形 payload 会提前断开),这套测试流程正是验证协议合规性的标准做法。
为什么重写一个库:与 SocketRocket 的对比
README 的 “Why a new library?” 一节解释了 PocketSocket 的诞生动机。当时 Objective-C 生态中 WebSocket 客户端选择有限,最知名的 SocketRocket 存在两个痛点:
- 全部代码收敛在单个文件中,难以在此基础上新增 permessage-deflate、连接超时等特性;
- 网络层与协议层耦合过深,无法灵活适配已有工程。
PocketSocket 的对策是三管齐下:
- 提供丰富且易于深入修改的工具集——把网络层与 Driver 层解耦,方便把库嵌进任何现有架构;
- 持续跟进协议演进——README 承诺只要主流 WebSocket 扩展草案开始稳定,就会第一时间纳入(permessage-deflate 正是这一理念的产物);
- 客户端到服务端的完整图景——在一个解耦的工具包内同时覆盖 iOS 与 OS X 上的全部使用场景。
值得注意的是,Hammerspoon 的 Podfile 同时引入了 PocketSocket 和 SocketRocket,前者用于PocketSocket/Client子模块,后者则被 extensions/websocket/libwebsocket.m 的hs.websocketLua 扩展使用(#import <SocketRocket/SRWebSocket.h>)。这说明两者定位不同:SocketRocket 作为 hs.websocket 模块的客户端实现,PocketSocket 则服务于其他依赖方——从仓库证据看,PocketSocket 在此仓库中仅以 CocoaPods 依赖形式存在,源码实体位于 Pods/PocketSocket/,并未被 Hammerspoon 自身代码直接引用。
许可证与作者
PocketSocket 采用 Apache License 2.0 授权(见 Pods/PocketSocket/LICENSE),Copyright 2014-Present Zwopple Limited。作者为 Robert Payne,贡献者包括 Jens Alfke(Couchbase Lite 与 MacRuby 的作者,主要贡献了 TLS 相关的webSocket:evaluateServerTrust:支持)。Apache 2.0 允许自由使用、修改与再分发,只需保留版权声明与许可证文本,这使它非常适合嵌入商业应用。
结语
从 README 的完整论述到 PSWebSocketDriver.m 的状态机实现,PocketSocket 展示了“协议内核与网络 IO 解耦”这一架构思想在 WebSocket 场景下的实践价值:客户端、服务端与可插拔 Driver 三位一体,permessage-deflate 默认启用,TLS 证书可自定义校验,Autobahn 全量合规——这些能力让它成为 iOS/OS X 原生实时通信场景中一个值得深入研究的参考实现。如果你正在评估或维护 Objective-C 代码库中的 WebSocket 方案,可以从 PSWebSocket.h 与 PSWebSocketTypes.h 入手,沿着 Driver 的状态机逐层阅读,很快就能建立对 RFC6455 各帧类型与握手流程的完整认知。
【免费下载链接】hammerspoonStaggeringly powerful macOS desktop automation with Lua项目地址: https://gitcode.com/gh_mirrors/ha/hammerspoon
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考