Python的并发和网络

来源:互联网 发布:全国淘宝店铺有多少个 编辑:程序博客网 时间:2024/05/18 20:35

并发

在计算机中,如果你的程序在等待,通常是因为以下两个原因:

  • I/O限制
    这个限制很常见。计算机的CPU速度非常快——比计算机内存快几百倍,比硬盘或者网络快几千倍。
  • CPU限制
    在处理数字运算任务时,比如科学计算或者图形计算,很容易遇到这个限制

进程

可以用很多方法来实现任务队列。对单机来说,标准库的multiprocessing模块有一个Queue函数。接下来模拟一个洗盘子的人和多个烘干进程,我们使用一个中间队列dish_queue。

关于multiprocessing的queue与joinablequeue的区别

关于守护进程

守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。

Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等。

代码

import multiprocessing as mpimport timedef washer(dishes, output):    for dish in dishes:        print('Washing', dish, 'dish')        time.sleep(5)        # 把洗好的碟子放在等待烘干队列上        output.put(dish)def dryer(input):    while True:        # 如果队列中没有任务可以get,该进程应该会陷入阻塞        dish = input.get()        print('Drying', dish, 'dish')        time.sleep(10)        # 完成get到的任务        input.task_done()# 需要if __name__ == '__main__'的原因:https://stackoverflow.com/questions/18204782/runtimeerror-on-windows-trying-python-multiprocessingif __name__ ==  '__main__':    dish_queue = mp.JoinableQueue()    # 其实可以产生多个dryer进程,这里只生成了一个dryer进程    dryer_proc = mp.Process(target=dryer, args=(dish_queue,))    # 以守护进程的形式运行该进程    dryer_proc.daemon = True    dryer_proc.start()    dishes = ['salad', 'bread', 'entree', 'dessert']    washer(dishes, dish_queue)    # 表示所有碟子都洗完了    dish_queue.join()

结果:

Washing salad dishWashing bread dishDrying salad dishWashing entree dishWashing dessert dishDrying bread dishDrying entree dishDrying dessert dish

线程

线程运行在进程内部,可以访问进程的所有内容,有点像多重人格。multiprocessing模块有一个兄弟模块threading,后者用线程来代替进程。multiprocessing和threading的区别之一就是threading没有terminate()函数。很难终止一个正在运行的线程,因为这可能会引起代码和失控连续性上的各种问题。

import threading, queueimport timedef washer(dishes, dish_queue):    for dish in dishes:        print('Washing', dish)        time.sleep(5)        dish_queue.put(dish)def dryer(dish_queue):    while True:        dish = dish_queue.get()        print('Drying', dish)        time.sleep(10)        dish_queue.task_done()dish_queue = queue.Queue()# 其实可以产生多个dryer线程,这里只生成了一个dryer线程dryer_thread = threading.Thread(target=dryer, args=(dish_queue,))dryer_thread.start()dishes = ['salad', 'bread', 'entree', 'desert']washer(dishes, dish_queue)dish_queue.join()

结果:

Washing saladWashing breadDrying saladWashing entreeWashing desertDrying breadDrying entreeDrying desert
# 开两个线程for n in range(2):    dryer_thread = threading.Thread(target=dryer, args=(dish_queue,))    dryer_thread.start()

结果:

Washing saladDrying saladWashing breadWashing entreeDrying breadWashing desertDrying entreeDrying desert

注意更多:
在Python中,线程不能加速受CPU限制的任务,原因是标准Python系统中使用了全局解释器锁(GIL)。GIL的作用是避免Python解释器中线程问题,但是实际上会让多线程程序运行速度比对应但线程版本甚至是多进程版本更慢。

对于Python有以下建议:

  1. 使用线程来解决I/O限制问题
  2. 使用进程、网络或者事件来处理CPU限制问题

解决同步IO问题的多进程、多线程以及异步IO

在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。

因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,所以我们必须使用多线程或者多进程来并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的进程或者线程不受影响。

多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降

由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法。

另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。

这里写图片描述

异步IO

消息模型是如何解决同步IO必须等待IO操作这一问题的呢?

异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程

1. 当遇到IO操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程

2. 当IO操作完成后,主线程将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果

在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息

这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。

关于如何提高Web服务端并发效率的异步编程技术

怎样理解阻塞非阻塞与同步异步的区别?

阻塞非阻塞:调用者的角度
同步异步:被调用者的角度

例子:
老张爱喝茶,废话不说,煮开水。

出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

1.老张把水壶放到火上,立等水开。(同步阻塞)
老张觉得自己有点傻

2.老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。

3.老张把响水壶放到火上,立等水开。(异步阻塞)
老张觉得这样傻等意义不大

4.老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
老张觉得自己聪明了。所谓同步异步,只是对于水壶而言。

普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下

所谓阻塞非阻塞,仅仅对于老张而言。立等的老张,阻塞;看电视的老张,非阻塞。情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。

虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

事件驱动模型

事件驱动模型就是一种非阻塞IO的具体实现(利用中间层来协调IO操作和CPU的操作)。

事件处理的机制里应该有个事件处理器,事件处理器位于元素和事件处理方法的中间位置,我们在定义事件的时候就是等于在事件处理器里定义元素和事件处理方法的关系

当这种对应关系定义好后,事件处理器就会启动一个死循环,这个循环反复检测元素的状态变化,当事件处理器发现某个状态产生了变化,处理器就会找到对应的事件处理方法,然后执行这个方法。

事件驱动编程实现的核心技术就是能让方法变成对象能在事件处理的流程里传递,方法得到事件管理器的指令后在合适的位置上被促发,这就是回调函数,而JavaScript语言里函数可以当做对象传递,也就保证了事件驱动编程上升到语言层级变成了可能,我想这就是nodejs作者使用google的V8引擎设计出nodejs的重要原因之一。

何时采用事件驱动模型

程序中有许多任务,而且任务之间高度独立(因此它们不需要互相通信,或者等待彼此),而且在等待事件到来时,某些任务会阻塞

当应用程序需要在任务间共享可变的数据时,这也是一个不错的选择,因为这里不需要采用同步处理。

更多: 基于线程与基于事件的并发编程之争

协程

http://blog.csdn.net/kobejayandy/article/details/11856735

https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001432090171191d05dae6e129940518d1d6cf6eeaaa969000

http://python.jobbole.com/86481/

http://blog.csdn.net/qq910894904/article/details/41699541

https://www.zhihu.com/question/20511233

http://blog.leiqin.info/2012/12/02/%E8%BF%9B%E7%A8%8B-%E7%BA%BF%E7%A8%8B%E5%92%8C%E5%8D%8F%E7%A8%8B%E7%9A%84%E7%90%86%E8%A7%A3.html

绿色线程和gevent

gevent就是一个基于事件的很棒的库:你只需要编写普通的代码,gevent会神奇地把它们转换成协程。协程就像可以互相通信的生成器,它们会记录自己的位置。gevent可以修改许多Python的标准对象,比如socket (import gevent from gevent import socket gevent.socket.x),从而使用它自己的机制来代替阻塞。如果遇到阻塞普通线程的情况,gevent会把控制权切换到另一个绿色进程。

协程无法处理C写成的Python扩展代码,比如一些数据库驱动程序。

gevent还不能完全兼容Python 3

使用gevent还有一个潜在的危险。对于基于事件的系统来说,执行的每段代码都应该尽可能快。尽管不会阻塞,执行复杂任务的代码还是会很慢。就是即使其中一段代码运行时间很长也要搞下去的意思,不能中途运行另外一段代码,而[在多线程中,一个线程运行太久,优先级比较低,就会被其他线程所抢占]。(http://blog.csdn.net/jason_cuijiahui/article/details/77101693)

gevent的示例

猴子补丁(monkey-patching)

除了使用gevent版本的socket之外,你可以使用猴子补丁(monkey-patching)函数。这个函数会修改标准模块,比如socket,直接让她们使用绿色线程而不是调用gevent版本。如果想在整个程序中应用gevent,这种方法非常有用,即使那些你无法直接接触到的代码也会被改变。

在程序的开头,添加下面的代码:

# 把程序中所有普通socket都修改成gevent版本,即使是标准库也不例外# 这个改动只对Python代码有效,对C写成的库无效from gevent import monkeymonkey.patch_socket()
# 不只是socketfrom gevent import monkeymonkey.patch_all()

补充

另外两个流行的事件驱动框架是tornado和gunicorn。它们都使用了底层事件处理和高速Web服务器。如果你想使用传统的Web服务器(比如Apache)来构建高速网站,整两个框架非常值得一看。

twisted

twisted是一个异步事件驱动的网络框架。你可以把函数关联到事件(比如数据接收或者连接关闭)上,当事情发生时这些函数会被调用。这种设计被称为回调(callback),如果你以前用过JavaScript,那一定不会陌生。

如果是第一次见到回调,可能会觉得它有点过时。对于有些开发者来说,基于回调的代码在应用规模变大之后会很难维护。

和gevent一样,twisted还没有兼容Python 3

twisted很大,支持很多基于TCP和UDP的网络协议。

一个系列

asyncio

asyncio模块在Python 3.4中首次出现。目前,它提供一种通用的时间循环,可以兼容twisted、gevent和其它异步方法。目标是一种一种标准、简洁、高性能的异步API。

更多

Redis

我们之前的洗盘子示例代码,无论使用的是进程还是线程,都运行在一台机器上。下面我们使用另一种方法来实现队列,让它可以既支持单机又支持网络。有时候用了进程和线程,单机仍然无法满足需求。

在第8章中,Redis的角色是数据库,而这里指的是它的并发特征。

可以使用Redis列表快速创建一个队列。Redis服务器部署在一台机器上;客户端可以部署在同一台机器上也可以部署在不同机器上,通过网络通信。无论是哪种情况,客户端都是通过使用TCP和服务器通信,因此它们是网络化。

一个或多个生产者客户端向列表的一端压入消息,一个或多个工人客户端通过阻塞弹出操作从列表中获得需要洗的盘子。如果列表为空,它们就会闲置。如果有一条消息,第一个空闲工人就会去处理。

和之前的基于进程和线程的示例一样,redis_washer.py会生成一个盘子序列:

# redis_washer.py# 循环会生成是个包含盘子名称的消息,最后一条消息是“退出”(quit)。程序会把所有消息都添加到Redis服务器上的dishes列表中,就像添加到Python列表一样。import redisconn = redis.Redis()print('Washer is starting')dishes = ['salad', 'bread', 'entree', 'dessert']for dish in dishes:    msg = dish.encode('utf-8')    conn.rpush('dishes', msg)    print('Washed', num)conn.rpush('dishes', 'quit')print('Washer is done')
# redis_dryer.py# 这段代码会等待第一个令牌是dishes的消息,并打印出烘干的盘子。如果遇到“退出”消息就终止循环import redisconn = redis.Redis()print('Dryer is starting')while True:    # 等待有对象可以取出    msg = conn.blpop('dishes')    if not msg:        break    val = msg[1].decode('utf-8')    if val == 'quit':        break    print('Dried', val)print('Dishes a dried')

启动烘干工人,然后启动清洗工人。在命令结尾加上&,让第一个程序在后台运行;它会一直运行下去,但是不会监听键盘。接着我们正常(在前台)启动清洗进程。你可以看到两个进程是混在一起的输出。

$ python redis_dryer.py &$ python redis_washer.py

下面对redis_dryer.py进行修改:

  • 创建多个dryer进程
  • 除了等待哨兵,给每个烘干进程添加一个超时时间timeout。
# redis_dryer2.pydef dryer():    import redis    import os    import time    conn = redis.Redis()    pid = os.getpid()    timeout = 20    print('Dryer process %s is starting' % pid)    while True:        # 若20秒未能取出元素,则msg返回为None,触发下面的if判断        msg = conn.blpop('dishes', timeout)        if not msg:            break        val = msg[1].decode('utf-8')        if val == 'quit':           break        print('%s: dried %s' % (pid, val))        time.sleep(0.1)    print('Dryer process %s is done' % pid)import multiprocessingDEYEARS=3for num in range(DRYERS):    p = multiprocessing.Process(target=dryer)    p.start()

队列之上

加入的功能越多,流水线就越有可能出问题。此时,有三种技术可供你使用。

  • 触发并忘记
    只传递内容,并不关心结果,即使没人处理。就是把“把盘子扔地上”方法。
  • 请求-响应
    对于每个盘子,流水线上的清洁工人需要收到烘干工人的确认,烘干工人需要收到放置工人的确认。
  • 背压或者节流
    使用于上游工人速度比下游工人快的情况。背压(Backpressure),就是消费者需要多少,生产者就生产多少。节流(Throttling),说白了就是丢弃。消费不过来,就处理其中一部分,剩下的丢弃。至于处理哪些和丢弃哪些,就有不同的策略,也就是sample (or throttleLast)、throttleFirst、debounce (or throttleWithTimeout)这三种。

以下有一些基于Python的队列包添加了额外的控制层(有些使用的是Redis)

  • celery
    这个包非常值得一看。它可以同步或者异步执行分布式任务,使用了我们之前介绍的方法:multiprocessing, gevent等。
  • thoonk
    这个包基于Redis构建,可以创建任务队列并实现发布-订阅
  • rq
    这个一个处理任务队列的Python库,同样基于Redis。
  • Queues
    这个网站介绍了队列化软件,其中有些是基于Python开发的。

网络

在讨论并发时,主要讨论的是时间:单机解决方案(进程、线程和绿色线程)。还简单介绍了网络化的解决方案(Redis、ZeroMQ)。现在,我们来单独介绍一下网络化,也就是跨空间的分布式计算。

模式

  • 请求-响应/客户端-服务器
    这个模式是同步的:用户会一直等待服务器的响应。
  • 推送/扇出
    你把数据发送到一个进程池中,空闲的工作进程会进行处理。一个典型的例子就是由负载均衡的Web服务器。(一个生产者,多个消费者)
  • 拉取/扇入
    你从一个或多个源接受数据。一个典型的例子是记录器,它会从多个进程接收文本信息并把它们写入一个日志文件。(多个生产者,一个消费者)
  • 发布-订阅
    这个模式中,会有发送数据的发布者。在简单的发布-订阅系统中,所有的订阅者都会收到一份副本。更常见的情况是,订阅者只关心特定类型的数据(通常被称为话题),发布者只会发送这些数据。因此和推送模式不同,可能会有超过一个订阅者收到数据。如果一个话题没有订阅者,相关的数据会被忽略。

发布-订阅模型

发布-订阅并不是队列,而是广播。一个或多个进程发布信息,每个订阅进程声明自己感兴趣的消息类型,然后每个消息都会被复制一份发给感兴趣的订阅进程。因此,一个消息可能只被处理一次,也可能多于一次,还可能完全不被处理。每个发布者只负责广播,并不知道谁(如果有人的话)在监听。

Redis

你可以使用Redis来快速搭建一个发布-订阅系统。发布者会发出包含话题和值的消息,订阅者会声明它们对什么话题感兴趣。

下面是发布者,redis_pub.py

import redisimport randomconn = redis.Redis()cats = ['siamese', 'persian', 'maine coon', 'norwegian forest']hats = ['stovepipe', 'bowler', 'tam-o-shanter', 'fedora']for msg in range(10):    cat = random.choice(cats)    hat = random.choice(hats)    # 每个话题是猫的一个品种,每个消息的值是帽子的一种类型    print('Publish: %s wears a %s' % (cat, hat))    conn.publish(cat, hat)

下面是一个订阅者,redis_sub.py

import redisconn = redis.Redis()topics = ['maine coon', 'persian']sub = conn.pubsub()sub.subscribe(topics)# listen()方法会返回一个字典,如果它的类型是'message',那就是由发布者发出的消息for msg in sub.listen():    if msg['type'] == 'message':        # channel键是话题(猫)        cat = msg['channel']        # data键包含消息的值(帽子)        hat = msg['data']        print('Subscribe: %s wears a %s' % (cat, hat))
$ python redis_sub.py$ python redis_pub.py

可以使用任意数量的订阅者(和发布者)。如果一个消息没有订阅者,那它会从Redis服务器中消失。然而,如果有订阅者,消息会停留在服务器中,直到所有的订阅者都获取完毕。

ZeroMQ

ZMQ是是个类似于Socket的一系列接口,他跟Socket的区别是:普通的socket是端到端的(1:1的关系),而ZMQ却是可以N:M 的关系,人们对BSD套接字的了解较多的是点对点的连接,点对点连接需要显式地建立连接、销毁连接、选择协议(TCP/UDP)和处理错误等,而ZMQ屏蔽了这些细节,让你的网络编程更为简单。ZMQ用于node与node间的通信,node可以是主机或者是进程。

引用官方的说法:“ZMQ(以下ZeroMQ简称ZMQ)是一个简单好用的传输层,像框架一样的一个socket library,他使得Socket编程更加简单、简洁和性能更高。是一个消息处理队列库,可在多个线程、内核和主机盒之间弹性伸缩。ZMQ的明确目标是“成为标准网络协议栈的一部分,之后进入Linux内核”。现在还未看到它们的成功。但是,它无疑是极具前景的、并且是人们更加需要的“传统”BSD套接字之上的一 层封装。ZMQ让编写高性能网络应用程序极为简单和有趣。”

rabbitMQ、activeMQ、zeroMQ、Kafka、Redis 比较

更多讨论

ZeroMQ实现发布-订阅模型

ZeroMQ没有核心服务器,因此每个发布者都会发送给所有订阅者。我们来使用ZeroMQ重写一下上面的猫-帽子示例。

发布者为zmq_pub.py,内容如下所示:

# 注意代码时如何用UTF-8来编码话题和值字符串的import zmqimport randomimport timehost = '*'port = 6789ctx = zmq.Context()pub = ctx.socket(zmq.PUB)pub.blind('tcp://%s:%s' % (host, port))cats = ['siamese', 'persian', 'maine coon', 'norwegian forest']hats = ['stovepipe', 'bowler', 'tam-o-shanter', 'fedora']time.sleep(1)for msg in range(10):    cat = random.choice(cats)    cat_byte = cat.encode('utf-8')    hat = random.choice(hats)    hat_bytes = hat.encode('utf-8')    print('Publish: %s wears a %s' % (cat, hat))    pub.send_mulitpart([cat_bytes, hat_bytes])

下面是订阅者zmq_sub.py:

import zmqhost = '127.0.0.1'port = 6789ctx = zmq.Context()sub = ctx.socket(zmq.SUB)sub.connect('tocp://%s:%s' % (host, port))# 若想订阅所有话题,则topics = ''topics = ['maine coon', 'persian']for topic in topics:    sub.setsockopt(zmq.SUBSCRIBE, topic.encode('utf-8'))while True:    cat_bytes, hat_bytes = sub.recv_multipart()    cat = cat_bytes.decode('utf-8')    hat = hat_bytes.decode('utf-8')    print('Subscribe: %s wears a %s' % (cat, hat))
$ python zmq_sub.py$ python zmq_pub.py

其他发布-订阅工具

  • RabbitMQ
    这是一个非常著名的消息发送器。pika是它的Python API。详情参见pika文档和发布-订阅教程
  • pypi.python.org
    在该网址的右上角的搜索框内输入pubsub来寻找类似pypubsub这样的Python包。
  • pubsubhubhub
    这个读起来非常顺口的协议允许订阅者注册对应发布者的回调函数。

TCP/IP

因特网是基于规则的,这些规则定义了如何创建链接、交换数据、终止连接、处理超时等。这些规则被称为协议,它们分布在不同的层中。分层的目的是兼容多种实现方法。

最底层处理的是电信号,其余层都是基于下面的层构建而成。在大约中间的位置是IP层,这层规定了网络位置和地址映射方法以及数据包(块)的传输方式。IP的上一层有两个协议描述了如何在两个位置之间移动比特。

传输层与网络层(IP层)的关系

在协议栈中,传输层位于网络层之上,传输层协议为不同主机上运行的进程提供逻辑通信,而网络层协议为不同主机提供逻辑通信。

在网络层,数据传输的基本单位是数据包(也称为分组)。在发送方,传输层的报文到达网络层时被分为多个数据块,在这些数据块的头部和尾部加上一些相关控制信息后,即组成了数据包(组包)。数据包的头部包含源结点和目标结点的网络地址(逻辑地址)。

在接收方,数据从低层到达网络层时,要将各数据包原来加上的包头和包尾等控制信息去掉(拆包),然后组合成报文,送给传输层。

传输层协议

  • UDP(用户数据报协议)
    这个协议被用来进行少量数据交换。一个数据报是一次发送的很少信息,就像明信片上的一个音符一样。UDP信息不需要确认,因此你永远无法确认它是否到达目的地。
  • TCP(传输控制协议)
    这个协议被用来进行长时间的连接。它会发送比特流并确保它们都能按序到达并且不会重复。TCP会在发送者和接收者之间通过秘密握手建立有保障的连接。

套接字

最底层的网络编程使用的是套接字,源于C语言和Unix操作系统。套接字层的编程时非常繁琐的。使用类似于ZeroMQ的库会简单很多(ZeroMQ 的主要部分是套接字 Socket,不过它并未直接使用传统的套接字,而是在传统的套接字 API上提供了一个抽象层,这让用户从复杂和重复的编程任务中解脱出来。ZeroMQ 支持多种类型的套接字 (类型被定义为套接字自身的一个属性)。发送端和接收端的套接字类型组合造就了多种不同的通信模式),但是了解一下底层的工作原理还是非常有用的。举例来说,网络发生错误时出现的错误信息通常是和套接字相关的。

CS-UDP

我们来编写一个非常简单的客户端-服务器通信示例。客户端发送一个包含字符串的UDP数据报给服务器,服务器会返回一个包含字符串的数据包。

# udp_server.pyfrom datetime import datetimeimport socketserver_address = ('localhost', 6789)max_size = 4096print('Start the server at', datetime.now())print('Waiting for a client to call.')# 服务器必须用socket包中的两个方法来建立网络连接:socket.socket和bind# socket.socket会创建一个套接字。AF_INET表示要创建一个因特网(IP)套接字。(还有其他类型的Unix域套接字,不过那些只能在本地运行。)SOCK_DGRAM表示我们要发送和接收数据报,换句话说,我们要使用UDP。server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)# bind会绑定(监听到达这个IP地址和端口的所有数据)到这个套接字上。server.bind(server_address)# 服务器会等待数据包到达(recvfrom),收到数据报后,服务器会被唤醒并获取数据和客户端的信息。client变量包含客户端的地址和端口,用于给客户端发送数据data, client = server.recvfrom(max_size)print('At', datetime.now(), client, 'said', data)server.sendto(b'Are you talking to me?', client)# 关闭连接server.close()
# udp_client.pyimport socketfrom datetime import datetimeserver_address = ('localhost', 6789)max_size = 4096print('Starting the client at', datetime.now())client = socket.socket(socket.AF_INEF, socket.SOCK_DGRAM)# 客户端需要知道服务器的地址和端口号,但是并不需要指定自己的端口号。它的端口号由系统自动分配。client.sentto(b'Hey!', server_address)data, server = client.recvfrom(maxsize)print('At', datetimenow(), server, 'said', data)client.close()

CS-TCP

由于UDP不可靠,我们准备使用TCP(传输控制协议)。TCP用来进行长时间连接,比如Web。TCP按照发送的顺序传输数据。如果出现任何问题,它会尝试重新传输。

# tcp_server.pyfrom datetime import datetimeimport socketaddress = ('localhost', 6789)maxsize = 1000print('Starting the server at', datetime.now())print('Waiting for a client to call.')# SOCK_STREAM 流协议TCPserver = socket.socket(socket.AF_INEF, socket.SOCK_STREAM)server.bind(address)# 表示最多可以和5个客户端连接,超过5个就会拒绝server.listen(5)# 表示接受第一个到达的消息client, addr = server.accept()# 指定最大的可接受消息长度为1000字节data = client.recv(max_size)print('At', datetime.now(), client, 'said', data)client.sendall(b'Are you talking to me?')client.close()server.close()
tcp_client.pyimport socketfrom datetime import datetimeaddress = ('localhost', 6789)max_size = 1000print('Starting the client at', datetime.now())client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 使用connect()来建立流。使用UDP时不需要这么做,因为每个数据报都是直接暴露在互联网中。client.connect(address)client.sendall(b'Hey')data = client.recv(max_size)print('At', datetime.now(), 'someone replied', data)# 关闭连接client,某个套接字client.close()# 关闭服务器server.close()

套接字的复杂性

  • UDP可以发送消息,但是消息的大小有限制,而且不能保证消息到达目的地
  • TCP发送字节流,不是消息。你不知道每次调用时系统会发送或者接收多少字节。(一个消息,可能分多次才能发送,可能分多次才能接收)
  • 如果要TCP传输完整的消息,需要一些额外的信息来把片段拼凑成整个消息:固定的消息大小(字节)、整个消息的大小或者一些特殊的哨兵字符
  • 由于消息是字节,不是Unicode文本字符串,你需要使用Python的bytes类型

python套接字编程教程

ZeroMQ

ZeroMQ是一个库,有时候被称为打了激素的套接字(sockets on steroids),ZeroMQ套接字实现了很多你需要但是普通套接字没有的功能:

  • 传输完整的消息
    普通套接字是基于流的,不能保证这一点。
  • 重连
  • 当发送方和接收方的时间不同步时缓存数据
    利用普通套接字时,若服务器是同步的,一次只能处理一个请求,会丢弃这段时间到达的其他请求。普通套接字编程所以需要为每个请求开启一个另外的线程/进程进行处理。而ZeroMQ会在触发某些限制之前一直缓冲消息,直到他们被处理。

ZeroMQ就像乐高积木,我们都知道用很少的乐高积木就能搭建出很多东西。在本例中,你可以用很少几个套接字类型和模型来构建网络。下面这些“乐高积木”是ZeroMQ套接字类型,看起来很像之前说过的网络模型:

  • REQ(同步请求)
  • REP(同步响应)
  • DEALER(异步请求,监听响应事件)
  • ROUTER(异步响应,监听请求事件)
  • PUB(发布)
  • SUB(订阅)
  • PUSH(扇出)
  • PULL(扇入)

首先安装Python的ZeroMQ库:

$ pip install pyzmq

请求-响应对

最简单的模式是一个请求-响应对。这是同步的:一个套接字发送请求,另一个发送响应。

# zmq_server.py# 这个示例代码会从一个发送者接收请求并发送响应。消息可以非常长——ZeroMQ会处理这些细节import zmqhost = '127.0.0.1'port = 6789# 创建一个Context对象:这是一个能够保存状态的ZeroMQ对象。context = zmq.Context()# 创建一个REP(用于响应)类型的ZeroMQ套接字server = context.socket(zmq.REP)# 调用bind(),让它监听特定的IP地址和端口server.bind("tcp://%s:%s" % (host, port))while True:    # 等待客户端的下一个请求    request_bytes = server.recv()    request_str = request_bytes.decode('utf-8')    pirnt("That voice in my head says: %s" % request_str)    reply_str = "Stop saying: %s" % request_str    reply_bytes = bytes(reply_str, 'utf-8')    server.send(reply_bytes)
# zmq_client.pyimport zmqhost = '127.0.0.1'port = 6789context = zmq.Context()client = context.socket(zmq.REQ)client.connect("tcp://%s:%s" % (host, port))for num in range(1, 6):    request_str = "message #%s" % num    request_bytes = request_str.encode('utf-8')    client.send(request_bytes)    reply_bytes = client.recv()    reply_str = reply_bytes.decode('utf-8')    print("Send %s, received %s" % (request_str, reply_str))

现在是时候启动它们了。和普通套接字不同一点是,你可以用任何顺序启动服务器和客户端(因为有重连机制?)。

$ python zmq_server.py &$ python zmq_client.py

注意:
消息需要用字节字符串形式发送,所以需要把示例中的字符串用UTF-8格式编码。你可以发送任意类型的消息,只要把它转换成bytes就行。我们的消息是简单的文本字符串,所以encode()和decode()可以实现文本字符串和字节字符串的转换。如果你的消息包含其他数据类型,可以使用类似MessagePack的库来处理

由于任何数量的REQ客户端都可以connect()到一个REP服务器,即使是基础的请求-响应模式也可以实现一些有趣的通信模式。服务器是同步的,一次只能处理一个请求,但是并不会丢弃这段时间到达的其他请求。ZeroMQ会在触发某些限制之前一直缓存这些消息,知道它们被处理;这就是ZeroMQ中的Q的意思。Q表示队列,M表示消息,Zero表示不需要任何消息发布者。

使用DEALER和ROUTER套接字异步连接到多个源和/或目标

虽然ZeroMQ不需要任何核心分发者(中间人),但是如果需要,你可以搭建一个。可以使用DEALER和ROUTER套接字异步连接到多个源和/或目标。

多个REQ套接字可以连接到一个ROUTER(异步响应,监听请求事件)。就像很多浏览器连接到一个代理服务器,后者连接到一个Web服务器群。你可以根据需要添加任意数量的客户端和服务器。

这里写图片描述

REQ套接字只能和ROUTER套接字连接;DEALER可以和后面的多个REP套接字连接。(?) ZeroMQ会处理具体的细节部分,确保请求的负载均衡并把响应发送给正确的目标。

另一种网络化模式被称为通风口,使用PUSH套接字来发送异步任务,使用PULL套接字来收集结果。(?)

ZeroMQ可以实现扩展和收缩

最后一个需要介绍的ZeroMQ特性是它可以实现扩展和收缩,只要改变创建的套接字连接类型即可

  • tcp适用于单机或者分布式的进程通信
  • ipc适用于单机的进程间通信
  • inproc使用于单个进程内线程间的通信
    inproc是一种进程间无锁的数据传输方式,可以替代前面的threading示例。

ZeroMQ与Netty

编程模型

http://blog.csdn.net/kobejayandy/article/details/20294909

二者比较

https://stackoverflow.com/questions/10156388/what-are-the-alternatives-to-zeromq-for-moving-protocol-buffer-payloads-around
http://blog.csdn.net/maixia24/article/details/38392457
http://www.infoq.com/cn/news/2013/11/netty4-twitter
http://grokbase.com/t/zeromq/zeromq-dev/1447cdh85e/yahoo-shows-big-performance-improvement-by-switching-storm-from-zeromq-to-netty
http://blog.csdn.net/wzhg0508/article/details/42012637

结合ZeroMQ与Netty各自的特点

https://stackoverflow.com/questions/23613654/can-netty-be-integrated-with-zeromq
https://github.com/spotify/netty-zmtp

其它

  • ActiveMQ(Apache)
    包含了几个使用简单文本STOMP协议的Python接口
  • RabbitMQ

scapy

有时候你需要深入网络流中处理字节。你可以想要调试Web API或者追踪一些安全问题。scapy库是一个优秀的Python数据包分析工具,比编写和调试C程序简单很多。实际上,它是一门简单的用来构建和分析数据包的语言。

注意:

  • scapy还不兼容Python3
  • scapy的安装教程复杂

网络服务

域名系统

这里写图片描述

Python的Email模块

标准库中有以下这些Email模块:

  • stmplib使用简单邮件传输协议(SMTP)发送邮件
  • email用来创建和解析邮件
  • poplib可以使用邮递协议(POP3)来读取邮件
  • imaplib可以使用因特网消息访问协议(IMAP)来读取邮件

Python SMTP服务器

如果你想编写自己的Python SMTP服务器,可以试试smtpd

Lamson是一个纯Python的SMTP服务器,可以在数据库中存储邮件,甚至可以过滤垃圾邮件。

其他协议

标准的ftplib模块可以使用文件传输协议(FTP)来发送字节。

Web服务和API

最简单的API是一个Web接口,可以提供类似JSON或者XML的结构化数据,而不是纯文本或者HTML。API既可以做的非常简单也可以是一套成熟的RESTful API。(普通Api和RESTful Api的区别

很多网站API需要通过token访问。网站可以通过token来判断谁在获取数据,也可以用它来限制请求频率。

远程处理

本书中的很多示例都是介绍如何在同一台机器上调用Python代码,通常还是在同一个进程中。但是Python的能力远不止这些,你可以调用其他机器上的代码,就像它们在本地一样。通过网络连接的一组计算机可以让你操作更多进程和/或线程。

远程过程调用(RPC)

远程过程调用(RPC)看起来和普通的函数一样,但其实运行在通过网络连接的远程机器上。

RPC 与 RESTFUL的区别

RPC面向的是函数,而RESTFUL面向的是资源。RESTful API需要通过URL编码参数或者请求体来调用,但是RPC函数是在自己的机器上调用。(更多)

RPC工作原理

客户端:

  1. 把客户端参数转化为比特(有时候被称为编组、序列化或者编码)
  2. 把编码后的字节发送给远程机器

远程机器:

  1. 接收编码后的请求字节
  2. 接受完毕后,远程机器会把字节解码成原始的数据结构
  3. 远程机器找到本地目标函数并用解码后的数据调用它
  4. 远程机器编码函数执行结果
  5. 远程机器把编码后的字节发送给客户端

RPC的实现

RPC是一种非常流行的技术,有很多种实现方式。

例子1

标准库中包含一种RPC实现,xmlrpc,使用XML作为传输格式

# xmlrpc_server.pyfrom xmlrpc.server import SimpleXMLRPCServerdef double(num):    return num*2# 服务器的一个地址和端口server = SimpleXMLRPCServer(("localhost", 6789))# 注册函数server.register_function(double, "double")# 启动服务器并等待server.serve_forever()
# xmlrpc_client.pyimport xmlrpc.client# 客户端通过ServerProxy和服务器连接且利用http作为传输方式proxy = xmlrpc.client.ServerProxy("http://localhost:6789/")num = 7result = proxy.double(num)print("Double %s is %s" % (num, result))
例子2

常用的传输方式是HTTP和ZeroMQ。(例子1用HTTP)

除了XML外,JSON、Protocol Buffers和MessagePack也是常用的编码方式。(例子1用XML)

有许多基于JSON的Python RPC包,但是它们要么不支持Python3,要么太难用。

这里我们使用MessagePack自己的Python RPC实现

$ pip install msgpack-rpc-python

这条命令还会安装tornado,这是一个基于事件的Python Web服务器,会被这个库用于传送数据。

# msgpack_server.pyfrom msgpackrpc import Server, Addressclass Servers():    def double(self, num):        return num*2# Services类把它的方法暴露为RPC服务server = Server(Services())server.listen(Address("localhost", 6789))server.start()
# msgpack_client.pyfrom msgpackrpc import Client, Addressclient = Client(Address("localhost", 6789)num = 8result = client.call('double', num)print("Double %s is %s" % (num, result))
例子3

使用ZeroMQ代替HTTP来进行内部服务的RPC调用,好处如下:

  • 多个并发的RPC调用可以使用同一个TCP连接,而HTTP需要按顺序调用(保持连接的情况下)。这是最大的一个好处。
  • 没有手动连接管理。HTTP也是可以做到的,说到底是库的问题,但据我所知只有个别的库可以这样实现HTTP。
  • 支持多个服务器进程响应请求。不需要HTTP负载平衡器。
  • 没有手动重试处理。停止服务器,调用一个RPC请求,然后启动服务器,然后得到一个响应-消息是队列化的而不是直接传送。

更多与实现

fabric

fabric包可以运行远程或本地命令、上传或者下载文件、用sudo权限运行命令

这个包使用安全shell(SSH:加密文本协议,基本上已经代替了telnet)来运行远程程序。

你需要把(Python)函数写入一个fabric文件并声明它们应该在远程还是本地执行。之后,使用时需要用fabric程序来运行,需要指定目标远程机器和目标函数。它比RPC简单舵。

fabric的作者正在合并一些和Python3相关的改动
$ pip install fabric

例子1

可以不使用SSH,直接用fabric运行本地Python代码。

# fab1.py# 本地python代码def iso():    from datetime import date    print(date.today().isoformat())
$ fab -f fab1.py -H localhost iso

注意:

  • -f fab1.py选项指定使用fabric文件fab1.py,而不是默认的fabfile.py
  • -H localhost选项指定运行本地的命令
  • iso是fab文件中要运行的文件名
  • 具体选项见官方文档

它的工作原理和之前的RPC有点像。

例子2

可以不使用SSH,直接用fabric运行本地shell命令。

# fab2.pyfrom fabric.api import localdef iso():    loacl('date -u')
$ fab -f fab1.py -H localhost iso

例子3

要在远程运行外部程序,被访问机器必须运行SSH服务器。在Unix类系统中,服务器是sshd;service sshd status可以检查服务器是否启动,如果需要service sshd start来启动它。

# fab3.py# 该文件在调用者机器上from fabric.api import rundef iso():    run('date -u')

fabric在遇到run()时会使用SSH连接命令行中-H指定的主机,并在该主机上运行fab3.py中的iso函数。

调用者

$ fab -f fab3.py -H hostip iso

可以将密码写入fabric文件:

# fab4.pyfrom fabric.api import runfrom fabric.context_managers import envenv.password = "your password goes here"def iso():    run('date -u')

把密码放在代码中非常不安全。更好的方法是使用公钥和密钥配置SSH,可以使用ssh-keygen。S

可以在本机上测试外部运行程序的逻辑:

$ fab -f fab3.py -H localhost iso

Salt

Salt最初的目的是实现远程运行,但是后来变成了一个完整的系统管理平台。它是基于ZeroMQ开发的,不是基于SSH,因此可以扩展到上千台服务器。

Salt还没有兼容Python 3。

其它

  • puppet(跟Rubby关系密切)
  • chef(跟Rubby关系密切)
  • ansible(默认使用SSH,并且不需要再机器上安装其他特殊软件)

salt和ansible都包含了fabric的功能,可以进行初始化配置、部署和远程执行。

大数据和MapReduce

着重点在于计算

Haddoop

Hadoop流

Hadoop流就像Unix的管道一样,把每一步产生的数据流直接传输给下一步,这样就可以免于中间数据存储到磁盘。

Hadoop的Python模块

Python Hadoop框架教程

Spotify开源了自己处理Hadoop流的Python部件Luigi,不过现在还不兼容Python 3

Spark

Spark的目标是相比Hadoop大大加快运行速度。它可以读取和处理所有Hadoop的数据结构和格式。

Disco

Disco使用Python来完成MapReduce过程,使用Erlang完成通信部分。不过,只可惜无法使用pip来安装。

在云上工作

分布式计算的八大误解:

  • 网络是可靠的
  • 延迟为零
  • 带宽无限
  • 网络是安全的
  • 拓扑结构不会改变
  • 传输成本为零
  • 网络是同构的

流行的云平台

Google

Amazon

OpenStack

阅读全文
0 0
原创粉丝点击