socket编程(4)

tcp基于字节流,无边界 接收方不能保证一次是整个消息 产生粘包
udp传输数据包,有边界

协议流与粘包

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

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

1.第一次读M1的全部,第二次读M2 的全部
2.第一次读M1和M2的全部
3.第一次读M1的全部和M2的一部分,第二次读M2的剩余部分。
4.第一次读M1的一部分,第二次读M1的剩余部分和M2的全部。

5.其他


粘包产生的原因

1从过程分析 应用缓冲区>套接口发送缓冲区
2TCP有MSS 分割 IPMTU分片
3TCP 流量控制 拥塞控制 延迟发送机制 等。。。


记住结论: TCP有粘包问题

 

粘包问题的解决方案

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

4、更复杂的应用层协议

 

TCP协议在传输层没有维护消息与消息之间的边界,那么我们就需要在应用层维护消息与消息之间的边界

这些解决方案有一个重要的问题--定长包的接收,我们怎样保证一次接收是一个定长的接收
我们刚刚说过了TCP是一个流协议,无法保证接收的长度。
那我们就需要一个函数,接收确定字节数的读操作

readn接收确切数目的读操作
writen发送确切数目的写操作

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

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

 

readn和writen

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函数更改如下:

 

回射客户/服务器

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个字节,接受
完之后,在把数据包的长度计算出来,这样无形之中就将消息与消息的边界区分,从而结局了粘包问题。
之前我们没有考虑粘包问题,如果是在局域网,一般不会有粘包问题,如果放到广域网,就会出现。

这就是readn() writen()函数的封装 并且进一步完善了回射客户/服务器,给它加上了自定义协议包,
不是以定长的方式来接收。

 

posted @ 2016-12-19 19:57  ren_zhg1992  阅读(131)  评论(0)    收藏  举报