1.非阻塞读和写
主要是针对str_cli函数的改写
1.1 阻塞IO代码
#include "unp.h"
int
main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT); //服务端服务端口9877
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); //命令行传入服务端地址
Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
str_cli(stdin, sockfd); /* 改写!! */
exit(0);
}
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) //标准输入stdin只要没有遇到EOF,就关注stdin的可读性
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) /* 已经在标准输入遇到过EOF了,是正常终止*/
return; /* normal termination */
else /*服务器过早终止*/
err_quit("str_cli: server terminated prematurely");
}
Write(fileno(stdout), buf, n); //正常情况往stdout写
}
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
stdineof = 1; /* stdin遇到EOF*/
Shutdown(sockfd, SHUT_WR); /* send FIN 关闭写一半*/
FD_CLR(fileno(fp), &rset); /* stdin清0 */
continue;
}
Writen(sockfd, buf, n); //正常情况往服务器写
}
}
}
1.2 更新为非阻塞IO
2个客户端缓冲区
/* include nonb1 */
#include "unp.h"
void
str_cli(FILE *fp, int sockfd)
{
int maxfdp1, val, stdineof;
ssize_t n, nwritten;
fd_set rset, wset;
char to[MAXLINE], fr[MAXLINE];
char *toiptr, *tooptr, *friptr, *froptr;
/*
套接字sockfd, stdin, stdout均设置为nonblock
*/
val = Fcntl(sockfd, F_GETFL, 0);
Fcntl(sockfd, F_SETFL, val | O_NONBLOCK);
val = Fcntl(STDIN_FILENO, F_GETFL, 0);
Fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK);
val = Fcntl(STDOUT_FILENO, F_GETFL, 0);
Fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK);
toiptr = tooptr = to; /* initialize buffer pointers */
friptr = froptr = fr;
stdineof = 0;
maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
for ( ; ; ) {
FD_ZERO(&rset);
FD_ZERO(&wset);
if (stdineof == 0 && toiptr < &to[MAXLINE])
FD_SET(STDIN_FILENO, &rset); /* read from stdin */
if (friptr < &fr[MAXLINE])
FD_SET(sockfd, &rset); /* read from socket */
if (tooptr != toiptr)
FD_SET(sockfd, &wset); /* data to write to socket */
if (froptr != friptr)
FD_SET(STDOUT_FILENO, &wset); /* data to write to stdout */
Select(maxfdp1, &rset, &wset, NULL, NULL);
if (FD_ISSET(STDIN_FILENO, &rset)) { /* stdin满足可读条件 */
/* 描述符 地址起始 长度 */
if ( (n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0) {
/*从标准输入读,且填入缓冲区对应区域,但是遇到错误*/
if (errno != EWOULDBLOCK) /* #define EWOULDBLOCK EAGAIN */
err_sys("read error on stdin");
} else if (n == 0) { /* 遇到EOF */
stdineof = 1; /* 标志位1 */
if (tooptr == toiptr) /* 没有要发往服务器的数据了 */
Shutdown(sockfd, SHUT_WR); /* stdin遇到了EOF,send FIN */
} else { /* 正常 */
toiptr += n; /* toiptr指针往后移动n(从stdin读了n个字节) */
FD_SET(sockfd, &wset); /* 让wset中socketfd这一位是打开的(从标准输入读入数据了,
本次循环肯定是可以往socket写数据的) */
}
}
if (FD_ISSET(sockfd, &rset)) { /* sockfd可读(有从服务端回射回来的数据)*/
if ( (n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0) {
if (errno != EWOULDBLOCK)
err_sys("read error on socket");
} else if (n == 0) {
if (stdineof) /* 正常终止:stdin遇到EOF */
return;
else
err_quit("str_cli: server terminated prematurely");
} else {
friptr += n; /* friptr指针往后移动n(从sockfd读了n个字节) */
FD_SET(STDOUT_FILENO, &wset); /* 让wset中stdout这一位是打开的(从sockfd读入数据了,
本次循环肯定是可以往stdout写数据的) */
}
}
/* 满足往stdout可写且有数据可以写*/
if (FD_ISSET(STDOUT_FILENO, &wset) && ( (n = friptr - froptr) > 0)) {
if ( (nwritten = write(STDOUT_FILENO, froptr, n)) < 0) {
if (errno != EWOULDBLOCK)
err_sys("write error to stdout");
} else {
/* 指针的移动和复位 */
froptr += nwritten; /* # just written */
if (froptr == friptr)
froptr = friptr = fr; /* back to beginning of buffer */
}
}
/* 满足往sockfd可写且有数据可以写*/
if (FD_ISSET(sockfd, &wset) && ( (n = toiptr - tooptr) > 0)) {
if ( (nwritten = write(sockfd, tooptr, n)) < 0) {
if (errno != EWOULDBLOCK)
err_sys("write error to socket");
} else {
/* 指针的移动和复位 */
tooptr += nwritten; /* # just written */
if (tooptr == toiptr) {
toiptr = tooptr = to; /* back to beginning of buffer */
if (stdineof)
Shutdown(sockfd, SHUT_WR); /* stdin遇到EOF,则send FIN */
}
}
}
}
}
1.3 更简单的多进程方法替代复杂的非阻塞法
#include "unp.h"
void
str_cli(FILE *fp, int sockfd)
{
pid_t pid;
char sendline[MAXLINE], recvline[MAXLINE];
/*子进程负责从套接字sockfd读,并写到stdout*/
if ( (pid = Fork()) == 0) { /* child: server -> stdout */
while (Readline(sockfd, recvline, MAXLINE) > 0)
Fputs(recvline, stdout);
/*套接字sockfd无数据可读,子进程要结束了,所以要终止父进程 */
kill(getppid(), SIGTERM); /* in case parent still running */
exit(0);
}
/* 父进程负责从stdin读入,往套接字sockfd写 */
/* parent: stdin -> server */
while (Fgets(sendline, MAXLINE, fp) != NULL)
Writen(sockfd, sendline, strlen(sendline));
/*stdin遇到EOF了,所以调shutdown发送FIN*/
Shutdown(sockfd, SHUT_WR); /* EOF on stdin, send FIN */
/*父进程等待子进程发出的SIGTERM信号*/
pause();
return;
}
2.非阻塞connect
有关于select和非阻塞connect的以下2个规则:1)当连接成功建立时,描述符变为可写;2)当连接遇到错误时,描述符变为既可读又可写
2.非阻塞accept
当一个已完成的连接准备好被accept的时候,select会把监听socket标记为可读。因此,如果用select等待外来的连接时,应该不需要把监听socket设置为非阻塞模式,因为如果select告诉我们连接已经就绪,accept就不应该被阻塞。不过这样做的时候有一个BUG:当客户端 在跟服务器建立连接之后发送了一个RST包,这个时候accept就会阻塞,直到有下一个已完成的连接准备好被accept为止。
代码引发rst
#include "unp.h"
int
main(int argc, char **argv)
{
int sockfd;
struct linger ling;
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
ling.l_onoff = 1; /* cause RST to be sent on close() */
ling.l_linger = 0;
Setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
Close(sockfd);
exit(0);
}
struct linger的l_onoff标志设为1,l_linger设为0。这个时候,如果关闭TCP连接时,会先在socket上发送一个RST包。这个时候会出现下面的问题:
A: select向服务器返回监听socket可读,但是服务器要在一段时间之后才能调用accept;
B: 在服务器从select返回和调用accept之前,收到从客户发送过来的RST;
C: 这个已经完成的连接被从队列中删除,我们假设没有其它已完成的连接存在;
D: 服务器调用accept,但是由于没有其它已完成的连接存在,因而服务器被阻塞了;
注意,服务器会被一直阻塞在accept调用上,直到另外一个客户建立一个连接为止;但是如果一直没有其它客户建立连接,那么服务器将仍然一直被阻塞在accept调用上,不处理任何其他已就绪的socket;(本问题和之前的拒绝服务攻击多少有点类似,不过对于这个新的缺陷,一旦有客户建立一个连接,服务器就会脱出阻塞中的accept)
解决这个问题的办法是:
A:如果使用select来获知何时有链接已就绪可以accept时,总是把监听socket设置为非阻塞模式
B: 在后面的accept调用中忽略以下错误:EWOULDBLOCK(源自Berkeley的实现在客户放弃连接时出现的错误)、 ECONNABORTED(Posix.1g的实现在客户放弃连接时出现的错误)、EPROTO(SVR4的实现在客户放弃连接时出现的错误)和 EINTR(如果信号被捕获).