在前面的文章中,我们为了避免粘包问题,实现了一个readn函数读取固定字节的数据。如果应用层协议的各字段长度固 定,用readn来读是非常方便的。例如设计一种客户端上传文件的协议,规定前12字节表示文件名,超过12字节的文件名截 断,不足12字节的文件名用'\0'补齐,从第13字节开始是文件内容,上传完所有文件内容后关闭连接,服务器可以 先调用readn读12个字节,根据文件名创建文件,然后在一个循环中调用read读文件内容并存盘,循环结束的条件是read返 回0。
字段长度固定的协议往往不够灵活,难以适应新的变化。前面讲过的TFTP协议的各字段是可变长的,以'\0'为 分隔符,文件名可以任意长,再看blksize等几个选项字段,TFTP协议并没有规定从第m字节到第n字节是blksize的值,而是 把选项的描述信息“blksize”与它的值“512”一起做成一个可变长的字段。
因此,常见的应用层协议都是带有可变长字段的,字段之间的分隔符用换行'\n'的比用'\0'的更常见 ,如HTTP协议。可变长字段的协议用readn来读就很不方便了,为此我们实现一个类似于fgets的readline函数。
首先来看一个跟read 相似的系统函数recv。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv函数与read函数 类似,但只能读取套接字描述符,而不能是一般的文件描述符,且多了一个标志参数。
flags参数比较重要的有两个 ,一个是MSG_OOB,即读取带外数据时候的选项,tcp头部有一个紧急指针16位的值。另一个是MSG_PEEK,即从缓冲区返回数 据但不清空缓冲区,这点与read是不同的。
下面使用封装后的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'就返回,一行最多为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”偷窥“ 一下现在缓冲区有多少个字符,然后查看是否存在换行符 '\n',如果存在,则使用readn连通换行符一起读取,如果不存在,则也先将前面的数据读取进bufp, 且移动bufp 的位置,回到while循环开头,再从当前bufp位置窥看,注意,当我们调用readn读取数据时,那部分缓冲区是会被清空的, 因为readn调用了read函数,还需注意一点是,如果第二次才读取到了'\n',则先用count保存了第一次读取的字符 个数,然后返回的ret需加上原先的数据大小。
使用 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)); } }
客户端的更改也是类似的,不再赘述,测试输出也是正常的。
以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索函数
, 字节
, hook recv的问题
, recv
, 字段的设置与读取
, 设置字段换行
, 字段
, ret
, readline
, recvmsg半包粘包套接字
, 一个
, socket粘包
读取分隔符
socket read recv、socket read recv性能、java socket readline、socket readline 阻塞、socket readline,以便于您获取更多的相关知识。