默认
发表评论 2
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议
Soul客户端自研IM -网络处理篇
阅读(42651) | 评论(2 收藏 淘帖
在编写网络层时,没有选用三方库GCDSyncSocket,因为对于熟悉网编的同学写出自己的网络库不是难题,但正在开发时却遇到几个需要注意的地方,在此分享一下。



1.在发送数据时



, intcnt = (int)write(self->_socketFD, data, size); 并会立刻发送,会有一定时间的延时,经调研是tcp默认开启了nagle算法。
此算法让连接里同一时间只容许存在一个未被确认的数据段,当客户端给服务端发消息时,发消息A,此时服务端收到A后会返回个ack,而此时客户端又发了个消息B,如此时A的ack没有收到时,客户端会先把B存到发送缓存里,等收到ack或超过一定时长后再发送B。
同时服务端也并不会立刻返回ack,而是会在一定时长内等待发送其他应答消息,和ack一起发送。
这么做的目的是充分利用网络带宽,TCP总是希望尽可能的发送足够大的数据.


Soul客户端自研IM -网络处理篇_timg.jpeg


但是在IM这种实时性比较高的业务场景中,此算法略显鸡肋,不如禁掉。
不过我选择的是另外一种方法:刷新发送缓存。

self->file = fdopen (self->_socketFD, "w+");
    intcnt = (int)write(self->_socketFD, data, size);
    fflush(self->file);


这样能实现和禁掉nagle算法相同的功能,并且主动权在开发者手里,可主动调用。

2.读取数据



(int)cnt = read(self->_socketFD,buffer,500);
有时无法一次性读到cnt = 500,猜想是因为tcp的流量控制,某一时刻接收缓存区空间不够,接收窗口告知发送窗口空间不够,发送方无法一次发送全部数据。
所以要多次read,以读到指定的length。

Soul客户端自研IM -网络处理篇_1596116504323.jpg


伪代码如:               
int needsize = 500;
int alreadyGetSize = 0;
while(1)
  int cnt = (int) read(self->_socketFD, buffer,needsize);
   alreadyGetSize += cnt;
if (alreadyGetSize == needsize ) {
printf("获取完毕");
}


3. connect超时机制



做到这里时,因为connect默认是阻塞式的,完全不处理大概阻塞个几分钟都有可能。而我们所能容忍的是5秒,如何让connect在阻塞时结束是关键。
而GCDAsyncsocket做法是,额外开一个线程定时器,如在5s内connect成功则关掉定时器,如5s到达时则close socket。
这么做是可以的,但我的设想是,如果能在一个线程里去完成超时任务,节省线程开销岂不是更好,而且GCDAsyncsocket 里 用gcd实现timer,block嵌套较深,复杂难懂。
既然connect是阻塞的,那么改成非阻塞就好了。
    //为设置超时时间,套接字阻塞->非阻塞
    if(fcntl(self->_socketFD,F_SETFL,fcntl(self->_socketFD,F_GETFL) |O_NONBLOCK)<0) {
        NSLog(@"FCNTL Error");
        return Connect_Failed;
    }


//建立三路握手
    intconnected =connect(self->_socketFD, (conststructsockaddr*)[databytes], (socklen_t)[datalength]);
然后用select函数监听套接字状态变化
    //设置connect阻塞时间
    tm.tv_sec = ConnectTimeOut;
    tm.tv_usec=0;
    FD_ZERO(&sockfd_set);
    FD_SET(self->_socketFD,&sockfd_set);
    selectVal =select(self->_socketFD+1,NULL, &sockfd_set,NULL, &tm);
    switch(selectVal) {
        case-1: {
            NSLog(@"Get Select Connect Failed");
            ret =Connect_Failed;
        }
        case0: {
            ret =Connect_Failed;
            NSLog(@"Time Out To Connect Failed");
        }
            break;
        default: {
            if(FD_ISSET(self->_socketFD,&sockfd_set)) {
                if(getsockopt(self->_socketFD,SOL_SOCKET,SO_ERROR, &error, (socklen_t*)&len) <0) {
                    ret =Connect_Failed;
                }
                NSLog(@"error=%d\n",error);
                if(error ==0) {
                    ret =1;
                }
                else{
                    ret =0;
                    errno= error;
                    NSLog(@"error=%d\n",error);
                }
            }
        }
            break;
    }

最后别忘记把套接字再改回阻塞式的
fcntl(self->_socketFD, F_SETFL, fcntl(self->_socketFD,F_GETFL) &~ O_NONBLOCK);
这样在当前线程就可完成连接超时机制。

4.异常close 问题



在soul上线一段时间之后,会发现一个很奇怪的bug,我对对方发的消息全失败,却能收到对方的消息。
经调研发现,当客户端A异常关闭close时,实际上只关闭了socket的写操作,同时发送finA表示关闭了A到B的数据传送,B收到finA后回个ack代表收到,然后B再回A一个finB,代表B关闭了到A的数据传递,而假设因为网络异常原因finA没有发送到B,或者B没有发送finB会如何呢? 很可能会出现上面描述的bug,反之亦然。


Soul客户端自研IM -网络处理篇_u=251659341,2162563015&amp;fm=26&amp;gp=0.jpg


目前的解决方案是,当A发送close后,立刻重连,当服务端收到重连请求后,立刻根据用户Id查询此用户有无重复连接在保持中,如 有则主动断开,去进行新的连接。
难点在于后端需要为每个用户维持一个连接记录,建立一个连接则把这个连接标记,记录下来。
当下一个连接请求到来时,close掉连接记录里的所有连接,再去应答新连接。




即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

上一篇:求助多个APP想提炼通用的IM服务端,有什么好的方案建议?下一篇:求教有没有对于IM软件详细的硬件和网络配置方面的资料?
推荐方案
评论 2
牛逼程序员 !
本文作者是Soul这个IM的ios端开发人员,但写文章还是头一回,请坛友见谅。

本文内容太少,正在建议作者按照以下提纲扩充内容:

  • 1)你是如何接手这个项目的?(介绍一下项目背景)
  • 2)接手这个任务后,你是怎么开始的?(技术调研过程、最终的技术权衡结果)
  • 3)遇到的挑战是什么?
  • 4)过程中有没有什么值得分享出来的(比如你现在写的这些点,就不错)
  • 5)当前的实现里还有哪些遗憾?
  • 6)这一块的未来优化方向是什么?

标题以建议改为:“Soul技术分享:Soul的iOS客户端网络通信底层从0到1实践分享”。
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部