当从一个文件描述符进行读写操作时,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. Codeforces Round #406 (Div. 2) A MONSTER

    A. The Monster time limit per test 1 second memory limit per test 256 megabytes input standard input ...

  2. c# 获取屏幕图片

    Rectangle bounds = Screen.GetBounds(Screen.GetBounds(Point.Empty)); using (Bitmap bitmap = new Bitma ...

  3. JS基础_if语句

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  4. centos7安装nginx服务

    Nginx发音引擎x是一个免费的开源高性能HTTP和反向代理服务器,负责处理互联网上一些最大的网站的负载. 本教程将教你如何在你的CentOS Linux 7.5机器上安装和管理Nginx. 安装Ng ...

  5. Laravel 查询数据按照时间分组

    首先取消严格模式: // config/database.php // 'strict' => true, // 严谨模式注释掉 查询构造器代码: //查询构造器部分代码 })->with ...

  6. cpu 100%怎样定位

    先用top定位最耗cpu的java进程 例如: 12430工具:top或者 htop(高级)方法:top -c 显示进程运行详细列表键入 P (大写P),按照cpu进行排序 然后用top -p 124 ...

  7. phpcms修改重置后台账号和密码

    通过Phpmyadmin等工具,打开数据库中找到v9_admin表: 把password字段值改为: 0b817b72c5e28b61b32ab813fd1ebd7f再把encrypt字段值改为: 3 ...

  8. vue入门:(底层渲染实现render函数、实例生命周期)

    vue实例渲染的底层实现 vue实例生命周期 一.vue实例渲染的底层实现 1.1实例挂载 在vue中实例挂载有两种方法:第一种在实例化vue时以el属性实现,第二种是通过vue.$mount()方法 ...

  9. Java高并发程序设计学习笔记(六):JDK并发包(线程池的基本使用、ForkJoin)

    转自:https://blog.csdn.net/dataiyangu/article/details/86573222 1. 线程池的基本使用1.1. 为什么需要线程池1.2. JDK为我们提供了哪 ...

  10. web项目部署在centos 7验证码显示不出来解决方案

    今天把项目部署在centos7上,发现验证码显示不出来,看了一下tomcat日志 Exception in thread "http-nio-8080-exec-3" java.l ...