创新的赣州网站建设,个体户能做网站备案吗,莱芜金点子招聘信息,中国做的比较好的网站有哪些基于I/O复用的服务器端
多进程服务器端的缺点和解决方法
为了构建并发服务器#xff0c;只要有客户端连接请求就会创建新进程。这的确是实际操作中采用的种方案#xff0c;但并非十全十美#xff0c;因为创建进程时需要付出极大代价。这需要大量的运算和内存空间#xff…基于I/O复用的服务器端
多进程服务器端的缺点和解决方法
为了构建并发服务器只要有客户端连接请求就会创建新进程。这的确是实际操作中采用的种方案但并非十全十美因为创建进程时需要付出极大代价。这需要大量的运算和内存空间由于每个进程都具有独立的内存空间所以相互间的数据交换也要求采用相对复杂的方法IPC属于相对复杂的通信方法。各位应该也感到需要IPC时会提高编程难度。“那有何解决方案能否在不创建进程的同时向多个客户端提供服务” 当然能本节讲解的I/O复用就是这种技术。大家听到有这种方法是否感到一阵兴奋但请不要过于依赖该模型该方案并不适用于所有情况应当根据目标服务器端的特点采用不同实现方法。下面先理解“复用”Multiplexing的意义。
理解复用
在网络编程中复用Multiplexing是指在一个物理通信链接如网络传输介质上同时传输多个独立的数据流。它通过将多个数据流合并成一个流并在接收端将其分解从而提高网络资源的利用效率。
复用技术可以通过以下几种方式实现 时间复用Time Division MultiplexingTDM将时间划分为若干个间隔每个间隔分配给不同的数据流进行传输。发送端按照一定的规则在每个时间间隔内发送数据接收端则根据间隔进行数据的提取和恢复。 频分复用Frequency Division MultiplexingFDM将频率范围划分为多个窄带信道每个信道专门用于传输一个数据流。数据流经过调制后在不同的频率上进行传输接收端则对信号进行解调得到原始数据。 码分复用Code Division MultiplexingCDM利用不同的码序列来区分各个数据流。发送端使用特定的码序列对数据进行扩展接收端则使用相同的码序列进行解扩从而将数据流分离。
以上这些复用技术都旨在实现多个数据流在同一物理通信链接上的传输以提高网络的带宽利用率和传输效率。在网络编程中我们可以使用不同的复用技术来实现同时处理多个客户端请求或在单个连接上传输多个数据流。
复用技术在服务器端的应用
对于网络编程中的IO复用IO multiplexing它是一种高效处理多个IO事件的机制。传统的IO模型中每个IO操作都会阻塞线程导致程序在处理一个IO时无法同时处理其他IO事件造成资源浪费。
而IO复用通过利用特定的系统调用函数如select、poll、epoll等来监视多个IO事件将多个IO操作集中在一个线程中进行管理和处理从而实现同时处理多个IO事件的能力。它的基本原理是将需要监听的IO事件加入到一个事件集合中然后通过系统调用阻塞等待其中任何一个事件就绪一旦有就绪事件程序就可以执行相应的操作。
IO复用的主要好处包括 资源利用率高使用IO复用可以避免每个IO操作都阻塞线程从而减少线程数量提高了资源利用效率。 响应速度快IO复用可以同时监听多个IO事件一旦有事件就绪立即进行处理大大减小了事件响应的延迟。 编程简洁相比于多线程或多进程模型使用IO复用可以简化代码降低开发和维护的难度。
总而言之IO复用是一种高效处理多个IO事件的机制它可以减少线程数量、提高资源利用率和响应速度是网络编程中常用的技术之一。
我再举个例子来理解一下IO复用服务器端某教室中有10名学生和1位教师这些孩子并非等闲之辈上课时不停地提问。学校没办法只能给每个学生都配1位教师也就是说教室中现有10位教师。此后只要有新的转校生就会增加1位教师因为转校生也喜欢提问。这个故事中如果把学生当作客户端把教师当作与客户端进行数据交换的服务器端进程则该教室的运营方式为多进程服务器端方式。 有一天该校来了位具有超能力的教师。这位教师可以应对所有学生的提问而且回答速度很快不会让学生等待。因此学校为了提高教师效率将其他老师转移到了别的班。现在学生提问前必须举手教师确认举手学生的提问后再回答问题。也就是说现在的教室以IO复用方式运行。 虽然例子有些奇怪但可以通过它理解IO复用方法教师必须确认有无举手学生,同样IO复用服务器端的进程需要确认举手收到数据的套接字并通过举手的套接字接收数据。
理解select函数并实现服务器端
运用select函数是最具代表性的实现复用服务器端方法。Windows平台下也有同名函数提供相 同功能因此具有良好的移植性。
select函数的功能和调用顺序
使用sclect函数时可以将多个文件描述符集中到一起统一监视项目如下。 □是否存在套接字接收数据 □无需阻塞传输数据的套接字有哪些 □哪些套接字发生了异常
select函数的使用方法与一般函数区别较大更准确地说它很难使用。但为了实现IO复用服务器端我们应掌握select函数并运用到套接字编程中。认为“select函数是IO复用的全部内容”也并不为过。接下来介绍select函数的调用方法和顺序。
步骤一: 设置文件描述符 指定监视范围 设置超时 步骤二: 调用select函数 步骤三: 查看调用结果
可以看到调用select函数前需要一些准备工作调用后还需查看结果。接下来按照上述顺序逐一讲解。
设置文件描述符
利用select函数可以同时监视多个文件描述符。当然监视文件描述符可以视为监视套接字.此时首先需要将要监视的文件描述符集中到一起。集中时也要按照监视项接收、传输、异常进行区分即按照上述3种监视项分成3类。 使用fd_set数组变量执行此项操作如图所示。该数组是存有0和1的位数组。 图中最左端的位表示文件描述符0所在位置。如果该位设置为1则表示该文件描述 符是监视对象。那么图中哪些文件描述符是监视对象呢很明显是文件描述符1和3。 “是否应当通过文件描述符的数字直接将值注册到fd_set变量” 当然不是针对fd_ set变量的操作是以位为单位进行的这也意味着直接操作该变量会比较 繁琐。难道要求各位自己完成吗实际上在fd_set变量中注册或更改值的操作都由下列宏完成
□FD_ZERO(fd_set *fdset)将fd_set变量的所有位初始化为0。 □ FD_SET(int fd, fd_set *fdset)在参数fdset指向的变量中注册文件描述符的信息。
□ FD_CLR(int fd, fd_set *fdset)从参数fdset指向的变量中清除文件描述符的信息。
□ FD_ISSET(int fd,fd_set *fdset)若参数fdset指向的变量中包含文件描述符的信息则返回“真”。 上述函数中FD_ISSET用于验证select函数的调用结果。
设置检查(监视)范围及超时
先简单的介绍一下select函数。
#includesys/select.h
#includesys/time.hint select(int maxfd,fd_set* readset,fd_set* writeset,fd_set* exceptset,const struct timeval *timeout);//成功时返回大于0的值失败时返回-1maxfd //监视对象文件描述符数量。readset //将所有关注“是否存在待读取数据”的文件描述符注册到fd_set型变量并传递其地址值。 writeset //将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set型变量并传递其地址值。 exceptset //将所有关注“是否发生异常”的文件描述符注册到fd_set型变量并传递其地址值。timeout //调用select函数后为防止陷入无限阻塞的状态传递超时time-out信息。返回值 //发生错误时返回-1超时返回时返回0。因发生关注的事件返回时返回大于0的值该值是发生事件的文件描述符数如上所述selcct函数用来验证3种监视项的变化情况。根据监视项声明3个fd_set型变量分别向其注册文件描述符信息并把变量的地址值传递到上述函数的第二到第四个参数。但在此之前调用select函数前需要决定下面2件事。“文件描述符的监视检查范围是”“如何设定select函数的超时时间” 第一文件描述符的监视范围与select函数的第一个参数有关。实际上select函数要求通过第一个参数传递监视对象文件描述符的数量。因此需要得到注册在fd_set变量中的文件描述符数。但每次新建文件描述符时其值都会增1故只需将最大的文件描述符值加1再传递到select函数即可。加1是因为文件描述符的值从0开始。 第二select函数的超时时间与select函数的最后一个参数有关其中timeval结构体定义如下。
struct timeval{long tv_sec; //secondslong tv_usec; //microseconds
}
本来select函数只有在监视的文件描述符发生变化时才返回。如果未发生变化就会进入阻塞状态。指定超时时间就是为了防止这种情况的发生。通过声明上述结构体变量将秒数填入tv_sec成员将微秒数填入tv_usec成员然后将结构体的地址值传递到select函数的最后一个参数。此时即使文件描述符中未发生变化只要过了指定时间也可以从函数中返回。不过这种情况下select函数返回0。因此可以通过返回值了解返回原因。如果不想设置超时则传递NULL参数。
调用select函数后查看结果
函数调用后查看结果也同样重要。我们已经讨论过select函数的返回值如果返回了大于0的整数说明相应数量的文件描述符发生变化。那么这个变化是如何变化的呢?select函数调用完成过后向其传递的fd_set变量中将发生变化。原来为1的所有位均变为0但发生变化的文件描述符对应位除外。因此可以认为值仍为1的位置上的文件描述符发生了变化。
select函数调用示例
#includestdio.h
#includeunistd.h
#includesys/time.h
#includesys/select.h
#define BUF_SIZE 30int main(int argc,char *argv){fd_set reads,temps;int result,str_len;char buf[BUF_SIZE];struct timeval timeout;FD_ZERO(reads);FD_SET(0,reads);while(1){tempsreads;timeout.tv_sec5;timeout.tv_usec0;resultselect(1,temp,0,0,timeout);if(result-1){puts(select() error!);break;}else if(result0){puts(Time-out!);}else{if(FD_ISSET(0,temps)){str_lenread(0,buf,BUF_SIZE);buf[str_len]0;printf(message from console: %s,buf);}}}
return 0;
}
上述内容有两个点要注意:
1.由于select函数调用后会修改监视fd_set中的内容所以我们需要保存一份副本上述代码保存在temps中。
2.由于select函数调用时timeout中的时间最后会被替换为超时前剩余时间所以这个timeout初始量也需要每次初始化。
实现IO复用服务器端
#includestdio.h
#includestdlib.h
#includestring.h
#includeunistd.h
#includearpa/inet.h
#includesys/socket.h
#includesys/time.h
#includesys/select.h#define BUF_SIZE 100
void error_handling(char *buf);int main(int argc,char *argv[]){int serv_sock,clnt_sock;struct sockaddr_in serv_addr,clnt_addr;struct timeval timeout;fd_set reads,cpy_reads;socklen_t addr_sz;int fd_max,str_len,fd_num,i;char buf[BUF_SIZE];if(argc!2){printf(Usage: %s port\n,argv[0]);exit(1);}serv_socksocket(PF_INET,SOCK_STREAM,0);memset(serv_addr,0,sizeof(serv_addr));serv_addr.sin_familyAF_INET;serv_addr.sin_addr.s_addrhtonl(INADDR_ANY);serv_addr.sin_porthtons(atoi([argv[1]]));if(bind(serv_sock,(struct sockaddr*)serv_addr,sizeof(serv_addr))-1)error_handling(bind() error);if(listen(serv_sock,5)-1)error_handling(listen() error);FD_ZERO(reads);FD_SET(serv_sock,reads);fd_maxserv_sock;while(1){cpy_readsreads;timeout.tv_sec5;timeout.tv_usec5000;if((fd_numselect(fd_max1,cpy_reads,0,0,timeout))-1)break;if(fd_num0)continue;for(i0;ifd_max1;i){if(FD_ISSET(i,cpy_reads)){ if(iserv_sock){ //连接请求到来addr_szsizeof(clnt_addr);clnt_sockaccept(serv_sock,(structsockaddr*)clnt_addr,addr_sz);FD_SET(clnt_sock,reads);if(fd_maxclnt_sock)fdclnt_sock;printf(connected client: %d \n,clnt_sock);}else{str_lenrad(i,buf,BUF_SIZE);if(str_len0){FD_CLR(i,reads);close(i);printf(closed client: %d \n,i);}else{write(i,buf,str_len);//回声}}}}}close(serv_sock);return 0;
}void error_handling(char *buf){fputs(buf,stderr);fputc(\n,stderr);exit(1);
}
基于Windows的实现
在Windows平台调用select函数
Windows同样提供select函数而且所有参数与Linux的select函数完全相同。只不过Window平台select函数的第一个参数是为了保持与包括Linux的UNIX系列操作系统的兼容性而添加的并没有特殊意义。
#include winsock2.h
int select(int nfds, fd_set *treadfds, fd_set *writefds, fd_set *excepfds, const struct
timeval * timeout);//成功时返回0失败时返回-1。返回值、参数的顺序及含义与之前的Linux中的select函数全相同,故省略。下面给出timeval 结构体定义。
typedef struct timeval{long tv_sec;long tv_usec;
} TIMEVAL;
可以看到基本结构与之前Linux中的定义相同但Windows中使用的是typedef声明。接下来观察fd_set结构体。Windows中实现时需要注意的地方就在于此。可以看到Windows的fd_set并非像Linux中那样采用了位数组。
typedef struct fd_set{u_int fd_count;SOCKET fd_array[FD_SETSIZE];
} fd_set;
Windows的fd_set由成员fd_count和fd_array构成fd_count用于接字句柄数,fd_array用于保存套接字句柄只要略加思考就能理解这样声明的原因。Linux的文件描述符从0开始递增因此可以找出当前文件描述符数量和最后生成的文件描述符之间的关系。但Windows的套接字句柄并非从0开始而且句柄的整数值之间并无规律可循因此需要直接保存句柄的数组和记录句柄数的变量。幸好处理fd_set结构体的FDXXX型的4个宏的名称、功能及使用方法与Linux完全相同(故省略这也许是微软为了保证兼容性所做的考量。