美文网首页
asynSocket源码解析之三

asynSocket源码解析之三

作者: upworld | 来源:发表于2017-01-21 22:04 被阅读165次

    有一段时间一直被一个socket问题纠结。

    现实情景

    现实情景如下(服务/客户端都采用Tcp协议,都采用异步io):

    • 我有一个服务端:假设服务端socket系统写缓存区为50K字节,不存在性能问题。
    • 我有一个客户端:假设socket系统读缓存区设置为 5K字节,客户端由于一些问题(假设为性能问题),不能及时读取服务端发送过来的数据。

    在这种情况下,数据户不会丢呢?客户端socket系统读缓存区设置的大小,对服务端有影响吗?
    为了描述方面,后面我们经常要用到两个名词,一个是服务端socket写buffer指代服务端socket系统写缓存区客户端socket读buffer指代客户端socket系统读缓存区

    假设场景

    一般服务端socket存在读/写行为,客户端socket也存在读和写行为。一会读一会写这样分析问题就很复杂。为了更好分析上面的问题,我们假设有这样一个假设场景

    假设服务端客户端都采用TCP协议通过异步socket通信;客户端和服务端socket连接一切正常;服务端socket写buffer有50K(假设系统设置是多大空间实际使用就是大多空间),客户端socket读buffer有5K(假设系统设置是多大空间实际使用就是大多空间);客户端不调用read读取数据,也不调用write写数据;服务端不调用read读取数据,但是要连续调用write一次发送5K,共计发送1M数据到客户端。

    下面所有讨论和分析都基于上面假设场景

    问题一:

    注意上面假设客户端socket读buffer只有5K,客户端不调用read读数据

    • 问题1.1:第一次服务端调用write刚好发送5K数据能发送成功吗?(注意上面假设条件:客户端不调用read读取数据)

    如果你认为问题1.1能够发送成功,那么请继续思考。

    • 问题1.2:服务端第二次调用write再发送5K字节数据能成功吗?注意上面假设条件:客户端socket系统读缓存区为5K)

    我先告诉你结果:问题1.1能够发送成功,问题1.2也能够发送成功。如果你知道为什么?首先我们要知道知道socket调用write到时先向本地socket写buffer填充数据,然后底层负责把数据传输到对方的socket读buffer。当服务端调用write发送5K数据,发现现在有50K缓存可用,那调用wrire会立即返回成功。不是等到数据成功到达客户端socket读buffer才返回,也不是等到客户端调用read把数据读取到内存才返回。

    下面我画了一个图,简单描述服务端socket写buffer和客户端socket读buffer的关系。


    Paste_Image.png

    如果上面你暂时不能理解,你也可以继续阅读。因为后面我们会写一个demo,包含服务端,客户端,模拟上面说的场景。你可以运行demo验证一下。

    问题二:

    一定要记住,上面假设过客户端始终不调用read读取数据。服务器端继续发送剩下的数据,按每次write发送5K数据计算。那么第10次发送的时候,服务端socket写buffer刚好被填满(因为上面假设服务端socket写buffer只有50K)。那现在考虑第11次调用write再发送5K数据。这时候我们来分析下面问题。

    • 问题2.1:服务端第11次调用write能成功吗?
    • 问题2.2:假设第X(x>11)次调用write写入失败。write返回值是多少呢?
    • 问题2.3:服务端最多能向socket成功写入多少数据?

    基于上面假设场景提出的问题,下面会做一些解答。然后再拿demo验证。

    • 解答问题一:服务端调用write发送数据的时候,并不是等到write发送的数据成功到达客户端之后才返回。而是先写入服务端socket系统缓存中,写完之后就返回。第二次调用write发送数据的时候,服务端socket系统缓存至少有45K,第二次写入5K数据当然会成功。
    • 解答问题二:服务端有socket系统缓存有50K,客户端socket系统缓存有5K。那么第10次服务端调用write发送数据的时候,服务端共计已经写入50K数据到socket系统缓存。在服务端第11次调用write发送5K的时候,正常情况下是能正常返回的,但不是绝对。55K怎么会成功写入到只有50K的socket系统缓存?我们可以这样想,write每次想socket写入数据的时候,TCP协议已经在帮我们把socket系统缓存的数据向客户端发送,即使客户端不调用read函数(其实read只读socket系统读buffer),可能在我们第x次调用write的时候,服务端第一次写入的5k数据,已经被发送到了客户端(客户端正好有5k的socket系统读buffer)。所以第11次能不能写成功,和当前网络,及调用write的时间间隔有关。
    • 解答问题四:所以如果服务端的socket系统读buffer只有50K,客户端socket系统读buffer只有5K,那么,服务端调用write最多能写入55K数据(50K服务端,5k在客户端)。

    验证

    demo简介:

    demo 基于CocoaAsyncSocket改进。里面有一个EchoServer模拟服务端不停发送数据。IPhoneConnectTest模拟客户端,只连上服务器端用。不调用read读取数据。用到一个第三方log库和GCDAsyncSocket最新代码。为了模拟一些条件,GCDAsyncSocket做了一些改动。具体改动后面会简要说一下。具体可以看git 提交节点及说明。

    其中有一个Config.h文件:

    #ifndef Config_h
    #define Config_h
    //IPhoneConnectTest demo 使用。注意iphone 和 mac端需要运行在一个局域网下
    #define kConnectIp @"10.0.100.109"
    //IPhoneConnectTest demo  和 EchoServer demo 同时使用一个端口
    #define kConnectPort 5555
    #endif /* Config_h */
    

    测试的时候要记住把客户端(IPhoneConnectTest)kConnectIp 改成mac电脑的ip地址。如下截图的192.168.1.102

    屏幕快照 2017-01-18 23.27.09.png

    demo服务端分析:

    EchoServer改自CocoaAsynSocket的一个例子。增加一个点击触发事件:

    - (IBAction)send:(id)sender{
        GCDAsyncSocket *lastSocket = [connectedSockets lastObject];
        int sendByte = 1024;
        char test[sendByte+1];
        char *p = test;
        for (NSInteger i = 0; i < sendByte; i++) {
            *p = 'a';
            p++;
        }
        *p = '\0';
        
        NSInteger count = 0;
        while (count++ < 1*1024) {
            NSString *welcomeMsg = [NSString stringWithCString:test encoding:NSUTF8StringEncoding];
            NSData *welcomeData = [welcomeMsg dataUsingEncoding:NSUTF8StringEncoding];
            [lastSocket writeData:welcomeData withTimeout:-1 tag:WELCOME_MSG];
        }
    }
    

    点击一次大概促发110241024 = 1M数据。

    GCDAsynSocket内部源码改动
    1. GCDAsynSocket内部增加一个设置socket系统读写buffer大小的方法。
    //设置socket系统的读写buffer。
    - (void)setSocket:(int)socket bufferSize:(int)size
    

    主要用来当EchoServer和IphoneConnectTest客户端建立连接之后,我们设置服务端socket系统的读写buffer 都为50K。
    设置代码如下:

    [self setSocket:childSocketFD bufferSize:1024*50];
    
    2.增加一个统计服务端成功写入数据的属性
    //统计实际调用write方法的次数。
    - @property (atomic, assign, readwrite)   NSInteger writeCount;
    //统计实际写入数据的字节数
    - @property (atomic, assign, readwrite)   NSInteger writeByetes;
    

    在doWriteData做了如下改动,统计writeByetes数据。并在if (errno == EWOULDBLOCK)的时候注释掉,socket唤醒通知(后面IphoneConnectTest会尝试读取所有服务器写入数据)用来验证服务器端写入的数据,只要socket保持连接,客户端任何时候,只要想读就能读取所有服务端写入的数据。

    - (void)doWriteData
    {
        ...
            ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite);
            if (result > 0) {
                self.writeByetes += result;
            }
            LogVerbose(@"wrote to socket = %zd count=%zd bytes=%zd", result,self.writeCount,self.writeByetes);
            
            // Check results
            if (result < 0)
            {
                if (errno == EWOULDBLOCK)
                {
                    waiting = YES;
                }
                else
                {
                    error = [self errnoErrorWithReason:@"Error in write() function"];
                }
            }
    
        ....
        if (waiting)
        {
    //      flags &= ~kSocketCanAcceptBytes;
    //      
    //      if (![self usingCFStreamForTLS])
    //      {
    //          [self resumeWriteSource];
    //      }
        }
    }
    

    demo客户端分析:

    IphoneConnectTest改自CocoaAsynSocket的一个例子。增加两个读取数据的函数。后面我们会用来统计服务端写入的数据,客户端想读的数据是否能够读取。

    
    - (void)readLength{
        [asyncSocket readDataToLength:1024 withTimeout:-1 tag:100];
    }
    
    - (void)readData{
         [asyncSocket readDataWithTimeout:-1 tag:101];
    }
    
    - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
        if (tag == 100) {
            DDLogVerbose(@"readLength = %zd",data.length);
        }else{
            DDLogVerbose(@"readDataToData = %zd",data.length);
        }
    }
    

    CGDAsynSocket内部改动:
    当和EchoServer建立连接的时候设置IphoneConnectTest的socket系统读写buffer为5K

      [self setSocket:socketFD bufferSize:5*1024];
    

    当IphoneConnectTest通过GCD监听到socket数据发生变化的时候,注销掉doReadData函数。这样IphoneConnectTest只能收到socket有数据可读,但是不读取数据。

            if (strongSelf->socketFDBytesAvailable > 0)
    //          [strongSelf doReadData];
            else
                [strongSelf doReadEOF];
    

    测试case

    第一步:下载CocoaAsyncSocketSample 或者 git clone https://github.com/upworldcjw/CocoaAsyncSocketSample.git

    第二步:打开CocoaAsyncSocketSample文件夹,修改Config.h里面的

    //对应Ip地址替换为本机ip地址
    #define kConnectIp @"10.0.100.109"
    

    端口号服务端和客户端默认都为5555(kConnectPort),可以修改也可以不修改(注意最好不要设置为常用端口号如8080,443等)。

    第三步:运行EchoServer.xcodeproj,会启动mac版的服务端界面如下

    Paste_Image.png

    xcode终端输出log如下:

    Paste_Image.png

    可以看出我们给EchoServer设置50K的发送/接收缓存区已经起效。

    第四步:运行IPhoneConnectTest.xcodeproj,会启动iphone版客户端(此时选择真机调试)。当运行成功之后,EchoServer会收到连接消息界面显示如下:


    Paste_Image.png

    ;IPhoneConnectTest客户端会终端会输出


    Paste_Image.png

    第五步:点击EchoServer的send按钮。会向客户端发送1024次数据,每次发送1024Byte数据。


    Paste_Image.png

    然后在服务端的终端上搜索socket = -1,会发现:

    Paste_Image.png

    回过头来看看客户端log输出

    Paste_Image.png

    看到socket = -1 count = 50 bytes=51200,来解释下这几个输出表示的意思。
    socket = - 1,表示服务端返回的值为-1,也就是调用write返回失败。这种情况下errno都是为EWOULDBLOCK。这个解释可以参考上一篇博客,为了验证这个问题可以在下面对应的地方放歌断点,验证下。


    Paste_Image.png

    count = 50.表示我们累计调用write成功为50次,剩下的失败(服务端socket系统写buffer被填满)。
    bytes=51200 为累计调用write成功写入数据的字节大小。目前可以计算刚好为50K。回头发现,我们把服务端socket系统写缓存设置的不就是50K。不用说肯定有关系。但是不要认为,客户端不读取buffer的时候,服务端最多能写入50K。其实正常情况下至少要>= 服务端socket设置的写缓存。下面分析下为啥这种情况下是50K,是因为服务端调用write太快,在2017-01-21 16:17:29:207 服务端socket写buffer 已经填满 (由于把后面的代码注释掉了,只要有一次写失败,以后就不会调用write)。但是客户端在2017-01-21 16:17:29:250 才第一次收到有数据回调。
    真相如下:


    Paste_Image.png Paste_Image.png

    但是我们把客户端的socket读buffer设置成5K,为啥第一次就会回调有11264字节可读呢。先解释下这个可读,表示数据已经从服务端传输到客户端,在客户端socket的buffer里面存储)。11264 = 11K,明显远远大于客户端设置的5K,从另一个方面也说明,我们设置的buffer太小的时候,虽然api会返回成功,但是socket其实可用buffer要比实际设置的大。(这个实际大小和设置大小具体啥关系,还不是太清楚)。

    修改条件继续测试。

    关闭EchoServer 和 IphoneConnectTest。我们把第五步每次发送数据大小改为512Byte,发送1M数据。

    继续上面的第三步,第四步,第五步。下面分别截图分析:
    继续在EchoServer搜索socket = -1

    Paste_Image.png

    客户端数据

    Paste_Image.png

    发现这次服务端发送数据88064/1024 = 86K,客户端最多接收37328/1024 = 36.45K。发现一个规律 86K - 36.45k 大概等于 50K。哈哈,50K就是服务端socket系统buffer 大小。我给大家解释下:

    下面简单画一个示意图

    Paste_Image.png

    服务端socket系统写buffer和客户端socket系统读buffer,可以理解为两个存储池子。连接两个池子的是TCP协议,可以理解为管道。虽然服务端socket系统写buffer之后50K,但是你在写入的过程,tcp协议会一块一块的把服务端的数据搬到客户端socket系统读buffer。当成功搬过去一块数据,那么服务端的socket的系统写buffer,剩余可写空间就变大了,就可以继续写。如果客户端始终不读取socket数据。那么一般来说服务端最多可以写入 服务端socket实际可写buffer + 客户端socket实际可读buffer的大小。基本原理就是这了。关于验证,客户端不及时读取数据,等了好一会再读,数据不会丢失。这个我在IphoneConntectTest里面写了两个按钮,可以自己测试。

    相关文章

      网友评论

          本文标题:asynSocket源码解析之三

          本文链接:https://www.haomeiwen.com/subject/yxskbttx.html