網絡粘包問題解決辦法

流協議與粘包:網絡

   咱們知道TCP是一個基於字節流的傳輸服務,這意味着TCP所傳輸的數據之間是無邊界的,像流水同樣,是沒法區分邊界的;而UDP是基於消息的傳輸服務,它傳輸的是數據報文,是有邊界的。socket

而對於數據之間有無邊界,反映在對方接收程序的時候,是不同的:對於TCP字節流來講,對等方在接收數據的時候,不可以保證一次讀操做,可以返回多少個字節,是一個消息,仍是二個消息,這些都是不肯定的;而對於UDP消息服務來講,它可以保證對等方一次讀操做返回的是一條消息函數

  因爲TCP的無邊界性,就會產生粘包問題,那粘包問題具體體現是怎樣的呢?下面用圖來進行闡述:spa

  

  假設主機A(Host A)要向主機B(Host B)發送兩個數據包:M1,M23d

  而對於對待接收方主機B來講,可能會有如下幾種狀況:code

  ①server

  

   也就是第一次讀操做恰好返回第一條消息(M1)的所有,接下來第二次讀操做返回第二條消息(M2)的所有,因此這就沒有粘包問題。blog

  ②接口

   

   一次讀操做就返回了M1,M2的全部,這樣M1和M2就粘在一塊兒了,這就能比較直觀的體會到粘包的表現了。ip

  ③

   

   一次讀操做返回了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是一個流協議,它不能保證對方一次接收接收到了多少個字節,那咱們就須要封裝一個函數:接收肯定字節數的讀操做

下面簡單介紹一下兩種方法的實現

一、發送定長包

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\n",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\n");
                        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\n",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定長的字節,若是隻發送幾個字節的內容也會佔用這麼多字節,這就會增長網絡的負擔

二、自定義協議

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\n",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\n");
                        break;
                }

                n = ntohl(recvbuf.len);
                ret = readn(conn,recvbuf.buf,n);
                if(ret == -1)
                {
                        ERR_EXIT("read");
                }
                else if(ret < n)
                {
                        printf("client close\n");
                        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\n",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\n");
                        break;
                }

                n = ntohl(recvbuf.len);
                ret = readn(sockfd,recvbuf.buf,n);
                if(ret == -1)
                {
                        ERR_EXIT("read");
                }
                else if(ret < n)
                {
                        printf("client close\n");
                        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)

這樣就很好的解決了粘包問題,在局域網中是不可能出現粘包問題的,可是若是將程序放到廣域網,若是不處理粘包問題會存在很大問題的。

相關文章
相關標籤/搜索