zoukankan      html  css  js  c++  java
  • 网络粘包问题解决办法

    流协议与粘包:

       我们知道TCP是一个基于字节流的传输服务,这意味着TCP所传输的数据之间是无边界的,像流水一样,是无法区分边界的;而UDP是基于消息的传输服务,它传输的是数据报文,是有边界的。

    而对于数据之间有无边界,反映在对方接收程序的时候,是不一样的:对于TCP字节流来说,对等方在接收数据的时候,不能够保证一次读操作,能够返回多少个字节,是一个消息,还是二个消息,这些都是不确定的;而对于UDP消息服务来说,它能够保证对等方一次读操作返回的是一条消息

      由于TCP的无边界性,就会产生粘包问题,那粘包问题具体体现是怎样的呢?下面用图来进行阐述:

      

      假设主机A(Host A)要向主机B(Host B)发送两个数据包:M1,M2

      而对于对待接收方主机B来说,可能会有以下几种情况:

      ①

      

       也就是第一次读操作刚好返回第一条消息(M1)的全部,接下来第二次读操作返回第二条消息(M2)的全部,所以这就没有粘包问题。

      ②

       

       一次读操作就返回了M1,M2的所有,这样M1和M2就粘在一起了,这就能比较直观的体会到粘包的表现了。

      ③

       

       一次读操作返回了M1的全部,并且还有M2的一部分(m2_1);第二次读操作返回了M2的另外一部分(M2_2)。

      ④

       

      一次读操作返回了M1的一部分(M1_1);第二次读操作返回了M1的另外一部分(M1_2),并且还有M2的全部。

      当然除了上面四种情况,可能还存在其它组合,因为主机B一次能接收的字节数是不确定的。

      下面来探讨下产生的原因。

      

      粘包产生的原因  

      

      ① 应用程要将自己缓冲区中的数据发送出去,首先要调用一个write方法,将应用程序的缓冲区的数据拷贝到套接口发送缓冲区(SO_SNDBUF),而该缓冲区有一个SO_SNDBUF大小的限制,如果应用缓冲区一条消息的大小超过了SO_SNDBUF的大小,那这时候就有可能产生粘包问题,因为消息被分隔了,一部分已经发送给发送缓冲区,且对方已经接收到了,另外一部分才放到了发送缓冲区,这样对方就延迟接收了消息的后一部分。这就导致了粘包问题的出现。

      ②TCP传输的段有最大段(MSS)的限制,所以也会对应用发送的消息进行分割而产生粘包问题。

      ③链路层它所传输的数据有一个最大传输单元(MTU)的限制,如果我们所发送的数据包超过了最大传输单元,会在IP层进行分组,这也可能导致消息的分割,所以也有可能出现粘包问题。

    当然还有其它原因,如TCP的流量控制、拥塞控制、TCP的延迟发送机制,对于上面说的理论理解起来比较抽象,只要记住一条:TCP会产生粘包问题既可。

      粘包解决方案:

      怎么才能解决粘包问题呢?

      既然TCP协议没有在传输层没有维护消息与消息之间的边界,所以:

      

      

        我们所要发送的消息是一个定长包,那么对等方在接收的时候已定长的方式来进行接收,就能确保消息与消息之间的边界。

       

        这种方式有个问题,就是如果消息本身就带这些字符的话,就无法就无法区分消息的边界了,这时就需要用到转义字符了。

      

        其中包头是定长的,如4个字节。

      

        这些解决方案有一个很重要的问题,就是定长包的接收,我们之前说了,TCP是一个流协议,它不能保证对方一次接收接收到了多少个字节,那我们就需要封装一个函数:接收确定字节数的读操作

    下面简单介绍一下两种方法的实现

    1、发送定长包

    server.c

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <errno.h>
    
    #define ERR_EXIT(m)
            do
            {
                    perror(m);
                    exit(EXIT_FAILURE);
            }while(0);
    
    ssize_t readn(int fd, void *buf, size_t count)//读取count个字节数,其中size_t是无符号
    的整数,ssize_t是有符号的整数
    {
        size_t nleft = count;//剩余的字节数
        printf("nleft = %d
    ",nleft);
        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)//对等方关闭了
                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;
    }
    
    void do_service(int conn)
    {
            char recvbuf[1024];
            while(1)
            {
                    memset(recvbuf, 0, sizeof(recvbuf));
                    int  ret=readn(conn,recvbuf,sizeof(recvbuf));
                    if(ret == 0)
                    {
                            printf("client close
    ");
                            break;
                    }
                    else if(ret == -1)
                    {
                            ERR_EXIT("read");
                    }
                    fputs(recvbuf,stdout);
                    writen(conn,recvbuf,ret);
            }
    }
    
    int main(void)
    {
            int listenfd;
            if((listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
            {
                    ERR_EXIT("socket");
            }
            struct sockaddr_in servaddr;
            memset(&servaddr, 0, sizeof(servaddr));
            servaddr.sin_family = AF_INET;
            servaddr.sin_port = htons(5188);
            servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
            /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
            /*inet_aton("127.0.0.1",&servaddr.sin_addr);*/
    
            //地址重用
            int on=1;
            if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0)
            {
                    ERR_EXIT("setsockopt");
            }
    
            if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
            {
                    ERR_EXIT("bind");
            }
    
            if(listen(listenfd,SOMAXCONN) < 0)
            {
                    ERR_EXIT("listen");
            }
    
            struct sockaddr_in peeraddr;
            socklen_t peerlen = sizeof(peeraddr);
            int confd;
    
            pid_t pid;
            while(1)
            {
                    if((confd = accept(listenfd,(struct sockaddr*)&peeraddr, &peerlen)) <
     0)
                    {
                            ERR_EXIT("accept");
                    }
                    printf("ip = %s, port = %d
    ",inet_ntoa(peeraddr.sin_addr),ntohs(peer
    addr.sin_port));
                    pid = fork();
                    if(pid == -1)
                    {
                            ERR_EXIT("fork");
                    }
                    if(pid == 0)
                    {
                            close(listenfd);
                            do_service(confd);
                            exit(EXIT_SUCCESS);
                    }
                    else
                    {
                            close(confd);
                    }
            }
    
            return 0;
    }
            

    client.c

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <errno.h>
    
    #define ERR_EXIT(m)
            do
            {
                    perror(m);
                    exit(EXIT_FAILURE);
            }while(0);
    
    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)
                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;
    }
    
    int main(void)
    {
            int sockfd;
            if((sockfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
            {
                    ERR_EXIT("socket");
            }
            struct sockaddr_in servaddr;
            memset(&servaddr, 0, sizeof(servaddr));
            servaddr.sin_family = AF_INET;
            servaddr.sin_port = htons(5188);
            servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
            /*inet_aton("127.0.0.1",&servaddr.sin_addr);*/
    
            if (connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
            {
                    ERR_EXIT("connect");
            }
    
            char sendbuf[1024] = {0};
            char recvbuf[1024] = {0};
    
            while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL)
            {
                    writen(sockfd,sendbuf,sizeof(sendbuf));
                    readn(sockfd,recvbuf,sizeof(recvbuf));
    
                    fputs(recvbuf,stdout);
                    memset(sendbuf,0,sizeof(sendbuf));
                    memset(recvbuf,0,sizeof(recvbuf));
            }
            close(sockfd);
            return 0;
    }

    Makefile

    .PHONY: clean all
    CC=gcc
    CFLAGE= -G -Wall
    BIN=server client 
    all:$(BIN)
    %.o:%.c
            $(CC) $(cflags) -C $< -O $@
    clean:
            rm -f *.o $(BIN)

    每次发送都是1024定长的字节,如果只发送几个字节的内容也会占用这么多字节,这就会增加网络的负担

    2、自定义协议

    server.c

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <errno.h>
    
    #define ERR_EXIT(m)
            do
            {
                    perror(m);
                    exit(EXIT_FAILURE);
            }while(0);
    
    struct packet
    {
            int len;
            char buf[1024];
    };
    
    ssize_t readn(int fd, void *buf, size_t count)//读取count个字节数,其中size_t是无符号
    的整数,ssize_t是有符号的整数
    {
        size_t nleft = count;//剩余的字节数
        printf("nleft = %d
    ",nleft);
        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)//对等方关闭了
                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;
    }
    
    void do_service(int conn)
    {
            //char recvbuf[1024];
            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");
                    }
                    else if(ret < 4)
                    {
                            printf("client close
    ");
                            break;
                    }
    
                    n = ntohl(recvbuf.len);
                    ret = readn(conn,recvbuf.buf,n);
                    if(ret == -1)
                    {
                            ERR_EXIT("read");
                    }
                    else if(ret < n)
                    {
                            printf("client close
    ");
                            break;
                    }
    
                    fputs(recvbuf.buf,stdout);
                    writen(conn,&recvbuf,4+n);
            }
    }
    
    int main(void)
    {
            int listenfd;
            if((listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
            {
                    ERR_EXIT("socket");
            }
            struct sockaddr_in servaddr;
            memset(&servaddr, 0, sizeof(servaddr));
            servaddr.sin_family = AF_INET;
            servaddr.sin_port = htons(5188);
            servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
            /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
            /*inet_aton("127.0.0.1",&servaddr.sin_addr);*/
    
            //地址重用
            int on=1;
            if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0)
            {
                    ERR_EXIT("setsockopt");
            }
    
            if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
            {
                    ERR_EXIT("bind");
            }
    
            if(listen(listenfd,SOMAXCONN) < 0)
            {
                    ERR_EXIT("listen");
            }
    
            struct sockaddr_in peeraddr;
            socklen_t peerlen = sizeof(peeraddr);
            int confd;
    
            pid_t pid;
            while(1)
            {
                    if((confd = accept(listenfd,(struct sockaddr*)&peeraddr, &peerlen)) <
     0)
                    {
                            ERR_EXIT("accept");
                    }
                    printf("ip = %s, port = %d
    ",inet_ntoa(peeraddr.sin_addr),ntohs(peer
    addr.sin_port));
                    pid = fork();
                    if(pid == -1)
                    {
                            ERR_EXIT("fork");
                    }
                    if(pid == 0)
                    {
                            close(listenfd);
                            do_service(confd);
                            exit(EXIT_SUCCESS);
                    }
                    else
                    {
                            close(confd);
                    }
            }
    
            return 0;
    }

    client.c

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <errno.h>
    
    #define ERR_EXIT(m)
            do
            {
                    perror(m);
                    exit(EXIT_FAILURE);
            }while(0);
    
    struct packet
    {
            int len;
            char buf[1024];
    };
    
    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)
                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;
    }
    
    int main(void)
    {
            int sockfd;
            if((sockfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
            {
                    ERR_EXIT("socket");
            }
            struct sockaddr_in servaddr;
            memset(&servaddr, 0, sizeof(servaddr));
            servaddr.sin_family = AF_INET;
            servaddr.sin_port = htons(5188);
            servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
            /*inet_aton("127.0.0.1",&servaddr.sin_addr);*/
    
            if (connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
            {
                    ERR_EXIT("connect");
            }
    
            //char sendbuf[1024] = {0};
            //char recvbuf[1024] = {0};
            struct packet sendbuf;
            struct packet recvbuf;
            memset(&sendbuf,0,sizeof(sendbuf));
            memset(&recvbuf,0,sizeof(recvbuf));
    
            int n;
    
            while(fgets(sendbuf.buf,sizeof(sendbuf.buf),stdin) != NULL)
            {
                    //writen(sockfd,sendbuf,sizeof(sendbuf));
                    //readn(sockfd,recvbuf,sizeof(recvbuf));
                    n =strlen(sendbuf.buf);
                    sendbuf.len = htonl(n);//网络字节序
                    writen(sockfd,&sendbuf,4+n);
    
                    int ret = readn(sockfd,&recvbuf.len,4);
                    if(ret == -1)
                    {
                         ERR_EXIT("read");
                    }
                    else if(ret < 4)
                    {
                            printf("client close
    ");
                            break;
                    }
    
                    n = ntohl(recvbuf.len);
                    ret = readn(sockfd,recvbuf.buf,n);
                    if(ret == -1)
                    {
                            ERR_EXIT("read");
                    }
                    else if(ret < n)
                    {
                            printf("client close
    ");
                            break;
                    }
    
                    fputs(recvbuf.buf,stdout);
                    memset(&sendbuf,0,sizeof(sendbuf));
                    memset(&recvbuf,0,sizeof(recvbuf));
            }
            close(sockfd);
            return 0;
    }

    Makefile

    .PHONY: clean all
    CC=gcc
    CFLAGE= -G -Wall
    BIN=server client
    all:$(BIN)
    %.o:%.c
            $(CC) $(cflags) -C $< -O $@
    clean:
            rm -f *.o $(BIN)

    这样就很好的解决了粘包问题,在局域网中是不可能出现粘包问题的,但是如果将程序放到广域网,如果不处理粘包问题会存在很大问题的。

  • 相关阅读:
    git添加文件的原理流程
    maven复制包error
    一行代码-Js简单消息弹框
    WEB-给自己所有的域名加上HTTPS
    记录-配置文件-将网站协议从 HTTP 升级为 HTTPS (基于 Nginx 配置)
    记录-Mac终端自动补全的配置(解决不能输入大写T的问题)
    记录-Navicat连接MySQL8.0出现2059错误
    记录-MySQL 修改ROOT密码
    记录-Maven的安装与配置
    简单使用TensorFlow.js在浏览器进行视频实时目标识别(基于YOLO V3)
  • 原文地址:https://www.cnblogs.com/Malphite/p/11604691.html
Copyright © 2011-2022 走看看