I/O复用使得程序能同时监听多个文件描述符,这对提高程序的性能至关重要。通常,网络程序在下列情况下需要使用I/O复用技术。
1.客户端程序要同时处理多个socket。
2.客户端程序要同时处理用户输入和网络连接。
3.TCP服务器要同时处理监听socket和连接socket。
4.服务器要同时监听多个端口,或者处理多种服务。
I/O复用虽然能同时监听多个文件描述符,但她本身是阻塞的,并且当多个文件描述符同时就绪时,如果不采取措施,程序就只能按顺序依次处理其中的每一个文件描述符。这使得服务器看起来就像是穿行工作的。如果要实现并发,只能使用多进程或多线程等。
Linux下实现I/O复用的系统调用主要有select、poll和epoll.
select:
在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。
select API
参数:
1.nfds参数指定被监听的文件的文件描述符的总数。它通常被设置为select监听的所有文件描述符的最大值加1,因为文件描述符是从0开始计数的。
2.readfds、writefds和exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。这三个参数都是输入输出型参数。
3.timeout参数用来设置select函数的超时时间。它是一个timeval结构体类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。不过我们不能完全相信select调用返回后的timeout值,比如调用失败时timeout的值是不确定的。select成功时返回就绪文件描述符的个数。如果在超时时间内没有任何文件描述符就绪,select将返回0.select失败是返回-1并设置errno。如果在select等待期间,若程序收到信号,则select立即返回-1,并设置errno为EINTR。errno为EBADF时,文件描述符为无效或该文件已关闭。为EINVAL时,参数n为负值。为ENOMEM时,核心内存不足。
server.c
/*************************************************************************
> File Name: server.c
> Author: ZX
> Mail: 18829897183@163.com
> Created Time: Wed 15 Mar 2017 02:15:31 AM PDT
************************************************************************/
#include <stdio.h>
#include <assert.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#define _SIZE_ 128
int gfd[_SIZE_];
int statup(const char* _ip, int _port)
{
assert(_ip);
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("sock");
exit(1);
}
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//链接建立后,client方主动断开链接后短时间内都无法再建立链接。
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(_ip);
local.sin_port = htons(_port);
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
perror("bind");
exit(2);
}
if(listen(sock, 5) < 0)
{
perror("listen");
exit(3);
}
return sock;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("Usage: %s [local_ip] [local_port]",argv[0]);
exit(4);
}
int listen_sock = statup(argv[1],atoi(argv[2]));
//设置位无效,全都设置为-1
int i = 0;
for(; i<_SIZE_; i++)
{
gfd[i] = -1;
}
int index = 0;
gfd[index++] = listen_sock;
while(1)
{
struct timeval timeout = {5,0};
fd_set rfds;
FD_ZERO(&rfds);//clear rfds
int j = 0;
int max_fd = -1;
for(; j<_SIZE_; j++)
{
if(gfd[j] > 0)
{
FD_SET(gfd[j], &rfds);//将数组的文件描述符设置进读文件描述符集
if(max_fd < gfd[j])
{
max_fd = gfd[j];
}
}
}
switch(select(max_fd+1, &rfds, NULL, NULL, &timeout))
{
case 0://select返回0,表示超时
perror("time out");
break;
case -1://出错
perror("select");
break;
default:
{
int k = 0;
for(; k<_SIZE_; k++)
{
if(gfd[k] < 0)
{
continue;
}
if(k==0 && FD_ISSET(listen_sock, &rfds))
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if(sock < 0)
{
perror("accept");
exit(5);
}
if(sock > 0)
{
printf("get a new client ip:%s port:%d\n",inet_ntoa(peer.sin_addr),\
ntohs(peer.sin_port));
int m = 0;
for(; m<_SIZE_; m++)
{//找到最小的无效的位置,将信道套接字(文件描述符)设置到无效位
if(gfd[m] == -1)
{
gfd[m] = sock;
break;
}
}
if(m == _SIZE_)
{//没有位置了,无法处理,就关闭文件描述符
printf("m == _SZIE_\n");
close(sock);
}
}
}
else if(FD_ISSET(gfd[k], &rfds))
{
char buf[1024];
memset(buf, 0, sizeof(buf)-1);
ssize_t _s = read(gfd[k], buf, sizeof(buf)-1);
if(_s > 0)
{
printf("client# %s\n", buf);
}
else if(_s == 0)
{
printf("client is quit...\n");
close(gfd[k]);
gfd[k] = -1;
}
else
{
perror("read");
}
}
}
}
break;
}
}
return 0;
}
Makefile
.PHONY:all
all:server client
client:client.c
gcc -o $@ $^
server:server.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f server client
客户端没有变化