网络开发中的Reactor(反应堆模式)和Proacrot(异步模式)
服务器程序重点处理IO事件,即:用户的请求读出来,反序列化,回调业务处理,回写。如果在按照面向过程的思路去写,就发挥不出CPU并发优势。那么有没有更优雅的设计方式呢?
有的兄弟,有的。
Reactor
反应堆模式本质是生产者、消费者模式。
- 主线程为生产者线程,阻塞进行epoll_wait,等待系统唤醒。
- 触发事件时,将处理请求放入线程池。唤醒工作线程处理。
- 工作线程处理请求,处理完成后等待新的任务唤醒。
示例代码如下:
#include"reactor.h"
#include <pthread.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include"threadpool.h"
#include"../epoll/epollMod.h"
#define SERV_PORT 8192
#define PTHREAD_MAX 20
#define MAX_EVENTS 1024 //监听上限数
threadpool_t* pPool; //线程池
int g_efd; //全局变量, 保存epoll_create返回的文件描述符
struct myevent_s g_events[MAX_EVENTS+1]; //epoll数组
rcb readcb = NULL;
void recvdata(int fd, int events, void *arg);
void senddata(int fd, int events, void *arg);
/*创建 socket, 初始化lfd */
void initlistensocket(int efd, short port)
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(lfd, F_SETFL, O_NONBLOCK); //将socket设为非阻塞
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
/* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */
eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]);
/* void eventadd(int efd, int events, struct myevent_s *ev) */
eventadd(efd, EPOLLIN | EPOLLET, &g_events[MAX_EVENTS]);
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin)); //bzero(&sin, sizeof(sin))
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(port);
bind(lfd, (struct sockaddr *)&sin, sizeof(sin));
listen(lfd, 20);
}
int init(int argc,char* argv[],rcb cb)
{
//初始化线程池
pPool = threadpool_create(3,100,100);
unsigned short port = SERV_PORT;
if (argc == 2)
port = atoi(argv[1]); //使用用户指定端口.如未指定,用默认端口
g_efd = epoll_create(MAX_EVENTS+1); //创建红黑树,返回给全局 g_efd
if (g_efd <= 0)
printf("create efd in %s err %s\n", __func__, strerror(errno));
initlistensocket(g_efd, port); //初始化监听socket
printf("server running:port[%d]\n", port);
readcb = cb;
}
int run()
{
struct epoll_event events[MAX_EVENTS+1]; //保存已经满足就绪事件的文件描述符数组
int checkpos = 0, i;
while (1)
{
/* 超时验证,每次测试100个链接,不测试listenfd 当客户端60秒内没有和服务器通信,则关闭此客户端链接 */
long now = time(NULL); //当前时间
for (i = 0; i < 100; i++, checkpos++)
{ //一次循环检测100个。 使用checkpos控制检测对象
if (checkpos == MAX_EVENTS)
checkpos = 0;
if (g_events[checkpos].status != 1) //不在红黑树 g_efd 上
continue;
long duration = now - g_events[checkpos].last_active; //客户端不活跃的时间
if (duration >= 60)
{
close(g_events[checkpos].fd); //关闭与该客户端链接
printf("[fd=%d] timeout\n", g_events[checkpos].fd);
eventdel(g_efd, &g_events[checkpos]); //将该客户端 从红黑树 g_efd移除
}
}
/*监听红黑树g_efd, 将满足的事件的文件描述符加至events数组中, 1秒没有事件满足, 返回 0*/
int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000);
if (nfd < 0)
{
printf("epoll_wait error, exit\n");
break;
}
else if(nfd > 0)
{
printf("epoll_wait ok, size = %d\n",nfd);
}
for (i = 0; i < nfd; i++)
{
/*使用自定义结构体myevent_s类型指针, 接收 联合体data的void *ptr成员*/
struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;
//处理数据,把数据扔进线程池
if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN))
{
threadpool_add(pPool,pushcb,ev);
printf("Push %d read event begin\n",ev->fd);
}
if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT))
{
threadpool_add(pPool,pushcb,ev);
printf("Push %d write event begin\n",ev->fd);
}
}
}
/* 退出前释放所有资源 */
threadpool_destroy(pPool);
return 0;
}
/* 当有文件描述符就绪, epoll返回, 调用该函数 与客户端建立链接 */
void acceptconn(int lfd, int events, void *arg)
{
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int cfd, i;
if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1)
{
if (errno != EAGAIN && errno != EINTR)
{
/* 暂时不做出错处理 */
}
printf("%s: accept, %s\n", __func__, strerror(errno));
return ;
}
do
{
for (i = 0; i < MAX_EVENTS; i++) //从全局数组g_events中找一个空闲元素
if (g_events[i].status == 0) //类似于select中找值为-1的元素
break; //跳出 for
if (i == MAX_EVENTS)
{
printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
break; //跳出do while(0) 不执行后续代码
}
int flag = 0;
if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0)
{
//将cfd也设置为非阻塞
printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
break;
}
/* 给cfd设置一个 myevent_s 结构体, 回调函数 设置为 recvdata */
eventset(&g_events[i], cfd, recvdata, &g_events[i]);
eventadd(g_efd, EPOLLIN | EPOLLET, &g_events[i]); //将cfd添加到红黑树g_efd中,监听读事件
} while(0);
printf("new connect [%s:%d][time:%ld], pos[%d]\n",
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
}
void* pushcb(void* arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
ev->call_back(ev->fd, ev->events, ev->arg);
return NULL;
}
void recvdata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = recv(fd, ev->buf, sizeof(ev->buf), 0); //读文件描述符, 数据存入myevent_s成员buf中
eventdel(g_efd, ev); //将该节点从红黑树上摘除
if (len > 0)
{
ev->len = len;
if(readcb!=NULL){
ev->len = readcb(fd,ev->buf,ev->len);
}
ev->buf[len] = '\0'; //手动添加字符串结束标记
eventset(ev, fd, senddata, ev); //设置该 fd 对应的回调函数为 senddata
eventadd(g_efd, EPOLLOUT| EPOLLET, ev); //将fd加入红黑树g_efd中,监听其写事件
}
else if (len == 0)
{
close(ev->fd);
/* ev-g_events 地址相减得到偏移元素位置 */
printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events);
}
else
{
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
}
void senddata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = send(fd, ev->buf, ev->len, 0); //直接将数据 回写给客户端。未作处理
if (len > 0)
{
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
eventdel(g_efd, ev); //从红黑树g_efd中移除
eventset(ev, fd, recvdata, ev); //将该fd的 回调函数改为 recvdata
eventadd(g_efd, EPOLLIN| EPOLLET, ev); //从新添加到红黑树上, 设为监听读事件
}
else
{
close(ev->fd); //关闭链接
eventdel(g_efd, ev); //从红黑树g_efd中移除
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
}
- 代码对epoll、线程池部分操作进行了非常简单的封装。
Proacrot
异步模式依赖内核来通知服务IO事件。申请一个缓冲区,准备一个处理函数,向内核注册。内核会在向缓冲区写入数据后,回调处理函数。
示例代码:我们可以借助boost的asio库来实现。
#include<boost/asio.hpp>
#include<boost/thread.hpp>
using namespace std;
using namespace boost::asio;
io_service service;
ip::tcp::endpoint ep(ip::tcp::v4(), 8192); // 监听端口8192
ip::tcp::acceptor acceptor(service, ep);
class client{
public:
client(io_service& service):sock(service)
{
}
char recvBuffer[4096];
char sendBuffer[4096];
ip::tcp::socket sock;
};
typedef boost::shared_ptr<ip::tcp::socket> socketPtr;
typedef boost::shared_ptr<client> clientPtr;
void senddata(clientPtr cli,const boost::system::error_code &err);
void recvdata(clientPtr cli,const boost::system::error_code &err)
{
if(err.failed())
{
printf("cli close \n");
cli->sock.close();
}
else
{
printf("recv %s \n",cli->recvBuffer);
memcpy(cli->sendBuffer,cli->recvBuffer,sizeof(cli->recvBuffer));
cli->sock.async_send(buffer(cli->sendBuffer),boost::bind(senddata,cli,_1));
}
}
void senddata(clientPtr cli,const boost::system::error_code &err)
{
if(err.failed())
{
printf("cli close \n");
cli->sock.close();
}
else
{
cli->sock.async_receive(buffer(cli->recvBuffer),boost::bind(recvdata,cli,_1));
}
}
void acceptconn(clientPtr cli, const boost::system::error_code &err) {
if ( err)
return;
printf("accept ok \n");
cli->sock.async_receive(buffer(cli->recvBuffer),boost::bind(recvdata,cli,_1));
//accept
clientPtr newcli(new client(service));
acceptor.async_accept(newcli->sock,boost::bind(acceptconn,newcli,_1));
}
int main()
{
clientPtr cli(new client(service));
acceptor.async_accept(cli->sock,boost::bind(acceptconn,cli,_1));
service.run();
return 0;
}
半同步半异步模式
如果你希望项目性能更好,可以考虑用异步来处理IO,同步来处理逻辑。——异步线程效率高,实时性强。同步线程效率较低,但逻辑简单。
也就是说,我们可以使用多个线程向内核请求IO事件,读出事件后立刻放进工作队列,然后继续读。
代码实现就留给读者了。
实际项目中的IO代码是怎么样的
用上go也是好起来了,得益于go routine带来的高并发,已经不需要处理那么复杂的模式。
- 主go routine程循环读,进行反序列化后,根据包头的UID决定要交给哪个go routine处理。
- 工作go routine干活。
func decodePacket(ctx context.Context, data []byte) (pkt *pb.Packet, req framework.IProto, proc framework.THandleFunc, isRsp bool, err error) {
if len(data) <= 0 {
err = xerrors.ErrServerDecode()
return
}
pkt = &pb.Packet{}
e := pkt.Unmarshal(data)
if e != nil || pkt.Head == nil {
err = xerrors.ErrServerDecode()
return
}
t, ok := cmdMap[pkt.Head.Cmd]
if !ok {
isRsp = true
return
}
req = t.reqFunc()
e = req.Unmarshal(pkt.Body)
if e != nil {
err = xerrors.ErrServerDecode().SetBasicErr(e)
return
}
pkt, req, proc, isRsp, err := decodePacket(ctx, data)
if err != nil || pkt.Head == nil {
log.Errorf(ctx, "fail to decode packet, srvName:%s, src:%s, err:%s", srvName, src, err.Error())
return
}
rKey := pkt.Head.RKey
if rKey == "" {
rKey = pkt.Head.UID
}
err = jobQueue.PushJob(ctx, rKey, func(ctx2 context.Context) {
procNetPacket(ctx2, pkt, req, srvName, src, proc)
})
return
}
浙公网安备 33010602011771号