TCP/IP 粘包问题

作者: 秦明Qinmin | 来源:发表于2017-11-05 06:55 被阅读297次

    场景

    在TCP通信的时候,连续多次发送数据,经常会遇到一些“奇怪”的问题,具体代码如下:

    服务器端:

    //
    //  ServerSocket.m
    //  TCP粘包
    //
    //  Created by qinmin on 2017/11/5.
    //  Copyright © 2017年 qinmin. All rights reserved.
    //
    
    #import "ServerSocket.h"
    #import <sys/socket.h>
    #import <netinet/in.h>
    #import <arpa/inet.h>
    
    #define kMAXLINE                 4096
    
    @interface ServerSocket()
    {
        int         _socketHandle;
        BOOL        _isFinish;
        NSInteger   _serverPort;
    }
    @end
    
    @implementation ServerSocket
    
    #pragma mark - LiferCycle
    - (instancetype)initWithPort:(NSInteger)port
    {
        if (self = [super init]) {
            _isFinish = YES;
            _serverPort = port;
        }
        
        return self;
    }
    
    #pragma mark - PublicMethod
    - (void)startServer
    {
        if (!_isFinish) {
            return;
        }
        
        _isFinish = NO;
        if ([NSThread isMainThread]) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                [self createServerSocket];
            });
        }else {
            [self createServerSocket];
        }
    }
    
    - (void)stopServer
    {
        if (_isFinish) {
            return;
        }
        
        _isFinish = YES;
        close(_socketHandle);
        
        if (_serverDidStopBlock) {
            _serverDidStopBlock();
        }
    }
    
    #pragma mark - PrivateMethod
    - (void)createServerSocket
    {
        int connnectHandle;
        struct sockaddr_in servaddr;
        char buff[kMAXLINE];
        
        if ((_socketHandle = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
            NSLog(@"socket error %s", strerror(errno));
            return;
        }
        
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(_serverPort);
        
        if(bind(_socketHandle, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
            NSLog(@"bind error: %s",strerror(errno));
            return;
        }
        
        if(listen(_socketHandle, 10) == -1) {
            NSLog(@"listen error: %s",strerror(errno));
            return;
        }
        
        if (_serverDidStartBlock) {
            _serverDidStartBlock();
        }
        
        // 目前只处理一个socket连接
        if((connnectHandle = accept(_socketHandle, (struct sockaddr*)NULL, NULL)) == -1) {
            NSLog(@"accept socket error: %s",strerror(errno));
            return;
        }
        
        size_t n;
        while (!_isFinish && (n = recv(connnectHandle, buff, kMAXLINE, 0)) > 0) {
            buff[n] = '\0';
    //        NSLog(@"recv msg from client: %s\n", buff);
            NSLog(@"recv msg from client length: %ld", n);
        }
        close(connnectHandle);
    }
    
    @end
    
    

    客户端:

    //
    //  ClientSocket.m
    //  TCP粘包
    //
    //  Created by qinmin on 2017/11/5.
    //  Copyright © 2017年 qinmin. All rights reserved.
    //
    
    #import "ClientSocket.h"
    #import <sys/socket.h>
    #import <netinet/in.h>
    #import <arpa/inet.h>
    
    #define kMAXLINE    4096
    
    static void* clientSocketSendQueueKey;
    static void* clientSocketRecvQueueKey;
    
    @interface ClientSocket()
    {
        int                 _sockfd;
        NSString            *_serverIP;
        NSInteger           _serverPort;
        dispatch_queue_t    _clientSocketSendQueue;
        dispatch_queue_t    _clientSocketRecvQueue;
        BOOL                _isStop;
    }
    @end
    
    @implementation ClientSocket
    
    #pragma mark - LiferCycle
    - (instancetype)initWithServerIP:(NSString *)IP port:(NSInteger)port
    {
        if (self = [super init]) {
            _serverIP = IP;
            _serverPort = port;
            _isStop = YES;
            _clientSocketSendQueue = dispatch_queue_create("client.socket.send.queue", NULL);
            _clientSocketRecvQueue = dispatch_queue_create("client.socket.recv.queue", NULL);
            dispatch_queue_set_specific(_clientSocketSendQueue, &clientSocketSendQueueKey, NULL, NULL);
            dispatch_queue_set_specific(_clientSocketSendQueue, &clientSocketRecvQueueKey, NULL, NULL);
        }
        
        return self;
    }
    
    #pragma mark - PublicMethod
    - (void)startConnect
    {
        if (!_isStop) {
            return;
        }
        
        dispatch_block_t block = ^() {
            _isStop = NO;
            [self createClientSocket];
        };
        
        if (dispatch_queue_get_specific(_clientSocketSendQueue, &clientSocketSendQueueKey)) {
            dispatch_sync(_clientSocketSendQueue, block);
        }else {
            dispatch_async(_clientSocketSendQueue, block);
        }
    }
    
    - (void)stopConnect
    {
        if (_isStop) {
            return;
        }
        
        dispatch_block_t block = ^() {
            close(_sockfd);
            _isStop = YES;
        };
        
        if (dispatch_queue_get_specific(_clientSocketSendQueue, &clientSocketSendQueueKey)) {
            dispatch_sync(_clientSocketSendQueue, block);
        }else {
            dispatch_async(_clientSocketSendQueue, block);
        }
    }
    
    - (void)sendData:(NSData *)data
    {
        dispatch_block_t block = ^() {
            const char *sendLine = data.bytes;
            NSUInteger lineLength = (data.length > kMAXLINE ? kMAXLINE : data.length);
            ssize_t len = 0;
            while (!_isStop && (len = send(_sockfd, sendLine, lineLength, 0)) > 0) {
                sendLine += lineLength;
                NSUInteger left = data.length - lineLength;
                if (left <= 0) {
                    break;
                }
                
                lineLength = (left > kMAXLINE ? kMAXLINE : left);
            }
            
            if (len < 0) {
                NSLog(@"send msg error: %s", strerror(errno));
            }
        };
        
        if (dispatch_queue_get_specific(_clientSocketSendQueue, &clientSocketSendQueueKey)) {
            dispatch_sync(_clientSocketSendQueue, block);
        }else {
            dispatch_async(_clientSocketSendQueue, block);
        }
    }
    
    - (void)recvData
    {
        dispatch_block_t block = ^() {
            char buff[kMAXLINE];
            size_t n = 0;
            while (!_isStop && (n = recv(_sockfd, buff, kMAXLINE, 0)) > 0) {
                buff[n] = '\0';
                // NSLog(@"recv msg from client: %s\n", buff);
                NSLog(@"recv msg from client length: %ld", n);
                
                if (_clientDidRecvDataBlock) {
                    _clientDidRecvDataBlock([NSData dataWithBytes:buff length:n]);
                }
            }
        };
        
        if (dispatch_queue_get_specific(_clientSocketRecvQueue, &clientSocketRecvQueueKey)) {
            dispatch_sync(_clientSocketRecvQueue, block);
        }else {
            dispatch_async(_clientSocketRecvQueue, block);
        }
    }
    
    #pragma mark - PrivateMethod
    - (void)createClientSocket
    {
        struct sockaddr_in servaddr;
        
        if((_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
            NSLog(@"socket error: %s", strerror(errno));
            return;
        }
        
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(_serverPort);
        if(inet_pton(AF_INET, _serverIP.UTF8String, &servaddr.sin_addr) <= 0) {
            NSLog(@"inet_pton error");
            return;
        }
        
        if(connect(_sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
            NSLog(@"connect error: %s",strerror(errno));
            return;
        }
    }
    
    @end
    

    数据发送

    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"demo" ofType:@"txt"]];
        
    //    NSLog(@"%ld", data.length);
        
        _client = [[ClientSocket alloc] initWithServerIP:@"127.0.0.1" port:6666];
        _server = [[ServerSocket alloc] initWithPort:6666];
        
        __weak typeof(self) wself = self;
        [_server setServerDidStartBlock:^{
            __strong typeof(self) sself = wself;
            [sself.client startConnect];
            [sself.client sendData:data];
            [sself.client sendData:data];
            [sself.client sendData:data];
            [sself.client sendData:data];
        }];
        
        [_server startServer];
    }
    

    待发送的数据大小:

    待发送文件.png

    结果:

    接受结果.png

    可以看出只有第一次是完整的数据大小,其它每次接收的数据都不是待发送数据的真实长度。

    粘包问题

    在做TCP通信的时候,如果需要在一条连接上连续发送不同结构的数据时,可能遇到其中的某些包完整,某些包不完整,也可能遇到某些包包含多个数据。这就是典型的TCP粘包现象。TCP粘包现象是指在使用TCP通信的时候,一个完成的消息可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包进行发送。

    提高网络利用率

    Nagle 算法

    TCP 中为了提高网络的利用率,经常使用一个叫做Nagle的算法。该算法是指发送端即使还有应发送的数据,但如果这部分数据很少的话,则进行延迟发送的一种处理机制,也就是仅在下列任意一种条件下才能发送数据,如果两条件都不满足,那么暂时等待一段时间以后再进行数据发送。

    1、已发送的数据都已经收到确认应客时。
    2、可以发送最大段长度(MSS) 的数据时。

    在使用 TCP 协议发送数据的时候,即使只发送一个字节,但是数据还是需要封装成TCP/IP包来发送。因此,最少需要加入一个 20 字节的 TCP 首部,20 字节的 IP 首部,这样发送的流量其实是数据的40倍左右。Nagle 算法就是为了解决频繁发送小包所导致的流量浪费和网络阻塞问题。

    延迟确认应答

    接收数据的主机如果每次都立刺回复确认应答的话,可能会返回一个较小的窗口。那是因为刚接收完数据,缓冲区已满。当某个接收端收到这个小窗口的通知以后,会以它为上限发送数据,从而又降低了网络的利用率。为此,引入了一个方法,那就是收到数据以后并不立即返回确认应答,而是延迟一段时间的机制,尝试减少接收方所发送的 ack 数量。
    1、在没有收到2x最大段长度的数据为止不做确认应答;
    2、其他情况下,延迟发送确认应答;

    粘包原因

    1、由Nagle算法造成的发送端的粘包。发送端需要等缓冲区满才发送数据出去,这就有可能把多个小的包封装成一个大的数据包进行发送。

    2、接收端接收不及时造成的接收端粘包。TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层不能及时的把TCP的数据取出来,就会造成缓冲区中存放了多个MSS数据。

    解决办法

    1、每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接。这种算法的局限在于每次都要进行三次握手四次挥手,既浪费流量,又使数据传输延时性增大,socket不能很好的复用。

    2、特殊切割符来分割包。这种方式必须严格要求包体中不会出现该特殊字符,因此,需要控制使用范围。

    3、每个包都是固定长度。这种方式会造成包的体积很难确定,浪费流量等问题。

    4、发送端使用了TCP强制数据立即传送的操作指令push。可能引发频繁发送小包所导致的流量浪费和网络阻塞问题。

    5、自定义协议,支持可变长度的包。可定制性强,对编码要求增加。


    (待续)

    相关文章

      网友评论

      • 秦明Qinmin:对的 这是为了解决小包传输的问题 :+1::+1::+1:
      • 音视频直播技术专家:这是正常的tcp/ip 协议啊,nagle 的出现主要目的是为了,减少小包造成网络用塞,这样会对事实性强的应用产生影响。如果应用对事实性要求高的话可以将nagle算法关闭。还可以使用udp 协议

      本文标题:TCP/IP 粘包问题

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