上节我们实现的TCP回显客户程序有缺陷,代码如下:
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (Fgets(sendline, MAXLINE, fp) != NULL) { /*从标准输入读取一行文本*/
Writen(sockfd, sendline, strlen(sendline)); /*发送到服务器*/
if (Readline(sockfd, recvline, MAXLINE) == 0) /*从服务器读取一行文本*/
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout); /*输出到标准输出*/
}
}
缺陷在于它仅仅阻塞在标准输入上,只有先处理完标准输入之后才能处理套接字的输入,而不能同时处理两者的输入。有了select函数,我们就能同时监听两个描述符解决这个问题。参考select函数的用法,我们修改了str_cli函数,代码如下:
void str_cli(FILE *fp, int sockfd)
{
int maxfdp1;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
FD_ZERO(&rset);
for ( ; ; ) {
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /*套接字输入*/
/*!!!!*/
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
if (FD_ISSET(fileno(fp), &rset)) { /*标准输入*/
/*!!!!*/
if (Fgets(sendline, MAXLINE, fp) == NULL)
return;
Writen(sockfd, sendline, strlen(sendline));
}
}
}
很不幸的是,修改之后的代码是不正确的。理由如下:
1. readline函数使用的是自己的缓冲区,fgets函数使用的是标准I/O(stdio)缓冲区。而select函数判读描述符是否可读是从read系统调用的角度来看的,它并不知道readline函数和fgets函数使用了缓冲区。这样就会出现当数据已经在缓冲区时,而select函数并不知道的情况。所以使用标准I/O库的函数(fgets等)不能和select函数混合使用!!!
2. 在批量方式下(将标准输入重定向到一个输入文件,将标准输出重定向到一个输出文件),我们会发现输出文件总是小于输入文件。原因在于在标准输入中遇到EOF时,str_cli函数就会返回,客户进程调用exit函数退出,随后关闭套接字描述符时会同时关闭接收端和发送端,这样在管道中等待接收的数据就被丢弃了。
为了解决第一个问题,我们取消以文本行为中心的操作,不使用标准I/O缓冲区,而使用read函数,改为单个字节的操作方式。
为了解决第二个问题,我们使用shutdown函数,执行TCP的半关闭操作,使得TCP连接的发送端被关闭的同时接收端仍能接收数据。
修改后的代码如下:
void str_cli(FILE *fp, int sockfd)
{
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;
stdineof = 0; /*标准输入遇到EOF时置1*/
FD_ZERO(&rset);
for ( ; ; ) {
if (stdineof == 0)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /*套接字可读*/
/*当我们在套接字遇到EOF时,如果我们已在标准输入遇到EOF,
那么服务器就是正常的终止,函数返回。否则服务器就是异常的终止。*/
if ((n = Read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1)
return;
else
err_quit("str_cli: server terminated prematurely");
}
Write(fileno(stdout), buf, n);
}
if (FD_ISSET(fileno(fp), &rset)) { /*标准输入可读*/
if ((n = Read(fileno(fp), buf, MAXLINE)) == 0) { /*标准输入遇到EOF*/
stdineof = 1;
Shutdown(sockfd, SHUT_WR); /*关闭连接的发送端*/
FD_CLR(fileno(fp), &rset);
continue;
}
Writen(sockfd, buf, n);
}
}
}