网络与多线程的设计模式

一、概述

本文是一篇关于TCP网络服务端的常用设计模式的笔记,方便自己和已有一定的网络及线程基础知识的人查阅。

二、方式介绍

1. 同步阻塞网络模式:

基本为以下函数的顺序执行:

int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
ssize_t read(int fd, void *buf, size_t count);
doSomething(const char *buf);
ssize_t write(int fd, const void *buf, size_t count);
int close(int fd);
int close(int sockfd);

        上面从accept到close(fd)的步骤可以放入循环体中进行,以实现多客户端处理。
        这种方式的缺点是显然的:
1)accept可能被阻塞,程序不能执行其他任何操作。

2)read可能被阻塞,使得另外的客户端来尝试连接时,都会阻塞直至失败。

3)同上,write也可能会出现阻塞。
2.I/O多路复用
        以上的问题,源自于调用都是同步阻塞的调用,为了克服这个问题,于是操作系统帮我们设计了编写非阻塞网络服务的工具select/poll.
假设我们使用select的方式实现,代码大概如下:

int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
fd_set readset;

FD_ZERO(&readset);        

FD_SET(sockfd, &readset); 

for(;;)

{

    int select(int nfds,  fd_set* readset,  fd_set* writeset,  fe_set* exceptset,  struct timeval* timeout);

    if(FD_ISSET(sockfd, &readset)

    {

        int clientfd = accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

                clientArray[].apend(clientfd);

                FD_SET(clientfd, &clientfd);

    }

    foreach(int fd, clientArray)

    {
        ssize_t read(int fd, void *buf, size_t count);
        doSomething(const char *buf);
        ssize_t write(int fd, const void *buf, size_t count);
         }
}
foreach(int fd, clientArray)
{
    int close(int fd);
}
int close(int sockfd);

        采用了select的实现后,程序不会因为accept/read/write而阻塞了,因为只有有新的连接,新的数据可读或者可写时,程序才会去调用这几个函数(要完成实现这一步,上面的write需要作些修改)。
        select/poll是怎么实现的呢?其实在你调用select的时候,它会把你给它的所有fd_set全部拷贝到内核层里去,然后不断地循环检查这些fd中,是否有状态的变化直至超时,然后将所有有变化的fd_set返回。
        可见,相对应用层而言,是轻松了,但内核却忙死了,select的缺点如下:
1)单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024)

2)内核、用户空间内存拷贝问题,select需要不断地复制大量的句柄数据结构,产生巨大的开销;

3)select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;

4)select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。
        poll做了些改进,使用链表来保存文件描述符,因此第一个问题不存在,但后面三个问题是一样的。
        为了解决这个问题,linux再进一步,设计了一个不用反复拷贝文件描述符的回调式接口——epoll,分为以下三个步骤:
1)int epoll_create(int size) ;
        打开一个虚拟的文件(linux下,一切皆文件嘛);
2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) ;
        将要监控的fd加入(op也可以为修改、删除等)刚打开的虚拟文件epfd中(其实是一个等待链表),告诉内核你要处理这些event。相应的,内核会向相应的IO驱动注册回调。
3)int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) ;
        然后你就等,直到fd代表的IO驱动收到新事件,回调内核的注册函数,将事件返回给你,或者超时。与select和poll不同的是,当有新的变化时,驱动调用这个注册函数后,会使得等待链表中的fd移动到另一个就绪链表中去,唤醒等待线程后,直接将所有的就绪链表数据拷贝给这个线程;而前两者,只是在链表里将对应的fd做个标志,使得被唤醒的线程还得一个个地找出已就绪的fd,当fd很多时,这显然是个比较耗时的操作。
        现在你只要不断的调用epoll_wait就好了,硬件一接收到新东西就会告诉应用层的你。你不用频繁的告诉内核你要监听啥事件,内核也不用拼命的帮你查询是否有新的事件,你甚至可以设为边沿触发(ET)方式,让内核对于变化的fd,只拷贝一次事件给你,直到你处理完了该fd的所有事件。大概的代码如下:

int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int epoll_create(int size) ;
struct epoll_event ev;

ev.data.fd=sockfd;

ev.events=EPOLLIN|EPOLLET;
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);

for(;;)

{

int nfds = epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

for(int i = 0; i < nfds; ++i)

{

if(events[n].data.fd == sockfd)

{

int clientfd = accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);    //将clientfd加入监听事件中

}

else

{

ssize_t read(int fd, void *buf, size_t count);

                doSomething(const char *buf);
                ssize_t write(int fd, const void *buf, size_t count);

}

}

foreach(int fd, clientArray)
{
    int close(int fd);
}
int close(int sockfd);


        此时,我们可以用很多可以用的开源库了,比如libevent。
3.多进程与多线程
        事情发展到这一步,好像算是完美了。但再回头仔细看看,万一在doSomething(const char *buf)里面阻塞了呢?
        于是,最终多进程、多线程不得不上场了。首先,必须明确的是,多进程与多线程是一把有时候不得不用的双刃剑,能给我们的代码带来效率的同时,也会增加代码的复杂库,这个复杂库包括编码和调试。
        进程相对于线程而言,内核上实现上多了一些需要维护的数据结构,其他大同小异,因此我们只讨论多线程的情况。
        一般多线程的使用模型有几种:
1)按需生成,即是在doSomething(const char *buf)函数里面,产生一个处理线程,并启动之;
2)线程池,即是在上面的for(;;)循环之前,先产生一堆线程保存到一个数据结构中,然后在doSomething(const char *buf)函数里面从该数据结构中申请一个线程用来处理接收到的数据;
        显然,2)比1)可以减少系统不断产生和释放线程的开销。
2.1)Leader Follower
        线程的引入,必然的带来数据同步的问题,比如我们假设上面的epoll例程为主线程,那么doSomething(const char *buf)函数里面使用子线程来处理数据的时候,主线程需要将数据通过锁+队列、共享内存、列表等等的方式同步告诉给子线程,这会增加开销,甚至可能导致我们前面辛辛苦苦设计的模型节省下来的时间全部消耗掉,于是又有人发明了Leader Follower方式。
        该方式在线程池的基础上,作了些改进。它将线程池中的线程规定为,一个时间,只有一个线程作为Leader进行epoll_wait操作,当这个Leader得到返回的事件后,它不着急于去处理这个事件,而是先将自己变成Follower,接着将线程池里的另外一个Follower线程变为Leader,使用其有机会进行epoll_wait操作(注意epoll_ctrl中设置EPOLLONESHOT),最后才去处理这个事件。
        这样,将接收和处理事件放在同一个线程里,就不存在上面的数据同步消耗了。
        典型的C++库有SPServer.
2.2) 锁+多队列
        当客户端的数据包是可以无序处理的时候,上述的处理是不错,但假如数据必须有序处理时,问题就来了,因为线程的调试我们是无法预知的,可能后收到数据的线程先被线程调用的情况也是有可能的,这时候,这设计就不能使用了,可考虑在线程池的基础上使用锁+多队列的。
        即是我们为每一个线程规定使用同一个消息队列,同时同一个fd的事件也使用同一个消息队列来保存,以确保单一个fd产生的所有事件一直被同一个线程进行处理,直到客户端断开连接。

    多线程本身就包含有很多的设计模式,后面有机会再写。

来自为知笔记(Wiz)

时间: 2024-10-03 20:16:29

网络与多线程的设计模式的相关文章

网络与多线程的设计例子

上一篇的<网络与多线程设计模式>讲的是一些设备上的纯理论的东西,本篇将介绍一个本人写的使用EPoll+TcpServer+多线程的开源库,已在https://bitbucket.org/johnson_he/epolltcpserver中供开源下载. 该库使用了Qt的工程方式进行封装(实在不想写configure),里面的log也使用了log4Qt的方式进行,如果不想使用Qtcreator的话,可以自行引用tcpserver文件夹里面的所有文件即可. 使用Qtcreator的好处是,里面的de

iOS开发网络篇—多线程断点下载

iOS开发网络篇—多线程断点下载 说明:本文介绍多线程断点下载.项目中使用了苹果自带的类,实现了同时开启多条线程下载一个较大的文件.因为实现过程较为复杂,所以下面贴出完整的代码. 实现思路:下载开始,创建一个和要下载的文件大小相同的文件(如果要下载的文件为100M,那么就在沙盒中创建一个100M的文件,然后计算每一段的下载量,开启多条线程下载各段的数据,分别写入对应的文件部分). 项目中用到的主要类如下: 完成的实现代码如下: 主控制器中的代码: 1 #import "YYViewControl

boost中asio网络库多线程并发处理实现,以及asio在多线程模型中线程的调度情况和线程安全。

1.实现多线程方法: 其实就是多个线程同时调用io_service::run for (int i = 0; i != m_nThreads; ++i)        {            boost::shared_ptr<boost::thread> pTh(new boost::thread(                boost::bind(&boost::asio::io_service::run,&m_ioService)));            m_l

联想高级Java研发面经+面试题:Spring+多线程+MySQL+设计模式

上个礼拜,之前的一个同事突然联系我说他去面了联想的JAVA开发工程师,想分享一下面试经历和面试题.我当时就拍板说,好啊! 然后就整理了一下,写了这篇文章:和大家分享一下这次面试经验和面试题. 薪资还可以啊,年薪40W+啊!多少人的梦想啊! 言归正传,和大家分享一下这次联想的面经和面试题: 联想面经: 第一轮:电话初面 第二轮:技术面谈 第三轮:高管复试 第四轮:HR最后确认 No.1:第一轮面试--电话初面 首先确认对联想的意向度(如果异地更会考虑对工作地点(北京)的意向度!联想很看重这个):其

网络与多线程---OC中多线程使用方法(一)

小编在此之前,通过一个小例子,简单的形容了一下进程与线程之间的关系,现在网络编程中的多线程说一下!!! *进程的基本概念 每一个进程都是一个应用程序,都有自己独立的内存空间,一般来说一个应用程序存在一个进程,但也有多个进程的情况:   同一个进程中的线程共享内存中内存和资源. *线程的基本概念 每一个程序都有一个主线程,程序启动时创建(调用main来启动).主线程的生命周期是和应用程序绑定的,程序退出(结束)时,主线程也就停止了.多线程技术表示,一个应用程序都多个线程,使用多线程能提供CPU的使

java多线程12设计模式

1.Single Threaded Execution Pattern(单线程运行模式) 2.Immutable Pattern(一成不变的模式) 3.Guarded Suspension Pattern(国防暂停模式) 4.Balking Pattern(止步模式,阻行模式) 5.Producer-Consumer Pattern(生产者-消费者模式) 6.Read-Write Lock Pattern(读-写锁模式) 7.Thread-Per-Message Pattern(每一个消息一个线

多线程的设计模式

并行设计模式属于设计优化的一部分,它是对一些常用的多线程结构的总结和抽象.与串行程序相比,并行结构的程序通常更为复杂.因此合理的使用并行模式在多线程开发中更具有意义,在这里主要介绍future.master-woeker和生产者-消费者模型. future模式有点类似于商品订单.比如在网购时,当看中某件商品时,就可以提交订单,当订单处理完成后,在家里等待商品送货上门即可.或者说更形象的我们发送Ajax请求的时候,页面是异步的进行后台处理,用户无需一直等待用户请求的结果,可以继续浏览或操作其他内容

多线程编程-设计模式之不可变对象模式

Immutable Object设计模式适用场景:1.被建模对象的状态变化不频繁:设置一个专门的线程用于被建模对象状态发生变化时创建新的不可变对象.而其他线程只是读取不可变对象的状态.此场景下一个小技巧就是Manipulator对不可变对象的引用使用volatile关键字进行修饰,既可以避免使用显示锁比如synchronize,又可以保证多线程间的内存可见性.2.同时对一组相关的数据进行写操作,因此需要保证原子性此场景下为了保证操作的原子性,通常的做法是使用显示锁,但若采用Immutable O

Git的使用及网络编程多线程多进程

Git的使用 1.打开CMD命令行,输入cd Desktop进入桌面 2.输入 mkdir + 'file name'创建文件,如果已有项目则输入 cd + file name进入文件,如果在Git上已有项目并且在已有项目进行修改,则输入cd clone + url 3.如果第一次使用,则先初始化,git init 4. git add + file name 把项目加入文件夹里面 5.git config -global 全局设置(第一次使用的时候设置) 6. git log 查看日志.git