利用recv和readn函数实现readline函数

在前面的文章中,我们为了避免粘包问题,实现了一个readn函数读取固定字节的数据。如果应用层协议的各字段长度固定,用readn来读是非常方便的。

常见的应用层协议都是带有可变长字段的,字段之间的分隔符用换行’\n’的比用’\0’的更常见,如HTTP协议。可变长字段的协议用readn来读就很不方便了,为此我们实现一个类似于fgets的readline函数。

recv函数

首先来看一个跟read 相似的系统函数recv。

 #include <sys/types.h>
 #include <sys/socket.h>
 ssize_t recv(int sockfd, void *buf, size_t len, int flags);

recv函数与read函数类似,但只能读取套接字描述符,而不能是一般的文件描述符,且多了一个标志参数。

recv有两个比较重要的flags参数,一个是MSG_PEEK,即从缓冲区返回数据但不清空缓冲区,这点与read是不同的,另一个是MSG_OOB,即读取带外数据时候的选项,tcp头部有一个紧急指针16位的值

readline函数的实现

使用封装后的recv函数实现readline函数

/* recv()只能读写套接字,而不能是一般的文件描述符 */
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;
    }
}

/* 读到'\n'就返回,加上'\n' 一行最多为maxline个字符 */
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;
    char *bufp = buf;
    int nleft = maxline;
    int count = 0;

    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)
            return ret; // 返回小于0表示失败
        else if (ret == 0)
            return ret; //返回0表示对方关闭连接了

        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 + count;
            }
        }
        if (nread > nleft)
            exit(EXIT_FAILURE);
        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;
        count += nread;
    }

    return -1;

在readline函数中,我们先用recv_peek”偷窥“一下现在缓冲区有多少个字符并读取到bufp,然后查看是否存在换行符’\n’。如果存在,则使用readn连通换行符一起读取,并清空缓冲区。如果不存在,也清空一下缓冲区,且移动bufp的位置,回到while循环开头,再次窥看。

需注意一点是,如果第二次才读取到了’\n’,则先用count保存了第一次读取的字符个数,然后返回的ret需加上原先的数据大小。

使用其他的方法,也可实现readline。先将缓冲区的数据读到缓存,在缓存中读取\n。由于此方法需要用到static,static带状态,不可重入。

readline函数的使用

使用readline函数也可以认为是解决粘包问题的一个办法,即以’\n’为结尾当作一条消息。对于服务器端来说可以在前面的fork程序的基础上把do_service函数更改如下

void do_echoser(int conn)
{
    char recvbuf[1024];
    while (1)
    {
        memset(recvbuf, 0, sizeof(recvbuf));
        int ret = readline(conn, recvbuf, 1024);
        if (ret == -1)
            ERR_EXIT("readline error");
        else if (ret  == 0)   //客户端关闭
        {
            printf("client close\n");
            break;
        }

        fputs(recvbuf, stdout);
        writen(conn, recvbuf, strlen(recvbuf));
    }
}