一、非堵塞读与写分析
1、老版本1分析--- 基于tcp的堵塞io模型
是read和write,结合起来处理的io模型,因为如果服务器没有数据过来,那么read将会一直堵塞下去。因为堵塞将会导致程序的效率不够高效。
伪代码如下如实:
while(fgets(buf, maxline,stdin) != NULL){
write(sockfd, buf, strlen(buf));
read(sockfd, buf, maxline);
}
分析堵塞条件:
1).write当中如果套接字发送缓冲区已满,write调用将会堵塞
2).read当中如果套接字接受缓冲区没有数据, 那么read将会一直堵塞下去,直到收到了来之服务器的数据。
2、老版本2分析---基于tcp的select中堵塞的io复用模型
在select来判断, 是否能读取数据, 可以排除老版本一当中的读取数据时候而造成的程序堵塞情况, 很大程度的提高了程序的执行效率。
伪代码如下所示:
for(;;){
FD_SET(fileno(stdin), &rset);
FD_SET(sockfd, &rset);
select(maxfd + 1, &rset, NULL, NULL, NULL);
if(FD_ISSET(fileno(stdin), &rset)){
相关处理过程
}
if(FD_ISSET(sockfd, &rset)){
相关处理过程
}
}
分析堵塞条件:1).然而当调用write函数的时候,也会应为套接字发送缓冲区已满,而导致堵塞。
2).如果标准输出比网络慢, 那么进程也有可能堵塞与后续的write调用。
3、非堵塞IO的读与写在select下的复用模型
我们需要在应用层维护两个缓冲区:to缓冲区是标准输入到服务器去的数据, fr缓冲区是容纳来自服务器到标准输出的数据
to缓冲区:tooptr指向缓冲区未发送到服务器的数据的头子节。toiptr指向标准输入读入的数据可以存放的下一个字节。有(toiptr - tooptr)个数据可以被发送到套接字。
可以从标准输入读入的字节数&to[MAXLINE] - toiptr。当tooptr = toiptr的时候, 这两个指针就恢复到缓冲区开始的位置。
fr缓冲区与to缓冲区有相同的道理。
模型图如下所示:
详细实现代码如下所示:
dg_cli_nonblockio.c
#include "unp.h"
void str_cli(FILE *fp, int sockfd){
int maxfdp1, flag, stdineof;
ssize_t n, nwritten;
fd_set wset, rset;
char to[MAXLINE], fr[MAXLINE];
char *toiptr, *tooptr, *friptr, *froptr;
//重点一:把标准输入、标准输出、连接服务器的套接字都设置成非堵塞条件
flag = Fcntl(sockfd, F_GETFL, 0);
Fcntl(sockfd, F_SETFL, flag | O_NONBLOCK);
flag = Fcntl(STDIN_FILENO, F_GETFL, 0);
Fcntl(STDIN_FILENO, F_GETFL, flag | O_NONBLOCK);
flag = Fcntl(STDOUT_FILENO, F_GETFL, 0);
Fcntl(STDOUT_FILENO, F_SETFL, flag | O_NONBLOCK);
toiptr = tooptr = to;
friptr = froptr = fr;
stdineof = 0;
maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd);
for(;;){
FD_ZERO(&rset);
FD_ZERO(&wset);
//标准输出未读到eof的时候, 并且to的空闲缓冲区至少在一个字节字节以上, 打开读描述符集中的标准输入位
if(stdineof == 0 && toiptr < &to[MAXLINE]){
FD_SET(STDIN_FILENO, &rset); /* read from stdin*/
}
//当fr缓冲区有空闲时候, 则设置描述符集中的标准输出位
if(friptr < &fr[MAXLINE]){
FD_SET(sockfd, &rset); /*read from socket*/
}
//tooptr一直小于等于toiptr的值,当不等于的时候,则表示to缓冲区有数据可以输出到socket
if(tooptr != toiptr){
FD_SET(sockfd, &wset); /*write to socket*/
}
// froptr一直小于等于friptr的值,当不等于的时候,则表示fr缓冲区有数据可以标准输出
if(froptr != friptr){
FD_SET(STDOUT_FILENO, &wset); /* write to stdout*/
}
Select(maxfdp1, &rset, &wset, NULL, NULL);
if(FD_ISSET(STDIN_FILENO, &rset)){
if((n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0){
if(errno != EWOULDBLOCK){ //当读取失败的时候有情况,第一非堵塞条件下没有成功读取数据,
//errno置为EWOULDBLOCK,这种错误可以忽略。另一种情况就是读取错误
fprintf(stdout, "read error from stdin\n");
}
}else if(n == 0){//read返回为0,标准输入结束,设置stdineof标志。当to缓冲区不在有数据发送的时候
stdineof = 1;//那么发送FIN分节, 如果有数据发送,则推迟FIN分节的发送
fprintf(stderr, "%s, EOF on stdin\n", gf_time()); /* gf_time() is a function that we finished by ourselves.*/
if(toiptr == tooptr) Shutdown(sockfd, SHUT_WR); /* send FIN*/
}else{
toiptr += n;
fprintf(stderr, "%s: read %d bytes from stdin\n", gf_time(), n); /*just read*/
FD_SET(sockfd, &wset); //try and write to sockfd below
}
}
if(FD_ISSET(sockfd, &rset)){
if((n = read(sockfd, friptr, (size_t)(&fr[MAXLINE] - friptr))) < 0){
if(errno != EWOULDBLOCK)
fprintf(stderr, "read error from sockfd\n");/*read error from socket*/
}else if(n == 0){
fprintf(stderr, "%s:EOF on sockfd\n", gf_time());
if(stdineof == 1) return; /*normal termination*/
}else{
fprintf(stderr, "%s: read %d bytes from sockfd\n", gf_time(), n); /*just read*/
friptr += n;
FD_SET(STDOUT_FILENO, &wset); /*try and write below*/
}
}
//标准输出的条件是标准输出描述符号可写,并且fr缓冲区有数据可以输出
if(FD_ISSET(STDOUT_FILENO, &wset) && ( (n = friptr - froptr) > 0 )){ // 判断的同时并且也获得了fr缓冲区中为数据的bytes大小
if((nwritten = write(STDOUT_FILENO, friptr, n)) < 0){//输出失败时候,EWOULDBLOCK错误忽略
if(errno != EWOULDBLOCK) fprintf(stderr, "write error to stdout\n");
}else{
fprintf(stderr, "%s: wrote %d bytes to stdout\n", gf_time(), nwritten);
friptr += nwritten; /**just written*/
//当输出指针(froptr)追上输入指针(friptr)的时候,指向fr缓冲区开始位置
if(friptr == froptr) friptr = froptr = fr; /*back to begining of buffer*/
}
}
if(FD_ISSET(sockfd, &wset) && ( (n = toiptr - tooptr) > 0)){
if((nwritten = write(sockfd, friptr, n)) < 0){
if(errno != EWOULDBLOCK) fprintf(stderr, "write error to socket\n");
}else{
fprintf(stderr, "%s: wrote %d bytes to socket\n", gf_time(), nwritten);
toiptr += nwritten; /*just written*/
if(toiptr == tooptr){
toiptr = tooptr = to; /*back to begining of buffer*/
if(stdineof) Shutdown(sockfd, SHUT_WR); /* send FIN */
//当stdin已经关闭, 并且to缓冲的数据都已经发送到了服务器, 那么接发送FIN分节到服务器。
}
}
}
}
}
详细分析如代码中的中文注释可得:
二、非堵塞IO问题分析
套接字默认状态是堵塞的。这也就说当我们发送出一个io请求的时候,如果不能及时的完成,那么进程将会进入睡眠状态,等待操作的进行。
(1)、输入操作,包括read, readv,recv,recvmsg,recvfrom五个函数。非堵塞的情况下如果不能被满足(对于tcp至少有一个数据可读,对于UDP套接字即有一个完整的数据报可读),相应的调用返回-1,并且errno将会返回EWOULDBLOCK错误
(2)、输出操作,包括write,writev、send、sendmsg、sendto五个函数。对与tcp而言,在堵塞套接字中,内核将会把数据从应用层缓冲区的数据复制到套接字缓冲区,如果套接字缓冲区空间不够哪儿内核将会进入休眠的状态,在非堵塞套接字中,如果套接字缓冲区的空间没有,那么将会立刻返回EWOULDBLOCK错误,若是有部分空间,则是返回该部分空间的字节数。对于UDP而言, UDP套接字不存在正真的缓冲区,内核直接将数据从应用层缓冲区换衣UDP首部和IP首部从协议栈往下传递