当从一个文件描述符进行读写操作时,accept、read、write这些函数会阻塞I/O。在这种会阻塞I/O的操作好处是不会占用cpu宝贵的时间片,但是如果需要对多个描述符操作时,阻塞会使同一时刻只能处理一个操作,从而使程序的执行效率大大降低。一种解决办法是使用多线程或多进程操作,但是这浪费大量的资源。另一种解决办法是采用非阻塞、忙轮询,这种办法提高了程序的执行效率,缺点是需要占用更多的cpu和系统资源。所以,最终的解决办法是采用IO多路转接技术。

  IO多路转接是先构造一个关于文件描述符的列表,将要监听的描述符添加到这个列表中。然后调用一个阻塞函数用来监听这个表中的文件描述符,直到这个表中有描述符要进行IO操作时,这个函数返回给进程有哪些描述符要进行操作。从而使一个进程能完成对多个描述符的操作。而函数对描述符的检测操作都是由系统内核完成的。

  linux下常用的IO转接技术有:select、poll和epoll。

select:

  头文件:#include <sys/select.h>、#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);

      nfds:要检测的文件描述符中最大的fd+1,nfds最大值为1024。select最多只能检测1024个文件描述符。

      readfds:读集合。读缓冲区中有数据时,readfds写入数据。fd_set文件描述符集类型,具体实现见下面。

      writefds:写集合。通常设为NULL。

      exceptfds:异常集合。通常设为NULL。

      timeout:设置超时返回。为NULL时只有检测到fd变化时返回。struct timeval a; a.tv_sec=10; a.tv_usec=0;

      返回值:成功返回要操作的描述符个数,超时返回0,失败返回-1。

      select最多只能检测1024个文件描述符,是由于fd_set在内核代码中的设置所限制

 //部分fd_set的内核代码

 #define __FDSET_LONGS     (__FD_SETSIZE/__NFDBITS)
#define __FD_SETSIZE 1024
#define __NFDBITS (8 * sizeof(unsigned long))
typedef __kernel_fd_set fd_set;
typedef struct {
unsigned long fds_bits [__FDSET_LONGS];
} __kernel_fd_set;

    void FD_CLR(int fd, fd_set *set);     从set集合中删除文件描述符fd。

    int  FD_ISSET(int fd, fd_set *set);   判断文件描述符fd是否在set集合中。

    void FD_SET(int fd, fd_set *set);    将fd添加到set集合中。

    void FD_ZERO(fd_set *set);           清空set集合。

 #include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <stdlib.h>
int main()
{
int fd=socket(AF_INET,SOCK_STREAM,);
struct sockaddr_in serv;
memset(&serv,,sizeof(serv));
serv.sin_addr.s_addr=htonl(INADDR_ANY);
serv.sin_port=htons();
serv.sin_family=AF_INET;
bind(fd,(struct sockaddr*)&serv,sizeof(serv)); listen(fd,); struct sockaddr_in client;
socklen_t cli_len=sizeof(client);
int maxfd=fd;
fd_set reads, temp;
FD_ZERO(&reads);
FD_SET(fd,&reads);
while()
{
temp=reads;
int ret=select(maxfd+,&temp,NULL,NULL,NULL);
if(-==ret)
{
perror("select error");
exit();
}
//客户端发起连接
if(FD_ISSET(fd,&temp))
{
//接受连接
int cfd=accept(fd,(struct sockaddr*)&client,&cli_len);
if(cfd==-)
{
perror("accept error");
exit();
}
FD_SET(cfd,&reads);
//更新最大文件描述符
maxfd=maxfd<cfd?cfd:maxfd; }
for(int i=fd+;i<=maxfd;++i)
{
if(FD_ISSET(i,&temp))
{
char buf[]={};
int len=recv(i,buf,sizeof(buf),);
if(len==-)
{
perror("recv error");
exit(); }
else if(len==)
{
printf("客户端断开连接\n");
close(i); FD_CLR(i,&reads);
}
else
{
printf("recv buf: %s\n",buf);
send(i,buf,strlen(buf)+,);
}
}
}
}
close(fd);
return ;
}

poll:

  头文件:#include <poll.h>

  函数:

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);

      fds:数组地址。内核检测fds中的文件描述符。

      nfds:数组的最大长度,数组中最后有效元素的下标+1。

      timeout:超时返回,-1永久阻塞,0不阻塞调用后立即返回,>0等待的时长,单位毫秒。

      返回值:成功返回要操作的个数,失败返回-1。

struct pollfd {
int fd; /*文件描述符*/
short events; /*等待的事件*/
short revents; /*实际发生的事件,内核给的反馈*/
}

pollfd常用事件:读事件,POLLIN;写事件,POLLOUT;错误事件,POLLERR(不能作为events的值);

 #include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <poll.h> #define SERV_PORT 8989 int main(int argc, const char* argv[])
{
int lfd, cfd;
struct sockaddr_in serv_addr, clien_addr;
int serv_len, clien_len; // 创建套接字
lfd = socket(AF_INET, SOCK_STREAM, );
// 初始化服务器 sockaddr_in
memset(&serv_addr, , sizeof(serv_addr));
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(SERV_PORT); // 设置端口
serv_len = sizeof(serv_addr);
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len); // 设置同时监听的最大个数
listen(lfd, );
printf("Start accept ......\n"); // poll结构体
struct pollfd allfd[];
int max_index = ;
// init
for(int i=; i<; ++i)
{
allfd[i].fd = -;
}
allfd[].fd = lfd;
allfd[].events = POLLIN; while()
{
int i = ;
int ret = poll(allfd, max_index+, -);
if(ret == -)
{
perror("poll error");
exit();
} // 判断是否有连接请求
if(allfd[].revents & POLLIN)
{
clien_len = sizeof(clien_addr);
// 接受连接请求
int cfd = accept(lfd, (struct sockaddr*)&clien_addr, &clien_len);
printf("============\n"); // cfd添加到poll数组
for(i=; i<; ++i)
{
if(allfd[i].fd == -)
{
allfd[i].fd = cfd;
break;
}
}
// 更新最后一个元素的下标
max_index = max_index < i ? i : max_index;
} // 遍历数组
for(i=; i<=max_index; ++i)
{
int fd = allfd[i].fd;
if(fd == -)
{
continue;
}
if(allfd[i].revents & POLLIN)
{
// 接受数据
char buf[] = {};
int len = recv(fd, buf, sizeof(buf), );
if(len == -)
{
perror("recv error");
exit();
}
else if(len == )
{
allfd[i].fd = -;
close(fd);
printf("客户端已经主动断开连接。。。\n");
}
else
{
printf("recv buf = %s\n", buf);
for(int k=; k<len; ++k)
{
buf[k] = toupper(buf[k]);
}
printf("buf toupper: %s\n", buf);
send(fd, buf, strlen(buf)+, );
} } }
} close(lfd);
return ;
}

  select和poll虽然没有前面几种方法的缺点,但是select和poll只返回个数,不会告诉进程具体是哪几个描述符要操作, 而且select和poll最多只能检测1024个。select每次调用时,都需要把fd集合从用户态和内核态之间相互拷贝,这在fd很多时会消耗大量资源。

  epoll检测的个数没有限制,它在内部构造维护了红黑树,减少了资源的消耗。

epoll:

  头文件:#include <sys/epoll.h>

  函数:

    int epoll_create(int size);     生成epoll专用的文件描述符,size:epoll上能关注的最大描述符个数。

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

      epfd:epoll_create生成的文件描述符。

      op:选项,EPOLL_CTL_ADD  注册,EPOLL_CTL_MOD  修改,EPOLL_CTL_DEL   删除。

      fd:关联的文件描述符。

      event:告诉内核要监听的事件

      返回值:成功返回0,失败返回-1。

    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);   等待IO事件发生,可以设置阻塞。

      epfd:要检测的句柄。

      events:回传待处理的数组。

      maxevents:events的大小。

      timeout:超时返回。-1永久阻塞;0立即返回;>0超时时间。

 typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t; struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
 #include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h> int main(int argc, const char* argv[])
{
if(argc < )
{
printf("eg: ./a.out port\n");
exit();
}
struct sockaddr_in serv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[]); // 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, );
// 初始化服务器 sockaddr_in
memset(&serv_addr, , serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len); // 设置同时监听的最大个数
listen(lfd, );
printf("Start accept ......\n"); struct sockaddr_in client_addr;
socklen_t cli_len = sizeof(client_addr); // 创建epoll树根节点
int epfd = epoll_create();
// 初始化epoll树
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); struct epoll_event all[];
while()
{
// 使用epoll通知内核fd 文件IO检测
int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[]), -); // 遍历all数组中的前ret个元素
for(int i=; i<ret; ++i)
{
int fd = all[i].data.fd;
// 判断是否有新连接
if(fd == lfd)
{
// 接受连接请求
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd == -)
{
perror("accept error");
exit();
}
// 将新得到的cfd挂到树上
struct epoll_event temp;
temp.events = EPOLLIN;
temp.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp); // 打印客户端信息
char ip[] = {};
printf("New Client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port)); }
else
{
// 处理已经连接的客户端发送过来的数据
if(!all[i].events & EPOLLIN)
{
continue;
} // 读数据
char buf[] = {};
int len = recv(fd, buf, sizeof(buf), );
if(len == -)
{
perror("recv error");
exit();
}
else if(len == )
{
printf("client disconnected ....\n");
// fd从epoll树上删除
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
if(ret == -)
{
perror("epoll_ctl - del error");
exit();
}
close(fd); }
else
{
printf(" recv buf: %s\n", buf);
write(fd, buf, len);
}
}
}
} close(lfd);
return ;
}

  epoll三种工作模式:

    水平触发:epoll默认工作模式,只要fd对应的缓冲区有数据,epoll_wait就会返回。epoll_wait调用次数越多,系统开销越大。

    边沿触发:fd默认是阻塞的,客户端发送一次数据epoll_wait就返回一次,不管数据是否读完。如果要读完数据,可以循环读取,但是recv会阻塞,解决方法是将fd设置为非阻塞。

    边沿非阻塞触发:将fd设置为非阻塞(open下设置O_NONBLOCK,或者利用fcntl()函数)。效率最高,可以将缓冲区数据完全读完。

Linux网络编程三、 IO操作的更多相关文章

  1. Linux网络编程(三)

    Linux网络编程(三) wait()还是waitpid() Linux网络编程(二)存在客户端断开连接后,服务器端存在大量僵尸进程.这是由于服务器子进程终止后,发送SIGCHLD信号给父进程,而父进 ...

  2. Linux 网络编程(IO模型)

    针对linux 操作系统的5类IO模型,阻塞式.非阻塞式.多路复用.信号驱动和异步IO进行整理,参考<linux网络编程>及相关网络资料. 阻塞模式 在socket编程(如下图)中调用如下 ...

  3. Linux系统编程--文件IO操作

    Linux思想即,Linux系统下一切皆文件. 一.对文件操作的几个函数 1.打开文件open函数 int open(const char *path, int oflags); int open(c ...

  4. Linux 网络编程三(socket代码详解)

    //网络编程客户端 #include <stdio.h> #include <stdlib.h> #include <string.h> #include < ...

  5. Linux网络编程(四)

    在linux网络编程[1-3]中,我们编写的网络程序仅仅是为了了解网络编程的基本步骤,实际应用当中的网络程序并不会用那样的.首先,如果服务器需要处理高并发访问,通常不会使用linux网络编程(三)中那 ...

  6. Linux网络编程-IO复用技术

    IO复用是Linux中的IO模型之一,IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理,从而不会在单个IO上阻塞了.Linux中,提 ...

  7. Linux 网络编程的5种IO模型:多路复用(select/poll/epoll)

    Linux 网络编程的5种IO模型:多路复用(select/poll/epoll) 背景 我们在上一讲 Linux 网络编程的5种IO模型:阻塞IO与非阻塞IO中,对于其中的 阻塞/非阻塞IO 进行了 ...

  8. Linux 网络编程的5种IO模型:信号驱动IO模型

    Linux 网络编程的5种IO模型:信号驱动IO模型 背景 上一讲 Linux 网络编程的5种IO模型:多路复用(select/poll/epoll) 我们讲解了多路复用等方面的知识,以及有关例程. ...

  9. Linux 网络编程的5种IO模型:异步IO模型

    Linux 网络编程的5种IO模型:异步IO模型 资料已经整理好,但是还有未竟之业:复习多路复用epoll 阅读例程, 异步IO 函数实现 背景 上一讲< Linux 网络编程的5种IO模型:信 ...

随机推荐

  1. .Net面试题三

    1..Net中类和结构的区别? 2.死锁地必要条件?怎么克服? 3.接口是否可以继承接口?抽象类是否可以实现接口?抽象类是否可以继承实体类? 4.构造器COnstructor是否可以被继承?是否可以被 ...

  2. springMVC 接受map参数的写法

    <form > <input type="hidden" name="map['userKey']" value="11111&qu ...

  3. 【原创】大叔经验分享(66)docker启动tomcat不输出catalina.out

    docker启动tomcat默认是: Run the default Tomcat server (CMD ["catalina.sh", "run"]): 查 ...

  4. 在webstorm中编译less,以及压缩css

    一.编译   在全局安装less npm install -g less 在webstorm setting ->tools -> filewatcher中设置 :  ../css/$Fi ...

  5. 【玩转SpringBoot】通过事件机制参与SpringBoot应用的启动过程

    生命周期和事件监听 一个应用的启动过程和关闭过程是归属到“生命周期”这个概念的范畴. 典型的设计是在启动和关闭过程中会触发一系列的“事件”,我们只要监听这些事件,就能参与到这个过程中来. 要想监听事件 ...

  6. linux 网卡相关命令

    1. ifconfig //查看网络相关信息 2. ifconfig eth0 192.168.1.103 netmask 255.255.255.0 //配置eth0的IP地址 3. route - ...

  7. 【2017-04-10】js来控制导航栏在滚动条拉到一定位置时显示

    <html> <head> <title>test</title> </head> <body> <div style=& ...

  8. 7.Nginx_Keepalived高可用配置

    2. 利用keepalived实现高可靠配置(HA) 2.1. 高可靠概念 HA(High Available):高可用性集群,是保证业务连续性的有效解决方案,一般有两个或两个以上的节点,且分为活动节 ...

  9. web开发中的支付宝支付和微信支付

    https://www.jianshu.com/p/155757d2b9eb <!-- wxPay --SDK--> <script src="https://res.wx ...

  10. python zip用法

    import requests url = "https://magi.com/search" querystring = {"q":"堕却乡&quo ...