本文主要介绍5种I/O模型,select函数以及利用select实现C/S模型。
1、5种I/O模型
(1)阻塞I/O:
一直等到数据到来,才会将数据从内核中拷贝到用户空间中。
(2)非阻塞I/O:
每过一段时间就询问是否有数据到来(轮询),调用recv()函数,若没有数据到来会返回错误。接着继续询问。
(3)I/O多路复用:
一个进程可以轮询多个I/O(文件描述符),将阻塞过程提前到select/poll/epoll中。
(4)信号驱动I/O:
当数据到达时,进程会收到信号SIGIO,在信号处理函数中,实现数据的读取。
(5)异步I/O:
以上四种方式都是同步方式,都是顺序执行的。而异步I/O不一定是顺序执行。通过调用aio_read函数,无论内核是否准备好数据,都会给用户进程返回一个值,因此不会使程序产生阻塞。同时,用户进程可以做其他的事情。若数据准备好了,内核会将数据复制给用户进程。当一切完成之后,内核会发送完成通知。
2、select函数
select函数原型:
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
select函数作用:
select函数用来管理多个I/O,当其中的一个或多个I/O检测到了我们感兴趣的事件,select就返回检测到的事件个数,并返回哪些I/O产生了事件。
参数介绍:
(1)maxfdp:一个整数,在Linux中,为所有文件描述符的最大值加1;
(2)readfds:如果I/O(文件描述符)可读,就把它放入readfds集合中;
(3)writefds:如果I.O(文件描述符)可写,就把它放入writefds集合中;
(4)exceptfds:如果想检测文件描述符是否有异常,就把它放入exceptfdsz集合中;
(5)timeout:结构体timeval,select的超时时间。若设置为NULL,则将select置于阻塞状态;若设置为0,则设置select为纯非阻塞函数,不管文件描述符是否有变换,会立即返回;若设置为大于0,则在timeout时间内,select一直阻塞。
3、利用select的C/S模型
客户端:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"common.h"
int main()
{
int sock ;
sock = socket(PF_INET,SOCK_STREAM,0);
if(sock <0)
{
err_exit("socket");
}
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(5888);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret ;
ret = connect (sock , (struct sockaddr*)&addr,sizeof(addr));
if(ret < 0)
{
err_exit("connect");
}
fd_set fds;
FD_ZERO(&fds); //清空集合
int maxfds;
if(STDIN_FILENO > sock)
maxfds=STDIN_FILENO;
else
maxfds=sock;
while(1)
{
FD_SET(STDIN_FILENO,&fds);
FD_SET(sock,&fds);
int nready=select(maxfds+1,&fds,NULL,NULL,NULL);
if(nready==-1) //出错
err_exit("select");
else if(nready == 0) //无数据
continue;
//键盘有输入
if(FD_ISSET(STDIN_FILENO,&fds))
{
struct packet writebuf;
memset(&writebuf,0,sizeof(writebuf));
//获取键盘输入
fgets(writebuf.data,sizeof(writebuf.data),stdin);
int n=strlen(writebuf.data);
writebuf.msgLen=htonl(n);
writen(sock,&writebuf,4+n); //发送
memset(&writebuf,0,sizeof(writebuf));
}
if(FD_ISSET(sock,&fds))
{
struct packet readbuf;
memset(&readbuf,0,sizeof(readbuf));
int ret=readn(sock,&readbuf.msgLen,4);
if(ret == -1)
err_exit("readn");
else if(ret == 0)
{
printf("peer1 close\n");
break;
}
int dataBytes = ntohl(readbuf.msgLen);
int readBytes= readn(sock,readbuf.data,dataBytes);
if(readBytes<0)
err_exit("read");
fputs(readbuf.data,stdout);
}
}
close(sock);
return 0;
}
服务器:
/*
单进程服务器
实现简单点到点的聊天功能
*/
#include<stdlib.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"common.h"
int main()
{
int listenfd;
listenfd = socket(PF_INET , SOCK_STREAM, 0);
if(listenfd <0)
{
err_exit("socket");
}
struct sockaddr_in addr;
addr.sin_family = PF_INET;
addr.sin_port = htons(5888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//设置地址可重复利用
int on = 1;
setsockopt(listenfd, SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
int ret = bind(listenfd , (const struct sockaddr *)&addr,sizeof(addr));
if(ret == -1)
{
err_exit("bind");
}
ret = listen(listenfd , SOMAXCONN);
if(ret < 0)
{
err_exit("listen");
}
struct sockaddr_in peerAddr ;
socklen_t peerlen = sizeof(peerAddr); //peerlen一定要有初始值
int conn;
conn = accept (listenfd ,(struct sockaddr *)&peerAddr,&peerlen);
if(conn <0)
{
err_exit("accept");
}
printf("客户端的IP地址是:%s,端口号是:%d\n",
inet_ntoa(peerAddr.sin_addr),ntohs(peerAddr.sin_port));
fd_set fds;
FD_ZERO(&fds); //清空集合
int maxfds;
if(STDIN_FILENO > conn)
maxfds=STDIN_FILENO;
else
maxfds=conn;
while(1)
{
FD_SET(STDIN_FILENO,&fds);
FD_SET(conn,&fds);
int nready=select(maxfds+1,&fds,NULL,NULL,NULL);
if(nready==-1) //出错
err_exit("select");
else if(nready == 0) //无数据
continue;
//键盘有输入
if(FD_ISSET(STDIN_FILENO,&fds))
{
struct packet writebuf;
memset(&writebuf,0,sizeof(writebuf));
//获取键盘输入
fgets(writebuf.data,sizeof(writebuf.data),stdin);
int n=strlen(writebuf.data);
writebuf.msgLen=htonl(n);
writen(conn,&writebuf,4+n); //发送
memset(&writebuf,0,sizeof(writebuf));
}
if(FD_ISSET(conn,&fds))
{
struct packet readbuf;
memset(&readbuf,0,sizeof(readbuf));
int ret=readn(conn,&readbuf.msgLen,4);
if(ret == -1)
err_exit("readn");
else if(ret == 0)
{
printf("peer1 close\n");
break;
}
int dataBytes = ntohl(readbuf.msgLen);
int readBytes= readn(conn,readbuf.data,dataBytes);
if(readBytes<0)
err_exit("read");
fputs(readbuf.data,stdout);
}
}
close(listenfd);
close(conn);
return 0;
}
common.h
#ifndef __COMMON__
#define __COMMON__
#include<errno.h>
#include<stdlib.h>
#include<unistd.h>
# define err_exit(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
struct packet
{
unsigned int msgLen ;
char data [1024];
};
void handler(int sig)
{
printf("recv a sig = %d \n",sig);
exit(EXIT_SUCCESS);
}
/*
readn 函数
读取count字节数据
*/
ssize_t readn(int fd,void *buf, size_t count)
{
int left = count ; //剩下的字节
char * ptr = (char*)buf ;
while(left>0)
{
int readBytes = read(fd,ptr,left);
if(readBytes< 0)//read函数小于0有两种情况:1中断 2出错
{
if(errno == EINTR)//读被中断
{
continue;
}
return -1;
}
if(readBytes == 0)//读到了EOF
{
return count - left;
}
left -= readBytes;
ptr += readBytes ;
}
return count ;
}
/*
writen 函数
写入count字节的数据
*/
ssize_t writen(int fd, void *buf, size_t count)
{
int left = count ;
char * ptr = (char *)buf;
while(left >0)
{
int writeBytes = write(fd,ptr,left);
if(writeBytes<0)
{
if(errno == EINTR)
continue;
return -1;
}
else if(writeBytes == 0)
continue;
left -= writeBytes;
ptr += writeBytes;
}
return count;
}
/*
从缓冲区中读取指定长度的数据,但不清楚缓冲区内容
*/
ssize_t read_peek(int sockfd, void *buf, size_t len )
{
while(1)
{
int ret = recv (sockfd , buf ,len ,MSG_PEEK);
if(ret == -1)
{
if(errno ==EINTR) //出现中断
continue ;
}
return ret ;
}
}
/*
按行读取数据
参数说明:
sockfd:套接字
buf :应用层缓冲区,保存读取到的数据
maxline:所规定的一行的长度
返回值说明:
*/
ssize_t readLine (int sockfd , void *buf ,size_t maxline)
{
int ret;
int nRead = 0;
int left = maxline ; //剩下的字节数
char * pbuf = (char *) buf ;
int count = 0;
while(1)
{
ret = read_peek ( sockfd, pbuf, left); //读取长度为left的socket缓冲区内容
if(ret <0)
{
return ret;
}
nRead = ret;
int i = 0;
for(;i<nRead;++i)//看看读取出来的数据中是否有换行符\n
{
if(pbuf[i]=='\n')//如果有换行符
{
ret = readn(sockfd , pbuf , i+1);//读取一行
if(ret != i+1) //一定会读到i+1个字符,否则是读取出错
{
exit(EXIT_FAILURE);
}
return ret + count ;
}
}
//窥探的数据中并没有换行符
//把这段没有\n的读出来
ret = readn(sockfd , pbuf,nRead);
if(ret != nRead )
{
exit(EXIT_FAILURE);
}
pbuf += nRead;
left -= nRead;
count += nRead;
}
return -1;
}
#endif //__COMMON__