文章目录
- WebSocket
- WebSocket特点
- SocketRocket
- 导入头文件设置代理
- SRWebSocket的初始化和建立连接
- SRWebSocketDelegate 代理方法实现
- 加上简单UI实现两个用户之间简单通信
- 浅看了一点点源码(理解的不深)
偶然之间了解到了利用WebSocket实现后端和前端的相互发送消息,就查了查在iOS里这个东西该怎么写,用舍友写的接口简单实现了两个用户的通信。
WebSocket
WebSocket
是一种在 Web 应用程序中实现双向通信的协议。它允许客户端和服务器之间建立一个持久性的连接,以便可以在任何时间点进行双向通信。- 传统的
HTTP
请求-响应模式只支持客户端发起请求,服务器做出响应。也就是说,当一个页面被加载时,它通常会发起一个HTTP
请求获取某些资源,例如HTML
、CSS
和JavaScript
文件等。一旦这些资源被发送到浏览器,连接就被关闭了,除非有其他请求发送。 - 但是,对于某些
Web
应用程序,需要更加实时的交互。这就是WebSocket
所提供的优势:它们允许客户端和服务器之间保持开放的连接,使得双方可以随时发送消息。 WebSocket
使用HTTP
协议进行握手,然后升级到WebSocket
协议。一旦这个升级完成,客户端和服务器将使用WebSocket
协议进行通信。WebSocket
通过使用标准的TCP/IP
套接字来实现,在网络上发送和接收数据。WebSocket
在很多场景下都非常实用,比如在线游戏、实时聊天、股票行情推送等,可以大大提高应用程序的交互效果和用户体验。
WebSocket特点
1. 建立在 TCP 协议之上,服务器端的实现比较容易。
2. 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
3. 数据格式比较轻量,性能开销小,通信高效。
4. 可以发送文本,也可以发送二进制数据。
5. 没有同源限制,客户端可以与任意服务器通信。
6. 协议标识符是ws(如果加密,则为wss),服务器网址就是URL。`ws://example.com:80/some/path`
7. 全双工通信。
SocketRocket
使用方法和之前的第三方库一样,利用cocoapods导库,具体操作就不详写了,在之前的博客里也写到过。
导入头文件设置代理
在需要用的地方导入#import <SocketRocket/SRWebSocket.h>
并遵循代理 SRWebSocketDelegate
声明一个属性 SRWebSocket *webSocket;
SRWebSocket的初始化和建立连接
self.socket = [[SRWebSocket alloc] initWithURLRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"ws://101.43.193.62:9090/ws?id=%d", self.senderSubscriberNumber]]]];
self.socket.delegate = self;
NSLog(@"Opening Connection...");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_async(queue, ^{
[self.socket open];
});
在这里看到,我为什么把 [self.socket open];
这句代码放在了一个异步并发队列里,在之前我直接在主线程调用open
方法,编译器给我报了一堆奇怪的东西,查找之后告诉我是线程优先级倒置导致的:
在iOS中,每个线程都有一个与之关联的Quality of Service(QoS)级别,用于指定它的相对优先级。 QOS_CLASS_USER_INTERACTIVE 表示线程需要立即响应并具有最高优先级,而 QOS_CLASS_DEFAULT 表示线程具有默认优先级。
这个问题的原因应该是这个第三方库底层的原因,我看了一些源码,发现调用了很多GCD的东西,暂时还没发现原因。
SRWebSocketDelegate 代理方法实现
//socket 连接成功
- (void)webSocketDidOpen:(SRWebSocket *)webSocket {
NSLog(@"Websocket Connected");
}
//socket 连接失败
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
NSLog(@":( Websocket Failed With Error %@", error);
self.socket = nil;
// 断开连接后每过1s重新建立一次连接
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self setSocket];
});
}
//socket连接断开
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
NSLog(@"WebSocket closed");
self.socket = nil;
}
//收到消息
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
NSLog(@"Received \"%@\"", message);
}
四个最常用方法分别表示,连接成功,连接失败,连接断开,和收到消息,我们在每个协议函数中做自己该做的事即可。
后端发给我的信息是这样的:
在接收到消息时本想用字典存储,转化的时候刚开始一直有问题,试了好多次才转化成功。
NSString *messageString = (NSString *)message;
NSData *messageData = [messageString dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *messageDictionary = [NSJSONSerialization JSONObjectWithData:messageData options:kNilOptions error:nil];
加上简单UI实现两个用户之间简单通信
浅看了一点点源码(理解的不深)
1.基本初始化方法
- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates
底层调用了_SR_commonInit
方法
//初始化
- (void)_SR_commonInit;
{
//得到url schem小写
NSString *scheme = _url.scheme.lowercaseString;
//如果不是这几种,则断言错误
assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]);
_readyState = SR_CONNECTING;
_webSocketVersion = 13;
//初始化工作的队列,串行
_workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
//给队列设置一个标识,标识为指向自己的,上下文对象为这个队列
dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL);
//设置代理queue为主队列
_delegateDispatchQueue = dispatch_get_main_queue();
//retain主队列?
sr_dispatch_retain(_delegateDispatchQueue);
//读Buffer
_readBuffer = [[NSMutableData alloc] init];
//输出Buffer
_outputBuffer = [[NSMutableData alloc] init];
//当前数据帧
_currentFrameData = [[NSMutableData alloc] init];
//消费者数据帧的对象
_consumers = [[NSMutableArray alloc] init];
_consumerPool = [[SRIOConsumerPool alloc] init];
//注册的runloop
_scheduledRunloops = [[NSMutableSet alloc] init];
....省略了一部分代码
}
整个过程:
- 判断协议类型
- 初始化了_workQueue,这个GCD队列用来处理主要的业务逻辑,包括处理错误、发送内容、关闭连接等。
- 初始化_delegateDispatchQueue,这个队列是用来向外发送通知的。我们可以通过
- (void)setDelegateOperationQueue:(NSOperationQueue*) queue
或- (void)setDelegateDispatchQueue:(dispatch_queue_t) queue
来自定义这个队列。
2.创建输入输出流
//初始化流
- (void)_initializeStreams;
{
//断言 port值小于UINT32_MAX
assert(_url.port.unsignedIntValue <= UINT32_MAX);
//拿到端口
uint32_t port = _url.port.unsignedIntValue;
//如果端口号为0,给个默认值,http 80 https 443;
if (port == 0) {
if (!_secure) {
port = 80;
} else {
port = 443;
}
}
NSString *host = _url.host;
CFReadStreamRef readStream = NULL;
CFWriteStreamRef writeStream = NULL;
//用host创建读写stream,Host和port就绑定在一起了
CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream);
//绑定生命周期给ARC _outputStream = __bridge transfer
_outputStream = CFBridgingRelease(writeStream);
_inputStream = CFBridgingRelease(readStream);
//代理设为自己
_inputStream.delegate = self;
_outputStream.delegate = self;
}
3.open
方法
//开始连接
- (void)open;
{
assert(_url);
//如果状态是正在连接,直接断言出错
NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once");
//自己持有自己
_selfRetain = self;
//判断超时时长
if (_urlRequest.timeoutInterval > 0)
{
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, _urlRequest.timeoutInterval * NSEC_PER_SEC);
//在超时时间执行
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
//如果还在连接,报错
if (self.readyState == SR_CONNECTING)
[self _failWithError:[NSError errorWithDomain:@"com.squareup.SocketRocket" code:504 userInfo:@{NSLocalizedDescriptionKey: @"Timeout Connecting to Server"}]];
});
}
//开始建立连接
[self openConnection];
}
//开始连接
- (void)openConnection;
{
//更新安全、流配置
[self _updateSecureStreamOptions];
//判断有没有runloop
if (!_scheduledRunloops.count) {
//SR_networkRunLoop会创建一个带runloop的常驻线程,模式为NSDefaultRunLoopMode。
[self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode];
}
//开启输入输出流
[_outputStream open];
[_inputStream open];
}
- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
{
[_outputStream scheduleInRunLoop:aRunLoop forMode:mode];
[_inputStream scheduleInRunLoop:aRunLoop forMode:mode];
//添加到集合里,数组
[_scheduledRunloops addObject:@[aRunLoop, mode]];
}
再往后就没看了,浅看了这两三个方法,后面还会调的方法:
didConnect
-
- 构建HTTP Header
-
- 发送HTTP Header
-
- 注册一个接收服务器返回Header信息的监听,并在回调内进行相应处理
- (void)safeHandleEvent:(NSStreamEvent)eventCode stream:(NSStream *)aStream
-
- 这是NSStream的回调方法,输入和输出流的共同回调
-
NSStreamEventOpenCompleted
连接打开;NSStreamEventHasBytesAvailable
可读取;NSStreamEventHasSpaceAvailable
可写入数据
-
- 在
NSStreamEventOpenCompleted
里面的[self _pumpScanner];
用来触发第5条中的3,来处理服务器返回的握手Header信息
- 在