zoukankan      html  css  js  c++  java
  • Linux 网络编程详解五(TCP/IP协议粘包解决方案二)

    ssize_t recv(int s, void *buf, size_t len, int flags);
    --与read相比,只能用于网络套接字文件描述符
    --当flags参数的值设置为MSG_PEEK时,recv可以从socket缓存中读取数据,但是不会将缓存中该部分数据清除
      使用read函数直接读取socket缓存区中的内容,会清空缓存区中的内容。假设两段报文粘包,read会清空缓存
      区中所有内容,从而导致后一段报文中的粘包的部分数据丢失
    --强调:粘包解决方案包尾加 ,必须使用recv()函数,并且设置参数flags的值是MSG_PEEK
    //粘包解决方案--包尾加
    
    //服务器
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <errno.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <signal.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    /*
     * 思想,客户端发送报文时,每段报文都以
    结尾,服务端先用recv函数中的flag参数查看缓存区中的数据
     * 以
    分割每个报文
     * 注意,包尾加
    这个方案和包头加上包体长度方案最大的区别是包尾加
    这个方案并不清楚每次的包体长度是多少,
     * 而包头加上包体长度方案确定确定包体的长度
     * */
    
    ssize_t readn(int fd, const void * buf, ssize_t count)
    {
        if (buf == NULL || fd < 0)
        {
            printf("readn() params not allow NULL!
    ");
            return -1;
        }
        //定义剩余字符数
        ssize_t lread = count;
        //定义每次读取字符个数
        ssize_t nread = 0;
        //定义字符串移动指针
        char *pbuf = (char *) buf;
        while (lread > 0)
        {
            nread = read(fd, pbuf, lread);
            if (nread == -1)
            {
                //屏蔽信号
                if (errno == EINTR)
                    continue;
                return -1;
            } else if (nread == 0)
            {
                printf("client is closed !
    ");
                return count - lread;
            }
            //重置剩余字节数
            lread -= nread;
            //指针后移
            pbuf += nread;
        }
        return count;
    }
    
    ssize_t writen(int fd, const void * buf, ssize_t count)
    {
        if (buf == NULL || fd < 0)
        {
            printf("writen() params not allow NULL!
    ");
            return -1;
        }
        //定义剩余字符数
        ssize_t lread = count;
        //定义每次写入字符数
        ssize_t nread = 0;
        //定义临时指针变量--假设pbuf的大小大于网络传输字符串的长度
        char * pbuf = (char *) buf;
        while (lread > 0)
        {
            nread = write(fd, pbuf, lread);
            if (nread == -1)
            {
                //屏蔽信号
                if (errno == EINTR)
                    continue;
                return -1;
            } else if (nread == 0)
            {
                printf("client is closed !
    ");
                return count - lread;
            }
            //重置剩余字节数
            lread -= nread;
            pbuf += nread;
        }
        return count;
    }
    
    ssize_t recv_peek(int fd, const void * buf, ssize_t count)
    {
        int ret = 0;
        while (1)
        {
            /*
             * 当recv中flags参数的值是MSG_PEEK时,
             * recv函数会将socket缓存区中的数据读取到内存,并且不会清空socket缓存区(read()获取recv()不加参数时读取完数据后,会清空缓存)
             * 遍历缓存区中的数据,找到
    ,分割报文数据
             * */
            ret = recv(fd,(void *)buf, count, MSG_PEEK);
            if (ret == -1 && errno == EINTR)
            {
                continue;
            }
            return ret;
        }
        return -1;
    }
    
    //读取缓存区中以
    结尾的字符串
    ssize_t mreadline(int fd, const void *buf, ssize_t count)
    {
        //定义临时指针变量
        char *pbuf = (char *) buf;
        //定义自定义buf剩余的字节数
        ssize_t lread = count;
        //定义recv每次使用的字节数
        ssize_t nread = 0;
        int ret = 0, i = 0;
        while (1)
        {
            ret = recv_peek(fd, pbuf, lread);
            if (ret < 0)
            {
                printf("recv_peek() failed !
    ");
                return -1;
            } else if (ret == 0)
            {
                printf("client is closed !
    ");
                return -1;
            }
            nread = ret;
            //遍历读取到的数据
            for (i = 0; i < ret; i++)
            {
                if (pbuf[i] == '
    ')
                {
                    //清空缓存区
                    memset(pbuf, 0, lread);
                    ret = readn(fd, pbuf, i + 1);
                    if (ret != i + 1)
                        return -1;
                    //返回已经读取到的数据
                    return ret;
                }
            }
            //如果没有读到
    ,需要判断自定义buf是否还有空间--这种情况是一段报文被分割在多个包体中发送
            //recv()函数的返回值只可能小于或者等于count
            if (nread == lread)
            {
                printf("自定义缓存buf的长度太小!
    ");
                return -1;
            }
            //说明自定义buf还有空间,可以再接收一次
            //为了读取下一段报文,需要先把socket缓冲区数据全部读取完成
            ret = readn(fd, pbuf, nread);
            if (ret != nread)
                return -1;
            lread -= nread;
            pbuf += nread;
        }
        return -1;
    }
    
    int main(int arg, char *args[])
    {
        //create socket
        int listenfd = socket(AF_INET, SOCK_STREAM, 0);
        if (listenfd == -1)
        {
            perror("socket() err");
            return -1;
        }
        //reuseaddr
        int optval = 1;
        if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval))
                == -1)
        {
            perror("setsockopt() err");
            return -1;
        }
        //bind
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(8080);
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");
        if (bind(listenfd, (struct sockaddr *) &addr, sizeof(addr)) == -1)
        {
            perror("bind() err");
            return -1;
        }
        //listen
        if (listen(listenfd, SOMAXCONN) == -1)
        {
            perror("listen() err");
            return -1;
        }
        //accept
        struct sockaddr_in peeraddr;
        socklen_t peerlen = sizeof(peeraddr);
        int conn = accept(listenfd, (struct sockaddr *) &peeraddr, &peerlen);
        if (conn == -1)
        {
            perror("accept() err");
            return -1;
        }
        char buf[1024] = { 0 };
        while (1)
        {
            //获取一段报文
            int rc = mreadline(conn, buf, 1024);
            if (rc == -1)
            {
                exit(0);
            }
            //打印报文数据
            fputs(buf, stdout);
            //将原来的报文数据发送回去
            rc = writen(conn, buf, strlen(buf));
            if (rc == -1)
            {
                exit(0);
            }
        }
        return 0;
    }
    //粘包解决方案--包尾加
    
    //客户端
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <errno.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <signal.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    ssize_t readn(int fd, const void *buf, ssize_t count)
    {
        //定义临时指针变量
        char *pbuf = (char *)buf;
        //定义每次已读数据
        ssize_t nread = 0;
        //定义剩余数据
        ssize_t lread = count;
        while (lread > 0)
        {
            nread = read(fd, pbuf, lread);
            /*
             * 情况分析:假设b缓冲区buf足够大
             * 如果nread==count,说明数据正好被读完
             * nread<count,说明数据没有被读完,这种情况就是由于粘包产生的
             * socket只接收了数据的一部分,TCP/IP协议不可能出现丢包情况
             * nread==0,说明对方关闭文件描述符
             * nread==-1,说明read函数报错
             * nread>count,这种情况不可能存在
             * */
            if (nread == -1)
            {
                //read()属于可中断睡眠函数,需要做信号处理
                if (errno == EINTR)
                    continue;
                perror("read() err");
                return -1;
            } else if (nread == 0)
            {
                printf("client is closed !
    ");
                //返回已经读取的字节数
                return count - lread;
            }
            //重新获取 剩余的 需要读取的 字节数
            lread = lread - nread;
            //指针后移
            pbuf = pbuf + nread;
        }
        return count;
    }
    
    /* fd:文件描述符
     * buf:数据缓存区
     * count:读取字符数
     * */
    ssize_t writen(int fd, const void *buf, ssize_t count)
    {
        //定义临时指针变量
        char *pbuf = (char *)buf;
        //每次写入字节数
        ssize_t nwrite = 0;
        //剩余未写字节数
        ssize_t lwrite = count;
        while (lwrite > 0)
        {
            nwrite = write(fd, pbuf, lwrite);
            if (nwrite == -1)
            {
                if (errno == EINTR)
                    continue;
                perror("write() err");
                return -1;
            } else if (nwrite == 0)
            {
                printf("client is closed !
    ");
                //对方关闭文件描述符,返回已经写完的字节数
                return count - lwrite;
            }
            lwrite -= nwrite;
            pbuf += nwrite;
        }
        return count;
    }
    
    ssize_t recv_peek(int fd, const void * buf, ssize_t count)
    {
        int ret = 0;
        while (1)
        {
            /*
             * 当recv中flags参数的值是MSG_PEEK时,
             * recv函数会将socket缓存区中的数据读取到内存,并且不会清空socket缓存区(read()获取recv()不加参数时读取完数据后,会清空缓存)
             * 遍历缓存区中的数据,找到
    ,分割报文数据
             * */
            ret = recv(fd, (void *)buf, count, MSG_PEEK);
            if (ret == -1 && errno == EINTR)
            {
                continue;
            }
            return ret;
        }
        return -1;
    }
    
    //读取缓存区中以
    结尾的字符串
    ssize_t mreadall(int fd, const void *buf, ssize_t count)
    {
        //定义临时指针变量
        char *pbuf = (char *) buf;
        //定义自定义buf剩余的字节数
        ssize_t lread = count;
        //定义recv每次使用的字节数
        ssize_t nread = 0;
        int ret = 0, i = 0;
        while (1)
        {
            ret = recv_peek(fd, pbuf, lread);
            if (ret < 0)
            {
                printf("recv_peek() failed !
    ");
                return -1;
            } else if (ret == 0)
            {
                printf("client is closed !
    ");
                return -1;
            }
            nread = ret;
            //遍历读取到的数据
            for (i = 0; i < ret; i++)
            {
                if (pbuf[i] == '
    ')
                {
                    //清空缓存区
                    memset(pbuf, 0, lread);
                    ret = readn(fd, pbuf, i + 1);
                    if (ret != i + 1)
                        return -1;
                    //返回已经读取到的数据
                    return ret;
                }
            }
            //如果没有读到
    ,需要判断自定义buf是否还有空间--这种情况是一段报文被分割在多个包体中发送
            //recv()函数的返回值只可能小于或者等于count
            if (nread == lread)
            {
                printf("自定义缓存buf的长度太小!
    ");
                return -1;
            }
            //说明自定义buf还有空间,可以再接收一次
            //为了读取下一段报文,需要先把socket缓冲区数据全部读取完成
            ret = readn(fd, pbuf, nread);
            if (ret != nread)
                return -1;
            lread -= nread;
            pbuf += nread;
        }
        return -1;
    }
    
    int main(int arg, char *args[])
    {
        //create socket
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd == -1)
        {
            perror("socket() err");
            return -1;
        }
        //connect
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(8080);
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");
        if (connect(sockfd, (struct sockaddr *) &addr, sizeof(addr)) == -1)
        {
            perror("connect() err");
            return -1;
        }
        int rc = 0;
        char buf[1024]={0};
        while (fgets(buf, sizeof(buf), stdin) != NULL)
        {
            //发送数据
            rc=writen(sockfd,buf,strlen(buf));
            if (rc != strlen(buf))
            {
                return -1;
            }
            //接收数据
            memset(buf, 0, sizeof(buf));
            rc = mreadall(sockfd,buf,sizeof(buf));
            if(rc==-1)
            {
                return -1;
            }
            //打印包体
            fputs(buf,stdout);
            memset(buf, 0, sizeof(buf));
        }
        return 0;
    }
  • 相关阅读:
    linux安装篇之mongodb安装及服务自启动配置
    Linux下启动mongodb
    java 实现 图片与byte 数组互相转换
    用java imageio调整图片DPI,例如从96调整为300
    StringRedisTemplate操作redis数据
    Docker 更换国内的Hub源
    2、Docker 基础安装和基础使用 一
    Centos 6.x Openssh 升级 7.7p1 版本
    1、Docker 简介
    2. Python环境安装
  • 原文地址:https://www.cnblogs.com/zhanggaofeng/p/6137168.html
Copyright © 2011-2022 走看看