IM之Socket

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不会有值
- 代码里面没有实现的音视频等资源文件发送,其实和图片没什么区别,只是数据量大一点。
消息收发都解决了,就这样吧,完整的代码在这里


本文只是自己的一些理解,如您发现任何错误,欢迎留言指正~!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

huhansome

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值