1 用epoll处理定时器的信号
1.1 定时器&信号
-
定时器用于执行定时任务,例如清理空闲的tcp连接。
-
传统做法:alarm()函数可设置定时器,触发SIGALRM信号。
-
在epoll中:Linux内核把定时器和信号抽象为fd,让epoll统一监视
1.2 使用定时器的理由
为什么要加入定时器呢?因为我们要使用进程心跳,epoll_wait()会一直阻塞,没办法加入cpactive;所以想到了定时器!定时器的时间一到,有两件事情要做:
1)更新进程的心跳;
2)清理空闲的客户端socket,路由器也会这么做。
加入信号集,缺省情况下信号会发给进程,如果使用了sigprocmask(SIG_BLOCK, &sigset, 0);当前进程就收不到信号了,只能收到信号集中的信号。收到信号后对信号进行自定义处理。
2 缓冲区Buffer优化
2.1 存在的问题
在IO复用的模型中,采用的非阻塞的socket,如果打算向socket中写入1000字节,实际上值写入了5000字节,怎么办?数据量很大或网络带宽的原因发送缓冲区满了......发生了打算写入1000B实际只写了500B的情况。
bug:我们将接收到的数据(1000字节)原封不动的发送给对端,如果发送缓冲区满了(只发送了500字节),有字节丢失。
2.2 解决思路
1)把需要写入的数据存放在缓冲区中,同时关注对端socket的写事件。
2)如果触发了写事件,把缓冲区中的数据写入socket,写多少算多少,没写完的下次再写,如果写完了,不再关注socket的写事件。
3 正向代理模块优化
3.1 代码优化
/*
* 程序名:inetd.cpp,正向网络代理服务程序。
* 作者:张咸武
*/
#include "_public.h"
using namespace idc;
// 代理路由参数的结构体。
struct st_route
{
int srcport; // 源端口。
char dstip[31]; // 目标主机的地址。
int dstport; // 目标主机的端口。
int listensock; // 源端口监听的socket。
}stroute;
vector<struct st_route> vroute; // 代理路由的容器。
bool loadroute(const char *inifile); // 把代理路由参数加载到vroute容器。
// 初始化服务端的监听端口。
int initserver(const int port);
int epollfd=0; // epoll的句柄。
#define MAXSOCK 1024 // 最大连接数。
int clientsocks[MAXSOCK]; // 存放每个socket连接对端的socket的值。
int clientatime[MAXSOCK]; // 存放每个socket连接最后一次收发报文的时间。
string clientbuffer[MAXSOCK]; // 存放每个socket发送内容的buffer。
// 向目标地址和端口发起socket连接。
int conntodst(const char *ip,const int port);
void EXIT(int sig); // 进程退出函数。
clogfile logfile;
//cpactive pactive; // 进程心跳。
int main(int argc,char *argv[])
{
if (argc != 3)
{
printf("\n");
printf("Using :./inetd logfile inifile\n\n");
printf("Sample:./inetd /tmp/inetd.log /etc/inetd.conf\n\n");
printf(" /project/tools/bin/procctl 5 /project/tools/bin/inetd /tmp/inetd.log /etc/inetd.conf\n\n");
printf("本程序的功能是正向代理,如果用到了1024以下的端口,则必须由root用户启动。\n");
printf("logfile 本程序运行的日是志文件。\n");
printf("inifile 路由参数配置文件。\n");
return -1;
}
// 关闭全部的信号和输入输出。
// 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程。
// 但请不要用 "kill -9 +进程号" 强行终止。
closeioandsignal(true); signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
// 打开日志文件。
if (logfile.open(argv[1])==false)
{
printf("打开日志文件失败(%s)。\n",argv[1]); return -1;
}
//pactive.addpinfo(30,"inetd"); // 设置进程的心跳超时间为30秒。
// 把代理路由参数配置文件加载到vroute容器。
if (loadroute(argv[2])==false) return -1;
logfile.write("加载代理路由参数成功(%d)。\n",vroute.size());
// 初始化服务端用于监听的socket。
for (auto &aa:vroute)
{
if ( (aa.listensock=initserver(aa.srcport)) < 0 )
{
// 如果某一个socket初始化失败,忽略它。
logfile.write("initserver(%d) failed.\n",aa.srcport); continue;
}
// 把监听socket设置成非阻塞。
fcntl(aa.listensock,F_SETFL,fcntl(aa.listensock,F_GETFD,0)|O_NONBLOCK);
}
// 创建epoll句柄。
epollfd=epoll_create1(0);
struct epoll_event ev; // 声明事件的数据结构。
// 为监听的socket准备读事件。
for (auto aa:vroute)
{
if (aa.listensock<0) continue;
ev.events=EPOLLIN; // 读事件。
ev.data.fd=aa.listensock; // 指定事件的自定义数据,会随着epoll_wait()返回的事件一并返回。
epoll_ctl(epollfd,EPOLL_CTL_ADD,aa.listensock,&ev); // 把监听的socket的事件加入epollfd中。
}
//
// 把定时器加入epoll。
//int tfd=timerfd_create(CLOCK_MONOTONIC,TFD_NONBLOCK|TFD_CLOEXEC); // 创建timerfd。
//struct itimerspec timeout; // 定时时间的数据结构。
//memset(&timeout,0,sizeof(struct itimerspec));
//timeout.it_value.tv_sec = 10; // 定时时间为10秒。
//timeout.it_value.tv_nsec = 0;
//timerfd_settime(tfd,0,&timeout,0); // 开始计时。alarm(10)
//ev.data.fd=tfd; // 为定时器准备事件。
//ev.events=EPOLLIN;
//epoll_ctl(epollfd,EPOLL_CTL_ADD,tfd,&ev); // 把定时器fd加入epoll。
//
//
// 把信号加入epoll。
//sigset_t sigset; // 创建信号集。
//sigemptyset(&sigset); // 初始化(清空)信号集。
//sigaddset(&sigset, SIGINT); // 把SIGINT信号加入信号集。
//sigaddset(&sigset, SIGTERM); // 把SIGTERM信号加入信号集。
//sigprocmask(SIG_BLOCK, &sigset, 0); // 对当前进程屏蔽信号集(当前程将收不到信号集中的信号)。
//int sigfd=signalfd(-1, &sigset, 0); // 创建信号集的fd。
//ev.data.fd = sigfd; // 为信号集的fd准备事件。
//ev.events = EPOLLIN;
//epoll_ctl(epollfd,EPOLL_CTL_ADD,sigfd,&ev); // 把信号集fd加入epoll。
//
struct epoll_event evs[10]; // 存放epoll返回的事件。
while (true) // 进入事件循环。
{
// 等待监视的socket有事件发生。
int infds=epoll_wait(epollfd,evs,10,-1);
// 返回失败。
if (infds < 0) { logfile.write("epoll() failed。\n"); EXIT(-1); }
// 遍历epoll返回的已发生事件的数组evs。
for (int ii=0;ii<infds;ii++)
{
logfile.write("已发生事件的fd=%d(%d)\n",evs[ii].data.fd,evs[ii].events);
// 如果定时器的时间已到,有两件事要做:1)更新进程的心跳;2)清理空闲的客户端socket,路由器也会这么做。
//if (evs[ii].data.fd==tfd)
//{
// logfile.write("定时器时间已到。\n");
// timerfd_settime(tfd,0,&timeout,0); // 重新开始计时。 alarm(10)
// pactive.uptatime(); // 1)更新进程心跳。
// // 2)清理空闲的客户端socket。
// for (int jj=0;jj<MAXSOCK;jj++) // 可以把最大的socket记下来,这个循环不必遍历整个数组。
// {
// // 如果客户端socket空闲的时间超过80秒就关掉它。
// if ( (clientsocks[jj]>0) && ((time(0)-clientatime[jj])>80) )
// {
// logfile.write("client(%d,%d) timeout。\n",clientsocks[jj],clientsocks[clientsocks[jj]]);
// close(clientsocks[clientsocks[jj]]);
// close(clientsocks[jj]);
// // 把数组中对端的socket置空,这一行代码和下一行代码的顺序不能乱。
// clientsocks[clientsocks[jj]]=0;
// // 把数组中本端的socket置空,这一行代码和上一行代码的顺序不能乱。
// clientsocks[jj]=0;
// }
// }
// continue;
//}
// 如果收到了信号。
//if (evs[ii].data.fd==sigfd)
//{
// struct signalfd_siginfo siginfo; // 信号的数据结构。
// int s = read(sigfd, &siginfo, sizeof(struct signalfd_siginfo)); // 读取信号。
// logfile.write("收到了信号=%d。\n",siginfo.ssi_signo);
// 在这里编写处理信号的代码。
// continue;
//}
// 如果发生事件的是listensock,表示有新的客户端连上来。
int jj=0;
for (jj=0;jj<vroute.size();jj++)
{
if (evs[ii].data.fd==vroute[jj].listensock) // 判断是哪个源端口有客户端连上来了。5058
{
// 从已连接队列中获取客户端连上来的socket。 // socket是7
struct sockaddr_in client;
socklen_t len = sizeof(client);
int srcsock = accept(vroute[jj].listensock,(struct sockaddr*)&client,&len);
if (srcsock<0) break;
if (srcsock>=MAXSOCK)
{
logfile.write("连接数已超过最大值%d。\n",MAXSOCK); close(srcsock); break;
}
// 向目标地址和端口发起连接,如果连接失败,会被epoll发现,将关闭通道。
int dstsock=conntodst(vroute[jj].dstip,vroute[jj].dstport); // socket是8
if (dstsock<0) { close(srcsock); break; }
if (dstsock>=MAXSOCK)
{
logfile.write("连接数已超过最大值%d。\n",MAXSOCK); close(srcsock); close(dstsock); break;
}
logfile.write("accept on port %d,client(%d,%d) ok。\n",vroute[jj].srcport,srcsock,dstsock);
// 为新连接的两个socket准备读事件,并添加到epoll中。
ev.data.fd=srcsock; ev.events=EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,srcsock,&ev);
ev.data.fd=dstsock; ev.events=EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,dstsock,&ev);
// 更新clientsocks数组中两端soccket的值和活动时间。
clientsocks[srcsock]=dstsock; clientatime[srcsock]=time(0);
clientsocks[dstsock]=srcsock; clientatime[dstsock]=time(0);
break;
}
}
// 如果jj<vroute.size(),表示事件在上面的for循环中已被处理,流程不必往下。
if (jj<vroute.size()) continue;
// 如果是客户端连接的socke有事件,分三种情况:1)客户端有报文发过来;2)客户端连接已断开;3)有数据要发给客户端。
// 如果从通道一端的socket读取到了数据,把数据存放在对端socket的缓冲区中。
// if (evs[ii].events==EPOLLIN) // 不要这么写,有读事件是1,有写事件是4,如果读和写都有,是5。
if (evs[ii].events&EPOLLIN) // 判断是否为读事件。
{
char buffer[5000]; // 存放从接收缓冲区中读取的数据。
int buflen=0; // 从接收缓冲区中读取的数据的大小。
// 从通道的一端读取数据。
if ( (buflen=recv(evs[ii].data.fd,buffer,sizeof(buffer),0)) <= 0 )
{
// 如果连接已断开,需要关闭通道两端的socket。
logfile.write("client(%d,%d) disconnected。\n",evs[ii].data.fd,clientsocks[evs[ii].data.fd]);
close(evs[ii].data.fd); // 关闭客户端的连接。
close(clientsocks[evs[ii].data.fd]); // 关闭客户端对端的连接。
clientsocks[clientsocks[evs[ii].data.fd]]=0; // 把数组中对端的socket置空,这一行代码和下一行代码的顺序不能乱。
clientsocks[evs[ii].data.fd]=0; // 把数组中本端的socket置空,这一行代码和上一行代码的顺序不能乱。
continue;
}
// 成功的读取到了数据,把接收到的报文内容原封不动的发给通道的对端。
// logfile.write("from %d to %d,%d bytes。\n",evs[ii].data.fd,clientsocks[evs[ii].data.fd],buflen);
// send(clientsocks[evs[ii].data.fd],buffer,buflen,0);
logfile.write("from %d,%d bytes\n",evs[ii].data.fd,buflen);
// 把读取到的数据追加到对端socket的buffer中。
clientbuffer[clientsocks[evs[ii].data.fd]].append(buffer,buflen);
// 修改对端socket的事件,增加写事件。
ev.data.fd=clientsocks[evs[ii].data.fd];
ev.events=EPOLLIN|EPOLLOUT;
epoll_ctl(epollfd,EPOLL_CTL_MOD,ev.data.fd,&ev);
// 更新通道两端socket的活动时间。
clientatime[evs[ii].data.fd]=time(0);
clientatime[clientsocks[evs[ii].data.fd]]=time(0);
}
// 判断客户端的socket是否有写事件(发送缓冲区没有满)。
if (evs[ii].events&EPOLLOUT)
{
// 把socket缓冲区中的数据发送出去。
int writen=send(evs[ii].data.fd,clientbuffer[evs[ii].data.fd].data(),clientbuffer[evs[ii].data.fd].length(),0);
// 以下代码模拟不能一次发完全部数据的场景。
//int ilen;
//if (clientbuffer[evs[ii].data.fd].length()>10) ilen=10;
//else ilen=clientbuffer[evs[ii].data.fd].length();
//int writen=send(evs[ii].data.fd,clientbuffer[evs[ii].data.fd].data(),ilen,0);
logfile.write("to %d,%d bytes\n",evs[ii].data.fd,writen);
// 删除socket缓冲区中已成功发送的数据。
clientbuffer[evs[ii].data.fd].erase(0,writen);
// 如果socket缓冲区中没有数据了,不再关心socket的写件事。
if (clientbuffer[evs[ii].data.fd].length()==0)
{
ev.data.fd=evs[ii].data.fd;
ev.events=EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_MOD,ev.data.fd,&ev);
}
}
}
}
return 0;
}
// 初始化服务端的监听端口。
int initserver(const int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if (sock < 0)
{
logfile.write("socket(%d) failed.\n",port); return -1;
}
int opt = 1; unsigned int len = sizeof(opt);
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,len);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
if (bind(sock,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0 )
{
logfile.write("bind(%d) failed.\n",port); close(sock); return -1;
}
if (listen(sock,5) != 0 )
{
logfile.write("listen(%d) failed.\n",port); close(sock); return -1;
}
return sock;
}
// 把代理路由参数加载到vroute容器。
bool loadroute(const char *inifile)
{
cifile ifile;
if (ifile.open(inifile)==false)
{
logfile.write("打开代理路由参数文件(%s)失败。\n",inifile); return false;
}
string strbuffer;
ccmdstr cmdstr;
while (true)
{
if (ifile.readline(strbuffer)==false) break;
// 删除说明文字,#后面的部分。
auto pos=strbuffer.find("#");
if (pos!=string::npos) strbuffer.resize(pos);
replacestr(strbuffer," "," ",true); // 把两个空格替换成一个空格,注意第四个参数。
deletelrchr(strbuffer,' '); // 删除两边的空格。
// 拆分参数。
cmdstr.splittocmd(strbuffer," ");
if (cmdstr.size()!=3) continue;
memset(&stroute,0,sizeof(struct st_route));
cmdstr.getvalue(0,stroute.srcport); // 源端口。
cmdstr.getvalue(1,stroute.dstip); // 目标地址。
cmdstr.getvalue(2,stroute.dstport); // 目标端口。
vroute.push_back(stroute);
}
return true;
}
// 向目标地址和端口发起socket连接。
int conntodst(const char *ip,const int port)
{
// 第1步:创建客户端的socket。
int sockfd;
if ( (sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) return -1;
// 第2步:向服务器发起连接请求。
struct hostent* h;
if ( (h = gethostbyname(ip)) == 0 ) { close(sockfd); return -1; }
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port); // 指定服务端的通讯端口。
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
// 把socket设置为非阻塞。
fcntl(sockfd,F_SETFL,fcntl(sockfd,F_GETFD,0)|O_NONBLOCK);
if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr))<0)
{
if (errno!=EINPROGRESS)
{
logfile.write("connect(%s,%d) failed.\n",ip,port); return -1;
}
}
return sockfd;
}
void EXIT(int sig)
{
logfile.write("程序退出,sig=%d。\n\n",sig);
// 关闭全部监听的socket。
for (auto &aa:vroute)
if (aa.listensock>0) close(aa.listensock);
// 关闭全部客户端的socket。
for (auto aa:clientsocks)
if (aa>0) close(aa);
close(epollfd); // 关闭epoll。
exit(0);
}
3.2 补充
当发生EPOLLIN事件时,修改对端的socket关注写事件,那么对面会触发写事件EPOLLOUT,然后发送出去(底层缓冲区没满),如果我们自己的clientbuffer中没有数据了,那么不再关注EPOLLOUT。