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


(待续)

相关文章

  • TCP/IP 粘包问题

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

  • TCP粘包处理

    TCP粘包 TCP粘包的处理

  • JAVA-每日一面 2022-01-25

    什么是 TCP 粘包/拆包以及TCP 粘包/拆包的解决办法 TCP 粘包/拆包1、要发送的数据大于 TCP 发送缓...

  • golang 解决 TCP 粘包问题

    什么是 TCP 粘包问题以及为什么会产生 TCP 粘包,本文不加讨论。本文使用 golang 的 bufio.Sc...

  • TCP/IP

    1.OSI&TCP/IP参考模型 2.TCP粘包/拆包 TCP是一个面向连接的字节流数据传输控制协议,没有自...

  • Socket粘包处理

    什么是粘包 TCP有粘包现象,而UDP不会出现粘包。 TCP(Transport Control Protocol...

  • 消息粘包 和 消息不完整 问题

    消息粘包 和 消息不完整 问题 消息粘包 和 消息不完整问题 其实都是应用层会带来的问题,和TCP 没关系,TCP...

  • Netty

    TCP 粘包/拆包 当通过TCP协议传输多次数据的时候,TCP协议会把一些消息合并发送,来节省IP调用次数,这样的...

  • TCP粘包

    什么是TCP粘包问题 多个TCP包粘在一个成为一个包,服务端处理接收的TCP包时,需要考虑拆包问题。 产生原因 发...

  • day32-粘包问题和报头的定制

    粘包问题 TCP协议作为流式协议,只有TCP协议存在粘包问题。 发送端可以是一K一K地发送数据,而接收端的应用程序...

网友评论

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

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

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