服务器程序重点处理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. The Communication Complexity of Threshold Private Set Intersection-2019:解读

    记录阅读论文的笔记. 什么是阈值PSI? Alice和Bob当两者的交集大小不小于\(n-t\)时(两者的不同元素的数量不大于阈值\(2t\)时),才会求交集. 所以阈值PSI会分为两步: 1.检测 ...

  2. Fractal pg walkthrough Easy

    nmap ┌──(root㉿kali)-[~] └─# nmap -p- -A 192.168.157.233 Starting Nmap 7.94SVN ( https://nmap.org ) a ...

  3. mysl 修改数据存储位置后服务启动后停止

    在 Windows 系统中安装完 mysql 后,如果是生产用的机器,通常会修改数据存储位置.基本步骤: 1. 停止 mysql 服务: 2. 修改 my.ini 文件中的 datadir=" ...

  4. h5移动端像素适配 postcss-pxtorem和amfe-flexible

    Vant 中的样式默认使用px作为单位,如果需要使用rem单位,推荐使用以下两个工具: postcss-pxtorem 是一款 postcss 插件,用于将单位转化为 rem; amfe-flexib ...

  5. 手把手教你在本地部署DeepSeek R1,搭建web-ui ,建议收藏!

    写在前面 最近,DeepSeek 发布的推理大模型 DeepSeek - R1 ,可以说是AI大模型领域杀出的一匹黑马.它在国外大模型排名 Arena 上成绩惊人,基准测试位列全类别大模型第三,在风格 ...

  6. 从SSH远程到Git Push:在Windows上一步到位实现免密码登录

    前言 我一直希望在Windows上能像在Linux系统中那样,通过SSH密钥实现免密码远程连接.每次远程连接到服务器时,手动输入密码既麻烦又不太安全,尤其是在我需要频繁操作的情况下. 之前的文章中已经 ...

  7. datawhale-leetcode打卡:001-012题

    这次这十二个题目属于是极限肝出来的,有两个参考了一下题解,还是很有意思.我会按照我个人的感觉去写这个东西. 螺旋矩阵(leetcode 054) 这个题目比较恶心的就是跑圈的过程怎么描述.首先,顺时针 ...

  8. css快速入门系列 —— 移动开发闲谈

    移动开发闲谈(Flex和css 库) 背景 目前在做移动小程序开发,效果必须和设计稿一模一样,一个像素都不能有差异. 虽然公司也提供了图生文的工具,但是有时生成的代码可读性不太好,二次修改也比较费劲, ...

  9. salesforce零基础学习(一百四十三)零碎知识点小总结(十一)

    本篇参考: https://help.salesforce.com/s/articleView?id=release-notes.rn_lab_dynamic_highlights_panel.htm ...

  10. Golang 实现本地持久化缓存

    // Copyright (c) 2024 LiuShuKu // Project Name : balance // Author : liushuku@yeah.net package cache ...