说到即时通讯,就一定有绕不开微信、QQ这样在国内具有统治地位的社交app。但其实,即时通讯在app中的应用还是非常广泛的。做即时通讯第三方服务的也非常多,像环信这样的厂商。但是即时通讯这种涉及自家用户数据的功能,还是掌握在自己的手里比较好。更重要的一点事,第三方服务并非免费的。所以,能够自己来,干嘛要被别人掣肘。
本文介绍一种即时通讯的实现方式。
一、Socket:
自定义socket的全部核心代码都在下面图里了,不推荐这么干啊,反正大牛也不会看这里。因为github社区里有更强大、更简单的socket的API。
#import "SocketDemo.h"
#include
#include
#include
@interface SocketDemo ()
@property (nonatomic, assign) int fd;
@end
@implementation SocketDemo
- (void)creatSocketClientWith:(const char *)ip port:(__uint16_t)port {
int err;
//创建socket
int fd = socket(AF_INET, SOCK_STREAM, 0);
_fd = fd;
BOOL success = (fd != -1);
struct sockaddr_in addr;
if (fd != -1) {
memset(&addr, 0, sizeof(addr));
addr.sin_len = sizeof(addr);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
//建立地址和套接字的联系
err = bind(fd, (const struct sockaddr *)&addr, sizeof(addr));
success = (err == 0);
}
if (success) {
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_len = sizeof(serveraddr);
serveraddr.sin_family = AF_INET;
//服务器端口
serveraddr.sin_port = htons(port);
//服务器的地址
serveraddr.sin_addr.s_addr = inet_addr(ip);
socklen_t addrLen;
addrLen = sizeof(serveraddr);
err = connect(fd, (struct sockaddr *)&serveraddr, addrLen);
success = (err == 0);
if (success) {
// getsockname是对tcp连接而言。套接字socket必须是已连接套接字描述符。
err = getsockname(fd, (struct sockaddr *)&addr, &addrLen);
success = (err == 0);
if (success) {
NSLog(@"连接服务器成功");
[NSThread detachNewThreadSelector:@selector(reciveMessage:) toTarget:self withObject:@(fd)];
}
} else{
NSLog(@"connect failed");
}
}
}
- (void)reciveMessage:(id)peerfd {
int fd = [peerfd intValue];
char buf[1024];
ssize_t bufLen;
size_t len = sizeof(buf);
//循环阻塞接收消息
do {
bufLen = recv(fd, buf, len, 0);
//当返回值小于等于零时,表示socket异常或者socket关闭,退出循环阻塞接收消息
if (bufLen <= 0) {
break;
}
//接收到的信息
NSString *msg = [NSString stringWithCString:buf encoding:NSUTF8StringEncoding];
NSLog(@"来自服务端,消息内容:%@", msg);
} while (true);
// 7.关闭
close(fd);
}
- (void)sendData:(NSData *)data {
const uint8_t *buffer = (const uint8_t *)[data bytes];
write(_fd, buffer, (size_t)data.length);
}
@end
源码:socketDemo
参考:http://www.jb51.net/article/105715.htm
二、CocoaAsyncSocket
推荐使用的就是CocoaAsyncSocket啦。理由也很简单,首先Socket是c语言库,开发起来还是有一定难度;另外就是CocoaAsyncSocket是有github这个强力社区支持的开源代码,稳定可靠性是值得信赖的,封装了很多功能,使用起来也很简单。
首先:pod 'CocoaAsyncSocket' ,或者手动也是可以的,依赖'CFNetwork','Security'这两个库就可以啦。
1.即时通讯时典型的可以使用单例管理的。创建管理类,并创建单例、初始化对象
+ (instancetype)sharedInstance {
static id instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[SocketDemo alloc] init];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
_rwQueue = dispatch_queue_create("com.enuui.read_write.queue", DISPATCH_QUEUE_SERIAL);
_delegateQueue = dispatch_queue_create("com.enuui.delegate.queue", DISPATCH_QUEUE_SERIAL);
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:_delegateQueue];
[self GCDTimer];
}
return self;
}
2.接下来就是连接socket啦。start、stop、和reStart是暴露出去使用的方法。host和port是不会变化的,所以用的宏定义。
注:如果对一个已经连接的socket再次连接,会导致socket抛出异常,程序崩溃,所以在连接或者重连socket之前都要确保socket出于非连接状态。
- (void)start {
if (_isConnected) {
return;
}
[self connect];
}
- (void)stop {
[self disConnect];
}
- (void)reStart {
[self disConnect];
[self connect];
}
- (void)connect {
//此处判断是否有身份验证信息
NSError *error = nil;
if (_socket.delegate == nil) {
[_socket setDelegate:self]; // check delegate
}
if (![_socket connectToHost:CUSTOM_SOCKET_HOST onPort:CUSTOM_SOCKET_PORT error:&error]) {
NSLog(@"连接失败:%@", error);
_isConnected = NO;
} else {
_isConnected = YES;
}
}
- (void)disConnect {
[_socket setDelegate:nil];
[_socket disconnect];
_isConnected = NO;
}
3.连接到服务器后,就是监听代理了。
//所有的代理都是在创建GCDAsyncSocket对象时传入的队列上异步执行
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"Tcp socket Connected %@:%d", host, port);
//连接到服务器,调用此代理
//可在此代理用向服务器发送身份验证信息
//开启心跳
[self startHeartbeat];
//读取
[_socket readDataWithTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
//可在此方法中更新已发送的信息状态
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
//处理接收到的消息
[self readData:data tag:tag];
//读取
[_socket readDataWithTimeout:-1 tag:0];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"socket did disconnect: %@", err);
_isConnected = NO;
if (err) { //异常断开连接
} else { //正常断开
}
//断开连接后,关闭定时器
[self stopHeartbear];
}
4.虽然在代理中监听了disconnect代理方法,但这个方法并不能保证在失去socket连接后一定会被执行。所以还需要其他方法来确保客户端正在连接服务器。
- (void)socketDidDisconnect:(GCDAsyncSocket*)sock withError:(NSError*)err
通常的做法就是,定时向服务器发送长连接指令(具体的指令由服务器指定),如果一段时间内没有收到服务器的返回消息,GCDAsyncSocket会得到失去连接的消息,会之行上面的失去连接的代理方法。
// GCD定时器
- (void)GCDTimer {
NSTimeInterval period = 15.0; //设置时间间隔
_hearTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _rwQueue);;
dispatch_source_set_timer(_hearTimer, dispatch_walltime(NULL, 0), period * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_hearTimer, ^{
[self heartbeatCheckSocket];
});
}
- (void)heartbeatCheckSocket {
//假设与服务器约定的指令为socket_connect_check
NSData *checkMsg = [@"socket_connect_check" dataUsingEncoding:NSUTF8StringEncoding];
[self.socket writeData:checkMsg withTimeout:-1 tag:1];
NSLog(@"heartbeat...");
}
- (void)startHeartbeat {
dispatch_resume(_hearTimer);
}
- (void)stopHeartbear {
dispatch_suspend(_hearTimer);
}
在连接到服务器的代理中开启心跳。当已经监听到连接断开,那么心跳就没有什么意义了,挂起就可以了。
三、源码: