TCP/IP 粘包问题

场景

在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、自定义协议,支持可变长度的包。可定制性强,对编码要求增加。



(待续)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的。因此TCP的socket编程,收发两端(客户...
    树根曰阅读 5,938评论 1 16
  • 个人认为,Goodboy1881先生的TCP /IP 协议详解学习博客系列博客是一部非常精彩的学习笔记,这虽然只是...
    贰零壹柒_fc10阅读 5,051评论 0 8
  • 1.这篇文章不是本人原创的,只是个人为了对这部分知识做一个整理和系统的输出而编辑成的,在此郑重地向本文所引用文章的...
    SOMCENT阅读 13,037评论 6 174
  • 传输层-TCP, TCP头部结构 ,TCP序列号和确认号详解 TCP主要解决下面的三个问题 1.数据的可靠传输...
    抓兔子的猫阅读 4,502评论 1 46
  • 协议基础 协议就是计算机之间通过网络实现通信时实现所达成的一种“约定”,这种约定使得那些由不同厂商的设备,不同的C...
    d9fc24a0c9a9阅读 2,349评论 0 6