您的位置:首页 > 其它

socket流协议与粘包

2016-01-03 11:28 513 查看
一、原理讲解

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

解决方案:本质上是要在应用层维护消息与消息的边界(下文的“包”可以认为是“消息”)

1、定长包

2、包尾加\r\n(ftp)

3、包头加上包体长度

4、更复杂的应用层协议

二、代码

第一种方法:定长包

[code]/*packsrv.c*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/errno.h>

#define ERR_EXIT(m)\
\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0);

/*自定义包*/
struct packet{
    int len;
    char buf[1024];
};

/*ssize_t:有符号整数。size_t:无符号整数*/
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 nwritten(int fd,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(conn){
    struct packet recvbuf;
    int n;  
    while(1){
        memset(&recvbuf,0,sizeof(recvbuf));

        int ret = readn(conn,&recvbuf.len,sizeof(int));
        /*int ret = readn(conn,recvbuf,sizeof(recvbuf));*/
        if(ret == -1)
            ERR_EXIT("read");
        if(ret < 4){
            printf("client close...\n");
            break;
        }
        ntohl(recvbuf.len);
        printf("包头数据的值是%d,",n);

        ret = readn(conn,recvbuf.buf,n);
        if(ret == -1){
            ERR_EXIT("read");
        }
        else if(ret < n){
            printf("client close\n");
            break;
        }
        printf("读入的字节即包的大小是%d\n",ret);
        fputs(recvbuf.buf,stdout);
        //printf("the sizeof your messge is %d\n:",ret);
        nwritten(conn,&recvbuf,sizeof(int)+n);
    }
}

int main(void){
    int listenfd;
    if((listenfd = socket(PF_INET,SOCK_STREAM,0)) < 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);

    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 conn;
    pid_t pid;
    while(1){
        if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip = %s,port = %d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
        pid = fork();
        if(pid == -1)
            ERR_EXIT("fork");
        if(pid == 0){
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);

        }
    return 0;
}


[code]/*packcli.c*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/errno.h>

#define ERR_EXIT(m)\
\
{\
    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 nwritten(int fd,void* buf,size_t count){
    ssize_t nleft = count;
    size_t nwritten = count;
    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 sock;
    if((sock = socket(PF_INET,SOCK_STREAM,0)) < 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");

    if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
        ERR_EXIT("connect");
    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){
        n = strlen(sendbuf.buf);
        //统一转换为网络字节序
        sendbuf.len = htonl(n);
        /*发送定长字节*/
        /*nwritten(sock,senbuf,sizeof(sendbuf));*/
        nwritten(sock,&sendbuf,sizeof(int)+n);//包头和message
        /*writen(sock,sendbuf,strlen(sendbuf));*/
        int ret = readn(sock,&recvbuf.len,sizeof(int));
        if(ret == -1){
            ERR_EXIT("read");
        }
        else if(ret < 4){
            printf("client close\n");
            break;
        }
        /*转换成本机字节序*/
        n = ntohl(recvbuf.len);
        ret = readn(sock,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(sock);
    return 0;
}


运行结果不再详述。

第二种方法:包尾加\r\n

[code]/*peeksrv.c*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/errno.h>

#define ERR_EXIT(m)\
\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0);

/*ssize_t:有符号整数。size_t:无符号整数*/
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 nwritten(int fd,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;
}
/*只是从缓冲区中获取数据,并不移除
 * read函数读完了以后会把数据从缓冲区移除*/
ssize_t recv_peek(int sockfd,void* buf,size_t len){
    while(1){
        int ret = recv(sockfd,buf,len,MSG_PEEK);
        if(ret == -1 && errno == EINTR){
            continue;
        }
        return ret;
    }
}
/*这里要用到readline函数,如果我们一个字符一个字符的判断有没有换行符
 * 会非常麻烦。这里我们用偷窥的方法*/
ssize_t readline(int sockfd,void* buf,size_t maxline){
    int ret;
    int nread;
    char* bufp = (char*)buf;
    int nleft = maxline;
    while(1){
        ret = recv_peek(sockfd,bufp,nleft);
        if(ret < 0)
            return ret;
        else if(ret == 0)
            return ret;
        nread = ret;
        int i;
        for(i = 0;i < nread; i++){
            if(bufp[i] == '\n'){
                /*i+1因为后面有一个\n*/
                ret = readn(sockfd,bufp,i+1);
                if(ret != i+1)
                    exit(EXIT_FAILURE);
                return ret;
            }
        }
        /*如果读取的字节数超过剩下的字节数,这不可能,是错的*/
        if(nread > nleft)
            exit(EXIT_FAILURE);
        nleft -= nread;
        ret = readn(sockfd,bufp,nread);
        if(ret != nread)
            exit(EXIT_FAILURE);
        bufp += nread;
    }
    return -1;
}
void do_service(conn){
    char recvbuf[1024];
    while(1){
        memset(&recvbuf,0,sizeof(recvbuf));
        int ret = readline(conn,recvbuf,1024);
        if(ret == -1)
            ERR_EXIT("read");
        if(ret == 0){
            printf("client close\n");
            break;
        }

        fputs(recvbuf,stdout);
        //printf("the sizeof your messge is %d\n:",ret);
        nwritten(conn,recvbuf,strlen(recvbuf));
    }
}

int main(void){
    int listenfd;
    if((listenfd = socket(PF_INET,SOCK_STREAM,0)) < 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);

    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 conn;
    pid_t pid;
    while(1){
        if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip = %s,port = %d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
        pid = fork();
        if(pid == -1)
            ERR_EXIT("fork");
        if(pid == 0){
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);

        }
    return 0;
}


[code]/*peekcli.c*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/errno.h>

#define ERR_EXIT(m)\
\
{\
    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 nwritten(int fd,void* buf,size_t count){
    ssize_t nleft = count;
    size_t nwritten = count;
    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;
}

ssize_t recv_peek(int sockfd,void* buf,size_t len){
    while(1){
        int ret = recv(sockfd,buf,len,MSG_PEEK);
        if(ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}
ssize_t readline(int sockfd,void* buf,size_t maxline){
    int ret;
    int nread;
    char* bufp =(char*)buf;
    int nleft = maxline;
    while(1){
        ret = recv_peek(sockfd,bufp,nleft);
        if(ret < 0){
            return ret;
        }
        else if(ret == 0){
            return ret;
        }
        nread = ret;
        int i;
        for(i = 0;i < nread;i++){
            if(bufp[i] == '\n'){
                ret = readn(sockfd,bufp,i+1);
                if(ret != i+1)
                    exit(EXIT_FAILURE);
                return ret;
            }
        }

        if(nread > nleft)
            exit(EXIT_FAILURE);
        nleft -= nread;
        if(ret != nread)
            exit(EXIT_FAILURE);
        bufp += nread;
    }
    return -1;
}
int main(void){
    int sock;
    if((sock = socket(PF_INET,SOCK_STREAM,0)) < 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");

    int conn; 
    if((conn = connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))) < 0)
        ERR_EXIT("connect");
    /*获取套接口本地的端口*/  
    struct sockaddr_in localaddr;
    socklen_t addrlen = sizeof(localaddr);
    if(getsockname(sock,(struct sockaddr*)&localaddr,&addrlen) < 0)
        ERR_EXIT("getsockname");
    printf("ip = %s,port = %d\n",inet_ntoa(localaddr.sin_addr),ntohs(localaddr.sin_port));
    /*获取对方的端口*/
    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    if(getpeername(conn,(struct sockaddr*)&peeraddr,&peerlen) < 0)
        ERR_EXIT("getpeername");
    printf("ip = %s,port = %d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));

    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};
    while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL){

        nwritten(sock,sendbuf,strlen(sendbuf));
        int ret = readline(sock,recvbuf,sizeof(recvbuf));
        if(ret == -1){
            ERR_EXIT("read");
        }
        else if(ret == 4){
            printf("client close\n");
            break;
        }
        fputs(recvbuf,stdout);
        memset(sendbuf,0,sizeof(sendbuf));
        memset(recvbuf,0,sizeof(recvbuf));
    }
    close(sock);
    return 0;
}


这和之前的代码都大同小异,所以无需多言。

三、补充函数

[code]int gethostname(char* name,size_t len);
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr,socklen_t len,int type);
/*gethostname -- 服务器程序使用了gethostname()函数来获取本机的名称,这个函数在实现上,会先查找/etc/hosts文件的内容,然后查询DNS服务器。如果/etc/hosts文件没有配置,返回的主机名就是localhost,也就是127.0.0.1 。所以,使用bind函数绑定的网络地址其实是一个127.0.0.1 。客户端想connect的话,就找不到连接了*/
/*gethostbyname() -- 用域名或主机名获取IP地址*/


gethostbyname

gethostbyaddr

写个小程序验证一下:

[code]#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/errno.h>
#include <netdb.h>
#include <unistd.h>

#define ERR_EXIT(m)\
\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0);

int getlocalip(char*ip){
    char host[100] = {0};
     if(gethostname(host,sizeof(host)) < 0)
         ERR_EXIT("gethostname");
    struct hostent* hp;
    if((hp = gethostbyname(host)) == NULL)
        return -1;
    strcpy(ip,inet_ntoa(*(struct in_addr*)hp->h_addr_list[0]));
    return 0;
}
int main(void){

    char host[100] = {0};
    if(gethostname(host,sizeof(host)) < 0)
        ERR_EXIT("gethostname");
    struct hostent* hp;
    /*给该结构体指针分配安全的地址*/
    hp = malloc(sizeof(struct hostent));
    if((hp = gethostbyname(host)) < 0)
        ERR_EXIT("gethostbyname");

    int i;
    i = 0;
    /*hp->h_addr_list[i]
     * hp->h_addr_list是一个二级指针
     * hp->h_addr_list[i]是一个一级指针,它里面存放的是一个地址*/
    while((hp->h_addr_list) != NULL){
        printf("%s\n",inet_ntoa(*(struct in_addr*)hp->h_addr_list[i]));
        i++;

    }
    char ip[16] = {0};
    getlocalip(ip);
    printf("localip = %s\n:",ip);
    return 0;
}


给出一个博客的连接,讲的比较详细。

http://andylin02.iteye.com/blog/662965
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: