服务器程序重点处理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带来的高并发,已经不需要处理那么复杂的模式。

  1. 主go routine程循环读,进行反序列化后,根据包头的UID决定要交给哪个go routine处理。
  2. 工作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
}

网络开发中的Reactor(反应堆模式)和Proacrot(异步模式)的更多相关文章

  1. Android开发中常见的设计模式(四)——策略模式

    策略模式定义了一些列的算法,并将每一个算法封装起来,而且使它们还可以相互替换.策略模式让算法独立于使用它的客户而独立变换. 假设我们要出去旅游,而去旅游出行的方式有很多,有步行,有坐火车,有坐飞机等等 ...

  2. Android开发中常见的设计模式(二)——Builder模式

    了解了单例模式,接下来介绍另一个常见的模式--Builder模式. 那么什么是Builder模式呢.通过搜索,会发现大部分网上的定义都是 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建 ...

  3. MVP模式在Android开发中的应用

    一.MVP介绍      随着UI创建技术的功能日益增强,UI层也履行着越来越多的职责.为了更好地细分视图(View)与模型(Model)的功能,让View专注于处理数据的可视化以及与用户的交互.同一 ...

  4. 【WebApi系列】浅谈HTTP在WebApi开发中的运用

    WebApi系列文章 [01]浅谈HTTP在WebApi开发中的运用 [02]聊聊WebApi体系结构 [03]详解WebApi参数的传递 [04]详解WebApi测试和PostMan [05]浅谈W ...

  5. iOS开发系列--网络开发

    概览 大部分应用程序都或多或少会牵扯到网络开发,例如说新浪微博.微信等,这些应用本身可能采用iOS开发,但是所有的数据支撑都是基于后台网络服务器的.如今,网络编程越来越普遍,孤立的应用通常是没有生命力 ...

  6. Unity3d Android Http 开发中的坑(吐槽

    在一般的U3D网络开发中,直接使用WWW类便足够正常使用,但我在发现使用WWW下载大文件时,会导致整个程序卡顿的情况(不清楚是否我个人电脑问题),所以干脆使用HttpWebRequest/HttpWe ...

  7. IOS网络开发概述

    概览 大部分应用程序都或多或少会牵扯到网络开发,例如说新浪微博.微信等,这些应用本身可能采用iOS开发,但是所有的数据支撑都是基于后台网络服务器的.如今,网络编程越来越普遍,孤立的应用通常是没有生命力 ...

  8. 结构型模式(Structural patterns)->外观模式(Facade Pattern)

    动机(Motivate): 在软件开发系统中,客户程序经常会与复杂系统的内部子系统之间产生耦合,而导致客户程序随着子系统的变化而变化.那么如何简化客户程序与子系统之间的交互接口?如何将复杂系统的内部子 ...

  9. NET设计模式 第二部分 结构性模式(11):外观模式(Façade Pattern)

    外观模式(Façade Pattern) ——.NET设计模式系列之十二 Terrylee,2006年3月 概述 在软件开发系统中,客户程序经常会与复杂系统的内部子系统之间产生耦合,而导致客户程序随着 ...

  10. MySQL主从复制之异步模式

    MySQL主从复制有异步模式.半同步模式.GTID模式以及多源复制模式,MySQL默认模式是异步模式.所谓异步模式,只MySQL 主服务器上I/O thread 线程将二进制日志写入binlog文件之 ...

随机推荐

  1. a instanceof A:判断对象a是否是类A的实例。如果是,返回true;如果不是,返回false

  2. MySQL查看、修改字符集及Collation

    前言在使用MySQL的过程中,可能会出现初始设计使用的字符集或Collation不符合当前需求的情况.如使用utf8的表(MySQL中的utf8即utf8mb3)要支持emoji,而utf8mb3不支 ...

  3. 还堵在高速路上吗?带你进入Scratch世界带你飞

    国庆假期高速路的风景 国庆假期正式启动人从众模式,无论是高速公路还是景区,不管是去程还是回程,每一次都堪称经典. 一些网友在经历漫长的拥堵后 哭笑不得地表示 "假期都在堵车中度过了" ...

  4. cJSON解析器总结[转载]

    一. 简介 cJson 是c语言编写的一个解析器. 是一个超轻巧,携带方便,单文件,简单的可以作为ANSI-C标准的JSON解析器.主要两个文件cJSON.c 和cJSON.h . 主要用来编码和解析 ...

  5. 从android中删除短信

    代码如下: getContentResolver().delete(Uri.parse("content://sms/#"),"address=?", new ...

  6. Deepseek学习随笔(1)--- 初识 DeepSeek

    什么是 DeepSeek? DeepSeek 是一款基于人工智能的对话工具,旨在帮助用户高效完成各种任务,包括文本生成.代码编写.数据分析等.通过自然语言处理技术,DeepSeek 能够理解用户的输入 ...

  7. 给大模型添加联网功能的免费方案,以langchain为例

    langchain介绍 LangChain 是一个用于开发由大型语言模型 (LLM) 驱动的应用程序的框架. 简单来说,它可以帮助你更轻松地构建利用 LLM(例如 OpenAI 的 GPT 模型.Go ...

  8. 深入理解 Docker 容器技术

    一.引言 在当今的云计算和软件开发领域,Docker 容器技术已经成为了一项不可或缺的工具.它极大地改变了应用程序的部署和运行方式,为开发者和运维人员带来了诸多便利. 二.Docker 容器是什么? ...

  9. SSM:Spring整合Mybatis时,连接池和SQLSessionFactory的联系!

  10. vue生命周期调用

    <template> <div> <!-- 用户页的面包屑导航 --> <nav aria-label="breadcrumb"> ...