现如今,提高网络服务器的性能,提高I/O的性能,我们知道I/O系统其实做了两件事,一是等待数据就绪,二是数据的搬移;尤其是远距离网络传输“等”状态尤其明显,所以提高I/O性能本质是减少等的比重,其越趋近于0,效率就越高;I/O模型中提供一种多路复用模型,一次等待多个文件描述符,只要存在就绪文件,就可进行读写,大大提高了I/O的性能;
多路链接中存在三个特殊的函数select、poll、epoll,他们只负责I/O系统中的等,一次等待多个文件描述符,所以一旦函数返回,就一定存在就绪事件,因此它们又叫I/O事件的通知机制。
今天我们先来介绍select服务器
一、select的原理
利用select函数,判断套接字上是否存在数据,或者能否向一个套接字写入数据。目的是防止应用程序在套接字处于锁定模式时,调用recv(或send)从没有数据的套接字上接收数据,被迫进入阻塞状态。
1.select函数
参数:
nfds:表示需要监视的最大文件描述符+1,这里监视的文件描述符既包括需要监视的读事件,又包括需要监视的写事件。
fd_set :fd_set是一个文件描述符的集合,其低层用位图实现,用比特位0、1表示是否关注该事件的读写或则该事件的读写是否就绪。
readfds/writefds/exceptfds:分别表示读事件的文件描述符集、写事件文件描述符集、错误事件文件描述符集。它们是输入输出型参数,输入输出的含义完全不同;作为输入参数,例如readfds,readfds表示要关心的读事件,一旦中间有一个读事件就绪,则立即返回;作为输出参数,readfds表示已就绪的读事件集合(此时可以对就绪事件直接读写);同理writefds和exceptfds也一样。
timeout表示设置的等待时间,它是一个结构体,成员一时秒,成员二是毫秒;设置为0表示非阻塞式等待,设置为NULL表示阻塞式等待。
返回值:
成功返回三个文件描述符集合已就绪文件描述符的数量,0表示等待超时,此时没有就绪fd,-1表示失败。
对位图集合操作的函数:
FD_CLR( s, *set) 从队列set删除句柄s;
FD_ISSET( s, *set) 检查句柄s是否存在与队列set中;
FD_SET( s, *set )把句柄s添加到队列set中;
FD_ZERO( *set ) 把set队列初始化成空队列.
2.Select工作流程
- 用FD_ZERO宏来初始化我们感兴趣的fd_set。
也就是select函数的第二三四个参数 - 用FD_SET宏来将套接字句柄分配给相应的fd_set。
如果想要检查一个套接字是否有数据需要接收,可以用FD_SET宏把套接接字句柄加入可读性检查队列列 - 调用select函数。
如果该套接字没有数据需要接收,select函数会把该套接字从可读性检查队列中删除掉, - 用FD_ISSET对套接字句柄进行检查。
如果我们所关注的那个套接字句柄仍然在开始分配的那个fd_set里,那么说明马上可以进行相应的IO操 作。比如一个分配给select第一个参数的套接字句柄在select返回后仍然在select第一个参数的fd_set里,那么说明当前数据已经来了, 马上可以读取成功而不会被阻塞。
3.select服务器编写的实现
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/select.h>
#include<unistd.h>
//打印输入标识
static void usage(const char* port)
{
printf("usage:%s [local_ip] [local_port]\n",port);
}
int startup(char* ip,int port)
{
//创建套接字
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
// 绑定套接字与服务器的ip和端口号
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
perror("bind");
exit(3);
}
//监听
if(listen(sock,10)<0){
perror("listen");
exit(4);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc!=3){
usage(argv[0]);
return 1;
}
//创建套接字(大函数)
int listen_sock = startup(argv[1],atoi(argv[2]));
fd_set rsdf;//读事件的文件描述符集
fd_set wsdf;//写事件的文件描述符集
int maxfd = -1;//为select第一参数准备
//初始化两个集合
FD_ZERO(&rsdf);
FD_ZERO(&wsdf);
//由于参数二三是输入输出型参数(参数改变),所以每次都要重新设置输入参数,我们用一个数组保存初始输入状态的事件文件描述符。(select缺点一)
int fds_array[sizeof(rsdf)*8];
int wfds_array[sizeof(fd_set)*8];
int nums = sizeof(fds_array)/sizeof(fds_array[0]);
int count = sizeof(wfds_array)/sizeof(wfds_array[0]);
int i = 0;
//初始化数组,-1表示非法状态
for(; i<nums; ++i){//read_array init
fds_array[i] = -1;
}
for(i=0;i<count;++i){//write_array init
wfds_array[i] = -1;
}
//将listen_sock加入到读事件集合(服务器最先关心的就是客户端的连接请求)
fds_array[0] = listen_sock;
//轮询(select缺点二)
while(1)
{
//设置读事件集合
for(i=0;i<nums;++i){//notice read events
if(fds_array[i]<0)
continue;
else{
FD_SET(fds_array[i],&rsdf);//相应时间位图置1
if(maxfd < fds_array[i])
maxfd = fds_array[i];//得到最大的文件描述符
}
}
//写时间的文件描述符集同上
for(i=0;i<count;++i){//notice write events
if(wfds_array[i]<0)
continue;
else{
FD_SET(wfds_array[i],&wsdf);
if(maxfd < wfds_array[i])
maxfd = wfds_array[i];
}
}
struct timeval timeout;
timeout.tv_sec = 10;
//select等待
int s = select(maxfd+1,&rsdf,&wsdf,0,&timeout);
if(s<0){//失败
perror("select");
exit(5);
}else if(s == 0){//超时
printf("timeout...\n");
}else{//at last have one fd ready
for(i=0;i<nums;++i){//遍历找到就绪文件
if(fds_array[i]<0){
continue;
}else if(i==0 && FD_ISSET(listen_sock,&rsdf)){//客户端的请求事件
int msg[1024];
struct sockaddr_in client;
socklen_t len = sizeof(client);
/建立和客户端的连接
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock<0){
perror("accept");
exit(6);
}
//和客户端成功建立连接,打印连接客户端的信息
printf("get a client:[%s:%d]\n",inet_ntoa(client.sin_addr),\
ntohs(client.sin_port));
int j=0;
for(j=1;j<nums;++j){
if(fds_array[j] < 0){
break;
}
}
//将连接后客户端的fd加入读写文件集合中
if(j==nums){
printf("fd_set is full\n");
close(fds_array[i]);
}else{
fds_array[j] = new_sock;
for(j=0;j<=count;++j){
if(wfds_array[j]<0){
break;
}
}
if(j == count){
printf("write fd_set id full\n");
}else{
wfds_array[j] = new_sock;
}
}
}else if(i!=0 && FD_ISSET(fds_array[i],&rsdf)){//若是普通文件,表明该文件读已就绪,则对文件进行读
char buf[1024];
ssize_t s = read(fds_array[i],buf,sizeof(buf)-1);//读
if(s<0){
perror("read");
close(fds_array[i]);
fds_array[i] = -1;
exit(7);
}else if(s==0){
printf("client is quit\n");
close(fds_array[i]);
fds_array[i] = -1;
}else{//读成功
buf[s] = 0;
printf("client# %s",buf);
int j=0;
for(;j<count;++j){
if(FD_ISSET(fds_array[i],&wsdf)){//读完后进行写,此时写就绪
break;
}
}
printf("Please Server Enter#: ");
fflush(stdout);
int s = read(0,buf,sizeof(buf)-1);
if(s<0){
perror("read");
exit(8);
}else{
write(fds_array[i],buf,strlen(buf));//写
printf("service echo#:%s",buf);
}
}
}else{}//
}
}
}
return 0;
}
4.客户端代码编写的实现
客户端我们采用标准输出重定向的方法,来代替write,提高效率;
利用函数dup ,我们可以复制一个文件描述符。传给该函数一个既有的文件描述符,它会返回一个新的文件描述符,这个文件描述符是传给它文件描述符的拷贝。这意味着这两个文件描述符共享同一个数据结构。
dup2函数与dup类似,它有两个参数newfd和oldfd,函数的含义是newfd是oldfd的一份写时拷贝,此时newfd指向oldfd指向的结构体;
客户端实现重定向的方法:
- 先保存标准输出的结构体内容,int ret = dup(1),保存1到ret,ret一般为3(因为默认打开0、1、2),此时ret指向标准输出;
- 进行文件描述符的重定向,函数dup(sock,1),将标准输出重定向到网络sock,此时输出到显示屏上额内容直接输入到sock中,进行数据网络传输
恢复标准输出,dup2(1,ret);
代码实现:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
static void usage(const char* proc)
{
printf("usage:%s [local_ip] [local_proc]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc!=3){
usage(argv[0]);
exit(1);
}
int sock = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&server,sizeof(server))<0){
perror("connect");
exit(2);
}
char buf[1024];
while(1){
printf("please enter#");
fflush(stdout);
int sfd = dup(1);
close(1);
dup2(sock,1);
ssize_t s = read(0,buf,sizeof(buf)-1);
printf("%s",buf);
dup2(sfd,STDOUT_FILENO);
s = read(sock,buf,sizeof(buf)-1);
buf[s] = 0;
printf("server# %s",buf);
// ssize_t s = read(0,buf,sizeof(buf)-1);
// if(s<0){
// perror("read");
// exit(3);
// }
// write(sock,buf,strlen(buf));
}
return 0;
}
结果图:
5.select服务器的优缺点
优点:
- select()的可移植性更好,在某些Unix系统上不支持poll()
- select() 对于超时值提供了更好的精度:微秒,而poll是毫秒
缺点:
- 单个进程可监视的fd数量被限制。
- 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
- 对fd进行扫描时是线性扫描。fd剧增后,IO效率较低,因为每次调用都对fd进行线性扫描遍历,所以随着fd的增加会造成遍历速度慢的性能问题
- select() 函数的超时参数在返回时也是未定义的,考虑到可移植性,每次在超时之后在下一次进入到select之前都需要重新设置超时参数。