linux epoll 开发指南-【ffrpc源码解析】

linux epoll 开发指南-【ffrpc源码解析】

摘要
关于epoll的问题很早就像写文章讲讲自己的看法,但是由于ffrpc一直没有完工,所以也就拖下来了。Epoll主要在服务器编程中使用,本文主要探讨服务器程序中epoll的使用技巧。Epoll一般和异步io结合使用,故本文讨论基于以下应用场合:

主要讨论服务器程序中epoll的使用,主要涉及tcp socket的相关api。
Tcp socket 为异步模式,包括socket的异步读写,以及监听的异步操作。
本文不会过多讨论API的细节,而是专注流程与设计。
Epoll 的io模型
Epoll是为异步io操作而设计的,epoll中IO事件被分为read事件和write事件,如果大家对于linux的驱动模块或者linux io 模型有接触的话,就会理解起来更容易。Linux中IO操作被抽象为read、write、close、ctrl几个操作,所以epoll只提供read、write、error事件,是和linux的io模型是统一的。

当epoll通知read事件时,可以调用io系统调用read读取数据
当epoll通知write事件时,可以调用io系统调用write发送数据
当error事件时,可以close回收资源
Ctrl相关的接口则用来设置socket的非阻塞选项等。
为什么要了解epoll的io模型呢,本文认为,某些情况下epoll操作的代码的复杂性是由于代码中的模型(或者类设计)与epoll io模型不匹配造成的。换句话说,如果我们的编码模型和epoll io模型匹配,那么非阻塞socket的编码就会很简单、清晰。

按照epoll模型构建的类关系为:

//! 文件描述符相关接口
typedef int socket_fd_t;
class fd_i
{
public:
virtual ~fd_i(){}

virtual socket_fd_t socket()          = 0;
virtual int handle_epoll_read()  = 0;
virtual int handle_epoll_write() = 0;
virtual int handle_epoll_del()          = 0;virtual void close()                          = 0;

};
int epoll_impl_t::event_loop()
{
int i = 0, nfds = 0;
struct epoll_event ev_set[EPOLL_EVENTS_SIZE];

do
{nfds  = ::epoll_wait(m_efd, ev_set, EPOLL_EVENTS_SIZE, EPOLL_WAIT_TIME);if (nfds < 0 && EINTR == errno){nfds = 0;continue;}for (i = 0; i < nfds; ++i){epoll_event& cur_ev = ev_set[i];fd_i* fd_ptr            = (fd_i*)cur_ev.data.ptr;if (cur_ev.data.ptr == this)//! iterupte event{if (false == m_running){return 0;}//! 删除那些已经出现error的socket 对象fd_del_callback();continue;}if (cur_ev.events & (EPOLLIN | EPOLLPRI)){fd_ptr->handle_epoll_read();}if(cur_ev.events & EPOLLOUT){fd_ptr->handle_epoll_write();}if (cur_ev.events & (EPOLLERR | EPOLLHUP)){fd_ptr->close();}}}while(nfds >= 0);return 0;

}
Epoll的LT模式和ET模式的比较
先简单比较一下level trigger 和 edge trigger 模式的不同。

LT模式的特点是:
若数据可读,epoll返回可读事件
若开发者没有把数据完全读完,epoll会不断通知数据可读,直到数据全部被读取。
若socket可写,epoll返回可写事件,而且是只要socket发送缓冲区未满,就一直通知可写事件。
优点是对于read操作比较简单,只要有read事件就读,读多读少都可以。
缺点是write相关操作较复杂,由于socket在空闲状态发送缓冲区一定是不满的,故若socket一直在epoll wait列表中,则epoll会一直通知write事件,所以必须保证没有数据要发送的时候,要把socket的write事件从epoll wait列表中删除。而在需要的时候在加入回去,这就是LT模式的最复杂部分。
ET模式的特点是:
若socket可读,返回可读事件
若开发者没有把所有数据读取完毕,epoll不会再次通知epoll read事件,也就是说存在一种隐患,如果开发者在读到可读事件时,如果没有全部读取所有数据,那么可能导致epoll在也不会通知该socket的read事件。(其实这个问题并没有听上去难,参见下文)。
若发送缓冲区未满,epoll通知write事件,直到开发者填满发送缓冲区,epoll才会在下次发送缓冲区由满变成未满时通知write事件。
ET模式下,只有socket的状态发生变化时才会通知,也就是读取缓冲区由无数据到有数据时通知read事件,发送缓冲区由满变成未满通知write事件。
缺点是epoll read事件触发时,必须保证socket的读取缓冲区数据全部读完(事实上这个要求很容易达到)
优点:对于write事件,发送缓冲区由满到未满时才会通知,若无数据可写,忽略该事件,若有数据可写,直接写。Socket的write事件可以一直发在epoll的wait列表。Man epoll中我们知道,当向socket写数据,返回的值小于传入的buffer大小或者write系统调用返回EWouldBlock时,表示发送缓冲区已满。
让我们换一个角度来理解ET模式,事实上,epoll的ET模式其实就是socket io完全状态机。

先来看epoll中read 的状态图:

当socket由不可读变成可读时,epoll的ET模式返回read 事件。对于read 事件,开发者需要保证把读取缓冲区数据全部读出,man epoll可知:

Read系统调用返回EwouldBlock,表示读取缓冲区数据全部读出
Read系统调用返回的数值小于传入的buffer参数,表示读取缓冲区全部读出。
示例代码

int socket_impl_t:: handle_epoll_read ()
{
if (is_open())
{
int nread = 0;
char recv_buffer[RECV_BUFFER_SIZE];
do
{
nread = ::read(m_fd, recv_buffer, sizeof(recv_buffer) - 1);
if (nread > 0)
{
recv_buffer[nread] = ‘\0’;
m_sc->handle_read(this, recv_buffer, size_t(nread));
if (nread < int(sizeof(recv_buffer) - 1))
{
break;//! equal EWOULDBLOCK
}
}
else if (0 == nread) //! eof
{
this->close();
return -1;
}
else
{
if (errno == EINTR)
{
continue;
}
else if (errno == EWOULDBLOCK)
{
break;
}
else
{
this->close();
return -1;
}
}
} while(1);
}
return 0;
}
再来看write 的状态机:

需要读者注意的是,socket模式是可写的,因为发送缓冲区初始时空的。故应用层有数据要发送时,直接调用write系统调用发送数据,若write系统调用返回EWouldBlock则表示socket变为不可写,或者write系统调用返回的数值小于传入的buffer参数的大小,这时需要把未发送的数据暂存在应用层待发送列表中,等待epoll返回write事件,再继续发送应用层待发送列表中的数据,同样若应用层待发送列表中的数据没有一次性发完,那么继续等待epoll返回write事件,如此循环往复。所以可以反推得到如下结论,若应用层待发送列表有数据,则该socket一定是不可写状态,那么这时候要发送新数据直接追加到待发送列表中。若待发送列表为空,则表示socket为可写状态,则可以直接调用write系统调用发送数据。总结如下:

当发送数据时,若应用层待发送列表有数据,则将要发送的数据追加到待发送列表中。否则直接调用write系统调用。
Write系统调用发送数据时,检测write返回值,若返回数值>0且小于传入的buffer参数大小,或返回EWouldBlock错误码,表示,发送缓冲区已满,将未发送的数据追加到待发送列表
Epoll返回write事件后,检测待发送列表是否有数据,若有数据,依次尝试发送指导数据全部发送完毕或者发送缓冲区被填满。
示例代码:

void socket_impl_t::send_impl(const string& src_buff_)
{
string buff_ = src_buff_;

if (false == is_open() || m_sc->check_pre_send(this, buff_))
{return;
}
//! socket buff is full, cache the data
if (false == m_send_buffer.empty())
{m_send_buffer.push_back(buff_);return;
}string left_buff;
int ret = do_send(buff_, left_buff);if (ret < 0)
{this ->close();
}
else if (ret > 0)
{m_send_buffer.push_back(left_buff);
}
else
{//! send okm_sc->handle_write_completed(this);
}

}
int socket_impl_t:: handle_epoll_write ()
{
int ret = 0;
string left_buff;

if (false == is_open() || true == m_send_buffer.empty())
{return 0;
}do
{const string& msg = m_send_buffer.front();ret = do_send(msg, left_buff);if (ret < 0){this ->close();return -1;}else if (ret > 0){m_send_buffer.pop_front();m_send_buffer.push_front(left_buff);return 0;}else{m_send_buffer.pop_front();}
} while (false == m_send_buffer.empty());m_sc->handle_write_completed(this);
return 0;

}
总结
  LT模式主要是读操作比较简单,但是对于ET模式并没有优势,因为将读取缓冲区数据全部读出并不是难事。而write操作,ET模式则流程非常的清晰,按照完全状态机来理解和实现就变得非常容易。而LT模式的write操作则复杂多了,要频繁的维护epoll的wail列表。

  在代码编写时,把epoll ET当成状态机,当socket被创建完成(accept和connect系统调用返回的socket)时加入到epoll列表,之后就不用在从中删除了。为什么呢?man epoll中的FAQ告诉我们,当socket被close掉后,其自动从epoll中删除。对于监听socket简单说几点注意事项:

监听socket的write事件忽略
监听socket的read事件表示有新连接,调用accept接受连接,直到返回EWouldBlock。
对于Error事件,有些错误是可以接受的错误,比如文件描述符用光的错误
示例代码:

int acceptor_impl_t::handle_epoll_read()
{
struct sockaddr_storage addr;
socklen_t addrlen = sizeof(addr);

int new_fd = -1;
do
{if ((new_fd = ::accept(m_listen_fd, (struct sockaddr *)&addr, &addrlen)) == -1){if (errno == EWOULDBLOCK){return 0;}else if (errno == EINTR || errno == EMFILE || errno == ECONNABORTED || errno == ENFILE ||errno == EPERM || errno == ENOBUFS || errno == ENOMEM){perror("accept");//! if too many open files occur, need to restart epoll eventm_epoll->mod_fd(this);return 0;}perror("accept");return -1;}socket_i* socket = create_socket(new_fd);socket->open();
} while (true);
return 0;

}
GitHub :https://github.com/fanchy/FFRPC

ffrpc 介绍: http://www.cnblogs.com/zhiranok/p/ffrpc_summary.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/402025.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Shell 脚本中如何使用make命令

最近开发的项目中需要编写Shell脚本对整个工程进行自动化编译&#xff0c;即在Shell脚本中使用make命令来进行编译&#xff0c;下面回顾一下Shell脚本中如何使用make命令&#xff09; 在开发一个系统时&#xff0c;一般是将一个系统分成几个模块&#xff0c;这样做提高了系统的…

c语言linux TCP长连接 socket收发范例 断开自动重连

原文链接&#xff1a;https://blog.csdn.net/chenhao0568/article/details/103420615 c语言linux TCP长连接 socket收发范例 断开自动重连 改进1&#xff1a;加入接收超时。可以做别的事&#xff0c;等有接收才响应 #include <stdio.h> #include <stdlib.h> #inc…

Shell 脚本知识回顾 (六) —— Shell 函数

一、Shell函数&#xff1a;Shell函数返回值、删除函数、在终端调用函数 函数可以让我们将一个复杂功能划分成若干模块&#xff0c;让程序结构更加清晰&#xff0c;代码重复利用率更高。像其他编程语言一样&#xff0c;Shell 也支持函数。Shell 函数必须先定义后使用。 Shell 函…

ICE相关链接

ZeroC IceGrid介绍及demo构建&#xff08;使用IceBox&#xff09; ice grid 第一篇 ICE通信之IceGrid服务&#xff08;二&#xff09; IceGrid注册器复制 Ice服务详解及应用_IceGrid(01)IceGrid应用 配置手册 https://blog.csdn.net/abcdefg367/category_8275964.html ICE通信框…

C# 托管资源和非托管资源(Dispose、析构函数)

https://www.cnblogs.com/herenzhiming/articles/9691524.html 资源分类&#xff1a; 托管资源指的是.NET可以自动进行回收的资源&#xff0c;主要是指托管堆上分配的内存资源。托管资源的回收工作是不需要人工干预的&#xff0c;有.NET运行库在合适调用垃圾回收器进行回收。…

Shell 脚本知识回顾 (五) —— Shell 循环

一、Shell for循环 与其他编程语言类似&#xff0c;Shell支持for循环。 for循环一般格式为&#xff1a;for 变量 in 列表 docommand1command2...commandN done 列表是一组值&#xff08;数字、字符串等&#xff09;组成的序列&#xff0c;每个值通过空格分隔。每循环一次&…

android开发工具下载

android studio eclipse sdk adt

Shell 脚本知识回顾 (四) —— Shell 命令及Shell 相关语句

一、Shell echo命令 echo是Shell的一个内部指令&#xff0c;用于在屏幕上打印出指定的字符串。命令格式&#xff1a;echo arg您可以使用echo实现更复杂的输出格式控制。 显示转义字符 echo "\"It is a test\""结果将是&#xff1a;"It is a test"…

qt工程。。。。。。

分享Qt多工程多目录的编译案例&#xff0c;subdirs Qt编译debug和release版本–CONFIG(debug,debug|release) QT工程pro设置实践(with QtCreator)----非弄的像VS同样才顺手? Qt创建动态库并添加动态库版本号 qmake&#xff1a;变量手册 QtCreator按顺序编译多个子项目

Shell 脚本知识回顾 (三) —— 替换、运算符、字符串、数组

一、Shell替换&#xff1a;Shell变量替换&#xff0c;命令替换&#xff0c;转义字符 如果表达式中包含特殊字符&#xff0c;Shell 将会进行替换。例如&#xff0c;在双引号中使用变量就是一种替换&#xff0c;转义字符也是一种替换。 举个例子&#xff1a; [cpp] view plaincop…

最幸福的事就是吃饺子

中午了&#xff0c;不知道吃什么&#xff0c;就去煮了点饺子&#xff0c;人呼呼的&#xff0c;吃完了很暖和~~下午出去&#xff0c;晚上回来&#xff0c;一天就这样过了~~转载于:https://blog.51cto.com/tina1314luky/1343466

Shell 脚本知识回顾 (二) —— Shell变量

一、Shell变量&#xff1a;Shell变量的定义、删除变量、只读变量、变量类型 Shell支持自定义变量。定义变量 定义变量时&#xff0c;变量名不加美元符号&#xff08;$&#xff09;&#xff0c;如&#xff1a; [cpp] view plaincopy variableName"value" 注意&…

Shell 脚本知识回顾 (一) —— 基础篇

一、Shell简介&#xff1a;什么是Shell&#xff0c;Shell命令的两种执行方式 Shell本身是一个用C语言编写的程序&#xff0c;它是用户使用Unix/Linux的桥梁&#xff0c;用户的大部分工作都是通过Shell完成的。Shell既是一种命令语言&#xff0c;又是一种程序设计语言。作为命令…

c实现面向对象编程(3)

http://blog.csdn.net/kennyrose/article/details/7564105

C 与 JAVA 的对比分析

Sun 公司推出的Java 是面向对象程序设计语言&#xff0c;其适用于Internet 应用的开发&#xff0c;称为网络时代重要的语言之一。Java 可以用认为是C 的衍生语言&#xff0c;与C 在大量元以内成分保持相同&#xff0c;例如此法结构、表达式语句、运算符等与C基本一致&#xff1…

红帽集群RHCS

1、简介&#xff1a;RHCS是RedHatClusterSuite的缩写&#xff0c;也就是红帽子集群套件&#xff0c;RHCS是一个能够提供高可用性、高可靠性、负载均衡、存储共享且经济廉价的集群工具集合&#xff0c;它将集群系统中三大集群架构融合一体&#xff0c;可以给web应用、数据库应用…

Java 基础——类的加载

当程序主动使用某个类时&#xff0c;如果该类还未被加载到内存中&#xff0c;系统会通过加载&#xff0c;连接&#xff0c;初始化三个步骤来对该类进行初始化&#xff0c;JVM将会连续完成这三个步骤&#xff0c;也把这三个步骤统称为类加载或类初始化&#xff1b; 类加载指的是…

HDUOJ-----1556Color the ball

Color the ball Time Limit: 9000/3000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 6787 Accepted Submission(s): 3549 Problem DescriptionN 个气球排成一排&#xff0c;从左到右依次编号为1,2,3....N.每次给定2个整数a b(a <…

Java 基础——数组解析

数组对于每一门编辑应语言来说都是重要的数据结构之一&#xff0c;当然不同语言对数组的实现及处理也不尽相同。 Java语言中提供的数组是用来存储固定大小的同类型元素。 可以声明一个数组变量&#xff0c;如numbers[100]来代替直接声明100个独立变量number0&#xff0c;number…

《在你身边,为你设计》-哪位知道下载、在线阅读地址啊?

《在你身边&#xff0c;为你设计》-前端UI必读出自腾讯CDChttp://cdc.tencent.com/?p6761今天听同事说这本书写的非常好&#xff0c;改变了他关于前端UI的许多看法&#xff0c;可谓&#xff1a;醍醐灌顶。可惜我网上找了下都需要Money买&#xff0c;哪位有在线阅读、PDF下载地…