本人有若干成套学习视频, 可试看! 可试看! 可试看, 重要的事情说三遍 包含Java
, 数据结构与算法
, iOS
, 安卓
, python
, flutter
等等, 如有需要, 联系微信tsaievan
.
GCDAsynSocket是一个建立在GCD上的TCP socket库, 这个项目同样包含基于Runloop的版本, 以及UDP socket库.
GCDAsynSocket项目是一个成熟的开源框架, 大约成型于2013年. 得益于广大网络开发者提交代码或者给出建议. 这个项目的目的是创建一个功能强大, 便于使用的socket库.
GCDAsynSocket的特点包括:
-
支持经典代理模式.
以下所有操作都会调用代理方法 : 连接, 接收, 读, 写, 进程, 断开连接, 错误处理等, 代理方法包含一个socket参数, 使得你便于区别不同的socket实例.
-
代理分发
每一个代理方法都会在可配置的线程被调用. 所以, 并发socket IO, 并发数据处理, 以及线程安全成为可能.
-
读写队列不阻塞, 超时可选
你告诉socket读什么, 写什么, 完成之后调用代理
-
socket自动接收
如果你告诉socket允许连接, 它就会调用代理告知连接成功, 当然, 你也可以立即断开连接
支持 IPv4和IPv6
支持SSL/TLS
基于GCD和kqueues的最新技术
-
在类中, 自己包含自己
你不必纠缠于各种流和sockets, 这个类都帮你处理了
GCDAsynSocket的一个强大特征之一就是它的队列结构, 这便于你控制这些socket, 而不必等到socket告诉你已经就绪. 例如:
// 开始异步连接
// 下面这个方法很快会return掉
// 连接完成之后, 代理方法socket:didConnectToHost:port: 就会立即被调用
[asyncSocket connectToHost:host onPort:port error:nil];
// 走到这一行代码时, socket还没有连接
// 只是试图开始异步连接.
// 但是这个框架就是设计用来让你便于使用socket的
// 你完全可以在这里就开始往socket里面读数据或者写数据
// 所以我们在这里发起我们消息头的读请求
// 读请求会自动被加入队列
// 等到socket连接成功后, 读请求会自动出列并执行
[asyncSocket readDataToLength:LENGTH_HEADER withTimeout:TIMEOUT_NONE tag:TAG_HEADER];
另外, 你可以发起多个 读/写 请求.
// 开始异步的写操作
[asyncSocket writeData:msgHeader withTimeout:TIMEOUT_NONE tag:TAG_HEADER];
// 我们不必等到上一个写操作完成, 就可以开始下一个写操作
[asyncSocket writeData:msgBody withTimeout:TIMEOUT_NONE tag:TAG_BODY];
// 开始异步读操作.
// 读且忽略欢迎信息
[asyncSocket readDataToData:msgSeparator withTimeout:TIMEOUT_NONE tag:TAG_WELCOME];
// 我们不必等到上一个读操作完成, 就可以开始下一个读操作
// 读服务器能力
[asyncSocket readDataToData:msgSeparator withTimeout:TIMEOUT_NONE tag:TAG_CAPABILITIES];
队列架构甚至扩展至可以支持SSL/TLS
// 发出 startTLS 确认 ACK.
// 记住, 这是一个异步操作
[asyncSocket writeData:ack withTimeout:TIMEOUT_NONE tag:TAG_ACK];
// 我们不必等到上一个写操作完成, 就可以开始发起startTLS
// startTLS请求会自动被加入队列
// 等到写操作完成之后, startTLS请求会自动出列并执行
// 一旦请求王城, SSL/TLS 就会自动升级并执行
[asyncSocket startTLS:tlsSettings];
// 再说一遍, 我们不会等到安全握手完成
// 我们可以立即将下一个操作加入队列
// 所以我们可以立即读取客户端的下一条请求
// 这个读请求会在安全连接之后发生
[asyncSocket readDataToData:msgSeparator withTimeout:TIMEOUT_NONE tag:TAG_MSG];
超时对于大多数操作来说是可选参数
另外, 你可能注意到了tag参数, 你传进去的tag参数, 在读写操作完成之后调用代理方法的时候再回传给你, 这个参数并不会写到socket里面, 再从socket里面写出来. 这样设计是为了方便你在代理方法中更简单地编码, 例如:
#define TAG_WELCOME 10
#define TAG_CAPABILITIES 11
#define TAG_MSG 12
...
- (void)socket:(AsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
if (tag == TAG_WELCOME)
{
// Ignore welcome message
}
else if (tag == TAG_CAPABILITIES)
{
[self processCapabilities:data];
}
else if (tag == TAG_MSG)
{
[self processMessage:data];
}
}
GCDAsyncSocket是线程安全的
执照
这个类是公开的
最初由 Robbie Hanson 于2010年第三季度创建
由 Deusty Designs 和 the Mac 开发社区维护
文档
代理方法
- socket: didConnnectToHost:port:
- socket: didReadData:WithTag:
- socket: didReadPartialDataOfLength:tag:
- socket: shouldTimeoutReadWithTag:elapsed:bytesDone:
- socket: didWriteDataWithTag:
- socket: didWritePartialDataOfLength:tag:
- socket: shouldTimeoutWriteWithTag:elapsed:bytesDone:
- socketDidSecure:
- socket: didAcceptNewSocket:
- newSocketQueueForConnectionFromAddress:onSocket:
- socketDidCloseReadStream:
- socketDidDisconnect:withError:
初始化
- init
- initWithSocketQueue:
- initWithDelegate:delegateQueue:
- initWithDelegate:delegateQueue:socketQueue:
配置
- delegate
- setDelegate:
- delegateQueue
- setDelegateQueue:
- getDelegate:delegateQueue:
- setDelegate:delegateQueue:
- autoDisconnectOnClosedReadStream
- setAutoDisconnectOnClosedReadStream:
- isIPv4Enabled
- setIPv4Enabled:
- isIPv6Enabled
- setIPv6Enabled:
- isIPv4PreferredOverIPv6
- setPreferIPv4OverIPv6:
接受
- acceptOnPort:error:
- acceptOnInterface:port:error:
连接
connectToHost: onPort: error:
connectToHost: onPort: withTimeout: error:
connectToHost: onPort: viaInterface: withTimeout: error:
ReadingreadDataWithTimeout: tag:
readDataWithTimeout: buffer: bufferOffset: tag:
readDataWithTimeout: buffer: bufferOffset: maxLength: tag:
readDataToLength: withTimeout: tag:
readDataToLength: withTimeout: buffer: bufferOffset: tag:
readDataToData: withTimeout: tag:
readDataToData: withTimeout: buffer: bufferOffset: tag:
readDataToData: withTimeout: maxLength: tag:
readDataToData: withTimeout: buffer: bufferOffset: maxLength: tag:
写
- writeData: withTimeout: tag:
诊断
- isDisconnected
- connectedHost
- connectedPort
- localHost
- localPort
- connectedAddress
- localAddress
- isIPv4
- isIPv6
断开连接
- disconnect
- disconnectAfterReading
- disconnectAfterWriting
- disconnectAfterReadingAndWriting
安全
- startTLS
高级
- performBlock:
- socketFD
- socket4FD
- socket6FD
- readStream
- writeStream
- sslContext
实用
- hostFromAddress:
- portFromAddress:
- getHost: port: fromAddress:
- CRLFData
- CRData
- LFData
- ZeroData
代理方法
GCDAsyncSocket是异步的, 所以对大多数方法来说, 当你在socket上开始一个操作时(连接, 接受, 读, 写), 方法都会立即返回, 操作的结果将会通过相关的响应代理方法返回给你.
socket: didConnectToHost: port:
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port
当socket成功连接并准备开始读写的时候调用, 主机参数是IP地址, 而不是DNS解析域名
socket: didReadData: withTag:
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
当socket完成读取请求数据进内存的时候调用, 如果发生错误, 则不会调用
tag参数是你请求读操作的时候传入的, 比如在readDataWithTimeout: tag: 方法中.
socket: didReadPartialDataOfLength: tag:
- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag
当socket还在读数据, 但是还没有完成读操作的时候调用. 当你使用readDataToData:或者 readDataToLength: 等方法时调用, 这个代理方法可以用在例如更新进度条等例子里.
tag参数是你请求读操作的时候传入的, 比如在readDataToLength: WithTimeout: tag: 方法中.
socket: shouldTimeoutReadWithTag: elapsed: bytesDone:
- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag
elapsed:(NSTimeInterval)elapsed
bytesDone:(NSUInteger)length
读操作超时, 未完成时调用. 这个方法允许你设置超时时长. 如果你返回一个正的时间间隔(>0), 读操作的超时就将按照设置的来进行, 如果你未实现这个代理方法, 或者返回一个非正时间间隔(<=0), 读操作的超时就跟平常一样
流逝时间(elapsed)参数是初始超时, 加上每一次调用这个方法时超时时间累加之和, 长度(length)参数表示到目前为止, 读操作已读的字节长度
注意: 如果你在这个代理方法中返回一个正数, 那么在一次读操作中, 这个方法会被调用多次
socket: didWriteDataWithTag:
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
当socket完成写入请求的时候调用, 如果发生错误, 则不会调用
tag参数是你请求读操作的时候传入的, 比如在writeDataWithTimeout: tag: 方法中.
socket: didWritePartialDataOfLength: tag:
- (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag
当socket已经写入一些数据, 但是还没有完成整个写操作时调用, 这个方法可能用于更新进度条之类的例子中
tag参数是你请求读操作的时候传入的, 比如在writeDataWithTimeout: tag: 方法
socket: shouldTimeoutWriteWithTag: elapsed: bytesDone:
- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag
elapsed:(NSTimeInterval)elapsed
bytesDone:(NSUInteger)length;
写操作超时, 未完成时调用. 这个方法允许你设置超时时长. 如果你返回一个正的时间间隔(>0), 写操作的超时就将按照设置的来进行, 如果你未实现这个代理方法, 或者返回一个非正时间间隔(<=0), 写操作的超时就跟平常一样
流逝时间(elapsed)参数是初始超时, 加上每一次调用这个方法时超时时间累加之和, 长度(length)参数表示到目前为止, 写操作已写入的字节长度
注意: 如果你在这个代理方法中返回一个正数, 那么在一次写操作中, 这个方法会被调用多次
socketDidSecure:
- (void)socketDidSecure:(GCDAsyncSocket *)sock
当socket成功完成SSL/TLS谈判的时候调用, 这个方法, 只有在你使用了startTLS方法之后才会调用, 否则不会调用
如果SSL/TLS谈判失败(例如证书无效等原因), socket将会立即关闭, 且socketDidDisconnect:withError:这个代理方法将会被立即调用, 错误代码就是SSL错误代码
在苹果的Security.framework中的SecureTransport.h文件中查阅SSL错误码以及其对应的含义
socket: didAcceptNewSocket:
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
当一个"服务器"socket接受一个连过来的"客户端"socket时, 将会调用此代理方法. 另一个socket将会产生去处理它.
你必须持有newSocket如果你希望保持连接的话, 否则, 新的socket实例将会被释放, 产生的连接将会关闭
默认新的socket将会使用同一个代理和代理队列, 当然, 你也可以随时改变它
默认socket将会产生自己内部的socket队列去运行. 这也是可以通过实现newSocketQueueForConnectionFromAddress:onSocket: method这个代理方法来配置
newSocketQueueForConnectionFromAddress: onSocket:
- (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock;
这个方法将会在socket:didAcceptNewSocket:方法之前立即调用, 这个方法允许监听socket去指定socketQueue(socket队列)给新的那个已经接受的socket, 如果这个方法没有实现, 或者返回NULL, 新的已经接受的socket将会创建一个自己的默认队列.
因为你无法自动释放一个dispatch_queue(派发队列), 这个方法使用"new"这个前缀在方法名前去指定这个返回的队列已经被持有了
因而你可以在实现中这样写:
return dispatch_queue_create("MyQueue", NULL);
如果你将多个socket放在同一个队列中, 那么就要注意, 每调用一次这个方法, 都要增加队列的引用计数
例如, 你的代码应该这样实现:
dispatch_retain(myExistingQueue);
return myExistingQueue;
socketDidCloseReadStream:
- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock
如果读流关闭将会调用此方法, 但写入流依然可写
这个方法只有在autoDisconnectOnClosedReadStream被设置为NO的时候才会调用, 看autoDisconnectOnClosedReadStream的讨论以获取更多信息
socketDidDisconnect: withError:
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error
当socket断开连接时调用, 不管是正常断开还是因为出错断开
如果你调用disconnect方法, 且socket还没有断开, 那么这个代理方法将会在disconnect方法return之前调用 (因为disconnect方法是同步的)
初始化
GCDAsyncSocket使用标准的代理范例, 但它在指定的代理分发队列中执行代理回调. 这使得在同一时间允许最大并发, 且保证线程安全
你必须在试图使用socket之前设置代理和代理队列, 否则将会报错
socket队列是一个分发队列, 在这个队列中, GCDAsyncSocket实例在这个队列中进行相应的操作. 你可以在初始化时可选地设置socket队列. 如果你不选择, 或者传入NULL, GCDAsyncSocket将自动创建它自己的队列, 如果你选择提供socket队列, 这个socket队列必须不是一个并发队列.
代理队列和socket队列可以是同一个
init
- (id)init
触发指定构造器, 参数值为nil. 你需要在使用socket之前设置代理以及代理队列
initWithSocketQueue:
- (id)initWithSocketQueue:(dispatch_queue_t)sq
以指定socket队列, 触发指定构造器. 你需要在使用socket之前设置代理以及代理队列
initWithDelegate: delegateQueue:
- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq
以给定的代理和代理队列触发指定构造器
initWithDelegate: delegateQueue: socketQueue:
- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq
指定构造器
以指定的代理和代理分发队列创建socket
socket分发队列是可选的. socket将在这条队列内部进行操作. 如果是NULL, 新的分发队列将自动被创建. 如果你选择提供一条socket队列, socket队列不能是并发队列
代理队列和socket队列可以是一样的
配置
delegate
- (id)delegate
返回当前socket设置的代理对象
setDelegate:
- (void)setDelegate:(id)delegate
推荐你在释放socket对象之前设置socket代理. 在disconnect方法中获取更多信息
delegateQueue
- (dispatch_queue_t)delegateQueue
返回当前socket设置的代理队列, 所有代理方法将在这个队列里异步调用
setDelegateQueue:
- (void)setDelegateQueue:(dispatch_queue_t)delegateQueue
为socket设置代理队列. 调用此方法后, 将来所有代理方法将被分发到指定队列
getDelegate: delegateQueue:
- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr
代理和代理队列通常同时出现, 这个方法提供了一个线程安全的途径在一个操作中去获取当前代理配置(包括代理及其队列)
setDelegate: delegateQueue:
- (void)setDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue
提供了一条简便且线程安全的方法在一个操作中去更改代理和代理队列
如果你打算更改代理和代理队列, 推荐使用这个方法
autoDisconnectOnClosedReadStream
- (BOOL)autoDisconnectOnClosedReadStream
一般来讲, socket不会关闭直到会话结束. 然而, 技术上可以实现远端关闭socket写入流. socket将会被通知没有更多的数据将要被读, 但socket将依然可写并且远端继续接收数据
这个令人困惑的实用参数阻止了一个观点, 那就是客户端在发送一个请求给服务器之后, 将关闭其写入流, 然后通知服务器, 没有更多的请求了. 实际上, 这一技术对服务器开发者没什么帮助
更糟糕的是, 从TCP的角度来说, 没有办法区别读流关闭和整个socket关闭, 这都将导致TCP栈收到一个FIN包. 唯一的方法区别就是依靠继续写入socket. 如果只有读流被关闭, 写将仍然可以进行, 否则, 将会发生错误(当远端发给我们一个RST包)
另外技术上的挑战和困惑是, 很多高级的socket/stream 没有提供解决此类问题的API. 如果读流被关闭, API将通知socket立即关闭, 同时也关闭写入流, 事实上, 苹果的CFStream就是这么做的. 起初看起来是一个糟糕的设计, 但事实上它简化了开发.
大部分情况下, 读流被关闭是因为远端关闭了socket. 因而在这个定上关闭socket就能够说得通了. 事实上, 这是大多数网络开发者希望看到的. 然而, 如果你在写一个服务器, 这个服务器跟过多的客户端交互, 你可能会遇到一个客户端, 使用这种不被鼓励的关闭写流的技术. 如果是这种情况的话, 你可以设置属性为NO, 然后利用 socketDidCloseReadStream 代理方法.
默认值为YES
setAutoDisconnectOnClosedReadStream:
- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag
设置autoDisconnectOnClosedReadStream配置选项, 详见autoDisconnectOnClosedReadStream方法
isIPv4Enabled
- (BOOL)isIPv4Enabled
默认情况下, IPv4和IPv6都可用的
对于接收连接时, 这意味着GCDAsyncSocket自动支持两种协议, 并且能够接收任意一种协议的链接
对于发送链接时, 这意味着GCDAsyncSocket能够连接运行任意一种协议的主机, 如果DNS解析返回IPv4结果, 那么GCDAsyncSocket自动使用IPv4, 如果DNS解析返回IPv6结果, 那么GCDAsyncSocket自动使用IPv6, 如果DNS解析返回IPv4结果和IPv6结果, 那么GCDAsyncSocket会使用优先的那一个, 优先的是IPv4, 但也可能是根据要求配置的.
setIPv4Enabled:
- (void)setIPv4Enabled:(BOOL)flag
启用和禁用IPv4的支持
注意: 如果socket已经连接或者接收连接, 更改这个属性无法影响当前socket, 只会改变以后的socket连接(当前的socket连接切断之后). 一旦设置, 这个偏好将会影响以后所有的基于GCDAsyncSocket实例的socket连接.
isIPv6Enabled
- (BOOL)isIPv6Enabled
默认情况下, IPv4和IPv6都可用的
对于接收连接时, 这意味着GCDAsyncSocket自动支持两种协议, 并且能够接收任意一种协议的链接
对于发送链接时, 这意味着GCDAsyncSocket能够连接运行任意一种协议的主机, 如果DNS解析返回IPv4结果, 那么GCDAsyncSocket自动使用IPv4, 如果DNS解析返回IPv6结果, 那么GCDAsyncSocket自动使用IPv6, 如果DNS解析返回IPv4结果和IPv6结果, 那么GCDAsyncSocket会使用优先的那一个, 优先的是IPv4, 但也可能是根据要求配置的.
setIPv6Enabled:
- (void)setIPv6Enabled:(BOOL)flag
启用和禁用IPv6的支持
注意: 如果socket已经连接或者接收连接, 更改这个属性无法影响当前socket, 只会改变以后的socket连接(当前的socket连接切断之后). 一旦设置, 这个偏好将会影响以后所有的基于GCDAsyncSocket实例的socket连接.
isIPv4PreferredOverIPv6
- (BOOL)isIPv4PreferredOverIPv6
默认情况下, 优先的协议是IPv4
setPreferIPv4OverIPv6:
- (void)setPreferIPv4OverIPv6:(BOOL)flag
设置有限的协议, 详情请看isIPv4Enabled方法
接收
一旦接收或者连接方法被调用, GCDAsyncSocket实例将被锁定, 其他的接收/连接将无法被调用, 除非socket连接断开
当socket接收一个连接的时候, GCDAsyncSocket调用以下代理方法(按时间先后顺序):
- newSocketQueueForConnectionFromAddress:onSocket:
- socket:didAcceptNewSocket:
你的服务器代码必须持有已经接收的socket(如果你想接收它的话), 否则, 新接收的socket将会在代理方法返回之后被销毁(此时socketDidDisconnect:withError:将会被调用)
acceptOnPort: error:
- (BOOL)acceptOnPort:(UInt16)port error:(NSError **)errPtr
告诉socket在指定端口监听和接收连接. 当一个连接被接收, 新的GCDAsyncSocket实例对象将被产生去处理这个连接, socket:didAcceptNewSocket: 这个代理方法将会被调用
这个socket将会在所有可得的接口监听(例如: wifi, 以太网等)
如果socket能够开始监听, 那么这个方法返回YES, 如果发生错误, 这个方法返回NO, 并且设置可选的errPtr参数. 例如, 代理没有设置, 或者已经接收连接, 都会导致这个发生错误
acceptOnInterface: port: error:
- (BOOL)acceptOnInterface:(NSString *)interface port:(UInt16)port error:(NSError **)errPtr
这个方法和acceptOnPort: error:差不多, 只是添加了可选参数去指定接口去监听
例如, 你可以指定socket只接收基于以太网的连接, 而不是其他连接, 例如wifi之上的
这个接口你可以通过名称来指定(例如 "en1" 或者 "lo0"), 或者通过IP地址来指定(例如 "192.168.4.34"). 你也可以通过特殊的字符串 "localhost" 或者 "loopback" 来指定socket值连接本地机器的socket
你可以通过命令行工具"ifconfig"查看这些接口列表, 或者通过编程, 利用 getifaddrs()这个函数来获取
如果要接收任何接口的连接, interface参数就传nil. 或者就简单地使用acceptOnPort: error:这个方法
如果socket能够开始监听, 那么这个方法返回YES, 如果发生错误, 这个方法返回NO, 并且设置可选的errPtr参数. 例如, 代理没有设置, 或者已经接收连接, 或者请求的接口找不到, 都会导致这个发生错误
连接
一旦接收或者连接方法被调用, GCDAsyncSocket实例将被锁定, 其他的接收/连接将无法被调用, 除非socket连接断开
connectToHost: onPort: error:
- (BOOL)connectToHost:(NSString *)host onPort:(UInt16)port error:(NSError **)errPtr
连接到指定主机和端口
这个方法将会调用connectToHost:onPort:viaInterface:withTimeout:error: 这个方法, 使用默认网络接口, 没有超时时长
返回YES表示异步的连接请求开始. 返回NO表示检测到发生错误并且会给errPtr参数赋值
connectToHost: onPort: withTimeout: error:
- (BOOL)connectToHost:(NSString *)host onPort:(UInt16)port withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr
连接到指定主机和端口, 超时时长可选
这个方法将会调用connectToHost:onPort:viaInterface:withTimeout:error: 这个方法, 使用默认网络接口
connectToHost: onPort: viaInterface: withTimeout: error:
- (BOOL)connectToHost:(NSString *)host
onPort:(UInt16)port
viaInterface:(NSString *)interface
withTimeout:(NSTimeInterval)timeout
error:(NSError **)errPtr
连接到指定主机和端口, 通过可选的网络接口, 以及可选的超时时长
主机名可以是域名(例如: "deusty.com") 或者 IP地址字符串 (例如 "192.168.0.2"). 网络接口可以是名字 (例如 "en1" 或者 "lo0")或者相应的IP地址 (例如 "192.168.4.35")
没有超时时长请传入负值
这个方法将会返回NO, 如果检测到错误的话,并且会给错误参数赋值(如果指定的话). 可能的错误是,一个空的主机, 无效的网络接口, 或者socket已经连接
如果没有检测到错误, 这个方法将会启动后台连接操作并立刻返回YES, 代理回调将被用来通知你socket连接成功, 或者找不到主机
由于这个类支持队列读和写, 你可以理科开始读或者写操作, 所有的读/写操作都将被加入队列, 在socket连接成功之后, 这些读写操作将会出列并且按顺序执行