socket 连接
即时通讯最大的特点就是实时性,基本感觉不到延时或是掉线,所以必须对socket
的连接进行监视与检测,在断线时进行重新连接,如果用户退出登录,要将socket
手动关闭,否则对服务器会造成一定的负荷。
一般来说,一个用户(对于ios来说也就是我们的项目中)只能有一个正在连接的socket
,所以这个socket
变量必须是全局的,这里可以考虑使用单例或是AppDelegate
进行数据共享,本文使用单例。如果对一个已经连接的socket
对象再次进行连接操作,会抛出异常(不可对已经连接的socket
进行连接)程序崩溃,所以在连接socket
之前要对socket
对象的连接状态进行判断
一 下载完包结构
RunLoop
和GCD
两个文件夹中有两套
一种基于NSRunloop
,一种基于GCD
,后面讲的都是用基于GCD
的CocoaAsyncSocket
,因为RunLoop
中的将被废弃
__deprecated_msg("The RunLoop versions of CocoaAsyncSocket are deprecated and will be removed in a future release. Please migrate to GCDAsyncSocket.")
二 项目中应用CocoaAsyncSocket
- 将
GCD
下四个文件拖入项目
GCDAsyncSocket.h
GCDAsyncSocket.m
GCDAsyncUdpSocket.h
GCDAsyncUdpSocket.m
- 创建单例类
// YHSocket.h
@interface YHSocket : NSObject
+ (instancetype)sharedSocket;
@end
// YHSocket.m
#import "YHSocket.h"
#import "GCDAsyncSocket.h"
@interface YHSocket ()<GCDAsyncSocketDelegate>
@property (nonatomic, strong) GCDAsyncSocket *asyncSocket;
@end
@implementation YHSocket
+ (instancetype)sharedSocket {
static YHSocket *scoket;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
scoket = [[self alloc] init];
});
return scoket;
}
/*
全局队列(代理的方法是在子线程被调用)
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)
主队列(代理的方法会在主线程被调用)
dispatch_get_main_queue()
代理里的动作是耗时的动作,要在子线程中操作
代理里的动作不是耗时的动作,就可以在主线程中调用
看情况写队列
*/
- (instancetype)init {
if (self = [super init]) {
_asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
}
return self;
}
- 然后写连接方法
添加端口和服务器地址属性
添加连接方法
// YHSocket.h
@interface YHSocket : NSObject
@property (nonatomic, assign) uint16_t port; // 端口
@property (nonatomic, copy) NSString *socketHost; // 服务器地址
+ (instancetype)sharedSocket;
- (void)startConnectSocket;
@end
实现
- (void)startConnectSocket {
NSError *error = nil;
[_asyncSocket acceptOnPort:self.port error:&error];
if (!error) {
NSLog(@"服务开启成功");
} else {
NSLog(@"服务开启失败 %@", error);
}
}
- 在
main.m
中测试
#import "YHSocket.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
YHSocket *socket = [YHSocket sharedSocket];
socket.port = 5528; // 测试端口
socket.socketHost = @"192.168.1.114"; // 我自己电脑IP
[socket startConnectSocket];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
运行后控制台打印
现在用的xcode8 beta2
打印东西多, 看关键哈~
此时已经连接成功
- 此时当程序停止运行的话 服务器就会停掉 实际中我们服务器会一直开启,365天不停止,开启运行循环
#import "YHSocket.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
YHSocket *socket = [YHSocket sharedSocket];
socket.port = 5528; // 测试端口
socket.socketHost = @"192.168.1.114"; // 我自己电脑IP
[socket startConnectSocket];
[[NSRunLoop mainRunLoop] run]; // 开启运行循环
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
- 接下来模拟客户端接入到服务器
遵守协议
@interface YHSocket ()<GCDAsyncSocketDelegate>
实现代理方法
/**
* 服务器监听到有客户端接入会调用这个代理方法
*
* @param sock 服务端
* @param newSocket 客户端
*
*/
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket {
NSLog(@"服务端 %@", sock);
NSLog(@"客户端 %@", newSocket);
}
代码运行 控制台输出
2016-07-15 21:53:29.517 Socket[7227:381191] 服务开启成功
终端中测试 用telnet
命令
此时已经连接成功
socket
通信流程图
看这句话 Connection closed by foreign host.
连接被外部服务器关闭了,服务器连上之后 还没来得及读和写就被释放了
因为 客户端
socket
对象是局部的, 被释放了所以我们要保存连接到服务器的客户端
定义数组 懒加载
@property (nonatomic, strong) NSMutableArray *clientSockets;// 客户端socket
- (NSMutableArray *)clientSockets {
if (_clientSockets == nil) {
_clientSockets = [NSMutableArray array];
}
return _clientSockets;
}
连接到服务器上后 存储起来
// 客户端socket 连接到服务器
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket {
NSLog(@"服务端 %@", sock);
NSLog(@"客户端 %@", newSocket);
[self.clientSockets addObject:newSocket];
}
此时我们在终端再次连接 发现 连接上之后就不再断开了
- 服务器接收客户端发送的数据
// 服务器读取客户端发送数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
NSLog(@"客户端 %@", sock);
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"data --- %@", dataStr);
}
// 在读取数据之前 服务端还需要监听 客户端有没有写入数据
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket {
NSLog(@"服务端 %@", sock);
NSLog(@"客户端 %@", newSocket);
[self.clientSockets addObject:newSocket];
// 监听客户端是否写入数据
// timeOut: -1 暂时不需要 超时时间 tag暂时不需要 传0
[newSocket readDataWithTimeout:-1 tag:0];
}
此时连接服务器 输入hello world
打印
WARNING: 读取一次数据 需要重新监听一次客户端的输入
不然再次发送数据 无法读取到
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
NSLog(@"客户端 %@", sock);
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"data --- %@", dataStr);
[sock readDataWithTimeout:-1 tag:0];
}
// 服务端接收到客户端发送的数据之后 返回数据给 客户端
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
NSLog(@"客户端 %@", sock);
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"data --- %@", dataStr);
// 服务端接收到客户端发送的数据之后 返回数据给 客户端
// 我们将接受到的数据返回回去
[sock writeData:data withTimeout:-1 tag:0];
[sock readDataWithTimeout:-1 tag:0];
}
输入 数据 终端直接返回数据 控制台会打印
上边所有客户端与服务器的交互 都是由客户端 与服务器连接上的客户端交互
服务端负责连接客户端
- 警告问题⚠️
使用CocoaAsyncSocket
“kCFStreamNetworkServiceTypeVoIP is deprecated in iOS 9 ” warning
解决方案
在iOS 9.0 中,kCFStreamNetworkServiceTypeVoIP
被弃用
解决方案:
- 1 导入` #import <PushKit/PushKit.h> `头文件
- 2 `r1`` r2` 中的 `kCFStreamNetworkServiceTypeVoIP` 替换为 “PKPushTypeVoIP”
r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, PKPushTypeVoIP);
r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, PKPushTypeVoIP);
相关链接:
https://github.com/robbiehanson/CocoaAsyncSocket/issues/402