从客户端读取数据
《Head First C 中文版》第11章 网络与套接字
第478页 read_in()
函数:
int read_in(int socket, char *buf, int len)
{
char *s = buf;
int slen = len;
int c = recv(socket, s, slen, 0);
while ((c > 0) && (s[c-1] != '\n')) {
s += c; slen -= c;
c = recv(socket, s, slen, 0);
}
if (c < 0)
return c;
else if (c == 0)
buf[0] = '\0';
else
s[c-1] = '\0'; // <---- 用\0替换\r。
return len - slen;
}
书上这个 read_in()
函数倒数第3行说 用\0替换\r
,实际并未成功,因为 s[c-1]
是读取的最后一个字符,通常是 \n
而不是 \r
。
紧接着的 return len - slen;
也不正确。read_in()
函数应返回读取的字符数,考虑最简单的情形:假设函数体第3行的 recv(socket, s, slen, 0);
一次性收到了所有的数据,则 while
循环体不会被执行,此时 slen == len
,则最后的 return len - slen;
显然有误。
即使需要多次调用 recv()
函数接收数据,最后的 return len - slen;
也不会返回正确的字符数,看看后面的测试例子就知道。
测试
为了能够方便地测试 read_in()
函数,我们使用了管道,主进程 fork()
一个子进程,子进程向管道发送数据,主进程从管道接收数据。
send()
函数打印拟发送的字符数和字符串,然后向管道写数据。
recv()
函数从管道读取数据,然后打印收到的字符数和字符串。
prints()
函数用于打印非 \0
结尾的字符串,并把 \r
等非图形字符转义打印,便于看得清楚。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
void error(const char *msg)
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(1);
}
void prints(const char *s, int len)
{
int i;
printf(" [");
for (i = 0; i < len; i++) {
switch (s[i]) {
case '\\': printf("\\\\"); break;
case '\r': printf("\\r"); break;
case '\n': printf("\\n"); break;
case '\0': printf("\\0"); break;
default :
if (isprint(s[i])) putchar(s[i]);
else printf("\\x%02x", s[i] & 0xff);
}
}
printf("]\r\n");
}
int send(int socket, const char * buf, int len, int flag)
{
printf("send :%3i bytes", len);
prints(buf, len);
return write(socket, buf, len);
}
int say(int socket, const char *s)
{
int r = send(socket, s, strlen(s), 0);
if (r == -1) fprintf(stderr, "say: %s\n", strerror(errno));
return r;
}
int recv(int socket, char * buf, int len, int flag)
{
int n = read(socket, buf, len);
printf("recv :%3i bytes", n);
prints(buf, n);
return n;
}
int read_in(int socket, char *buf, int len)
{
char *s = buf;
int slen = len;
int c = recv(socket, s, slen, 0);
while ((c > 0) && (s[c-1] != '\n')) {
s += c; slen -= c;
c = recv(socket, s, slen, 0);
}
if (c < 0)
return c;
else if (c == 0)
buf[0] = '\0';
else
s[c-1] = '\0'; // <---- 用\0替换\r。
return len - slen;
}
int main()
{
int fd[2];
if (pipe(fd) == -1) error("Can't create the pipe");
pid_t pid = fork();
if (pid == -1) error("Can't fork process");
if (!pid) { // 这里你在子进程中。
close(fd[0]); // 子进程不会读取管道,所以我们关闭读取端
const char *ss[] = { "Who'", "s t", "here?\r\n" };
int i;
for (i = 0; i < 3; i++) say(fd[1], ss[i]);
return 0;
}
close(fd[1]); // 父进程不需要向管道写数据,关闭写入端
char buf[255];
int len = read_in(fd[0], buf, sizeof(buf));
printf("read_in:%3i bytes", len);
if (len != -1) prints(buf, strlen(buf));
else puts(" []");
}
编译并运行,结果如下(你的机器上 recv
执行次数可能会有所不同):
$ gcc -std=c99 a.c && ./a.exe
send : 4 bytes [Who']
send : 3 bytes [s t]
send : 7 bytes [here?\r\n]
recv : 4 bytes [Who']
recv : 10 bytes [s there?\r\n]
read_in: 4 bytes [Who's there?\r]
可见 \r
并没有被移除,read_in()
函数的返回值也不是读取的字符数。
修正
只需修改 read_in()
函数的最后几行就能解决这个问题:
int read_in(int socket, char *buf, int len)
{
char *s = buf;
int slen = len;
int c = recv(socket, s, slen, 0);
while ((c > 0) && (s[c-1] != '\n')) {
s += c; slen -= c;
c = recv(socket, s, slen, 0);
}
if (c < 0)
return c;
else if (c == 0)
buf[0] = '\0';
else if (c > 1 && s[c-2] == '\r')
s[c-2] = '\0'; // <---- 用\0替换\r。
else
s[c-1] = '\0';
return strlen(buf);
}
再次编译并运行,现在得到正确的结果(你的机器上 recv
执行次数可能会有所不同):
$ gcc -std=c99 a.c && ./a.exe
send : 4 bytes [Who']
send : 3 bytes [s t]
send : 7 bytes [here?\r\n]
recv : 4 bytes [Who']
recv : 10 bytes [s there?\r\n]
read_in: 12 bytes [Who's there?]
补充
上一小节修正程序 else if (c > 1 && s[c-2] == '\r') s[c-2] = '\0';
,其实有一个很微妙的 bug,考虑一种极端情形,如果行末的 "\r\n"
是由 recv()
分两次读取的,这时 c == 1
, '\n'
前面的 '\r'
是上一次 recv()
读取的,就无法正确删除 '\r'
。
当然,如果输入是由书中所说的 telnet
提供的,行末的 "\r\n"
总是一起来的,这种极端情形应该几百万年也不会出现一次。即使是由上面第二小节的测试程序故意分两次发送行末的 "\r\n"
, recv()
多半也是一次就接收了,正如前面的测试结果所展现的。
也就是说,这种微妙的 bug 可能一辈子也遇不到一次,这是最难发现也最难调试的 bug 。如果是火箭发射之类关键程序,潜伏的 bug 是最可怕的,平时一直都不会出问题,总是运行正常,关键时刻出问题就是灾难性的。曾有报道“”。
言归正传,知道哪里有 bug ,解决起来是相当简单的:记录到目前为止所读取的字符数,然后用 buf[n-2]
代替 s[c-2]
判断是否 '\r'
并删除之(如果是的话)。顺便也就得到了函数应该返回的值,不必调用因扫描整个字符串而比较耗时的 strlen()
函数了。
唠叨了这么多,有耐心看到这儿的童鞋,能否在评论中吱一声,说说您的看法。最后说一句,修正后程序的局部变量 slen
不再需要了,直接用传入的参数 len
代之可也。C 语言的函数参数是传值的,程序中对 len
的修改不会影响到调用函数(当然,指针变量需另外讨论,这也是 C 语言初学者的经典话题了)。
int read_in(int socket, char *buf, int len)
{
char *s = buf;
int c = recv(socket, s, len, 0);
int n = c;
while ((c > 0) && (s[c-1] != '\n')) {
s += c; len -= c;
n += c = recv(socket, s, len, 0);
}
if (c < 0) return c;
else if (c == 0) buf[n=0] = '\0';
else if (n > 1 && buf[n-2] == '\r') buf[n-=2] = '\0';
else buf[--n] = '\0';
return n;
}
按照黄兄的建议重构:取消变量 s
,改 do ... while
避免用到两次 recv()
函数。
int read_in(int socket, char *buf, int len)
{
int n = 0, c;
do
c = recv(socket, buf + n, len, 0);
while ((c > 0) && (buf[(n+=c) - 1] != '\n'));
if (c < 0) return c;
else if (c == 0) buf[n=0] = '\0';
else if (n > 1 && buf[n-2] == '\r') buf[n-=2] = '\0';
else buf[--n] = '\0';
return n;
}
return strlen(buf);
这个 strlen() 函数需要扫描 buf,速度很慢。其实是可以避免的。
删除 int slen = len; 这一句,后面出现的三处 slen 用 len 代替。
int read_in(int socket, char *buf, int len)
{
char *s = buf;
int c = recv(socket, s, len, 0);
int n = c;
while ((c > 0) && (s[c-1] != '\n')) {
s += c; len -= c;
n += c = recv(socket, s, len, 0);
}
if (c < 0)
return c;
else if (c == 0)
buf[n = 0] = '\0';
else if (n > 1 && buf[n-2] == '\r')
buf[n -= 2] = '\0'; // <---- 用\0替换\r。
else
buf[--n] = '\0';
return n;
}
可以考虑把该函数的签名改为:
void read_in(int socket, char *buf, int len);
如果确实需要返回值,可以由调用者计算 strlen(buf) 得到。
$ gcc --version
gcc (GCC) 4.8.0 20130502 (prerelease)
Copyright © 2013 Free Software Foundation, Inc.
本程序是自由软件;请参看源代码的版权声明。本软件没有任何担保;
包括没有适销性和某一专用目的下的适用性担保。
$ gcc -std=c99 a.c && ./a.out
send : 4 bytes [Who']
send : 3 bytes [s t]
send : 7 bytes [here?\r\n]
recv : 4 bytes [Who']
recv : 10 bytes [s there?\r\n]
read_in: 4 bytes [Who's there?\r]
我猜楼主应该是在 Windows XP 32-bit 操作系统的 Cygwin 环境了运行。
$ uname -a
CYGWIN_NT-5.2 DELL1200 1.7.18(0.263/5/3) 2013-04-19 10:39 i686 Cygwin
$ gcc --version
gcc (GCC) 4.5.3
Copyright © 2010 Free Software Foundation, Inc.
本程序是自由软件;请参看源代码的版权声明。本软件没有任何担保;
包括没有适销性和某一专用目的下的适用性担保。
代码用了 strncasecmp(),所以 read_in() 函数没能正确删除 '\r' 也没事,如果比较函数名中没有 n,则有问题。
因此 read_in() 函数的两个 bug 都没能对该页代码造成伤害,不知是有幸还是不幸?
但是,有没有可能出现这种情况:前几次调用 recv 时返回正数,最后一次调用 recv 时返回零?