一、I/O复用
定义:I/O 复用使得程序能同时监听多个文件描述符,这对于提高程序的性能至关重要。
网络程序在下列情况下需要使用 I/O 复用技术:
- ◼ TCP 服务器同时要处理监听套接字和连接套接字。
- ◼ 服务器要同时处理 TCP 请求和 UDP 请求。
- ◼ 程序要同时处理多个套接字。
- ◼ 客户端程序要同时处理用户输入和网络连接。
- ◼ 服务器要同时监听多个端口。
- 需要指出的是,I/O 复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当 多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依处理其中的每一 个文件描述符,这使得服务器看起来好像是串行工作的。如果要提高并发处理的能力,可以 配合使用多线程或多进程等编程方法。
二、I/O复用的系统调用
I/O复用方法的功能:检查若干个描述符有没有关系的事件产生。
1、select
(1)在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。
(2)select参数
①nfds是指定被监听的文件描述符的总数目;
fd_set 是结构指针类型:
②readfds指向可读事件对应的文件描述符集合;
③writefds是指向可写事件对应的文件描述符集合;
④exceptfds是指向异常事件对应的文件描述符集合。
fd_set结构体定义:
⑤timeout是用来设置select函数的超时时间。它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它,用来告诉应用程序select等待了多久。
(3)sclect成功时返回就绪(可读、可写和异常)文件描述符的总数。
- 如果在超时时间内没有任何文件描述符就绪,select将返回0。select失败时返回-1并设置errno。
- 如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为 EINTR。
2、文件描述符就绪条件
(1)在网络编程中,以下情况socket可读:
- ①socket内核接收缓存区中的字节数大于或等于其低水位标记SO RCVLOWAT。此时我们可以无阻塞地读该socket,并且读操作返回的字节数大于0。
- ②socket通信的对方关闭连接。此时对该socket的读操作将返回0。监听socket上有新的连接请求。
- ③socket 上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。
(2)在网络编程中,以下情况socket可写:
- ①socket内核发迭缓存区中的可用字节数大于或等于其低水位标记so_SNDLOWAT。此时我们可以无阻塞地写该socket,并且写操作返回的字节数大于0。
- ②socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE 信号。
- ③socket使用非阻塞connect连接成功或者失败(超时)-之后。
- ④socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。
- 注意:select能处理的异常情况只有一种:socket上接收到带外数据。
3、select的用法示例
//select用法
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/select.h>
#include<sys/time.h>
#define STDIN 0
int main()
{
int fd=STDIN;//键盘对应的描述符
fd_set fdset;//收集描述符
while(1)
{
FD_ZERO(&fdset);//清空集合
FD_SET(fd,&fdset);//添加描述符(fd)到集合
struct timeval tv={5,0};
int n=select(fd+1,&fdset,NULL,NULL,&tv);
if(n==-1)
{
printf("select err\n");//退出
}
else if(n==0)
{
printf("time out\n");//超时
}
else
{
//检测描述符上是否有数据,如果为真,就是有数据
if(FD_ISSET(fd,&fdset))//按位与,同1为1
{
char buff[128]={0};
read(fd,buff,127);
printf("buff=%s\n",buff);
}
}
}
}
4、使用select实现的TCP服务
//tcp服务器端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/select.h>
#include<sys/time.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#define MAXFD 10
void fds_init(int fds[])
{
for(int i=0;i<MAXFD;i++)
{
fds[i]=-1;
}
}
//把描述符添加到数组中
void fds_add(int fd,int fds[])
{
for(int i=0;i<MAXFD;i++)
{
if(fds[i]==-1)//说明是空的,该元素是空闲的
{
fds[i]=fd;//把对应的元素置成fd
break;
}
}
}
//移除一个描述符
void fds_del(int fd,int fds[])
{
for(int i=0;i<MAXFD;i++)
{
if(fds[i]==fd)
{
fds[i]==-1;//说明移除成功
break;
}
}
}
int accept_client(int sockfd)
{
struct sockaddr_in caddr;
int len=sizeof(caddr);
int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
return c;
}
int socket_init();
int main()
{
int fds[MAXFD];//记录(收集)描述符的数组
fds_init(fds);
int sockfd=socket_init();//监听套接字
if(sockfd==-1)
{
exit(0);
}
fds_add(sockfd,fds);//添加了一个数组
fd_set fdset;//集合收集描述符
while(1)
{
FD_ZERO(&fdset);
int maxfd=-1;
for(int i=0;i<MAXFD;i++)
{
if(fds[i]==-1)
{
continue;
}
FD_SET(fds[i],&fdset);//将数组中有效的描述符,添加到集合中
if(fds[i]>maxfd)//找到数组中描述符的最大值
{
maxfd=fds[i];
}
}
struct timeval tv={5,0};
int n=select(maxfd+1,&fdset,NULL,NULL,&tv);//阻塞五秒钟
if(n==-1)
{
printf("select err\n");
}
else if(n==0)
{
printf("time out\n");
}
else
{
for(int i=0;i<MAXFD;i++)
{
if(fds[i]==-1)
{
continue;
}
if(FD_ISSET(fds[i],&fdset))
{
if(fds[i]==sockfd)//监听套接字,accept
{
int c=accept_client(fds[i]);
if(c!=-1)
{
fds_add(c,fds);//添加新接收的连接
}
}
else//连接套接字,recv
{
char buff[128]={0};
int num=recv(fds[i],buff,127,0);
if(num<=0)
{
close(fds[i]);
fds_del(fds[i],fds);
}
else
{
printf("recv:&s\n",buff);
send(fds[i],"ok",2,0);
}
}
}
}
}
}
}
int socket_init()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port= htons(6000);//大端端口,tcp
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//环回地址
int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res==-1)
{
printf("bind err\n");
return -1;
}
res=listen(sockfd,5);
if(res==-1)
{
return -1;
}
return sockfd;
}
//客户端cli
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
{
printf("socket err\n");
exit(0);
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);//1024以内是专用;我们用4096以上,root
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//三次握手开始
if(res==-1)
{
printf("connect failed\n");
exit(0);
}
//从键盘获取数据
while(1)
{
printf("input: \n");
char buff[128]={0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
send(sockfd,buff,strlen(buff)-1,0);
memset(buff,0,128);//清空
int n=recv(sockfd,buff,127,0);
if(n<=0)//判断服务器是否关闭
{
break;
}
printf("buff=%s\n",buff);
}
close(sockfd);
}
如有错误,敬请指正。
您的收藏与点赞都是对我最大的鼓励和支持!