常见的并发网络服务程序设计方案

方案一.accept+read/write模式

这种模式其实不是并发服务器,而是iterator服务器,因为它一次只能服务一个客户。同时,这种方案不适合长连接,倒是很适合daytime这种write-only短连接服务.以下是python代码展示的用方案一实现echo server的大致做法

import socket;

def handle(client_socket,client_address):
      while True:
           data=client_socket.recv(4096)
           if data
              data=client_socket.send(data)
           else
              print "disconnect",client_address
              client_socket.close()
              break

if __name__=="__main__":
      listen_address=("0.0.0.0",2007)
      server_socket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
      server_socket.bind(listen_address)
      server_socket.listen(5)

      while True:
           (client_socket,client_address)=server_socket.accept()
           print "got connection from",client_address
           handle(client_socket,client_address)

方案二.accept+fork模型

这是传统的Unix并发网络编程方案,也称之为child-per-client,俗称process-per-connection.这种方案适合并发连接数不是很大的情况,同时适合"计算响应的工作量远大于fork()的开销"这种情况,比如数据库服务器.适合长连接,不适合短连接。

#!/usr/bin/python

from SocketServer import BaseRequestHandler,TcpServer
from SocketServer import ForkingTcpServer,ThreadingTcpServer

class EchoHandler(BaseRequestHandler):
        def handle(self):
              print "got connection from ",self.client_address
              while True:
                      data=self.request.recv(4096)
                      if data:
                            sent=self.request.send(data)
                      else:
                          print "disconnect",self.client_address
                          self.request.close()
                          break

if __name__=="__main__":
       listen_address=("0.0.0.0",2007)
       server=ForkingTcpServer(listen_address,EchoHandler)
       server.server_forever()

ForkingTcpServer会对每个客户端连接新建一个子进程,在子进程中调用EchoHandler.handle(),从而同时服务多个客户端.

方案三.accept+thread模型

这是传统的Java网络编程方案thread-per-connection.这种方案的伸缩性受到线程数的限制,一两百个还行,几千个的话对操作系统的scheduler恐怕是个不小的负担。具体代码与方案二差不多,只不过将进程换成线程。

以上三种方案都是阻塞式网络编程,程序流程(thread of control)通常阻塞在read()上,等待数据到达。但是Tcp是个全双工协议,同时支持read()和write()操作,当一个线程/进程阻塞在read()上,但程序又想给这个TCP连接发送数据,那该怎么办?

接下来的方案是使用IO multiplexing,也就是select/poll/epoll/kqueue这一系列的"多路选择器",让一个thread of control能处理多个连接。"IO 复用"其实复用的不是IO连接,而是复用线程。使用select/poll几乎肯定要配合non-blocking IO,而使用non-blocking IO肯定要使用应用层buffer. 这就是Reactor模式,让event-driven网络编程有章可循。

Reactor模式的主要思想:网络编程中有很多事务性工作,可以提取为公用的框架或库,而用户只需要填上关键的业务逻辑代码,并将回调注册到框架中,就可以实现完整的网络服务。

方案四.单线程reactor模式

单线程Reactor的流程:在没有事件的时候,线程阻塞等待在select/poll/epoll_wait上,事件到达后由网络库处理IO,再把消息通知回调客户端代码。Reactor事件循环的所在线程通常叫做IO线程。通常由网络库负责读写socket,用户代码负责解码、计算、编码。

注意由于只有一个线程,因此事件是顺序处理的,一个线程同时只能做一件事情。如果我们想要延迟计算,那么也不能用sleep()之类的阻塞调用,而应该注册超时回调,以避免阻塞当前IO线程.这种方式适合IO密集的应用,不太适合CPU密集的应用,因为较难发挥多核的威力。

server_socket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server_socket.bind(‘ ‘,2007)
server_socket.listen(5)

poll=select.poll()
connections={}
handlers={}

def handle_request(fileno,event):
      if event & select.POLLIN
         client_socket=connections[fileno]
         data=client_socket.recv(4096)
         if data:
               handle_input(client_socket,data)
         else
            poll.unregister(fileno)
            client_socket.close()
            del connections[fileno]
            del handlers[fileno]

def handle_accept(fileno,event):
       (client_socket,client_address)=server_socket.accpet()
       print "got connection from",client_address
       poll.register(client_socket.fileno(),select.POLLIN)
       connections[client_socket.fileno()]=client_socket
       handlers[client_socket.fileno()]=handle_request

poll.register(server_socket.fileno(),select.POLLIN)
handlers[server_socket.fileno()]=handle_accept

while True:
     events=poll.poll(10000)
     for fileno,event in events:
           handler=handlers[fileno]
           handler(fileno,evnet)

与前面不同的是,事件的处理通过handler转发到各个函数中,不再集中在一团。

方案五.reactor+thread pool

在收到客户端请求之后,不在Reactor线程计算,而是我们使用固定大小线程池,全部的IO工作都在一个Reactor线程完成,而计算任务交给thread pool.如果计算任务彼此独立,而且IO的压力不大,那么这种方案非常适用.

但是如果IO的压力比较大,一个Reactor处理不过来,我们可以采用多个Reactor来分担负载.

方案六.reactors in threads

这是Netty内置的多线程方案,这种方案的特点是one loop per thread,有一个main Reactor负责accept连接,然后把连接挂在某个sub Reactor中,这样该连接的所有操作都在那个sub Reactor所处的线程中完成.多个连接可能被分派到多个线程中,以充分利用CPU.

在把多个连接分散到多个Reactor线程之后,小规模计算可以在当前IO线程完成并发回结果,从而降低响应的延迟。

c++多线程服务器端编程模式为:one loop per thread + thread pool

event loop用作non-blocking IO 和定时器

thread pool用来做计算,具体可以是任务队列或生产者消费者队列.

实用的方案

方案 名称 接受新连接 网络IO 计算任务
 1  thread-per-connection 1个线程 N线程 在网络线程进行
2 单线程Reactor 1个线程 在连接线程中进行 在连接线程中进行
3 Reactor+ 线程池 1个线程 在连接线程进行 C2线程
4 one loop per thread 1个线程 C1线程 在网络线程进行
5 one loop per thread +thread pool 1个线程 C1线程 C2线程

N表示并发连接数目,C1和C2是与连接数无关,与CPU数目有关的常数。

参考:

《Linux多线程服务器端编程》

时间: 2024-07-29 08:12:12

常见的并发网络服务程序设计方案的相关文章

高性能、高并发网络通信系统的架构设计

1 引言 随着互联网和物联网的高速发展,使用网络的人数和电子设备的数量急剧增长,其也对互联网后台服务程序提出了更高的性能和并发要求.本文的主要目的是阐述如何进行高并发.高性能网络通信系统的架构设计,以及这样的系统的常用技术,但不对其技术细节进行讨论.本篇只起抛砖引玉的之效,如有更好的设计方案和思路,望您共分享之![注:此篇用select来讲解,如想用epoll,设计思路是一致的] 我们首先来看看课本或学习资料上关于处理并发网络编程的三种常用方案,以及对应的大体思路和优缺点,其描述如下所示:  

深入浅出 Java Concurrency (38): 并发总结 part 2 常见的并发场景[转]

常见的并发场景 线程池 并发最常见用于线程池,显然使用线程池可以有效的提高吞吐量. 最常见.比较复杂一个场景是Web容器的线程池.Web容器使用线程池同步或者异步处理HTTP请求,同时这也可以有效的复用HTTP连接,降低资源申请的开销.通常我们认为HTTP请求时非常昂贵的,并且也是比较耗费资源和性能的,所以线程池在这里就扮演了非常重要的角色. 在线程池的章节中非常详细的讨论了线程池的原理和使用,同时也提到了,线程池的配置和参数对性能的影响是巨大的.不尽如此,受限于资源(机器的性能.网络的带宽等等

深入浅出 Java Concurrency (39): 并发总结 part 3 常见的并发陷阱

常见的并发陷阱 volatile volatile只能强调数据的可见性,并不能保证原子操作和线程安全,因此volatile不是万能的.参考指令重排序 volatile最常见于下面两种场景. a. 循环检测机制 volatile boolean done = false; while( ! done ){        dosomething();    } b. 单例模型 (http://www.blogjava.net/xylz/archive/2009/12/18/306622.html)

常见的服务器网络问题

常见的服务器网络问题1.节点.节点的意义及中国骨干网节点的分布?CHINANET骨干网的拓扑结构逻辑上分为两层,即核心层和大区层.核心层核心层由北京.上海.广州.沈阳.南京.武汉.成都.西安等8个城市的核心节点组成.核心层的功能主要是提供与国际internet的互联,以及提供大区之间信息交换的通路.其中北京.上海.广州核心层节点各设有两台国际出口路由器,负责与国际internet互联,以及两台核心路由器与其他核心节点互联:其他核心节点各设一台核心路由器. 核心节点之间为不完全网状结构.以北京.上

高并发网络编程之epoll详解

select.poll和epoll的区别 在linux没有实现epoll事件驱动机制之前,我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序.在大数据.高并发.集群等一些名词唱的火热之年代,select和poll的用武之地越来越有限了,风头已经被epoll占尽. select()和poll() IO多路复用模型 select的缺点: 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述

Linux下高并发网络编程

1.修改用户进程可打开文件数限制 在Linux平台上,无论编写客户端程序还是服务端程序,在进行高并发TCP连接处理时, 最高的并发数量都要受到系统对用户单一进程同时可打开文件数量的限制(这是因为系统 为每个TCP连接都要创建一个socket句柄,每个socket句柄同时也是一个文件句柄). 可使用ulimit命令查看系统允许当前用户进程打开的文件数限制: [[email protected] ~]$ ulimit -n 1024 这表示当前用户的每个进程最多允许同时打开1024个文件,这1024

微信高并发资金交易系统设计方案——百亿红包背后的技术支撑

背景介绍 2017年1月28日,正月初一,微信公布了用户在除夕当天收发微信红包的数量--142亿个,而其收发峰值也已达到76万每秒.百亿级别的红包,如何保障并发性能与资金安全?这给微信带来了超级挑战.面对挑战,微信红包在分析了业界"秒杀"系统解决方案的基础上,采用了SET化.请求排队串行化.双维度分库表等设计,形成了独特的高并发.资金安全系统解决方案.实践证明,该方案表现稳定,且实现了除夕夜系统零故障运行. 本文将为读者介绍百亿级别红包背后的系统高并发设计方案,包括微信红包的两大业务特

使用dispatch_group实现并封装分组并发网络请求

在实际开发中我们通常会遇到这样一种需求:某个页面加载时通过网络请求获得相应的数据,再做某些操作.有时候加载的内容需要通过好几个请求的数据组合而成,比如有两个请求A和B,我们通常为了省事,会将B请求放在A请求成功的回调中发起,在B的成功回调中将数据组合起来,这样做有明显的问题: 1.请求如果多了,需要写许多嵌套的请求 2.如果在除了最后一个请求前的某个请求失败了,就不会执行后面的请求,数据无法加载 3.请求变成同步的,这是最大的问题,在网络差的情况下,如果有n个请求,意味着用户要等待n倍于并发请求

Java Socket常见异常处理 和 网络编程需要注意的问题

在java网络编程Socket通信中,通常会遇到以下异常情况: 第1个异常是 java.net.BindException:Address already in use: JVM_Bind. 该异常发生在服务器端进行new ServerSocket(port)(port是一个0,65536的整型值)操作时.异常的原因是以为与port一样的一个端口已经被启动,并进行监听.此时用netstat -an命令,可以看到一个Listending状态的端口.只需要找一个没有被占用的端口就能解决该问题了. 第