RPC框架的网络线程模型

2023-11-03

一、RPC的网络IO模型

1、连接独占线程或进程: 在这个模型中,线程/进程处理来自绑定连接的消息,在连接断开前不退也不做其他事情。当连接数逐渐增多时,线程/进程占用的资源和上下文切换成本会越来越大,性能很差,这就是C10K问题的来源。这种方法常见于早期的web server,现在很少使用。

2、单线程reactor: 以libevent, libev等event-loop库为典型。这个模型一般由一个event dispatcher等待各类事件,待事件发生后原地调用对应的event handler,全部调用完后等待更多事件,故为"loop"。这个模型的实质是把多段逻辑按事件触发顺序交织在一个系统线程中。一个event-loop只能使用一个核,故此类程序要么是IO-bound,要么是每个handler有确定的较短的运行时间(比如http server),否则一个耗时漫长的回调就会卡住整个程序,产生高延时。在实践中这类程序不适合多开发者参与,一个人写了阻塞代码可能就会拖慢其他代码的响应。由于event handler不会同时运行,不太会产生复杂的race condition,一些代码不需要锁。此类程序主要靠部署更多进程增加扩展性。单线程reactor的运行方式及问题如下图所示:
在这里插入图片描述

3、N:1线程库: 又称为Fiber,以GNU Pth, StateThreads等为典型,一般是把N个用户线程映射入一个系统线程。同时只运行一个用户线程,调用阻塞函数时才会切换至其他用户线程。N:1线程库与单线程reactor在能力上等价,但事件回调被替换为了上下文(栈,寄存器,signals),运行回调变成了跳转至上下文。和event loop库一样,单个N:1线程库无法充分发挥多核性能,只适合一些特定的程序。只有一个系统线程对CPU cache较为友好,加上舍弃对signal mask的支持的话,用户线程间的上下文切换可以很快(100~200ns)。N:1线程库的性能一般和event loop库差不多,扩展性也主要靠多进程。

4、多线程reactor: 以boost::asio为典型。一般由一个或多个线程分别运行event dispatcher,待事件发生后把event handler交给一个worker线程执行。 这个模型是单线程reactor的自然扩展,可以利用多核。由于共用地址空间使得线程间交互变得廉价,worker thread间一般会更及时地均衡负载,而多进程一般依赖更前端的服务来分割流量,一个设计良好的多线程reactor程序往往能比同一台机器上的多个单线程reactor进程更均匀地使用不同核心。不过由于cache一致性的限制,多线程reactor并不能获得线性于核心数的性能,在特定的场景中,粗糙的多线程reactor实现跑在24核上甚至没有精致的单线程reactor实现跑在1个核上快。由于多线程reactor包含多个worker线程,单个event handler阻塞未必会延缓其他handler,所以event handler未必得非阻塞,除非所有的worker线程都被阻塞才会影响到整体进展。事实上,大部分RPC框架都使用了这个模型,且回调中常有阻塞部分,比如同步等待访问下游的RPC返回。

多线程reactor的运行方式及问题如下:

img

5、M:N线程库: 即把M个用户线程映射入N个系统线程。M:N线程库可以决定一段代码何时开始在哪运行,并何时结束,相比多线程reactor在调度上具备更多的灵活度。但实现全功能的M:N线程库是困难的,它一直是个活跃的研究话题。我们这里说的M:N线程库特别针对编写网络服务,在这一前提下一些需求可以简化,比如没有时间片抢占,没有(完备的)优先级等。M:N线程库可以在用户态也可以在内核中实现,用户态的实现以新语言为主,比如GHC threads和goroutine,这些语言可以围绕线程库设计全新的关键字并拦截所有相关的API。而在现有语言中的实现往往得修改内核,比如Windows UMS和google SwitchTo(虽然是1:1,但基于它可以实现M:N的效果)。相比N:1线程库,M:N线程库在使用上更类似于系统线程,需要用锁或消息传递保证代码的线程安全。

多核扩展性

  • 理论上代码都写成事件驱动型能最大化reactor模型的能力,但实际由于编码难度和可维护性,用户的使用方式大都是混合的:回调中往往会发起同步操作,阻塞住worker线程使其无法处理其他请求。一个请求往往要经过几十个服务,线程把大量时间花在了等待下游请求上,用户得开几百个线程以维持足够的吞吐,这造成了高强度的调度开销,并降低了TLS相关代码的效率。任务的分发大都是使用全局mutex + condition保护的队列,当所有线程都在争抢时,效率显然好不到哪去。更好的办法也许是使用更多的任务队列,并调整调度算法以减少全局竞争。比如每个系统线程有独立的runqueue,由一个或多个scheduler把用户线程分发到不同的runqueue,每个系统线程优先运行自己runqueue中的用户线程,然后再考虑其他线程的runqueue。这当然更复杂,但比全局mutex + condition有更好的扩展性。这种结构也更容易支持NUMA。

  • 当event dispatcher把任务递给worker线程时,用户逻辑很可能从一个核心跳到另一个核心,并等待相应的cacheline同步过来,并不很快。如果worker的逻辑能直接运行于event dispatcher所在的核心上就好了,因为大部分时候尽快运行worker的优先级高于获取新事件。类似的是收到response后最好在当前核心唤醒正在同步等待RPC的线程。

异步编程

  • 异步编程中的流程控制对于专家也充满了陷阱。任何挂起操作,如sleep一会儿或等待某事完成,都意味着用户需要显式地保存状态,并在回调函数中恢复状态。异步代码往往得写成状态机的形式。当挂起较少时,这有点麻烦,但还是可把握的。问题在于一旦挂起发生在条件判断、循环、子函数中,写出这样的状态机并能被很多人理解和维护,几乎是不可能的,而这在分布式系统中又很常见,因为一个节点往往要与多个节点同时交互。另外如果唤醒可由多种事件触发(比如fd有数据或超时了),挂起和恢复的过程容易出现race condition,对多线程编码能力要求很高。语法糖(比如lambda)可以让编码不那么“麻烦”,但无法降低难度。

  • 共享指针在异步编程中很普遍,这看似方便,但也使内存的ownership变得难以捉摸,如果内存泄漏了,很难定位哪里没有释放;如果segment fault了,也不知道哪里多释放了一下。大量使用引用计数的用户代码很难控制代码质量,容易长期在内存问题上耗费时间。如果引用计数还需要手动维护,保持质量就更难了,维护者也不会愿意改进。没有上下文会使得RAII无法充分发挥作用, 有时需要在callback之外lock,callback之内unlock,实践中很容易出错。


二、RPC的线程池模型(网络和业务)

1、线程池的作用: 线程使应用能够更加充分合理的协调利用cpu 、内存、网络、i/o等系统资源。线程的创建需要开辟虚拟机栈,本地方法栈、程序计数器等线程私有的内存空间。在线程的销毁时需要回收这些系统资源。频繁的创建和销毁线程会浪费大量的系统资源,增加并发编程的风险。另外,在服务器负载过大的时候,如何让新的线程等待或者友好的拒绝服务?这些丢失线程自身无法解决的。所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务。线程池的作用包括:

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,监控和调优。利用线程池管理并复用线程、控制最大并发数等。

2、线程池大小的确定: 线程池大小确定有一个简单且使用面比较广的公式:

  • CPU密集型任务(N+1): 这种任务消耗的主要是CPU资源,可以将线程数设置为N(CPU核心数)+1,比CPU核心数多出来一个线程是为了防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的影响。一旦任务停止,CPU就会出于空闲状态,而这种情况下多出来一个线程就可以充分利用CPU的空闲时间。
  • I/O密集型(2N): 这种任务应用起来,系统大部分时间用来处理I/O交互,而线程在处理I/O的是时间段内不会占用CPU来处理,这时就可以将CPU交出给其他线程使用。因此在I/O密集型任务的应用中,可以配置多一些线程,具体计算方是2N。

3、网络线程池:tars网络模型多线程reactor:

网络IO处理

  • 避免惊群(新版Linux内核已优化惊群),tars网络线程中只有第一个网络线程(NetThread)持有监听套接字。虽然所有的连接请求都是通过负责监听的网络线程接入,但新建立的请求连接可以通过TC_EpollServer统筹分配给其他的网络线程进行管理。初始化时,监听socket注册进相应网络线程的epoll结构中,监听事件为EPOLLIN,data类型为ET_LISTEN(有数据可读也是EPOLLIN事件,用data区分是何种类型的EPOLLIN消息)。

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:

    EPOLL_CTL_ADD:注册新的fd到epfd中;

    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

    EPOLL_CTL_DEL:从epfd中删除一个fd;

  struct epoll_event {
     __uint32_t events; /* Epoll events */
     epoll_data_t data; /* User data variable */
  };
  typedef union epoll_data {
     void *ptr;
     int fd;
     __uint32_t u32;
     __uint64_t u64;
  } epoll_data_t;
  • NetThread网络线程主循环中的不断阻塞在epoll_wait监听返回的各类事件。当有ET_LISTEN消息时,调用网络线程的accept方法接受连接请求,accept方法内获取新请求连接的fd、ip、port等信息,构建一个connection结构,并把connection分配给工作线程。后面的所有连接相关的读写关闭等操作均通过该结构来完成。

    void TC_EpollServer::NetThread::run()
    {
        //循环监听网路连接请求
        while(!_bTerminate)
        {
            _list.checkTimeout(TNOW);
    
            int iEvNum = _epoller.wait(2000);
    
            for(int i = 0; i < iEvNum; ++i)
            {
                try
                {
                    const epoll_event &ev = _epoller.get(i);
    
                    uint32_t h = ev.data.u64 >> 32;
    
                    switch(h)
                    {
                    case ET_LISTEN:
                        {
                            //监听端口有请求
                            auto it = _listeners.find(ev.data.u32);
                            if( it != _listeners.end())
                            {
                                if(ev.events & EPOLLIN)
                                {
                                    bool ret;
                                    do
                                    {
                                        ret = accept(ev.data.u32, it->second->_ep.isIPv6() ? AF_INET6 : AF_INET);
                                    }while(ret);
                                }
                            }
                        }
                        break;
                    case ET_CLOSE:
                        //关闭请求
                        break;
                    case ET_NOTIFY:
                        //发送通知
                        processPipe();
                        break;
                    case ET_NET:
                        //网络请求
                        processNet(ev);
                        break;
                    default:
                        assert(true);
                    }
                }
                catch(exception &ex)
                {
                    error("run exception:" + string(ex.what()));
                }
            }
        }
    }
    
  • 新建的connection回传给TC_EpollServer结构进行分配(TC_EpollServer是tarsServer部分的统筹管理者,管理Server持有的资源,包括网络线程、处理线程等等)。TC_EpollServer通过简单的负载均衡(连接的fd值对网络线程取模)获取一个网络线程,将connection指派给该线程进行管理。被指派的网络线程将新的conneciton加入自己的connectionlist中进行维护 (一个连接链表,包括超时踢除等等逻辑, 每个网络线程都有一个独立的链接列表ConnectionList,负责管理该网络线程的所有Connection,ConnectionList继承于TC_ThreadLock,是线程安全的),同时将这个连接fd注册进网络线程的epoll中监听EPOLLIN和EPOLLOUT事件(epoll的data是连接的uid)。EPOLLIN和EPOLLOUT分别表征该连接fd有数据写入可读及可写。

//监听主线程通过简单的负载均衡(连接的fd值对网络线程取模)获取一个网络线程

 NetThread* getNetThreadOfFd(int fd)
 {
    return _netThreads[fd % _netThreads.size()];
 }


//TC_EpollServer通过简单的负载均衡(连接的fd值对网络线程取模)获取一个网络线程,将connection指派给该线程进行管理,每个线程都有一个connectList列表对链接进行管理
void TC_EpollServer::addConnection(TC_EpollServer::NetThread::Connection * cPtr, int fd, int iType)
{
    TC_EpollServer::NetThread* netThread = getNetThreadOfFd(fd);

    if(iType == 0)
    {
        netThread->addTcpConnection(cPtr);
    }
    else
    {
        netThread->addUdpConnection(cPtr);
    }
}

//将这个连接fd注册进网络线程的epoll中监听EPOLLIN和EPOLLOUT事件
void TC_EpollServer::NetThread::addTcpConnection(TC_EpollServer::NetThread::Connection *cPtr)
{
    uint32_t uid = _list.getUniqId();

    cPtr->init(uid);

    _list.add(cPtr, cPtr->getTimeout() + TNOW);

    cPtr->getBindAdapter()->increaseNowConnection();

...
    //注意epoll add必须放在最后, 否则可能导致执行完, 才调用上面语句
    _epoller.add(cPtr->getfd(), cPtr->getId(), EPOLLIN | EPOLLOUT);
}

4、当网络线程netthread解析出来一个完整的请求包时,会调用netthread内的适配器BindAdapter变量进行任务插入,即将任务插入到线程安全的队列中typedef TC_ThreadQueue<tagRecvData*, deque<tagRecvData*> > recv_queue;

void TC_EpollServer::NetThread::Connection::insertRecvQueue(recv_queue::queue_type &vRecvData)
{
    if(!vRecvData.empty())
    {
        int iRet = _pBindAdapter->isOverloadorDiscard();

        if(iRet == 0)//未过载
        {
            _pBindAdapter->insertRecvQueue(vRecvData);
        }
        else if(iRet == -1)//超过接受队列长度的一半,需要进行overload处理
        {
            recv_queue::queue_type::iterator it = vRecvData.begin();

            recv_queue::queue_type::iterator itEnd = vRecvData.end();

            while(it != itEnd)
            {
                (*it)->isOverload = true;

                ++it;
            }

            _pBindAdapter->insertRecvQueue(vRecvData,false);
        }
        else//接受队列满,需要丢弃
        {
            recv_queue::queue_type::iterator it = vRecvData.begin();

            recv_queue::queue_type::iterator itEnd = vRecvData.end();

            while(it != itEnd)
            {
                delete (*it);
                ++it;
            }
        }
    }
}

//每个bindadapter关联了一个线程安全的接收队列
//BindAdapter的队列插入操作。
void TC_EpollServer::BindAdapter::insertRecvQueue(const recv_queue::queue_type &vtRecvData, bool bPushBack)
{
    {
        if (bPushBack)
        {
            _rbuffer.push_back(vtRecvData);
        }
        else
        {
            _rbuffer.push_front(vtRecvData);
        }
    }

    TC_ThreadLock::Lock lock(_handleGroup->monitor);

    _handleGroup->monitor.notify();
}

5、具体servantHandle业务线程:每个bindadpater关联一个handlegroup,每个handlegroup中包含了多个handle线程。class Handle : public TC_Thread每个handle继承了tc_thread可以理解为为一个单独的工作线程。handle线程中调用servantHandle的处理逻辑,即先根据tars协议解包,并调用servant的dispatch函数进行业务的分发处理。

//一个BindAdapter唯一对应一个Servant,也唯一对应一个HandleGroup成员变量,和一个接收队列成员变量
//一个handlegroup 有多个handle,一个handlegroup可以对应多个bindadpter.
//但是一个HandleGroup可以同时为多个BindAdapter服务。BindAdapter唯一对应Server的一个Obj,也唯一对应一个EndPoint(Ip-Port)。
struct HandleGroup : public TC_HandleBase
 {
        string                      name;
        TC_ThreadLock               monitor;
        vector<HandlePtr>           handles;
        map<string, BindAdapterPtr> adapters;
 };

//bindadapter关联的队列任务弹出操作。
bool TC_EpollServer::BindAdapter::waitForRecvQueue(tagRecvData* &recv, uint32_t iWaitTime)
{
    bool bRet = false;

    bRet = _rbuffer.pop_front(recv, iWaitTime);

    if(!bRet)
    {
        return bRet;
    }

    return bRet;
}
//一个handle中的处理逻辑
void TC_EpollServer::Handle::handleImp()
{
    startHandle();

    while (!getEpollServer()->isTerminate())
    {
        ...
        //上报心跳
        heartbeat();

        //为了实现所有主逻辑的单线程化,在每次循环中给业务处理自有消息的机会
        handleAsyncResponse();
        handleCustomMessage(true);

        tagRecvData* recv = NULL;

        map<string, BindAdapterPtr>& adapters = _handleGroup->adapters;

        for (auto& kv : adapters)
        {
            BindAdapterPtr& adapter = kv.second;

            try
            {

                while (adapter->waitForRecvQueue(recv, 0))
                {
                    //上报心跳
                    heartbeat();

                    //为了实现所有主逻辑的单线程化,在每次循环中给业务处理自有消息的机会
                    handleAsyncResponse();

                    tagRecvData& stRecvData = *recv;

                    int64_t now = TNOWMS;


                    stRecvData.adapter = adapter;

                    //数据已超载 overload
                    if (stRecvData.isOverload)
                    {
                        handleOverload(stRecvData);
                    }
                    //关闭连接的通知消息
                    else if (stRecvData.isClosed)
                    {
                        handleClose(stRecvData);
                    }
                    //数据在队列中已经超时了
                    else if ( (now - stRecvData.recvTimeStamp) > (int64_t)adapter->getQueueTimeout())
                    {
                        handleTimeout(stRecvData);
                    }
                    else
                    {
                        handle(stRecvData);
                    }
                    handleCustomMessage(false);
                    delete recv;
                    recv = NULL;
                }
            }
          ...
    }

    stopHandle();
}

//handle中调用servanthandle的处理逻辑
void ServantHandle::handleTarsProtocol(const TarsCurrentPtr &current)
{
  ...
    map<string, ServantPtr>::iterator sit = _servants.find(current->getServantName());

    if (sit == _servants.end())
    {
        current->sendResponse(TARSSERVERNOSERVANTERR);
#ifdef _USE_OPENTRACKING
        finishTracking(TARSSERVERNOSERVANTERR, current);
#endif
        return;
    }

    int ret = TARSSERVERUNKNOWNERR;

    string sResultDesc = "";

    vector<char> buffer;

    try
    {
        //业务逻辑处理
        ret = sit->second->dispatch(current, buffer);
    }
    ...
    //单向调用或者业务不需要同步返回
    if (current->isResponse())
    {
        current->sendResponse(ret, buffer, TarsCurrent::TARS_STATUS(), sResultDesc);
    }
#ifdef _USE_OPENTRACKING
    finishTracking(ret, current);
#endif
}

6、业务线程池: 根据配置好的线程数目提前创建好一定数目的工作线程数,每个线程都在一个线程安全队列中获取执行任务,进行处理。

void TC_ThreadPool::ThreadWorker::run()
{
    //调用处理部分
    while (!_bTerminate)
    {
        auto pfw = _tpool->get(this);
        if(pfw)
        {
            try
            {
                pfw();
            }
            catch ( ... )
            {
            }

            _tpool->idle(this);
        }
    }

    //结束
    _tpool->exit();
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

RPC框架的网络线程模型 的相关文章

  • oracle PL/SQL小结

    PL SQL 代码块 DECLARE optional BEGIN required EXCEPTION optional END required 若使用dbms output输出时 先要设置 set serveroutput on 显示
  • SQL-labs的第27a关——union和select被屏蔽 延时盲注(Get)

    注意 该关无法返回错误 所以不适合报错注入 一 判断闭合方式 输入语句 id 1 26 26 1 2 00 返回页面如下 输入语句 id 1 26 26 1 1 00 返回页面如下 将双引号作为闭合方式 各个语句反应正常 可以确定双引号就是
  • APNS推送通知的流程

    http www cnblogs com chen1987lei archive 2011 05 09 2041090 html 1 将app注册notification里面 并从APNS上获取测试机的deviceToken BOOL ap
  • 开心档-开发入门网之Git基本操作

    Git 基本操作 Git 的工作就是创建和保存你项目的快照及与之后的快照进行对比 本章将对有关创建与提交你的项目快照的命令作介绍 Git 常用的是以下 6 个命令 git clone git push git add git commit
  • yum install iptables #CentOS系统 apt-get install iptables #Debian系统

    yum install iptables CentOS系统 apt get install iptables Ubuntu系统
  • java并发总结

    一 并发基础 1 进程与线程 进程 程序由指令和数据组成 但这些指令要运行 数据要读写 就必须将指令加载至 CPU 数据加载至内存 在指令运行过程中还需要用到磁盘 网络等设备 进程就是用来加载指令 管理内存 管理 IO 的 当一个程序被运行

随机推荐

  • SpringBoot-获取上下文

    SpringBoot 获取上下文 1 创建上下文工具类SpringContextUtil 如下为简单的上下文工具类 可以根据自己的需要添加上下文相关的管理方法 package com supre springboot import org
  • kubeadm部署的k8s1.20版本get cs报错

    报错内容如下 root k8s master1 kubectl get cs Warning v1 ComponentStatus is deprecated in v1 19 NAME STATUS MESSAGE ERROR sched
  • 遗传算法详解及matlab代码实现

    这里写目录标题 1 定义 主要特点 对象 基本操作 核心内容 2 常用词汇 基因型 genotype 表现型 编码 coding 解码 decoding 个体 individual 种群 population 适应度 fitness 3 形
  • 抓取中国银行汇率函数

    抓取中国银行汇率表数据 string file source 要抓取的内容页 string file target 本机生成的文件 function getRate file source file target if file sourc
  • NGINX引入线程池 性能提升9倍

    NGINX引入线程池 性能提升9倍 喜欢 作者 Valentin Bartenev 译者 韩陆 发布于 2015年6月23日 估计阅读时间 6分钟 智能化运维 Serverless DevOps 2017年有哪些最新运维技术趋势 CNUTC
  • 单链表的基本操作实现

    一 实验目的 巩固线性表的数据结构的存储方法和相关操作 学会针对具体应用 使用线性表的相关知识来解决具体问题 二 实验内容 1 建立一个由n个学生成绩的顺序表 n的大小由自己确定 每一个学生的成绩信息由自己确定 实现数据的对表进行插入 删除
  • 有没有python时间序列的教程推荐?手把手教你使用Python绘制时间序列图!

    前言 那么让我来详细讲解 手把手教你使用Python绘制时间序列图 的完整攻略 介绍 时间序列图是一种用于展示随时间变化的数据的图表 可以帮助我们从数据中识别出时间上的模式和趋势变化 Python作为一种强大的数据分析工具 当然也可以用来绘
  • 倍增RMG

    include
  • 实现数据字典的缓存、加载、刷新和映射的集成框架

    前言 在业务开发的过程中 总是会遇到字典打交道 比如说 性别 类型 状态等字段 都可以归纳为字典的范围 字典的组成分成 字典类型 字典数据 其中 字典数据 归属于一类的 字典类型 可以通过 字典类型 获取 字典数据 例如开头提到的 性别就为
  • 蓝桥杯python基础练习报时助手

    这道题比较简单我们可以直接用字典和if语句来完成 按照题目意思创建一个字典1 20和30 40 50 因为创建全部的字典太麻烦 我们可以将不存在字典的建转化为字典中的建 第二步可以运用if语句进行判断 m 0时直接 输出即可 m h gt
  • centos mail报错:550 Mailbox not found or access denied

    运行了几年的发邮件脚本 最近体发邮件都发生了报错 无法发出 smtp server 550 Mailbox not found or access denied 报错信息提示邮箱找不到 但是接收人邮箱确定没有错误 因为一直正常运行 网上说5
  • 【Unity XR】Unity开发OpenXR

    Unity开发OpenXR 介绍OpenXR相关依赖插件 OpenXR OpenXR Plugin XR Interaction Toolkit XR Plugin Management 安装OpenXR相关依赖插件 Package Man
  • qt MVD 框架入门教学归纳实例:QListView + QAbstractItemModel + QStyledItemDelegate 实现自定义进度条(同时显示文件名 + 实时跟新进度)

    前置理论基础 关于QT的MVD框架这里就不做赘述 通篇介绍的话得占不少版面 其实作为qt开发者 基本上只要有个能跑起来的demo 相关的技术点不难理解 新手学习mvd的难点在于没有一个小型的 直观的demo能直接梳理出三者的关系 关于MVD
  • Ubuntu显示美化 优化 常用插件

    本文不再更新 已迁移到MD文档 参见 Ubuntu显示美化 优化 常用插件 神奏的博客 CSDN博客 1 安装 Extension Manager ubuntu snap商店或者deb商店打开 搜索 Extension Manager 安装
  • vue实现穿梭框功能

  • 路由器的几种模式

    1 AP 无线热点模式 路由器的WAN口接入网线 在其他设备通过路由器的无线连接上网 2 Client 客户端的模式 将无线路由器作为无线网卡来使用 通过无线的方式连接到其他路由上 然后设备通过网线连接上路由器 3 Router 无线路由模
  • Linux的cat命令

    1 cat 显示文件连接文件内容的工具 cat 是一个文本文件查看和连接工具 查看一个文件的内容 用cat比较简单 就是cat 后面直接接文件名 比如 de gt root localhost cat etc fstab de gt 为了便
  • Linux下SVN的安装及SVN常用命令

    SVN的介绍 SVN是一个开源的版本控制系統 svn版本管理工具管理随时间改变的各种数据 这些数据放置在一个中央资料档案库 repository 中 这个档案库很像一个普通的文件服务器 它能记住你每次的修改 查看所有的修改记录 恢复到任何历
  • vue前端项目的结构以及组成部分

    本文结构 在初入前端的时候 一个项目中的文件夹会非常多 与Vue官网的简单demo相差非常大 这也是对前端项目文件组成和几个大的模块的一些介绍 文末还有一些不成文的代码规范 本文目录 项目代码组成 前端项目组成 前端的几大模块 项目编写规范
  • RPC框架的网络线程模型

    一 RPC的网络IO模型 1 连接独占线程或进程 在这个模型中 线程 进程处理来自绑定连接的消息 在连接断开前不退也不做其他事情 当连接数逐渐增多时 线程 进程占用的资源和上下文切换成本会越来越大 性能很差 这就是C10K问题的来源 这种方