1.Unix域套接字
socket套接字同样可以用于本地通信
unix域套接字编程(本地进程间通信)
常用于前后台进程间通信
区分 流式与数据报式
1.UNIX域(流式)套接字服务器端:
socket-->bind-->listen-->accept-->recv/send-->close;
UNIX域(流式)套接字客户端:
socket-->connect-->send/recv-->close;
2.UNIX域(数据报)套接字服务器端:
socket-->bind-->recvfrom/sendto-->close
UNIX域(数据报)套接字客户端:
socket-->recvfrom/sendto-->close
socket运行后会产生套接字文件。
remove("./sockfd");
每次运行服务器都会产生套接字,每次运行之前都得删除本地的套接字文件
注意地址信息:
struct sockaddr_un //
{
sa_family_t sun_family; //协议族
char sun_path[108]; //套接字文件的路径
};
2.IO模型与服务器模型
缓存 IO:
缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。在 Linux 的 缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中, 也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区 拷贝到应用程序的地址空间
缓存 IO 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据 拷贝操作所带来的 CPU 以及内存开销是非常大的。
网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。
刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区 中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read 操作发生时,它会经历两个阶段:
第一阶段:等待数据准备 (Waiting for the data to be ready)。
第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。
对于socket流而言,
第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
第二步:把数据从内核缓冲区复制到应用进程缓冲区。
网络应用需要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延 迟,给应用带来的性能瓶颈大于后者。
1.缓存IO
在INIX/Linux下主要有4中I/O模型:
阻塞I/O:
最常用、最简单、效率最低
非阻塞I/O:
可防止线程阻塞在I/O操作上,需要轮询(占用cpu)
I/O多路复用:
允许同时对多个I/O进行控制
信号驱动I/O:
一种异步通讯模型
(1)阻塞I/O模式
阻塞I/O模式是最普遍使用的I/O模式,大部分程序使用的都是阻塞I/O模式。
缺省情况下,套接字建立后所处于的模式就是阻塞I/O模式。
很多函数在调用过程中都会发生阻塞。
read/recv/recvfrom
write/send/sendto
accept/connect
最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。
当用户进程调用了recv()/recvfrom()这个系统调用,
1)kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在 一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的 数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一 个过程的。而在用户进程这边,整个进程会被阻塞。
2)第二个阶段:当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用 户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
也就是说阻塞模式下用户进程需要等待两次,一次为等待io中的数据就绪,一次是等 待内核把数据拷贝到用户空间
实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给 网络编程带来了一个很大的问题,如在调用send()的同时,进程将被阻塞,在此期间,进程 将无法执行任何运算或响应任何的网络请求。
举例:readfifo.c
/*===============================================
* 文件名称:readfifo.c
* 创 建 者:
* 创建日期:2022年08月18日
* 描 述:
================================================*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int fd1 = open("./f1", O_RDWR);
int fd2 = open("./f2", O_RDWR);
int fd3 = open("./f3", O_RDWR);
char buf[64] = {0};
int ret;
while(1)
{
ret = read(fd1, buf, sizeof(buf));//read会阻塞,只能fd1读完了,才能轮到2,3
if(ret != -1)
{
printf("fd1=%s\n", buf);
}
memset(buf, 0, sizeof(buf));
ret = read(fd2, buf, sizeof(buf));
if(ret != -1)
{
printf("fd2=%s\n", buf);
}
memset(buf, 0, sizeof(buf));
ret = read(fd3, buf, sizeof(buf));
if(ret != -1)
{
printf("fd3=%s\n", buf);
}
memset(buf, 0, sizeof(buf));
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
一个简单的改进方案是在服务器端使用多线程(或多进程)。
多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一 个连接的阻塞都不会影响其他的连接。
具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远 远大于线程,所以如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的CPU资源,譬如需要进行大规模或长时间的数据运算或文件访 问,则进程较为安全。
并发服务器模型 TCP
多进程并发服务器 tcpserver_fork.c
基本原理: 每连接一个客户端,创建一个子进程,子进程负责处理connfd(客户请求)
父进程处理sockfd(连接请求)。
细节处理: 回收子进程资源
服务器:
/*===============================================
* 文件名称:fork_server.c
* 创 建 者:
* 创建日期:2022年08月18日
* 描 述:
================================================*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
int tcpserver_init(int port); //连接函数
int recv_data(int connfd); //接收函数
void signal_handler(int sig); //回收函数
int main(int argc, char *argv[])
{
signal(SIGCHLD,signal_handler);//子进程结束,父进程回收
int sockfd = tcpserver_init(8888); //建立连接
struct sockaddr_in client;
int m=sizeof(client);
printf("wait a client ...\n");
while(1)
{
//connfd进行数据的收发
int connfd=accept(sockfd,(struct sockaddr *)&client,&m);
if(connfd<0)
{
perror("accept");
return -1;
}
printf("client ip=%s client port=%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
printf("%d is link\n",connfd);//显示连接端的信息
pid_t pid = fork();//建立子进程
if(pid<0)
{
perror("fork");
exit(-1);
}
else if(pid==0)//子进程处理connfd
{
close(sockfd);
recv_data(connfd);
exit(0);
}
else//父进程处理sockfd与回收资源
{
continue;
}
}
close(sockfd);
return 0;
}
int tcpserver_init(int port)
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0)
{
perror("socket");
return -1;
}
printf("sockfd = %d\n",sockfd);
//端口复用函数
int on=1;
int k = setsockopt(sockfd, SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
if(k<0)
{
perror("setsockopt");
return -1;
}
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);//形参
server.sin_addr.s_addr=inet_addr("0");
int len = sizeof(server);
int ret = bind(sockfd,(struct sockaddr *)&server,len);
if(ret<0)
{
perror("bind");
return -1;
}
ret =listen(sockfd , 5);
if(ret<0)
{
perror("listen");
return -1;
}
return sockfd;
}
int recv_data(int connfd)
{
//接收数据
char buf[64]={0};
while(1)
{
int n =recv( connfd ,buf ,sizeof(buf),0);
if(n<0)
{
perror("recv");
return -1;
}
else if(n==0)//客户端退出
{
printf("%d is unlink\n",connfd);
close(connfd);
break;
}
else
{
printf("message: %s\n",buf);
memset(buf,0,sizeof(0));
}
}
return 0;
}
void signal_handler(int sig)
{
waitpid(-1,NULL,WNOHANG);
}
客户端:
/*===============================================
* 文件名称:fork_client.c
* 创 建 者:
* 创建日期:2022年08月18日
* 描 述:
================================================*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror("sockfd");
return -1;
}
printf("sockfd = %d\n", sockfd);
struct sockaddr_in serveraddr,clientaddr;
memset(&serveraddr, 0 ,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(8888);
serveraddr.sin_addr.s_addr = inet_addr("0"); //自动路由,寻找IP地址
int len = sizeof(serveraddr);
int ret = connect(sockfd, (struct sockaddr *)&serveraddr, len);
if(ret == -1)
{
perror("connect");
return -1;
}
printf("connect success!\n");
//接收和发送数据
char buf[1024] = {0};
while(1)
{
gets(buf);
send(sockfd, buf, strlen(buf), 0);
memset(buf, 0, sizeof(buf));
}
close(sockfd);
return 0;
}
多线程并发服务器 tcpserver_pthread.c
基本原理: 每连接一个客户端,创建一个子线程,子线程负责处理connfd(客户请 求),主线程处理sockfd(连接请求)。
//注意:子线程结束时,需要进行资源回收
服务器:
/*===============================================
* 文件名称:pthread_server.c
* 创 建 者:
* 创建日期:2022年08月18日
* 描 述:
================================================*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
int tcpserver_init(int port); //连接函数
int recv_data(int connfd); //接收函数
void *fun(void * arg); //线程函数
int main(int argc, char *argv[])
{
int sockfd = tcpserver_init(8888); //建立连接
struct sockaddr_in client;
int m=sizeof(client);
printf("wait a client ...\n");
while(1)
{
//connfd进行数据的收发
int connfd=accept(sockfd,(struct sockaddr *)&client,&m);
if(connfd<0)
{
perror("accept");
return -1;
}
printf("client ip=%s client port=%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
printf("%d is link\n",connfd);//显示连接端的信息
pthread_t tid;
pthread_create(&tid,NULL,fun,(void*)&connfd);
}
close(sockfd);
return 0;
}
int tcpserver_init(int port)
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0)
{
perror("socket");
return -1;
}
printf("sockfd = %d\n",sockfd);
//端口复用函数
int on=1;
int k = setsockopt(sockfd, SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
if(k<0)
{
perror("setsockopt");
return -1;
}
struct sockaddr_in server;
//memset(server,0,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);//形参
server.sin_addr.s_addr=inet_addr("0");
int len = sizeof(server);
int ret = bind(sockfd,(struct sockaddr *)&server,len);
if(ret<0)
{
perror("bind");
return -1;
}
ret =listen(sockfd , 5);
if(ret<0)
{
perror("listen");
return -1;
}
return sockfd;
}
int recv_data(int connfd)
{
//接收数据
char buf[64]={0};
while(1)
{
int n =recv( connfd ,buf ,sizeof(buf),0);
if(n<0)
{
perror("recv");
return -1;
}
else if(n==0)//客户端退出
{
printf("%d is unlink\n",connfd);
close(connfd);
break;
}
else
{
printf("message: %s\n",buf);
memset(buf,0,sizeof(0));
}
}
return 0;
}
void *fun(void *arg)
{
pthread_detach(pthread_self());//回收线程
int connfd = *(int *)arg;
recv_data(connfd);
return NULL;
}
客户端:
/*===============================================
* 文件名称:pthread_client.c
* 创 建 者:
* 创建日期:2022年08月18日
* 描 述:
================================================*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror("sockfd");
return -1;
}
printf("sockfd = %d\n", sockfd);
struct sockaddr_in serveraddr,clientaddr;//
memset(&serveraddr, 0 ,sizeof(serveraddr));//服务器的相关信息
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(8888); //端口号一致才能发送
serveraddr.sin_addr.s_addr = inet_addr("0"); //自动路由,寻找IP地址
int len = sizeof(serveraddr);
int ret = connect(sockfd, (struct sockaddr *)&serveraddr, len);//建立连接
if(ret == -1)
{
perror("connect");
return -1;
}
printf("connect success!\n");
//接收和发送数据
char buf[64] = {0};
while(1)
{
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1]='\0';
send(sockfd, buf, strlen(buf), 0);
memset(buf, 0, sizeof(buf));
}
close(sockfd);
return 0;
}
上述多线程的服务器模型似乎完美的解决了为多个客户机提供问答服务的要求,但其实并不 尽然。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统 资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。 并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就 会造成性能上的瓶颈。
总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线 程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
2.非阻塞IO: 可防止进程阻塞在I/O操作上,需要轮询
非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数 据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起 recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常 被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。 需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
所以事实上,在非阻塞IO模型中,用户进程需要不断的主动询问kernel数据好了没 有,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
int flag = fcntl(fd,F_GETFL,0); //获得属性
flag = flag | O_NONBLOCK; //修改属性(添加非阻塞)
fcntl(fd,F_SETFL,flag); //设置属性
eg: readfifo_noblocking.c
/*===============================================
* 文件名称:readfifo.c
* 创 建 者:
* 创建日期:2022年08月18日
* 描 述:
================================================*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
void setnoblock(int fd)
{
int flag = fcntl(fd,F_GETFL,0);//获得属性
flag = flag | O_NONBLOCK; //修改属性(添加非阻塞)
fcntl(fd,F_SETFL,flag); //设置属性
}
int main(int argc, const char *argv[])
{
int fd1 = open("./f1", O_RDWR);
int fd2 = open("./f2", O_RDWR);
int fd3 = open("./f3", O_RDWR);
setnoblock(fd1);//修改套接字,使其非阻塞
setnoblock(fd2);
setnoblock(fd3);
char buf[64] = {0};
int ret;
while(1)
{
ret = read(fd1, buf, sizeof(buf));//read会阻塞,只能fd1读完了,才能轮到2,3
if(ret != -1)
{
printf("fd1=%s\n", buf);
}
memset(buf, 0, sizeof(buf));
ret = read(fd2, buf, sizeof(buf));
if(ret != -1)
{
printf("fd2=%s\n", buf);
}
memset(buf, 0, sizeof(buf));
ret = read(fd3, buf, sizeof(buf));
if(ret != -1)
{
printf("fd3=%s\n", buf);
}
memset(buf, 0, sizeof(buf));
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数 据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模 型,而是在其他IO模型中使用非阻塞IO这一特性。
3.IO多路复用: 允许同时对多个I/O进行控制
多路复用I/O
应用程序中同时处理多路输入输出流,若采用阻塞模式,则达不到预期的目的
多采用非阻塞模式,对多个输入进行轮询,太浪费cpu
若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通讯问题,使程序变得复杂
比较好的方法是使用I/O多路复用。
基本思想:
先构建一张有关文件描述符的表,然后调用一个函数清零。检测,当这些文件描述符中一个或多个已准备好进行I/O是,函数返回。
函数返回时,告诉进程那个文件描述符已就绪,可以进程I/O操作。
大部分Unix/Linux都支持select函数,该函数用于探测多个文件句柄的状态变化
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds,
struct timeval *timeout);
功能:select 机制
参数说明:
nfds: 最大文件描述符+1
readfds: 读事件的表
writefds: 写事件的表
exceptfds: 异常事件的表
timeout:超时检测
NULL: 一直阻塞,直到文件描述符准备好为止
返回值:
成功: 准备好的文件描述符的个数
失败: -1
void FD_CLR(int fd, fd_set *set); // 把表中删除一个文件描述符
int FD_ISSET(int fd, fd_set *set);//检测文件描述符是否准备好了,准备好了返回1,否则返回0
void FD_SET(int fd, fd_set *set); // 加入到表中
void FD_ZERO(fd_set *set); // 清空表
基本原理:
首先创建一张文件描述符表(fd_set),通过使用特有的函数(select),让内核帮 助上层用户循环检测是否有可操作的文件描述符,如果有则告诉应用程序去操作。
eg: readfifo_select.c
/*===============================================
* 文件名称:readfifo.c
* 创 建 者:
* 创建日期:2022年08月18日
* 描 述:
================================================*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <unistd.h>
#include <sys/time.h>
int main(int argc, const char *argv[])
{
int fd1 = open("./f1", O_RDWR);
int fd2 = open("./f2", O_RDWR);
int fd3 = open("./f3", O_RDWR);
//1.创建一张文件描述符表
fd_set readfds,tempfds;
//2.清空表
FD_ZERO(&readfds);
//3.将文件描述符加入表中
FD_SET(fd1,&readfds);
FD_SET(fd2,&readfds);
FD_SET(fd3,&readfds);
int maxfd = fd3;//记录最大的文件描述符
tempfds = readfds ;//保存一张全为0的表
char buf[64] = {0};
int ret,i;
while(1)
{
readfds = tempfds;
ret = select(maxfd+1 ,&readfds ,NULL,NULL,NULL );//阻塞直到有文件可读
if(ret<0)
{
perror("select");
return -1;
}
for(i=fd1;i<=maxfd;i++)//遍历文件描述符,查看谁可读
{
if(FD_ISSET(i,&readfds))
{
read(i,buf,sizeof(buf));
printf("%s \n",buf);
memset(buf,0,sizeof(buf));
}
}
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
eg: tcpserver_select.c
/*===============================================
* 文件名称:tcpserver_select.c
* 创 建 者:
* 创建日期:2022年08月19日
* 描 述:
================================================*/
/*===============================================
* 文件名称:clinet_unix_udp.c
* 创 建 者:
* 创建日期:2022年08月18日
* 描 述:
================================================*/
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0)
{
perror("socket");
return -1;
}
//端口复用
int on =1;
int k = setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
if(k<0)
{
perror("setsockopt");
return -1;
}
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(8888);
server.sin_addr.s_addr=inet_addr("0");
int len = sizeof(server);
int ret = bind(sockfd,(struct sockaddr *)&server,len);
if(ret<0)
{
perror("bind");
return -1;
}
ret =listen(sockfd , 5);
if(ret<0)
{
perror("listen");
return -1;
}
//1.创建一张文件描述符表
fd_set readfds,tmpfds;
//2.清空表
FD_ZERO(&readfds);
int maxfd = sockfd;
//3.将文件描述符加入表中
FD_SET(sockfd,&readfds);
tmpfds = readfds;
char buf[64] ={0};
int i;
while(1)
{
readfds = tmpfds;
ret = select(maxfd+1,&readfds,NULL,NULL,NULL);
if(ret<0)
{
perror("select");
return -1;
}
for(i=sockfd;i<=maxfd;i++)//检测谁准备好
{
if(FD_ISSET(i,&readfds))
{
if(i==sockfd) //监听到连接
{
int connfd = accept(i,NULL,NULL);
if(connfd<0)
{
perror("accept");
return -1;
}
printf("%d is link\n",connfd);
FD_SET(connfd,&tmpfds);//加入队列
if(maxfd<connfd) //maxfd指向最大值
{
maxfd=connfd;
}
}
else//connfd准备好
{
ret = recv(i,buf,sizeof(buf),0);
if(ret<0)
{
perror("recv");
return -1;
}
else if(ret==0)
{
printf("%d is unlink\n",i);
FD_CLR(i,&readfds);
close(i);
break;
}
else
{
printf("%d :message =%s\n",i,buf);
memset(buf,0,sizeof(buf));
}
}
}
}
}
close(sockfd);
return 0;
}
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真 正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用 一个线程就可以管理多个 socket,系统不需要建立新的进程或者线程,也不必维护这些线 程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少 了资源占用。
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还 多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后 最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个 socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多 个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
这种模型的特征在于每一个执行周期都会探测一次或一组事件,一个特定的事件会触 发某个特定的响应。我们可以将这种模型归类为“事件驱动模型”
相比其他模型,使用select的事件驱动模型只用单线程(进程)执行,占用资源少,不消 耗太多CPU,同时能够为多客户端提供服务。 如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值
但这个模型依旧有着很多问题: 当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个 柄该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,那么就会导 致后续的事件迟迟得不到处理,并且会影响新的事件轮询,在很大程度上降低了事件探测的 及时性。
所以select机制只适用于"短作业" 处理机制. (处理时间短)
select缺点:
1.内核使用轮询的方式来检查文件描述符集合中的描述符是否就绪,文件描 述符越多,消耗的时间资源越多。
2.文件描述符集合使用的数组,有大小限制,1024
3.每次文件描述符集合更新时,重新拷贝到内核中