I/O复用:
一.概念
进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或者多个I/O条件就绪(也就是说输入已准备好被读取或者描述符能承载更多的输出),它就通知进程。这个能力称之为I/O复用,使用select和poll函数支持。pselect是POSIX变种,epoll是Linux独有。
二.I/O模型
UNIX下5种可用的I/O模型:
阻塞式I/O
非阻塞式I/O
I/O复用(select或poll):用于处理多个描述符
信号驱动式I/O(SIGIO)
异步I/O(aio_系列函数)
下面分别介绍这几种I/O模型:
1.阻塞式I/O
最常用的I/O模型。默认情况下,所有套接字都是阻塞的。
2.非阻塞式I/O
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。
3.I/O复用模型
调用select或者poll函数,使其中的某一个阻塞,而不是阻塞在真正的I/O系统调用上。
4.信号驱动式I/O
使用信号,让内核在描述符就绪时发送SIGIO信号通知。
5.异步I/O
告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到我们的缓冲区)完成后通知我们。
三.同步IO和异步IO的比较
同步IO操作:导致请求进程阻塞,直到IO操作完成。
异步IO操作:不导致请求进程阻塞。
四.使用select函数
timeout告诉内核等待其所指定描述符中的任何一个就绪可花多长时间。NULL表示永远等待下去,0表示根本不等待。若需等待一段时间,则需指定时间。
readset,writeset,exceptset指定我们要让内核测试读、写和异常条件的描述符。
maxfdp1参数指定待测试的描述符个数,它的值是待测试的最大描述符加1。
描述符集:数据类型fd_set
void FD_ZERO(fd_set *fdset);
void FD_SET(int fd , fd_set *fdset);
void FD_CLR(int fd , fd_set *fdset);
void FD_ISSET(int fd , fd_set *fdset);
1.client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/time.h>
#include "proto.h"
#define MAXLINE 1024
static int max(int a,int b)
{
if(a>b)
return a;
return b;
}
static void str_cli(FILE *fp,int sockfd)
{
int maxfdp;
fd_set rset;
char sendline[MAXLINE],recvline[MAXLINE];
FD_ZERO(&rset);
while(1)
{
FD_SET(fileno(fp),&rset);
FD_SET(sockfd,&rset);
maxfdp = max(fileno(fp),sockfd)+1;
if((select(maxfdp,&rset,NULL,NULL,NULL)) <0)
{
perror("select()");
exit(1);
}
if(FD_ISSET(sockfd,&rset)) /*socket is readable*/
{
if(read(sockfd,recvline,MAXLINE) == 0)
{
fprintf(stderr,"str_cli: server terminated prematurely");
exit(1);
}
fputs(recvline,stdout);
}
if(FD_ISSET(fileno(fp),&rset)) /*input is readable*/
{
if(fgets(sendline,MAXLINE,fp) == NULL)
return; /*all done*/
write(sockfd,sendline,strlen(sendline));
}
}
return;
}
int main(int argc , char *argv[])
{
int i,sockfd[5];
struct sockaddr_in saddr;
if(argc != 2)
{
fprintf(stderr,"Usage: ./client server_ip\n");
exit(1);
}
for (i=0;i<5;i++)
{
sockfd[i] = socket(AF_INET,SOCK_STREAM,0);
if(sockfd[i] <0)
{
perror("socket()");
exit(1);
}
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET,argv[1],&saddr.sin_addr);
if(connect(sockfd[i],(void *)&saddr,sizeof(saddr))<0)
{
perror("connect()");
exit(1);
}
}
str_cli(stdin,sockfd[0]);
return 0;
}
2.server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <signal.h>
#include <errno.h>
#include "proto.h"
#define MAXLINE 1024
static void sig_chld(int signo)
{
pid_t pid ;
int stat;
while((pid = waitpid(-1,&stat,WNOHANG)) >0)
printf("child %d terminated\n",pid);
return ;
}
static void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while((n = read(sockfd,buf,MAXLINE)) >0)
write(sockfd,buf,n);
if (n <0 && errno == EINTR)
goto again;
else if (n <0)
{
fprintf(stderr,"read error\n");
exit(1);
}
return ;
}
int main(int argc, char *argv[])
{
int listenfd,connfd;
pid_t pid;
struct sockaddr_in laddr,raddr;
socklen_t raddr_len;
listenfd = socket(AF_INET,SOCK_STREAM,0);
if(listenfd <0)
{
perror("socket()");
exit(1);
}
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);
if(bind(listenfd,(void *)&laddr,sizeof(laddr)) <0)
{
perror("bind()");
exit(1);
}
if(listen(listenfd,200) <0)
{
perror("listen()");
exit(1);
}
signal(SIGCHLD,sig_chld);
while(1)
{
raddr_len = sizeof(raddr);
if((connfd = accept(listenfd,(void*)&raddr,&raddr_len)) <0)
{
if (errno == EINTR)
continue;
else
{
fprintf(stderr,"accept error");
exit(1);
}
}
if((pid = fork()) == 0)
{
close(listenfd);
str_echo(connfd);
exit(1);
}
close(connfd);
}
return 0;
}
五.使用shutdown函数
一种关闭TCP连接其中一半的方法。也就是说我们想给TCP服务器发送一个FIN,告诉它我们已经完成了数据发送,但是仍然保持套接字描述符打开以便读取。
终止网络连接的通常方法是调用close函数,但close函数的两个限制可以用shutdown来避免。
使用shutdown可以不管引用计数就TCP的正常连接终止序列。
howto:
SHUT_RD:关闭连接的读这一半
SHUT_WR:关闭连接的写这一半
SHUT_RDWR:连接的读半部和写半部都关闭