Linux 网络编程详解五(TCP/IP协议粘包解决方案二)

ssize_t recv(int s, void *buf, size_t len, int flags);
--与read相比,只能用于网络套接字文件描述符
--当flags参数的值设置为MSG_PEEK时,recv可以从socket缓存中读取数据,但是不会将缓存中该部分数据清除
  使用read函数直接读取socket缓存区中的内容,会清空缓存区中的内容。假设两段报文粘包,read会清空缓存
  区中所有内容,从而导致后一段报文中的粘包的部分数据丢失
--强调:粘包解决方案包尾加\n,必须使用recv()函数,并且设置参数flags的值是MSG_PEEK
//粘包解决方案--包尾加\r\n
//服务器
#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>

/*
 * 思想,客户端发送报文时,每段报文都以\n结尾,服务端先用recv函数中的flag参数查看缓存区中的数据
 * 以\n分割每个报文
 * 注意,包尾加\r\n这个方案和包头加上包体长度方案最大的区别是包尾加\r\n这个方案并不清楚每次的包体长度是多少,
 * 而包头加上包体长度方案确定确定包体的长度
 * */

ssize_t readn(int fd, const void * buf, ssize_t count)
{
    if (buf == NULL || fd < 0)
    {
        printf("readn() params not allow NULL!\n");
        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 !\n");
            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!\n");
        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 !\n");
            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()不加参数时读取完数据后,会清空缓存)
         * 遍历缓存区中的数据,找到\n,分割报文数据
         * */
        ret = recv(fd,(void *)buf, count, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
        {
            continue;
        }
        return ret;
    }
    return -1;
}

//读取缓存区中以\n结尾的字符串
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 !\n");
            return -1;
        } else if (ret == 0)
        {
            printf("client is closed !\n");
            return -1;
        }
        nread = ret;
        //遍历读取到的数据
        for (i = 0; i < ret; i++)
        {
            if (pbuf[i] == '\n')
            {
                //清空缓存区
                memset(pbuf, 0, lread);
                ret = readn(fd, pbuf, i + 1);
                if (ret != i + 1)
                    return -1;
                //返回已经读取到的数据
                return ret;
            }
        }
        //如果没有读到\n,需要判断自定义buf是否还有空间--这种情况是一段报文被分割在多个包体中发送
        //recv()函数的返回值只可能小于或者等于count
        if (nread == lread)
        {
            printf("自定义缓存buf的长度太小!\n");
            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;
}
//粘包解决方案--包尾加\r\n
//客户端
#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 !\n");
            //返回已经读取的字节数
            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 !\n");
            //对方关闭文件描述符,返回已经写完的字节数
            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()不加参数时读取完数据后,会清空缓存)
         * 遍历缓存区中的数据,找到\n,分割报文数据
         * */
        ret = recv(fd, (void *)buf, count, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
        {
            continue;
        }
        return ret;
    }
    return -1;
}

//读取缓存区中以\n结尾的字符串
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 !\n");
            return -1;
        } else if (ret == 0)
        {
            printf("client is closed !\n");
            return -1;
        }
        nread = ret;
        //遍历读取到的数据
        for (i = 0; i < ret; i++)
        {
            if (pbuf[i] == '\n')
            {
                //清空缓存区
                memset(pbuf, 0, lread);
                ret = readn(fd, pbuf, i + 1);
                if (ret != i + 1)
                    return -1;
                //返回已经读取到的数据
                return ret;
            }
        }
        //如果没有读到\n,需要判断自定义buf是否还有空间--这种情况是一段报文被分割在多个包体中发送
        //recv()函数的返回值只可能小于或者等于count
        if (nread == lread)
        {
            printf("自定义缓存buf的长度太小!\n");
            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;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值