待解决问题
- 之前的代码无法感知服务器进程发来的 FIN 段,如果服务器崩溃,客户需要花费很长时间才认识到问题。
因为有可能阻塞在fgets函数上,一直等待服务器端的数据回送。然后客户端不放弃一直重发,等到9分钟后终于放弃了。
RST的问题上一次改进解决了。
select函数
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
1.select参数详解
int select(int nfds, //整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1
fd_set *readfds, //(可选)指针,指向一组等待可读性检查的套接口。
fd_set *writefds, //(可选)指针,指向一组等待可写性检查的套接口。
struct timeval *timeout//select()最多等待时间,对阻塞操作则为NULL
);
后面两个指针依靠三个函数来设置。
fd_set 每一位的0/1代表着是否对该位感兴趣,第几个事件发生时需要关注的,第几位为1.
2.select返回值
返回值 | 备注 |
---|---|
>0 > 0 | 任何一个关注的事件就绪,套接字可读,是就绪的个数。可用于多并发 |
=0 = 0 | 对端TCP发送了一个FIN |
<0 < 0 <script type="math/tex" id="MathJax-Element-18"><0</script> | 主机崩溃重启,对端发送RST,在errno中有确切的错误码 |
改进的代码阅读
#include "unp.h"
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);//标准IO文件指针所在位
FD_SET(sockfd, &rset);//套接字
maxfdp1 = max(fileno(fp), sockfd) + 1;//只关注这两个,因此最大值就是两个最大值+1
Select(maxfdp1, &rset, NULL, NULL, NULL);//只给出等待可读性检查的套接口
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
if (Fgets(sendline, MAXLINE, fp) == NULL)//文件输入完了
//但是可能是批量输入的,还没等到服务器的回复,或者还有请求在路上。
//因此只能是发送FIN表没数据要发送了。可以改为shutdown
//则可以进一步改进,这里不return,而是清除IO位,
return; /* all done */
Writen(sockfd, sendline, strlen(sendline));
}
}
}
针对批量改进
#include "unp.h"
void
str_cli(FILE *fp, int sockfd)
{
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;
stdineof = 0;
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)) { /* socket is readable */
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1)
return; /* normal termination */
else
err_quit("str_cli: server terminated prematurely");
}
Write(fileno(stdout), buf, n);
}
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
stdineof = 1;
Shutdown(sockfd, SHUT_WR); /* send FIN */
FD_CLR(fileno(fp), &rset);
continue;
}
Writen(sockfd, buf, n);
}
}
}
把fgets改掉的原因
忠告:
不要将 stdio 库提供的 C 语言函数与 IO 复用混合使用!!
假设我们没有改掉,而是继续使用fgets(),模拟一下情景:
1. 监听了标准输入,某个时刻 select 监听到了标准输入上有数据,于是客户端调用 fgets 读取一行数据。
2. stdio 的函数是带有缓冲区。fgets 把 STDIN_FILENO 上的所有数据读完了,保存到了自己的缓冲,并返回一行给客户。
3. 程序又回到了 select 调用上。此时,select 永远再也不会触发标准输入上的 IO 事件,因为 select 认为,你的数据早已被读取完,它并不知道 stdio 使用了缓冲区。