IM之Socket
说到即时通讯,就会让人想到socket,然而现在三方的即时通讯SDK也是数不胜数。本文就简单说一下github上非常火热的CocoaAsyncSocket,我们会利用这个库来实现消息(文本消息,图片消息,以及数据流)发送接收。
CocoaAsyncSocket
之前这个库分两种版本(GCD版 和RunLoop版,现在RunLoop版已经去掉了,可能是因为功能有些重复吧)。
在讨论socket之前,我们先了解一下TCP和UDP协议
- TCP 面向连接,端到端可靠的数据包发送。在发送数据的时候采用了Nagel算法(将多次间隔小数据量小的数据合并成一个大的数据块然后封包发送),这样就造成了接收端难于分辨数据包。
- UDP 是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法, 由于UDP支持的是一对多的模式,在每个UDP包中有消息头,这样,对于接收端来说,就容易进行区分处理了。
数据粘包是TCP特有的问题,因为UDP数据包是有消息边界的。于是我们这里主要要解决的事TCP传输数据封包来解决数据粘包问题。我们试图想把在数据包里面加入更多的消息信息
(这里把它称为消息头),在消息头里面加入消息类型,消息数据大小等,在接收端用同样的机制去解包,来解决数据粘包的问题。
首先我们创建了一个SocketManager来管理我们的socket相关的事务,客户端代码如下:
typedef NS_ENUM(NSUInteger, MessageType) {
MessageTypeTxt,
MessageTypePic,
MessageTypeDataStream, //音视频 数据流
};
/
@class GCDAsyncSocket;
@protocol SocketManagerDelegate <NSObject>
@optional
- (void)socket:(GCDAsyncSocket *_Nonnull)socket didReadData:(NSData *_Nonnull)data withType:(MessageType)messageType;
@end
@interface SocketManager : NSObject
@property (strong, nonatomic, readonly, nonnull) NSString *host;
@property (readonly) uint16_t port;
@property (weak, nonatomic, nullable) id<SocketManagerDelegate>delegate;
+ (_Nonnull instancetype)shareManager;
- (BOOL)connectToHost:( NSString *_Nonnull )host onPort:(uint16_t)port;
- (void)disConnect;
- (void)sendMessage:(NSData *_Nonnull)aMessageData withMessageType:(MessageType)messageType;
@end
这样SocketManager就管理了我们的连接和数据封包发送,下面是消息封包发送
-(void)sendMessage:(NSData *)aMessageData withMessageType:(MessageType)messageType
{
NSUInteger size = aMessageData.length;
NSMutableDictionary *headInfo = [NSMutableDictionary dictionary];
[headInfo setObject:@(messageType) forKey:@"messageType"];
[headInfo setObject:[NSString stringWithFormat:@"%ld",size] forKey:@"size"];
NSString *jsonStr = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:headInfo options:NSJSONWritingPrettyPrinted error:nil] encoding:NSUTF8StringEncoding];
NSData *headData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *sendData = [NSMutableData dataWithData:headData];
//分界
[sendData appendData:[GCDAsyncSocket CRLFData]];
[sendData appendData:aMessageData];
//第二个参数,请求超时时间
[socket writeData:sendData withTimeout:-1 tag:0];
}
正如我们所想的那样,在数据包里面我们加入了消息头信息和边界,这样我们在拆包的时候就能根据边界和消息头顺利的拆包,不至于分不清数据包的大小而少包、多包。其他的话就是一些sockect的代理方法,包括消息发送成功的回调,读取到消息的回调等。我们在socket连接成功的时候去发送心跳包,在断开连接的时候去做一个断线重连:
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
NSLog(@"连接到服务器");
self.heartTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(sendHeartPackage:) userInfo:nil repeats:YES];
}
- (void)sendHeartPackage:(NSTimer *)timer
{
[self sendMessage:[[NSString stringWithFormat:@"cheater connected...%@",[NSDate date]] dataUsingEncoding:NSUTF8StringEncoding] withMessageType:MessageTypeTxt];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err
{
NSLog(@"断开连接");
[self.heartTimer invalidate];
self.heartTimer = nil;
if (err) { //断线 或者其他原因
[socket connectToHost:self.host onPort:self.port error:nil];
}else{ //手动断线
self->_host = nil;
self->_port = 0;
}
}
我们这里实现客户端和服务器相互收发消息,拆包的代码如下:
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
//先读取到当前数据包头部信息
if (!self.packageHeadInfo) {
self.packageHeadInfo = [NSJSONSerialization
JSONObjectWithData:data
options:NSJSONReadingMutableContainers
error:nil];
if (!self.packageHeadInfo) {
NSLog(@"error:当前数据包的头为空");
return;
}
NSUInteger packageLength = [self.packageHeadInfo[@"size"] integerValue];
//读到数据包的大小
[sock readDataToLength:packageLength withTimeout:-1 tag:0];
return;
}
//正式的包处理
NSUInteger packageLength = [self.packageHeadInfo[@"size"] integerValue];
//说明数据有问题
if (packageLength <= 0 || data.length != packageLength) {
NSLog(@"error:当前数据包数据大小不正确");
return;
}
NSNumber *type = self.packageHeadInfo[@"messageType"];
if (self.delegate && [self.delegate respondsToSelector:@selector(socket:didReadData:withType:)]) {
[self.delegate socket:socket didReadData:data withType:[type unsignedIntegerValue]];
}
self.packageHeadInfo = nil;
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
我们首先是读取到边界线,然后去解析消息头里面的信息来判断本次接收到的数据是否可靠(无效,不完整)。
有其他几点需要提一下:
- 服务器端接收到客户端连接之后一定要保存新建立的socket,不然会断开连接的。文档说的很清楚:
* You must retain the newSocket if you wish to handle the connection.
* Otherwise the newSocket instance will be released and the spawned connection will be closed.
在这一步,服务端应该将socket和用户关联上。
- 发送(读取)数据包有个tag参数,该参数在相应方法回调的时候去返回回来,比如发送数据包tag为10,那么在发送数据包成功的回调里面会将tag带回,可以通过tag值判断是否有数据未发送成功等
- 断线重连问题,如果手动断开连接,在回调方法中error不会有值
- 代码里面没有实现的音视频等资源文件发送,其实和图片没什么区别,只是数据量大一点。
消息收发都解决了,就这样吧,完整的代码在这里
本文只是自己的一些理解,如您发现任何错误,欢迎留言指正~!