概述
套接字的默认状态是阻塞的,可能被阻塞的套接字调用分为以下四类:
- 输入操作:包括read、readv、recv、recvfrom和recvmsg共5个函数。(1)如果某个进程对一个阻塞的TCP套接字调用这些输入函数之一,而且该套接字的接受缓冲区中没有数据可读,该进程将被投入睡眠,直到有一些数据到达。(2)对于非阻塞的套接字,如果输入操作不能被满足,相应调用将立即返回一个EWOULDBLOCK错误。
- 输出操作:包括write、writev、send、sendto和sendmsg共5个函数。(1)对于阻塞套接字,如果其发送缓冲区中没有空间,进程将被投入睡眠,直到有空间为止。(2)对于一个非阻塞的TCP套接字,如果其发送缓冲区中根本没有空间,返回将是内核能够复制到该缓冲区中的字节数。
- 接受外来连接:例如connfd = Accept();(1)如果迟迟没有客户端请求连入,则服务器进程将投入睡眠。(2)如果对一个非阻塞的套接字调用accept函数,并且尚无新的连接到达,accept调用将立即返回一个EWOULDBLOCK错误。
- 发起外出连接:即用于TCP的connect函数
非阻塞式I/O与阻塞式I/O的比喻
- 阻塞式I/O:假设我要到菜鸟驿站(取快递的地方就是内核缓冲区)去快递,但是我不知道快递什么时候过来,这时候我只能死等着(睡眠),直到菜鸟驿站给我发送短信通知,我才被唤醒,然后去取快递。
- 非阻塞式I/O:假设我要到菜鸟驿站取快递,这次我采取每20分钟去菜鸟驿站看快递到了没:如果快递没到,我就立即回寝室(返回EWOULDBLOCK);如果在每20分钟的轮询中查到快递已到达,那么我就去菜鸟驿站(内核缓冲区)取快递。
非阻塞读和写:str_cli函数
在以往的基础上添加两个缓冲区:to和from。
to缓冲区中包含从标准输入读取的空间、要发往服务器的数据(正在排队)和已发送成功的数据。
from缓冲区中包含从TCP套接字读取的空间,要发往标准输出的数据(正在排队)和已发送成功的数据。
操作to和from的主要代码如下:
//缓冲区的创建以及初始化
char to[MAXLINE], fr[MAXLINE];
char *toiptr, *tooptr, *friptr, *froptr;
toiptr = tooptr = to;
friptr = froptr = fr;
//如果文件未结束且to缓冲区中尚有可从标准输入读入的空闲空间
if (stdineof == 0 && toiptr < &to[MAXLINE])
FD_SET(STDIN_FILENO, &rset);
//设置Select轮询描述符状态
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)
err_sys("read error on stdin");
} else if (n == 0) {
fprintf(stderr, "%s:EOF on stdin\n", gf_time());
stdineof = 1; //设置读入结束标志
if (tooptr == toiptr)//要发往服务器数据空间为0,则发送操作完成
Shutdown(sockfd, SHUT_WR); //置TCP为半关闭,传输后续未完成数据
} else {
fprintf(stderr, "%s:read %d bytes from stdin\n", gf_time(), n);
toiptr += n; //读入成功,移动指针,使得要发往服务器数据空间增加:增加一组排队发往服务器的数据
FD_SET(sockfd, &wset);
}
}
总结:使用非阻塞I/O使程序能够发挥动态性的优势,只要I/O操作有可能发生,就执行合适的读操作或写操作。通过Select函数,我们可以让内核告知我们何时某个I/O操作可以发生。
str_cli的简单版本
利用fork把当前进程划分为一个父进程和一个子进程。子进程把来自服务器的文本行复制到标准输出,父进程把来自标准输入的文本行复制到服务器,如图。
代码如下,从中可以看到如何在一个函数中分别操纵父子进程。
#include "unp.h"
void str_cli1(FILE *fp, int sockfd){
pid_t pid;
char sendline[MAXLINE], recvline[MAXLINE];
if( (pid = Fork()) == 0) {//if包裹范围内为子进程
while(Readline(sockfd, recvline, MAXLINE) > 0)
Fputs(recvline, stdout);
kill(getppid(), SIGTERM);
}
while (Fgets(sendline, MAXLINE, stdin) != NULL)
Writen(sockfd, sendline, strlen(sendline));
Shutdown(sockfd, SHUT_WR);
pause();
return;
}