我们在前面曾经说过,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区,所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

一、粘包问题可以用下图来表示:

假设主机A send了两条消息M1和M2 各10k 给主机B,由于主机B一次提取的字节数是不确定的,接收方提取数据的情况可能是:

• 一次性提取20k 数据
• 分两次提取,第一次5k,第二次15k
• 分两次提取,第一次15k,第二次5k
• 分两次提取,第一次10k,第二次10k
• 分三次提取,第一次6k,第二次8k,第三次6k
• 其他任何可能

二、粘包问题的解决方案

本质上是要在应用层维护消息与消息的边界(下文的“包”可以认为是“消息”)
1、定长包
2、包尾加\r\n(ftp)
3、包头加上包体长度

4、更复杂的应用层协议

对于条目2,缺点是如果消息本身含有\r\n字符,则也分不清消息的边界。

对于条目1,即我们需要发送和接收定长包。因为TCP协议是面向流的,read和write调用的返回值往往小于参数指定的字节数。对于read调用(套接字标志为阻塞),如果接收缓冲区中有20字节,请求读100个字节,就会返回20。对于write调用,如果请求写100个字节,而发送缓冲区中只有20个字节的空闲位置,那么write会阻塞,直到把100个字节全部交给发送缓冲区才返回。为避免这些情况干扰主程序的逻辑,确保读写我们所请求的字节数,我们实现了两个包装函数readn和writen,如下所示。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
 
ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char *)buf;

while (nleft > 0)
    {

if ((nread = read(fd, bufp, nleft)) < 0)
        {

if (errno == EINTR)
                continue;
            return -1;
        }

else if (nread == 0) //对方关闭或者已经读到eof
            return count - nleft;

bufp += nread;
        nleft -= nread;
    }

return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char *)buf;

while (nleft > 0)
    {

if ((nwritten = write(fd, bufp, nleft)) < 0)
        {

if (errno == EINTR)
                continue;
            return -1;
        }

else if (nwritten == 0)
            continue;

bufp += nwritten;
        nleft -= nwritten;
    }

return count;

}

需要注意的是一旦在我们的客户端/服务器程序中使用了这两个函数,则每次读取和写入的大小应该是一致的,比如设置为1024个字节,但定长包的问题在于不能根据实际情况读取数据,可能会造成网络阻塞,比如现在我们只是敲入了几个字符,却还是得发送1024个字节,造成极大的空间浪费。

此时条目3是比较好的解决办法,其实也可以算是自定义的一种简单应用层协议。比如我们可以自定义一个包体结构

struct packet {
    int len;
    char buf[1024];
};

先接收固定的4个字节,从中得知实际数据的长度n,再调用readn 读取n个字符,这样数据包之间有了界定,且不用发送定长包浪费网络资源,是比较好的解决方案。服务器端在前面的fork程序的基础上把do_service函数更改如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 
void do_service(int conn)
{
    struct packet recvbuf;
    int n;
    while (1)
    {
        memset(&recvbuf, 0, sizeof(recvbuf));
        int ret = readn(conn, &recvbuf.len, 4);
        if (ret == -1)
            ERR_EXIT("read error");
        else if (ret < 4)   //客户端关闭
        {
            printf("client close\n");
            break;
        }

n = ntohl(recvbuf.len);
        ret = readn(conn, recvbuf.buf, n);
        if (ret == -1)
            ERR_EXIT("read error");
        if (ret < n)   //客户端关闭
        {
            printf("client close\n");
            break;
        }

fputs(recvbuf.buf, stdout);
        writen(conn, &recvbuf, 4 + n);
    }
}

注意:客户端是直接将整个结构体发送过来,能这样分步解包的前提是结构体没有填充字段。

客户端程序的修改与上类似,不再赘述。

对于条目4,举例如 如TLV 编解码格式

struct TLV
{
    uint8_t tag;
    uint16_t len;
    char value[0];
}__attribute__((packed));

度的结构体,用这种方式定义最好。使用起来非常方便,创建时,malloc一段结构体大小加上可变长数据长度的空间给它,可变长部分可按数组的方式

空间使用情况。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 
int main(void)
{
    char *szMsg = "aaaaaaaaa";
    cout << sizeof(TLV) << endl; //the size of TLV
    uint16_t len = strlen(szMsg) + 1;
    struct TLV *pTLV;
    pTLV = (struct TLV *)malloc(sizeof(struct TLV) + sizeof(char) * len);
    pTLV->tag = 0x2;
    pTLV->len = len;
    memcpy(pTLV->value, szMsg, len);
    cout << pTLV->value << endl;
    free(pTLV);
    pTLV = NULL;
    return 0;
}

参考:

《Linux C 编程一站式学习》

《TCP/IP详解 卷一》

《UNP》

http://www.cppblog.com/aa19870406/archive/2012/06/14/178803.html

如何定义变长的TLV结构体?

TLV是一种常用的用于通信的结构体格式。T表示tag,L表示length,V表示value。其中T和L是固定大小的,V是可变大小,L表示的是V的长度。通常用于结构化网络通信中的数据流。如0x3 3 'aa\0',0x3 5 'aaaa\0',其中0x3表示tag的值,3 or 5表示的是后面的字符串的长度。由于V是可变长度的,所以在定义TLV结构时,需要将V定义成为可变大小。可定义如下:

struct TLV
{
    uint8_t tag;
    uint16_t len;
    char value[0];
}__attribute__((packed));

注意value分配的是0大小,最后一个成员为可变长的数组,对于TLV(Type-Length-Value)形式的结构,或者其他需要变长度的结构体,用这种方式定义最好。使用起来非常方便,创建时,malloc一段结构体大小加上可变长数据长度的空间给它,可变长部分可按数组的方式访问,释放时,直接把整个结构体free掉就可以了。__attribute__(packed)用来强制不对struct
TLV进行4字节对齐,目的是为了获取真实的TLV的空间使用情况。

int main()
{
    char *szMsg = "aaaaaaaaa";
    cout << sizeof(TLV) << endl; //the size of TLV
    uint16_t len = strlen(szMsg) + 1;
    struct TLV *pTLV;
    pTLV = (struct TLV*)malloc(sizeof(struct TLV) + sizeof(char)*len);
    pTLV->tag = 0x2;
    pTLV->len = len;
    memcpy(pTLV->value, szMsg, len);
    cout << pTLV->value << endl;
    free(pTLV);
    pTLV = NULL;
    return 0;
}

这里有关于设置变长TLV的详细说明:http://www.douban.com/note/213324857/这里有一个问题,如何实现嵌套TLV结构呢?大家有什么好的思路吗?欢迎交流
简单实现了一下嵌套TLV,不知道有没有问题。

#include <iostream>
using namespace std;

struct TLVNODE
{
    uint8_t tag;
    uint16_t len;
    char value[0];
}__attribute__ ((packed));

struct TLV
{
    int hei;
    uint8_t tag;
    uint16_t len;
    struct TLVNODE value[0];
} __attribute__ ((packed));

int main()
{
    //char *szMsg = "aaaaaaaaaaa";
    cout << sizeof(TLV) << endl;
    //uint16_t len = strlen(szMsg) + 1;

char *szNodeMsg = "bbbbbbbbbb";
    uint16_t nodelen = strlen(szNodeMsg) + 1;
    struct TLVNODE *pNode = (struct TLVNODE *) malloc(sizeof(struct TLVNODE) + sizeof(char)*nodelen); 
    pNode->tag = 0x3;
    pNode->len = nodelen;
    memcpy(pNode->value, szNodeMsg, nodelen);

struct TLV *pTlv;
    uint16_t nodeSize = sizeof(struct TLVNODE) + sizeof(char)*nodelen;
    pTlv = (struct TLV*)malloc(sizeof(struct TLV) + nodeSize);
    pTlv->tag = 0x2;
    pTlv->len = nodeSize;
//    pTlv->value[0] = (struct TLVNODE)*pNode;
    memcpy(pTlv->value, pNode, nodeSize);
    free(pNode);
    pNode = NULL;
    cout << sizeof(*pTlv) << endl;
    /*for (int i = 0; i < len; ++i)
    {
        pTlv->value[i] = szMsg[i]; 
    }*/

/*memcpy(pTlv->value, szMsg, len);*/
    //cout << pTlv->value << endl;
    free(pTlv);
    pTlv = NULL;
    return 0;
}

tcp流协议产生的粘包问题和解决方案的更多相关文章

  1. UNIX网络编程——tcp流协议产生的粘包问题和解决方案

    我们在前面曾经说过,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体 ...

  2. Socket 编程中,TCP 流的结束标志与粘包问题

    因为 TCP 本身是无边界的协议,因此它并没有结束标志,也无法分包. socket和文件不一样,从文件中读,读到末尾就到达流的结尾了,所以会返回-1或null,循环结束,但是socket是连接两个主机 ...

  3. python中TCP协议中的粘包问题

    TCP协议中的粘包问题 1.粘包现象 基于TCP实现一个简易远程cmd功能 #服务端 import socket import subprocess sever = socket.socket() s ...

  4. python 全栈开发,Day35(TCP协议 粘包现象 和解决方案)

    一.TCP协议 粘包现象 和解决方案 黏包现象让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)执行远程命令的模块 需要用到模块subprocess sub ...

  5. Socket粘包问题终极解决方案—Netty版(2W字)!

    上一篇我们讲了<Socket粘包问题的3种解决方案>,但没想到评论区竟然炸了.介于大家的热情讨论,以及不同的反馈意见,本文就来做一个扩展和延伸,试图找到问题的最优解,以及消息通讯的最优解决 ...

  6. socket粘包问题及解决方案

    一.粘包问题 问题1: 无法确认对方发送过来数据的大小. 'client.py' import socket client = socket.socket() client.connect( ('12 ...

  7. tcp协议传输方法&粘包问题

    socket实现客户端和服务端 tcp协议可以用socket模块实现服务端可客户端的交互 # 服务端 import socket #生成一个socket对象 soc = socket.socket(s ...

  8. 基于tcp协议下粘包现象和解决方案,socketserver

    一.缓冲区 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区.write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送 ...

  9. Socket编程(4)TCP粘包问题及解决方案

    ① TCP是个流协议,它存在粘包问题 TCP是一个基于字节流的传输服务,"流"意味着TCP所传输的数据是没有边界的.这不同于UDP提供基于消息的传输服务,其传输的数据是有边界的.T ...

随机推荐

  1. WF4.0(3)----变量与参数

    已经写了两篇关于WF4.0的博客,算是基础博客,如果是WF比较熟悉就直接跳过吧,如果你对工作流不是很熟悉,或者想了解一下基础的东西,本文还是比较适合你的.工作流中变量,参数,表达式属于数据模型中概念, ...

  2. 说说CSS样式中你不知道的“大于号”

    继承在一定程度上让程序在编写的过程中更加方便,但是有时候也会给我们的程序带来一定的困扰,所以认真的学习继承的原理,以及处理的方法很重要.下面是Css中处理继承的一个方法.在一段CSS代码中见到一个大于 ...

  3. Android Studio安装&&安装bug

    1.安装SDK:Android SDK安装 2.安装Android Studio 3.配置HTTP Proxy: 转自:Android Studio设置HTTP代理(可用) 因为大陆的内网的防火墙很厉 ...

  4. 机器视觉之 ICP算法和RANSAC算法

    临时研究了下机器视觉两个基本算法的算法原理 ,可能有理解错误的地方,希望发现了告诉我一下 主要是了解思想,就不写具体的计算公式之类的了 (一) ICP算法(Iterative Closest Poin ...

  5. MAC下PHP开发

    ZendStudio 10.5安装: http://blog.sina.com.cn/s/blog_7c8dc2d50101nhvb.html PHP+MySQL+Apache开发环境安装:XAMPP ...

  6. TCP/IP协议族——IP工作原理及实例具体解释(上)

     IP协议具体解释 本文主要介绍了IP服务特点,头部结构,IP分片知识,并用tcpdump抓取数据包.来观察IP数据报传送过程中IP的格式,以及分片的过程. IP头部信息:IP头部信息出如今每一个 ...

  7. 修改字段结构之ArcGIS Diagrammer

    在ArcGIS中,修改字段名称.类似和物理顺序是一件不好办的事,特别是需要修改字段比较多的情况下.通常的做法是新建字段-字段计算器赋值-删除原有字段的方法来达到修改的目的.这里介绍另外一种方法. 现需 ...

  8. The 6 inspectors in XCode

    Name Shortcut Key Description file helper Command + Option + 1 shows you all the file details relate ...

  9. hadoop safemode error

    http://www.cnblogs.com/hustcat/archive/2010/06/30/1768506.html 1.safemode bin/hadoop fs -put ./input ...

  10. MySQL的各种SHOW

    . SHOW语法 13.5.4.1. SHOW CHARACTER SET语法 13.5.4.2. SHOW COLLATION语法 13.5.4.3. SHOW COLUMNS语法 13.5.4.4 ...