cook book:10:模块与包+11:网络与Web编程
1:构建一个模块的层级包 代码组织层由很多分层模块构成的包
# 封装成包:在文件系统上组织你的代码,并确保每个目录都定义了一个__init__.py文件 graphics/ __init__.py primitive/ __init__.py line.py fill.py text.py formats/ __init__.py png.py jpg.py # 做到了这一点,能够执行各种import语句 import graphics.primitive.line from graphics.primitive import line import graphics.formats.jpg as jpg
# 定义模块的层次结构就像在文件系统上建立目录结构一样容易
# 文件__init__.py的目的是要包含不同运行级别的包的可选的初始化代码
# import graphics, 文件graphics/__init__.py将被导入,建立graphics命名空间的内容。
# import graphics.format.jpg这样导入,文件graphics/__init__.py和
文件graphics/formats/__init__.py将在文件graphics/formats/jpg.py导入之前导入
# 绝大部分时候让__init__.py空着就好 # 有些情况可以在__init__.py加语句来完成某些功能, # 自动加载子模块 # 模块结构 graphics/ __init__.py primitive/ __init__.py line.py fill.py text.py formats/ __init__.py png.py jpg.py # graphics/formats/__init__.py文件写入如下语句 from . import jpg from . import png # 这样一个文件,用户可以仅仅通过import grahpics.formats来代替
import graphics.formats.jpg以及import graphics.formats.png # __init__.py的其他常用用法包括将多个文件合并到一个逻辑命名空间 # 即使没有__init__.py文件存在,python仍然会导入包 # 如果你没有定义__init__.py时,实际上创建了一个所谓的“命名空间包”,万物平等,
如果你着手创建一个新的包的话,还是包含一个__init__.py文件最好
2:控制模块被全部导入的内容 from module import *
# 使用’from module import *’ 语句时,希望对从模块或包导出的符号进行精确控制 # 模块中定义一个变量 __all__ 来明确地列出需要导出的内容 # somemodule.py def spam(): pass def grok(): pass blah = 42 # 只导出 'spam' 和 'grok' __all__ = ['spam', 'grok'] # __all__这个列表里面的变量都能被*导出,其他的都不会被*导出
# 不建议使用from module import *,
# 在定义了大量变量名的模块中频繁使用,不做任何事, 这样的导入将会导入所有不以下划线开头的
# 定义了 __all__ , 那么只有被列举出的东西会被导出
# __all__ 定义成一个空列表, 没有东西将被导入。
# __all__ 包含未定义的名字, 在导入时引起AttributeError
3:使用相对路径名导入包中子模块 代码组织成包,想用import语句从另一个包名没有硬编码过的包中导入子模块。
# 相对导入:一个模块导入同一个包的另一个模块 目录如下: mypackage/ __init__.py A/ __init__.py spam.py grok.py B/ __init__.py bar.py # 模块mypackage.A.spam要导入同目录下的模块grok from . import grok # 模块mypackage.A.spam要导入不同目录下的模块B.bar from ..B import bar # import语句都没包含顶层包名,而是使用了spam.py的相对路径。
# 包内,既可以使用相对路径也可以使用绝对路径来导入 # mypackage/A/spam.py from mypackage.A import grok # OK from . import grok # OK import grok # Error (not found) # mypackage.A这样使用绝对路径名的不利之处是这将顶层包名硬编码到你的源码中。
如果你想重新组织它,你的代码将更脆,很难工作。 举个例子,如果你改变了包名,
你就必须检查所有文件来修正源码。 同样,硬编码的名称会使移动代码变得困难。
举个例子,也许有人想安装两个不同版本的软件包,只通过名称区分它们。
如果使用相对导入,那一切都ok,然而使用绝对路径名很可能会出问题 # import语句的 . 和 .. 看起来很滑稽, 但它指定目录名.为当前目录,..B为目录../B。这种语法只适用于import from . import grok # OK import .grok # ERROR # 使用相对导入看起来像是浏览文件系统,但是不能到定义包的目录之外。
也就是说,使用点的这种模式从不是包的目录中导入将会引发错误 # 相对导入只适用于在合适的包中的模块。尤其是在顶层的脚本的简单模块中,
它们将不起作用。如果包的部分被作为脚本直接执行,那它们将不起作用 % python3 mypackage/A/spam.py # Relative imports fail # 如果你使用Python的-m选项来执行先前的脚本,相对导入将会正确运行 % python3 -m mypackage.A.spam # Relative imports work
4:将模块分割成多个文件 一个模块分割成多个文件,不想将分离的文件统一成一个逻辑模块时使已有的代码遭到破坏
# 程序模块可以通过变成包来分割成多个独立的文件 # mymodule.py class A: def spam(self): print('A.spam') class B(A): def bar(self): print('B.bar') # mymodule.py分为两个文件,每个文件定义的一个类 # 做到这一点,首先用mymodule目录来替换文件mymodule.py # 这个目录下,创建以下文件 mymodule/ __init__.py a.py b.py # a.py class A: def spam(self): print('A.spam') # b.py from .a import A class B(A): def bar(self): print('B.bar') # __init__.py:在 __init__.py 中,将2个文件粘合在一起 from .a import A from .b import B # 使用:产生的包MyModule将作为一个单一的逻辑模块 import mymodule a = mymodule.A() a.spam() # A.spam b = mymodule.B() b.bar() # B.bar
# 不管你是否希望用户使用很多小模块或只是一个模块 # 在一个大型的代码库中,你可以将这一切都分割成独立的文件,让用户使用大量的import语句 from mymodule.a import A from mymodule.b import B ... # 这样能工作,但这让用户承受更多的负担,用户要知道不同的部分位于何处。
# 通常情况下,将这些统一起来,使用一条import将更加容易 from mymodule import A, B # 对后者而言,让mymodule成为一个大的源文件是最常见的。 # 如何合并多个文件合并成一个单一的逻辑命名空间。
这样做的关键是创建一个包目录,使用 __init__.py 文件来将每部分粘合在一起 # 当一个模块被分割,你需要特别注意交叉引用的文件名。
在这一章节中,B类需要访问A类作为基类。用包的相对导入 from .a import A 来获取 # 整个章节都使用包的相对导入来避免将顶层模块名硬编码到源代码中。
这使得重命名模块或者将它移动到别的位置更容易
# 延迟导入:__init__.py文件一次导入所有必需的组件的 # 对于一个很大的模块,想组件在需要时被加载。 要做到这一点,__init__.py有细微的变化
# __init__.py def A(): from .a import A return A() def B(): from .b import B return B() # 类A和类B被替换为在第一次访问时加载所需的类的函数。对于用户,这看起来不会有太大的不同 import mymodule # 会自动执行mymodule模块的__init__,所以A函数和B函数都存在 a = mymodule.A() a.spam() # 延迟加载的主要缺点是继承和类型检查可能会中断。需要稍微改变代码 if isinstance(x, mymodule.A): # Error:mymodule.A返回的是一个函数
if isinstance(x, mymodule.a.A): # Ok:mymodule.a.A返回的是一个类
import mymodule
a = mymodule.A() # 返回的是A()
# print(mymodule.a)
# <module 'mymodule.a' from 'E:\\Users\\ywt\\PycharmProjects\\ywt_test\\mymodule\\a.py'>
# print(mymodule.a.A)
# <class 'mymodule.a.A'>
# A()调用了A函数后,在mymodule命名空间内部存储了类A,对应模块空间a.py
5:利用命名空间导入目录分散的代码 大量的代码,每个部分被组织为文件目录如一个包,希望能用共同的包前缀将所有组件连接起来,不是将每一个部分作为独立的包来安装
# 定义一个顶级Python包作为一个大集合分开维护子包的命名空间 # 统一不同的目录里统一相同的命名空间,要删去用来将组件联合起来的__init__.py文件 # 有Python代码的两个不同的目录如下 foo-package/ spam/ blah.py bar-package/ spam/ grok.py # 这2个目录里,都有着共同的命名空间spam。在任何一个目录里都没有__init__.py文件。 # 将foo-package和bar-package都加到python模块路径 import sys sys.path.extend(['foo-package', 'bar-package']) import spam.blah import spam.grok # 两个不同的包目录被合并到一起,你可以导入spam.blah和spam.grok,并且它们能够工作
把foo-package,bar-package两个目录加入path路径,后面import spam.blah和import spam.grok
导入模块会从path路径的默认模块找
# 包命名空间”的一个特征 # 包命名空间是一种特殊的封装设计,为合并不同的目录的代码到一个共同的命名空间。
对于大的框架,这可能是有用的,因为它允许一个框架的部分被单独地安装下载。
它也使人们能够轻松地为这样的框架编写第三方附加组件和其他扩展 # 包命名空间的关键是确保顶级目录中没有__init__.py文件来作为共同的命名空间。
缺失__init__.py文件使得在导入包的时候会发生有趣的事情:这并没有产生错误,
解释器创建了一个由所有包含匹配包名的目录组成的列表。特殊的包命名空间模块被创建,
只读的目录列表副本被存储在其__path__变量中 import spam spam.__path__
# _NamespacePath(['foo-package/spam', 'bar-package/spam'])
# 在定位包的子组件时,目录__path__将被用到(例如, 当导入spam.grok或者spam.blah的时候).
# 包命名空间的一个重要特点是任何人都可以用自己的代码来扩展命名空间。举个例子,假设你自己的代码目录像这样 my-package/ spam/ custom.py # 将你的代码目录和其他包一起添加到sys.path,这将无缝地合并到别的spam包目录中: >>> import spam.custom >>> import spam.grok >>> import spam.blah # 一个包是否被作为一个包命名空间的主要方法是检查其__file__属性。如果没有,
那包是个命名空间。这也可以由其字符表现形式中的“namespace”这个词体现出来 >>> spam.__file__ Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'module' object has no attribute '__file__' >>> spam <module 'spam' (namespace)> >>>
6:重新加载模块 对源码进行了修改,重新加载已经加载的模块 importlib.reload
# imp.reload()来重新加载先前加载的模块 import test_002 import importlib print(importlib.reload(test_002)) # <module 'test_002' from 'E:\\Users\\ywt\\PycharmProjects\\ywt_test\\test_002.py'>
# 重新加载模块在开发和调试过程中常常很有用。但在生产环境中的代码使用会不安全
# reload()擦除了模块底层字典的内容,并通过重新执行模块的源代码来刷新它。
模块对象本身的身份保持不变。因此,该操作在程序中所有已经被导入了的地方更新了模块
# reload()没有更新像”from module import name”这样使用import语句导入的定义 import spam from spam import grok spam.bar() # bar grok() # grok # 不退出Python修改spam.py的源码,修改grok()函数 def grok(): print('New grok') # 回到交互式会话,重新加载模块 import imp imp.reload(spam) spam.bar() # bar grok() # grok 旧的输出 spam.grok() # New grok 新的输出 # 上面2个版本的grok()函数被加载
7:运行目录或压缩文件 一个已成长为包含多个文件的应用不再是一个简单的脚本,向用户提供一些简单的方法运行这个程序
# 应用程序已经有多个文件 # 可以把应用程序放进它自己的目录并添加一个__main__.py文件 myapplication/ spam.py bar.py grok.py __main__.py # 如果__main__.py存在,可以简单地在顶级目录运行Python解释器 python3 myapplication # 解释器将执行__main__.py文件作为主程序。 # 将代码打包成zip文件,这种技术同样也适用 bash % ls spam.py bar.py grok.py __main__.py bash % zip -r myapp.zip *.py bash % python3 myapp.zip ... output from __main__.py ...
# 创建一个目录或zip文件并添加__main__.py文件来将一个更大的Python应用打包是可行的。
这和作为标准库被安装到Python库的代码包是有一点区别的。相反,这只是让别人执行的代码包
# 由于目录和zip文件与正常文件有一点不同,你可能还需要增加一个shell脚本,使执行更加容易。
例如,如果代码文件名为myapp.zip,你可以创建这样一个顶级脚本
#!/usr/bin/env python3 /usr/local/bin/myapp.zip
8:读取位于包中的数据文件 包中包含代码需要去读取的数据文件
# 文件组织 mypackage/ __init__.py somedata.dat spam.py # spam.py文件需要读取somedata.dat文件中的内容 # spam.py import pkgutil data = pkgutil.get_data('mypackage', 'somedata.dat')
print(data) # b'1111111\r\n2222222222\r\n333333333333333\r\n4444444444444444'
# 由此产生的变量是包含该文件的原始内容的字节字符串bytes类型。
# 读取数据文件,倾向于编写使用内置的I/ O功能的代码,open() # open()种方法也有一些问题 # 一:一个包对解释器的当前工作目录几乎没有控制权。因此,编程时任何I/O操作都必须使用绝对文件名。
由于每个模块包含有完整路径的__file__变量,这弄清楚它的路径不是不可能,但它很凌乱 # 二:包通常安装作为.zip或.egg文件,这些文件并不像在文件系统上的一个普通目录里那样被保存。
因此,你试图用open()对一个包含数据文件的归档文件进行操作,它根本不会工作 # pkgutil.get_data()函数是一个读取数据文件的高级工具,
不用管包是如何安装以及安装在哪。它只是工作并将文件内容以字节字符串返回给你 # get_data()的第一个参数是包含包名的字符串。你可以直接使用包名,也可以使用特殊的变量,
比如__package__。第二个参数是包内文件的相对名称。如果有必要,
可以使用标准的Unix命名规范到不同的目录,只要最后的目录仍然位于包中
9:将文件夹加入到sys.path
# 无法导入你的Python代码因为它所在的目录不在sys.path里, # 添加新目录到Python路径 # 方法一:使用PYTHONPATH环境变量来添加 import sys print(sys.path) sys.path.append('xxx') print(sys.path) # 自定义应用程序中,这样的环境变量可在程序启动时设置或通过shell脚本。 # 方法二:创建一个.pth文件,将目录列举出来 # myapplication.pth /some/dir /other/dir
# pth文件需要放在某个Python的site-packages目录,
通常位于/usr/local/lib/python3.3/site-packages 或者 ~/.local/lib/python3.3/sitepackages。
当解释器启动时,.pth文件里列举出来的存在于文件系统的目录将被添加到sys.path。
安装一个.pth文件可能需要管理员权限,如果它被添加到系统级的Python解释器
# 倾向于写一个代码手动调节sys.path的值、 import sys sys.path.insert(0, '/some/dir') sys.path.insert(0, '/other/dir') # 虽然这能“工作”,但是在实践中极为脆弱,应尽量避免使用。这种方法的问题是,
它将目录名硬编码到了源代码。如果你的代码被移到一个新的位置,这会导致维护问题。
更好的做法是在不修改源代码的情况下,将path配置到其他地方。
使用模块级的变量来精心构造一个适当的绝对路径,有时你可以解决硬编码目录的问题,比如__file__ import sys from os.path import abspath, join, dirname sys.path.insert(0, join(abspath(dirname(__file__)), 'src')) # 将src目录添加到path里,和执行插入步骤的代码在同一个目录里。 # site-packages目录是第三方包和模块安装的目录。如果你手动安装你的代码,
它将被安装到site-packages目录。虽然用于配置path的.pth文件必须放置在site-packages里,
但它配置的路径可以是系统上任何你希望的目录。因此,你可以把你的代码放在一系列不同的目录,只要那些目录包含在.pth文件里
import sys
from os.path import abspath, join, dirname
# os.path.abspath(path) 返回path规范化的绝对路径
sys.path.insert(0, join(abspath(dirname(__file__)), 'src'))
print(__file__) # E:\Users\ywt\PycharmProjects\ywt_test\mypackage\spam.py
print(dirname(__file__)) # E:\Users\ywt\PycharmProjects\ywt_test\mypackage
print(abspath(dirname(__file__))) # E:\Users\ywt\PycharmProjects\ywt_test\mypackage
print(join(abspath(dirname(__file__)), 'src')) # E:\Users\ywt\PycharmProjects\ywt_test\mypackage\src
__file__代码运行到的模块提取出签名的路径然后+src添加到sys.path路径里面
10: 通过字符串名导入模块 想导入一个模块,但是模块的名字在字符串里,对字符串调用导入命令。 importlib.import_module()
# importlib.import_module()函数来手动导入名字为字符串给出的一个模块或者包的一部分 import importlib math = importlib.import_module('math') print(math.sin(2)) # 0.9092974268256817 sp = importlib.import_module('mypackage.spam') print(sp.a) # 10 # import_module只是简单地执行和import相同的步骤,但是返回生成的模块对象。
你只需要将其存储在一个变量,然后像正常的模块一样使用 # 如果你正在使用的包,import_module()也可用于相对导入。但是,你需要给它一个额外的参数 import importlib # Same as 'from . import b' b = importlib.import_module('.b', __package__)
importlib.import_module里面的参数只能到包或者模块不能到变量
# 导入模块实际使用如下:mypackage文件下有a.py和b.py,运行主入口main函数和mypackage同一级目录
import importlib
b = importlib.import_module('mypackage')
print(b.a) # 10
b = importlib.import_module('.b', 'mypackage')
print(b.b) # 20
# import_module()手动导入模块的问题通常出现在以某种方式编写修改或覆盖模块的代码时候。
例如,也许你正在执行某种自定义导入机制,需要通过名称来加载一个模块,通过补丁加载代码 # 在旧的代码,有时你会看到用于导入的内建函数__import__()。尽管它能工作,
但是importlib.import_module() 通常更容易使用
11:通过钩子远程加载模块 自定义Python的import语句,使得它能从远程机器上面透明的加载模块
# 设计导入语句的扩展功能 # 代码结构 testcode/ spam.py fib.py grok/ __init__.py blah.py # py文件简单内容 # spam.py print("I'm spam") def hello(name): print('Hello %s' % name) # fib.py print("I'm fib") def fib(n): if n < 2: return 1 else: return fib(n-1) + fib(n-2) # grok/__init__.py print("I'm grok.__init__") # grok/blah.py print("I'm grok.blah") # 下面的操作是允许这些文件作为模块被远程访问。 也许最简单的方式就是将它们发布到一个web服务器上面。
在testcode目录中像下面这样运行Python bash % cd testcode bash % python3 -m http.server 15000 Serving HTTP on 0.0.0.0 port 15000 ... # 服务器运行起来后再启动一个单独的Python解释器。 确保你可以使用 urllib 访问到远程文件 >>> from urllib.request import urlopen >>> u = urlopen('http://localhost:15000/fib.py') >>> data = u.read().decode('utf-8') >>> print(data) # fib.py print("I'm fib") def fib(n): if n < 2: return 1 else: return fib(n-1) + fib(n-2) >>> # 从这个服务器加载源代码是接下来的基础。 为了替代手动的通过 urlopen() 来收集源文件,
通过自定义import语句来在后台自动帮我们做到 # 加载远程模块的第一种方法是创建一个显式的加载函数来完成它 import imp import urllib.request import sys def load_module(url): u = urllib.request.urlopen(url) source = u.read().decode('utf-8') mod = sys.modules.setdefault(url, imp.new_module(url)) code = compile(source, url, 'exec') mod.__file__ = url mod.__package__ = '' exec(code, mod.__dict__) return mod # 这个函数会下载源代码,并使用 compile() 将其编译到一个代码对象中,
然后在一个新创建的模块对象的字典中来执行它。下面是使用这个函数的方式 >>> fib = load_module('http://localhost:15000/fib.py') I'm fib >>> fib.fib(10) 89 >>> spam = load_module('http://localhost:15000/spam.py') I'm spam >>> spam.hello('Guido') Hello Guido >>> fib <module 'http://localhost:15000/fib.py' from 'http://localhost:15000/fib.py'> >>> spam <module 'http://localhost:15000/spam.py' from 'http://localhost:15000/spam.py'> >>>
待学
12:导入模块的同时修改模块
待学
13:安装私有的包
待学
14:创建新的Python环境
待学
15:分发包
待学
16:作为客户端与HTTP服务交互
# 通过HTTP协议以客户端的方式访问多种服务,下载数据或者与基于REST的API进行交互 # 简单的http请求使用urllib.request 模块和request模块就行了 from urllib import request, parse # 访问的URL url = 'http://httpbin.org/get' # 查询参数字典(如果有) parms = { 'name1': 'value1', 'name2': 'value2' } # 对查询字符串进行编码 querystring = parse.urlencode(parms) # 发出GET请求并读取响应 u = request.urlopen(url + '?' + querystring) resp = u.read() # resp返回的是bytes类型
# 使用POST方法在请求主体中发送查询参数,将参数编码后作为可选参数提供给 urlopen() 函数 from urllib import request, parse url = 'http://httpbin.org/post' parms = { 'name1': 'value1', 'name2': 'value2' } querystring = parse.urlencode(parms) u = request.urlopen(url, querystring.encode('ascii')) resp = u.read()
# 发出的请求中提供一些自定义的HTTP头 # 修改 user-agent 字段 # 可以创建一个包含字段值的字典,并创建一个Request实例然后将其传给 urlopen() from urllib import request, parse url = 'http://httpbin.org/post' parms = { 'name1': 'value1', 'name2': 'value2' } headers = { 'User-agent': 'none/ofyourbusiness', 'Spam': 'Eggs' } querystring = parse.urlencode(parms) # querystring = name1=value1&name2=value2 req = request.Request(url, querystring.encode('ascii'), headers=headers) # 提出请求并阅读响应 u = request.urlopen(req) resp = u.read()
# 交互的服务复杂的使用requests import requests url = 'http://httpbin.org/post' parms = { 'name1': 'value1', 'name2': 'value2' } headers = { 'User-agent': 'none/ofyourbusiness', 'Spam': 'Eggs' } resp = requests.post(url, data=parms, headers=headers) print(resp.text)
# requests库能以多种方式从请求中返回响应结果的内容 # resp.text 带给我们的是以Unicode解码的响应文本。
resp.content 得到原始的二进制数据。
resp.json 那么就会得到JSON格式的响应内容
# requests 库发起一个HEAD请求并从响应中提取出一些HTTP头数据的字段
import requests
resp = requests.head('http://www.python.org/index.html')
status = resp.status_code # 301
# print(resp.headers)
# {'Server': 'Varnish', 'Retry-After': '0',
# 'Location': 'https://www.python.org/index.html',
# 'Content-Length': '0', 'Accept-Ranges': 'bytes',
# 'Date': 'Fri, 24 Sep 2021 03:22:12 GMT', 'Via': '1.1 varnish',
# 'Connection': 'close', 'X-Served-By': 'cache-tyo11968-TYO',
# 'X-Cache': 'HIT', 'X-Cache-Hits': '0', 'X-Timer': 'S1632453732.358167,VS0,VE0',
# 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains'}
# print(type(resp.headers)) # <class 'requests.structures.CaseInsensitiveDict'>
# print(resp.headers['Server']) # Varnish
# resp.headers返回的是一个class类型,可以当成字典那样键值对取值
# last_modified = resp.headers['last-modified']
# content_type = resp.headers['content-type']
# content_length = resp.headers['content-length']
# 当前网址301被重定向无法使用了
# requests通过基本认证登录Pypi的例子
import requests
resp = requests.get('http://pypi.python.org/pypi?:action=login', auth=('user','password'))
# 利用requests将HTTP cookies从一个请求传递到另一个的例子 import requests resp1 = requests.get('https://www.baidu.com/') resp2 = requests.get('https://www.baidu.com/', cookies=resp1.cookies) # print(resp2.text.encode().decode('utf-8')) # 返回的是字符串类型需要先编码再解码 print(resp2.content.decode('utf-8')) # 这样返回的数据才是正常的,返回的二进制自己指定utf8进行解码, # 上面的返回的text自己根据默认的unicode编码把bytes类型解码成字符串了,需要是utf8的编码才是正确的 # resp2.encoding = 'utf-8'这种方式指定编码也可以 import requests resp1 = requests.get('https://www.baidu.com/') resp2 = requests.get('https://www.baidu.com/', cookies=resp1.cookies) resp2.encoding = 'utf-8' print(resp2.text)
# 用requests上传内容 files参数 import requests url = 'http://httpbin.org/post' files = { 'file': ('data.csv', open('data.csv', 'rb')) } r = requests.post(url, files=files) # 使用files参数
# 简单HTTP客户端代码,内置的 urllib 模块通常就足够了 # 不仅仅只是简单的GET或POST请求,使用第三方模块比如 requests # 如果nc坚持使用标准的程序库而不考虑像 requests 这样的第三方库,
使用底层的 http.client 模块来实现自己的代码 from http.client import HTTPConnection c = HTTPConnection('www.python.org', 80) c.request('HEAD', '/index.html') # head请求 resp = c.getresponse() print('Status', resp.status) for name, value in resp.getheaders(): print(name, value)
# 涉及代理、认证、cookies以及其他一些细节方面的代码使用 urllib 就显得特别别扭和啰嗦 import urllib.request auth = urllib.request.HTTPBasicAuthHandler() auth.add_password('pypi', 'http://pypi.python.org', 'username', 'password') opener = urllib.request.build_opener(auth) r = urllib.request.Request('http://pypi.python.org/pypi?:action=login') u = opener.open(r) resp = u.read() # 这些操作在 requests 库中都变得简单的多
# 测试HTTP客户端代码关注:cookies、认证、HTTP头、编码方式等 # 用httpbin服务这个站点会接收发出的请求,然后以JSON的形式将相应信息回传回来 import requests r = requests.get('http://httpbin.org/get?name=Dave&n=37', headers={'User-agent': 'goaway/1.0'}) print(r.json()) print(r.json()['headers']) print(r.json()['args']) # 同一个真正的站点进行交互前,先在 httpbin.org 这样的网站上做实验常常是可取的办法。
尤其是当我们面对3次登录失败就会关闭账户这样的风险时尤为有用(不要尝试自己编写HTTP认证客户端来登录你的银行账户) request库学习网址:http://docs.python-requests.org
17:创建TCP服务器 实现一个服务器,通过TCP协议和客户端通信 AF_INET:基于网络的套件字 SOCK_STREAM:面向连接的套接字
# 创建一个TCP服务器的一个简单方法是使用 socketserver 库 # 下面是一个简单的应答服务器 from socketserver import BaseRequestHandler, TCPServer class EchoHandler(BaseRequestHandler): def handle(self): # handle编写主逻辑 print('Got connection from', self.client_address) while True: msg = self.request.recv(8192) if not msg: break self.request.send(msg) if __name__ == '__main__': serv = TCPServer(('', 20000), EchoHandler) serv.serve_forever() # 定义了一个特殊的处理类EchoHandler,实现了一个 handle() 方法,用来为客户端连接服务 # request 属性是服务端socket,client_address 有客户端地址
# 定义一个不同的处理器 # 使用 StreamRequestHandler 基类将一个类文件接口放置在底层socket上 from socketserver import StreamRequestHandler, TCPServer class EchoHandler(StreamRequestHandler): def handle(self): print('Got connection from', self.client_address) # self.rfile是用于读取的类似文件的对象 for line in self.rfile: print(line) # self.wfile是用于写入的类似文件的对象 self.wfile.write(line) print('for 循环退出') # for循环变量一个rfile文件句柄或者类似文件句柄得对象,一定要读到一个\r\n后for循环里面得代码才运行 # 读取到\r\n才算一次for循环 # 这里for循环其实一直没有退出一直在接收数据 if __name__ == '__main__': serv = TCPServer(('', 20000), EchoHandler) serv.serve_forever()
# socketserver 模块可以很容易的创建简单的TCP服务器,默认情况下这种服务器是单线程的,一次只能为一个客户端连接服务 # 处理多个客户端,可以初始化一个 ForkingTCPServer 或者是 ThreadingTCPServer 对象 from socketserver import ThreadingTCPServer, BaseRequestHandler class EchoHandler(BaseRequestHandler): def handle(self): print('Got connection from', self.client_address) while True: msg = self.request.recv(8192) if not msg: break self.request.send(msg) if __name__ == '__main__': serv = ThreadingTCPServer(('', 20000), EchoHandler) serv.serve_forever() # 使用fork或线程服务器有个潜在问题就是它们会为每个客户端连接创建一个新的进程或线程 # 客户端连接数是没有限制的,因此一个恶意的黑客可以同时发送大量的连接让你的服务器奔溃
# 为了避免同时发送大量的连接让你的服务器奔溃,创建一个预先分配大小的工作线程池或进程池, 先创建一个普通的非线程服务器,然后在一个线程池中使用 serve_forever() 方法来启动它们 from socketserver import TCPServer, BaseRequestHandler from threading import Thread class EchoHandler(BaseRequestHandler): def handle(self): print('Got connection from', self.client_address) while True: msg = self.request.recv(8192) if not msg: break self.request.send(msg) NWORKERS = 16 serv = TCPServer(('', 20000), EchoHandler) for i in range(NWORKERS): t = Thread(target=serv.serve_forever) t.daemon = True t.start() serv.serve_forever() # 开始16个线程每个线程设置守护线程后然后主线程再设置1个serv.serve_forever # 相当于一个17个线程等着用户进来进行处理 # 相当于绑定了一个ip地址和端口为银行,开启了17个柜台为客户服务,17个柜台的主逻辑都一样的
# 一个 TCPServer 在实例化的时候会绑定并激活相应的 socket # 有时候想通过设置某些选项去调整底下的 socket` ,可以设置参数 bind_and_activate=False if __name__ == '__main__': serv = TCPServer(('', 20000), EchoHandler, bind_and_activate=False) # 先不激活socket,设置参数为False # 设置各种套接字选项 serv.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) # 把参数设置进socket里面 # 绑定并激活 serv.server_bind() # 绑定设置参数 serv.server_activate() # 激活socket serv.serve_forever() # socket一直运行监听 # 上面的 socket 设置选项是一个非常普遍的配置项,它允许服务器重新绑定一个之前使用过的端口号。 由于要被经常使用到,它被放置到类变量中,可以直接在 TCPServer 上面设置。 在实例化服务器的时候去设置它的值 if __name__ == '__main__': TCPServer.allow_reuse_address = True serv = TCPServer(('', 20000), EchoHandler) serv.serve_forever()
重新绑定之前使用过的端口号:
端口有好几个状态,可以重复绑定哪些已经关闭但是没有关闭完全的端口
不加这个allow_reuse_address状态参数只能绑定哪些完全已经关闭的端口
# 两种不同的处理器基类( BaseRequestHandler 和 StreamRequestHandler ) # StreamRequestHandler 更加灵活点,能通过设置其他的类变量来支持一些新的特性 import socket from socketserver import StreamRequestHandler class EchoHandler(StreamRequestHandler): # 可选设置(显示默认值) timeout = 5 # 所有套接字操作超时 rbufsize = -1 # 读取缓冲区大小 wbufsize = 0 # 写入缓冲区大小 disable_nagle_algorithm = False # 设置TCP_节点延迟套接字选项 def handle(self): print('Got connection from', self.client_address) try: for line in self.rfile: # self.wfile是用于写入的类似文件的对象 self.wfile.write(line) except socket.timeout: print('Timed out!')
# 绝大部分Python的高层网络模块(比如HTTP、XML-RPC等)都是建立在 socketserver 功能之上 # 直接使用 socket 库来实现服务器也并不是很难 # 使用socket编写一个服务器简单例子 from socket import socket, AF_INET, SOCK_STREAM def echo_handler(address, client_sock): print('Got connection from {}'.format(address)) while True: msg = client_sock.recv(8192) if not msg: break client_sock.sendall(msg) client_sock.close() def echo_server(address, backlog=5): sock = socket(AF_INET, SOCK_STREAM) sock.bind(address) sock.listen(backlog) while True: client_sock, client_addr = sock.accept() echo_handler(client_addr, client_sock) if __name__ == '__main__': echo_server(('', 20000))
18:创建UDP服务器 SOCK_DGRAM:面向无连接的套接字 AF_INET:基于网络的套件字
# UDP服务器也可以通过使用 socketserver 库很容易的被创建 # 简单的时间服务器 from socketserver import BaseRequestHandler, UDPServer import time class TimeHandler(BaseRequestHandler): def handle(self): print(f"{self.client_address}连接进来") # 获取消息和客户端套接字 msg, sock = self.request reps = time.ctime() sock.sendto(reps.encode('utf8'), self.client_address) serv = UDPServer(('127.0.0.1', 20000), TimeHandler) serv.serve_forever() # 定义一个实现 handle() 特殊方法的类,为客户端连接服务 # 这个类的 request 属性是一个包含了数据报和底层socket对象的元组。client_address 包含了客户端地址 客户端连接实例: import socket sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sk.sendto(b'hello', ('127.0.0.1', 20000)) print(sk.recvfrom(1024)) # (b'Sun Sep 26 09:55:58 2021', ('127.0.0.1', 20000))
# 典型的UDP服务器接收到达的数据报(消息)和客户端地址。如果服务器需要做应答,
它要给客户端回发一个数据报。UDP对于数据报的传送, 应该使用socket的 sendto() 和 recvfrom() 方法。
尽管传统的 send() 和 recv() 也可以达到同样的效果, 但是前面的两个方法对于UDP连接而言更普遍 # 由于没有底层的连接,UPD服务器相对于TCP服务器来讲实现起来更加简单。
不过,UDP天生是不可靠的(因为通信没有建立连接,消息可能丢失)。 因此需要由自己来决定该怎样处理丢失消息的情况。
通常来说,如果可靠性对于你程序很重要,可以借助于序列号、重试、超时以及一些其他方法来保证。
UDP通常被用在那些对于可靠传输要求不是很高的场合。例如,在实时应用如多媒体流以及游戏领域, 无需返回恢复丢失的数据包(程序只需简单的忽略它并继续向前运行)
# UDPServer 类是单线程的,一次只能为一个客户端连接服务 # 实现并发操作实例化一个 ForkingUDPServer 或 ThreadingUDPServer 对象 from socketserver import BaseRequestHandler, ThreadingUDPServer import time class TimeHandler(BaseRequestHandler): def handle(self): print(f"{self.client_address}连接进来") # 获取消息和客户端套接字 msg, sock = self.request reps = time.ctime() sock.sendto(reps.encode('utf8'), self.client_address) serv = ThreadingUDPServer(('127.0.0.1', 20000), TimeHandler) serv.serve_forever()
# 直接使用socket 来实现一个UDP服务器也很简单 import socket import time def time_server(address): sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sk.bind(address) while 1: msg, addr = sk.recvfrom(1024) # 一直阻塞在这里等待连接,连接进来才运行下面的内容 print(f'{addr}连接进来') if not msg: break sk.sendto(time.ctime().encode('utf8'), addr) time_server(('127.0.0.1', 20000))
19:通过CIDR地址生成对应的IP地址集 一个CIDR网络地址比如“123.45.67.89/27”,将其转换成它所代表的所有IP (比如,“123.45.67.64”, “123.45.67.65”, …, “123.45.67.95”))
# ipaddress 模块计算CIDR地址生成对应的IP地址集 import ipaddress net4 = ipaddress.ip_network('123.45.67.64/27') # print(net) # 123.45.67.64/27 这是:IPv4Network for i in net4: print(i) # 输出:123.45.67.64到123.45.67.95 net6 = ipaddress.ip_network('12:3456:78:90ab:cd:ef01:23:30/125') # print(net6) # 12:3456:78:90ab:cd:ef01:23:30/125 for i in net6: print(i) # 输出:12:3456:78:90ab:cd:ef01:23:30到12:3456:78:90ab:cd:ef01:23:37 # Network 允许像数组一样的索引取值 import ipaddress net4 = ipaddress.ip_network('123.45.67.64/27') print(net4.num_addresses) # 32 print(net4[0]) # 123.45.67.64 print(net4[1]) # 123.45.67.65 # 可以执行网络成员检查之类的操作 import ipaddress net4 = ipaddress.ip_network('123.45.67.64/27') a = ipaddress.ip_address('123.45.67.69') print(a in net4) # True b = ipaddress.ip_address('123.45.67.123') print(b in net4) # False # 一个IP地址和网络地址能通过一个IP接口来指定 import ipaddress inet = ipaddress.ip_interface('123.45.67.73/27') print(inet.network) # 123.45.67.64/27
print(inet.ip) # 123.45.67.73
# ipaddress 模块有很多类可以表示IP地址、网络和接口。操作网络地址(比如解析、打印、验证等)的时候会很有用 # ipaddress 模块跟其他一些和网络相关的模块比如 socket 库交集很少,
不能使用 IPv4Address 的实例来代替一个地址字符串,首先得显式的使用 str() 转换它 a = ipaddress.ip_address('127.0.0.1') from socket import socket, AF_INET, SOCK_STREAM s = socket(AF_INET, SOCK_STREAM) s.connect((a, 8080)) # TypeError: Can't convert 'IPv4Address' object to str implicitly s.connect((str(a), 8080)) # 这样使用才正常
20:创建一个简单的REST接口 使用一个简单的REST接口通过网络远程控制或访问应用程序
# 构建一个REST风格的接口最简单的方法是创建一个基于WSGI标准(PEP 3333)的很小的库 test001.py import cgi def notfound_404(environ, start_response): start_response('404 Not Found', [('Content-type', 'text/plain')]) return [b'Not Found'] class PathDispatcher: def __init__(self): self.pathmap = {} def __call__(self, environ, start_response): path = environ['PATH_INFO'] params = cgi.FieldStorage(environ['wsgi.input'], environ=environ) method = environ['REQUEST_METHOD'].lower() environ['params'] = {key: params.getvalue(key) for key in params} handler = self.pathmap.get((method, path), notfound_404) return handler(environ, start_response) def register(self, method, path, function): self.pathmap[method.lower(), path] = function return function test002.py import time from test_001 import PathDispatcher from wsgiref.simple_server import make_server _hello_resp = '''\ <html> <head> <title>Hello {name}</title> </head> <body> <h1>Hello {name}!</h1> </body> </html>''' def hello_world(environ, start_response): start_response('200 OK', [('Content-type', 'text/html')]) params = environ['params'] resp = _hello_resp.format(name=params.get('name')) yield resp.encode('utf-8') _localtime_resp = '''\ <?xml version="1.0"?> <time> <year>{t.tm_year}</year> <month>{t.tm_mon}</month> <day>{t.tm_mday}</day> <hour>{t.tm_hour}</hour> <minute>{t.tm_min}</minute> <second>{t.tm_sec}</second> </time>''' def localtime(environ, start_response): start_response('200 OK', [('Content-type', 'application/xml')]) resp = _localtime_resp.format(t=time.localtime()) yield resp.encode('utf-8') # 创建dispatcher和register函数 dispatcher = PathDispatcher() dispatcher.register('GET', '/hello', hello_world) dispatcher.register('GET', '/localtime', localtime) # 启动基本服务器 httpd = make_server('', 8080, dispatcher) print('Serving on port 8080...') httpd.serve_forever()
# 使用一个浏览器或urllib
和它交互
u = urlopen('http://localhost:8080/hello?name=Guido')
u = urlopen('http://localhost:8080/localtime')
# 实现一个简单的REST接口,需让程序代码满足Python的WSGI标准即可。
WSGI被标准库支持,同时也被绝大部分第三方web框架支持,代码遵循这个标准,在后面的使用过程中就会更加的灵活 # WSGI中,如下方式以一个可调用对象形式来实现程序 import cgi def wsgi_app(environ, start_response): pass
# environ 属性是一个字典,包含了从web服务器如Apache[参考Internet RFC 3875]提供的CGI接口中获取的值。
可以将这些不同的值提取出来 def wsgi_app(environ, start_response): method = environ['REQUEST_METHOD'] path = environ['PATH_INFO'] # 解析查询参数 params = cgi.FieldStorage(environ['wsgi.input'], environ=environ) # 展示了一些常见的值 # environ['REQUEST_METHOD'] 代表请求类型如GET、POST、HEAD等 # environ['PATH_INFO'] 表示被请求资源的路径 # 调用 cgi.FieldStorage() 可以从请求中提取查询参数并将它们放入一个类字典对象中以便后面使用 # start_response 参数是一个为了初始化一个请求对象而必须被调用的函数。
第一个参数是返回的HTTP状态值,第二个参数是一个(名,值)元组列表,用来构建返回的HTTP头 def wsgi_app(environ, start_response): pass start_response('200 OK', [('Content-type', 'text/plain')]) # 为了返回数据,一个WSGI程序必须返回一个字节字符串序列。可以像下面这样使用一个列表来完成 def wsgi_app(environ, start_response): pass start_response('200 OK', [('Content-type', 'text/plain')]) resp = [] resp.append(b'Hello World\n') resp.append(b'Goodbye!\n') return resp # 还可以使用 yield def wsgi_app(environ, start_response): pass start_response('200 OK', [('Content-type', 'text/plain')]) yield b'Hello World\n' yield b'Goodbye!\n'
最后返回的必须是字节字符串。如果返回结果包含文本字符串,必须先将其编码成字节。
并没有要求你返回的一定是文本,你可以很轻松的编写一个生成图片的程序
# WSGI程序通常被定义成一个函数 # 也可以使用类实例来实现,只要它实现了合适的 __call__() 方法 class WSGIApplication: def __init__(self): ... def __call__(self, environ, start_response) ... # 上面使用这种技术创建 PathDispatcher 类
# 这个分发器仅仅只是管理一个字典,将(方法,路径)对映射到处理器函数上面。
当一个请求到来时,它的方法和路径被提取出来,然后被分发到对应的处理器上面去。
另外,任何查询变量会被解析后放到一个字典中,以 environ['params'] 形式存储。
后面这个步骤太常见,所以建议你在分发器里面完成,这样可以省掉很多重复代码。 使用分发器的时候,
你只需简单的创建一个实例,然后通过它注册各种WSGI形式的函数。 编写这些函数应该超级简单了,
只要你遵循 start_response() 函数的编写规则,并且最后返回字节字符串即可。 # 当编写这种函数的时候还需注意的一点就是对于字符串模板的使用。
没人愿意写那种到处混合着 print() 函数 、XML和大量格式化操作的代码。
我们上面使用了三引号包含的预先定义好的字符串模板。
这种方式的可以让我们很容易的在以后修改输出格式(只需要修改模板本身,而不用动任何使用它的地方) # 使用WSGI还有一个很重要的部分就是没有什么地方是针对特定web服务器的。
因为标准对于服务器和框架是中立的,你可以将你的程序放入任何类型服务器中 if __name__ == '__main__': from wsgiref.simple_server import make_server # 创建dispatcher和register函数 dispatcher = PathDispatcher() pass # 启动基本服务器 httpd = make_server('', 8080, dispatcher) print('Serving on port 8080...') httpd.serve_forever() # WSGI本身是一个很小的标准。因此它并没有提供一些高级的特性比如认证、cookies、重定向等。
这些自己实现起来也不难。如果你想要更多的支持,可以考虑第三方库,比如 WebOb 或者 Paste
21:通过XML-RPC实现简单的远程调用 一个简单的方式去执行运行在远程机器上面的Python程序中的函数或方法
# 一个远程方法调用的最简单方式是使用XML-RPC # 一个实现了键-值存储功能的简单服务器 from xmlrpc.server import SimpleXMLRPCServer class KeyValueServer: _rpc_methods_ = ['get', 'set', 'delete', 'exists', 'keys'] def __init__(self, address): self._data = {} self._serv = SimpleXMLRPCServer(address, allow_none=True) for name in self._rpc_methods_: self._serv.register_function(getattr(self, name)) def get(self, name): return self._data[name] def set(self, name, value): self._data[name] = value def delete(self, name): del self._data[name] def exists(self, name): return name in self._data def keys(self): return list(self._data) def serve_forever(self): self._serv.serve_forever() if __name__ == '__main__': kvserv = KeyValueServer(('', 15000)) kvserv.serve_forever() # 从一个客户端机器上面来访问服务器: >>> from xmlrpc.client import ServerProxy >>> s = ServerProxy('http://localhost:15000', allow_none=True) >>> s.set('foo', 'bar') >>> s.set('spam', [1, 2, 3]) >>> s.keys() ['spam', 'foo'] >>> s.get('foo') 'bar' >>> s.get('spam') [1, 2, 3] >>> s.delete('spam') >>> s.exists('spam') False >>>
客户端调用服务端里面运行的代码得到返回值返回给客户端
# XML-RPC 可以很容易的构造一个简单的远程调用服务 # 1:创建一个服务器实例self._serv = SimpleXMLRPCServer(address, allow_none=True) # 2:通过register_function() 来注册函数 # 3:最后使用方法 serve_forever() 启动它 # 上面是写在类里面,也可以写在普通函数里 from xmlrpc.server import SimpleXMLRPCServer def add(x,y): return x+y serv = SimpleXMLRPCServer(('', 15000)) serv.register_function(add) serv.serve_forever()
# XML-RPC暴露出来的函数只能适用于部分数据类型,
比如字符串、整形、列表和字典,对于其他类型就得需要做些额外的功课了 # 想通过 XML-RPC 传递一个对象实例,实际上只有他的实例字典被处理 from xmlrpc.client import ServerProxy class Point: def __init__(self, x, y): self.x = x self.y = y p = Point(2, 3) s = ServerProxy('http://localhost:15000', allow_none=True) # s.set('foo', p) # print(s.get('foo')) # {'x': 2, 'y': 3} # s.set('aaa', '10') # print(s.get('aaa')) # 10 # 对二进制数据的处理也有一些问题(当前测试没问题,二进制也可以处理) from xmlrpc.client import ServerProxy class Point: def __init__(self, x, y): self.x = x self.y = y p = Point(2, 3) s = ServerProxy('http://localhost:15000', allow_none=True) s.set('foo', b'Hello World') print(s.get('foo')) # Hello World
# 不应该将 XML-RPC 服务以公共API的方式暴露出来 # 通常分布式应用程序是一个好的选择 # XML-RPC的一个缺点是它的性能。SimpleXMLRPCServer 的实现是单线程的,
不适合于大型程序,虽然它是可以通过多线程来执行的。 由于 XML-RPC 将所有数据都序列化为XML格式,
所以它会比其他的方式运行的慢一些。 但是它也有优点,这种方式的编码可以被绝大部分其他编程语言支持。
通过使用这种方式,其他语言的客户端程序都能访问你的服务
22:在不同的Python解释器之间交互 不同的机器上面运行着多个Python解释器实例,希望能够在这些解释器之间通过消息来交换数据
# multiprocessing.connection 模块可以很容易的实现解释器之间的通信 from multiprocessing.connection import Listener import traceback def echo_client(conn): try: while True: msg = conn.recv() conn.send(msg) except EOFError: print('Connection closed') def echo_server(address, authkey): serv = Listener(address, authkey=authkey) while True: try: client = serv.accept() # 接收连接进来生成一个类似socket套接字的对象client echo_client(client) # 把套接字传递给echo_client函数 except Exception: traceback.print_exc() echo_server(('', 25000), authkey=b'peekaboo') # 客户端连接服务器并发送消息
from multiprocessing.connection import Client c = Client(('localhost', 25000), authkey=b'peekaboo') c.send('hello') print(c.recv()) # hello c.send(42) print(c.recv()) # 42 c.send([1, 2, 3, 4, 5]) print(c.recv()) # [1, 2, 3, 4, 5] # 跟底层socket不同,每个消息会完整保存(每一个通过send()发送的对象能通过recv()来完整接受)。
另外,所有对象会通过pickle序列化。因此,任何兼容pickle的对象都能在此连接上面被发送和接收
# 实现各种消息传输的包和函数库ZeroMQ、Celery等 # 可以自己在底层socket基础之上来实现一个消息传输层。 # 也可以使用现成的库multiprocessing.connection,使用一些简单的语句即可实现多个解释器之间的消息通信 # 解释器运行在同一台机器上面,可以使用另外的通信机制,Unix域套接字或者是Windows命名管道 # 使用UNIX域套接字来创建一个连接,将地址改写一个文件名即可 s = Listener('/tmp/myconn', authkey=b'peekaboo') # 想使用Windows命名管道来创建连接,使用一个文件名 s = Listener(r'\\.\pipe\myconn', authkey=b'peekaboo') # 不要使用 multiprocessing 来实现一个对外的公共服务。
Client() 和 Listener() 中的 authkey 参数用来认证发起连接的终端用户。
如果密钥不对会产生一个异常。该模块最适合用来建立长连接(而不是大量的短连接),
例如,两个解释器之间启动后就开始建立连接并在处理某个问题过程中会一直保持连接状态
23:实现远程方法调用 在一个消息传输层如 sockets
、multiprocessing connections
或 ZeroMQ
的基础之上实现一个简单的远程过程调用(RPC)
# 将函数请求、参数和返回值使用pickle编码后,在不同的解释器直接传送pickle字节字符串, 可以很容易的实现RPC。 # 一个简单的PRC处理器,可以被整合到一个服务器中去 import pickle class RPCHandler: def __init__(self): self._functions = { } def register_function(self, func): self._functions[func.__name__] = func def handle_connection(self, connection): try: while True: # 收到消息 func_name, args, kwargs = pickle.loads(connection.recv()) # 运行RPC并发送响应 try: r = self._functions[func_name](*args,**kwargs) connection.send(pickle.dumps(r)) except Exception as e: connection.send(pickle.dumps(e)) except EOFError: pass # 使用这个处理器,你需要将它加入到一个消息服务器中。你有很多种选择, 使用 multiprocessing 库是最简单的。下面是一个RPC服务器例子 from multiprocessing.connection import Listener from threading import Thread def rpc_server(handler, address, authkey): sock = Listener(address, authkey=authkey) while True: client = sock.accept() # tcp连接,接收用户进来生成一个client套接字 # 每次进来一个套接字就启动一个线程,线程启用handle_connection函数,并且传递进去参数client这个套接字 # handle_connection里面的逻辑是使用client这个套接字recv消息然后使用pickle.loads解析出数据 # func_name函数名称 args和kwargs是参数,得到函数和参数后内部运行函数之后把结果发送出去 t = Thread(target=handler.handle_connection, args=(client,)) t.daemon = True # 设置守护线程 t.start() # 启动线程 # 一些远程功能 def add(x, y): return x + y def sub(x, y): return x - y # 向处理程序注册 handler = RPCHandler() handler.register_function(add) handler.register_function(sub) # 运行服务器 rpc_server(handler, ('localhost', 17000), authkey=b'peekaboo') # 从一个远程客户端访问服务器,你需要创建一个对应的用来传送请求的RPC代理类 import pickle class RPCProxy: def __init__(self, connection): self._connection = connection def __getattr__(self, name): def do_rpc(*args, **kwargs): self._connection.send(pickle.dumps((name, args, kwargs))) result = pickle.loads(self._connection.recv()) if isinstance(result, Exception): raise result return result return do_rpc # 使用这个代理类,你需要将其包装到一个服务器的连接上面, from multiprocessing.connection import Client c = Client(('localhost', 17000), authkey=b'peekaboo') proxy = RPCProxy(c) # proxy.add调用类RPCProxy的__getattr__方法 # 对象.属性都会调用__getattribute__方法,如果某个属性在__getattribute__方法中未能找到,此时会调用__getattr__方法 # 这里proxy.add找不到add所以都会调用__getattr__方法返回do_rpc函数 # proxy.add(2, 3) == do_rpc(2, 3) # 发送数据给服务端:(pickle.dumps((name, args, kwargs))) name==add(函数名称),args和kwargs是参数 # 发送数据给服务端后等待接收返回数据 print(proxy.add(2, 3)) # 5 proxy.sub(2, 3) # -1 proxy.sub([1, 2], 4) # TypeError: unsupported operand type(s) for -: 'list' and 'int' # 很多消息层(比如 multiprocessing )已经使用pickle序列化了数据。 这样的话,对 pickle.dumps() 和 pickle.loads() 的调用要去掉
# RPCHandler 和 RPCProxy 的基本思路比较简单
# 一个客户端想要调用一个远程函数,比如 foo(1, 2, z=3) ,代理类创建一个包含了函数名和参数的元组 ('foo', (1, 2), {'z': 3}) 。
这个元组被pickle序列化后通过网络连接发生出去。 这一步在 RPCProxy 的 __getattr__() 方法返回的 do_rpc() 闭包中完成。
服务器接收后通过pickle反序列化消息,查找函数名看看是否已经注册过,然后执行相应的函数。
执行结果(或异常)被pickle序列化后返回发送给客户端。我们的实例需要依赖 multiprocessing 进行通信。
不过,这种方式可以适用于其他任何消息系统。例如,如果想在ZeroMQ之上实习RPC,
仅仅只需要将连接对象换成合适的ZeroMQ的socket对象即可
# 底层需要依赖pickle,安全问题就需要考虑了 (黑客可以创建特定的消息,能够让任意函数通过pickle反序列化后被执行)。
因此你永远不要允许来自不信任或未认证的客户端的RPC。特别是你绝对不要允许来自Internet的任意机器的访问,
这种只能在内部被使用,位于防火墙后面并且不要对外暴露
# pickle的替代,可以使用json,xml或一些其他的编码格式来序列化消息 # JSON编码方案,把pickle.loads() 和 pickle.dumps() 替换成 json.loads() 和 json.dumps() # jsonrpcserver.py import json class RPCHandler: def __init__(self): self._functions = { } def register_function(self, func): self._functions[func.__name__] = func def handle_connection(self, connection): try: while True: func_name, args, kwargs = json.loads(connection.recv()) try: r = self._functions[func_name](*args,**kwargs) connection.send(json.dumps(r)) except Exception as e: connection.send(json.dumps(str(e))) except EOFError: pass # jsonrpcclient.py import json class RPCProxy: def __init__(self, connection): self._connection = connection def __getattr__(self, name): def do_rpc(*args, **kwargs): self._connection.send(json.dumps((name, args, kwargs))) result = json.loads(self._connection.recv()) return result return do_rpc
# RPC的一个比较复杂的问题是如何去处理异常
# 方法产生异常时服务器不应该奔溃
# 返回给客户端的异常所代表的含义就要好好设计了。 使用pickle,异常对象实例在客户端能被反序列化并抛出。
使用其他的协议,得想想另外的方法了。 不过至少,应该在响应中返回异常字符串。我们在JSON的例子中就是使用的这种方式
24:简单的客户端认证 分布式系统中实现一个简单的客户端连接认证功能,又不像SSL那样的复杂
# 利用 hmac 模块实现一个连接握手,从而实现一个简单而高效的认证过程 import hmac import os def client_authenticate(connection, secret_key): """ 向远程服务验证客户端。(客户端) connection:表示网络连接。 secret_key:是只有客户端/服务器都知道的密钥。 """ message = connection.recv(32) hash = hmac.new(secret_key, message) digest = hash.digest() connection.send(digest) def server_authenticate(connection, secret_key): """ 请求客户端身份验证(服务端) """ message = os.urandom(32) connection.send(message) hash = hmac.new(secret_key, message) digest = hash.digest() response = connection.recv(len(digest)) return hmac.compare_digest(digest, response) # 比较 # 原理是当连接建立后,服务器给客户端发送一个随机的字节消息(这里例子中使用了 os.urandom() 返回值)。
客户端和服务器同时利用hmac和一个只有双方知道的密钥来计算出一个加密哈希值。然后客户端将它计算出的摘要发送给服务器,
服务器通过比较这个值和自己计算的是否一致来决定接受或拒绝连接。摘要的比较需要使用 hmac.compare_digest() 函数。
使用这个函数可以避免遭到时间分析攻击,不要用简单的比较操作符(==)。 使用上面这些函数,你需要将它集成到已有的网络或消息代码中 # sockets,服务器代码如下 from socket import socket, AF_INET, SOCK_STREAM secret_key = b'peekaboo' # 密匙 def echo_handler(client_sock): if not server_authenticate(client_sock, secret_key): client_sock.close() return while True: msg = client_sock.recv(8192) if not msg: break client_sock.sendall(msg) def echo_server(address): s = socket(AF_INET, SOCK_STREAM) s.bind(address) s.listen(5) while True: c,a = s.accept() echo_handler(c) echo_server(('', 18000))
客户端中,运行以下代码 from socket import socket, AF_INET, SOCK_STREAM secret_key = b'peekaboo' s = socket(AF_INET, SOCK_STREAM) s.connect(('localhost', 18000)) client_authenticate(s, secret_key) s.send(b'Hello World') resp = s.recv(1024)
# 服务端完整代码 from socket import socket, AF_INET, SOCK_STREAM import os import hmac secret_key = b'peekaboo' def server_authenticate(connection, secret_key): """ 请求客户端身份验证(服务端) """ message = os.urandom(32) connection.send(message) hash = hmac.new(secret_key, message) digest = hash.digest() response = connection.recv(len(digest)) return hmac.compare_digest(digest, response) def echo_handler(client_sock): if not server_authenticate(client_sock, secret_key): client_sock.close() return while True: msg = client_sock.recv(8192) if not msg: break client_sock.sendall(msg) def echo_server(address): s = socket(AF_INET, SOCK_STREAM) s.bind(address) s.listen(5) while True: c, a = s.accept() echo_handler(c) echo_server(('', 18000))
# 服务端逻辑
# echo_server创建服务器把socke套接字创建并且绑定地址,然后使用s.accept()阻塞代码一直等待客户端连接进来
# 当有客户端连接进来生成c, a,c是小的套接字,对应某个连接进来的客户都端,得到这个小的柜台套接字启动echo_handler代码逻辑
# 利用c这个柜台,调用server_authenticate服务端身份验证这个函数,如果验证成功就进入 while循环,接收和发送消息
# 客户端完整代码 from socket import socket, AF_INET, SOCK_STREAM import hmac secret_key = b'peekaboo' def client_authenticate(connection, secret_key): message = connection.recv(32) hash = hmac.new(secret_key, message) digest = hash.digest() connection.send(digest) s = socket(AF_INET, SOCK_STREAM) s.connect(('localhost', 18000)) client_authenticate(s, secret_key) s.send(b'Hello World') resp = s.recv(1024)
# 客户端创建套接字然后连接上服务器,然后调用client_authenticate认证函数,认证成功才可以收发数据
# hmac 认证的一个常见使用场景是内部消息通信系统和进程间通信。
例如,编写的系统涉及到一个集群中多个处理器之间的通信,
可以使用本方案来确保只有被允许的进程之间才能彼此通信。
事实上,基于 hmac 的认证被 multiprocessing 模块使用来实现子进程直接的通信
# 连接认证和加密是两码事。 认证成功之后的通信消息是以明文形式发送的,
任何人只要想监听这个连接线路都能看到消息(尽管双方的密钥不会被传输)
25:在网络服务中加入SSL 基于sockets的网络服务,客户端和服务器通过SSL协议认证并加密传输的数据
# ssl 模块能为底层socket连接添加SSL的支持 # ssl.wrap_socket() 函数接受一个已存在的socket作为参数并使用SSL层来包装它 # 一个简单的应答服务器,在服务器端为所有客户端连接做认证 from socket import socket, AF_INET, SOCK_STREAM import ssl KEYFILE = 'server_key.pem' # 服务器的私钥 CERTFILE = 'server_cert.pem' # 服务器证书(提供给客户端) def echo_client(s): """收发消息函数,当客户端和服务端认证成功后就可以调用这个收发消息函数""" while True: data = s.recv(8192) if data == b'': break s.send(data) s.close() print('断开连接') def echo_server(address): s = socket(AF_INET, SOCK_STREAM) s.bind(address) s.listen(1) # 使用需要客户端证书的SSL层包装,需要传递s这个套接字参数 s_ssl = ssl.wrap_socket(s, keyfile=KEYFILE, certfile=CERTFILE, server_side=True ) # 等待连接进来 while True: try: c, a = s_ssl.accept() # 等待连接进来生成柜台c和客户端忘了信息a print('Got connection', c, a) echo_client(c) except Exception as e: print('{}: {}'.format(e.__class__.__name__, e)) echo_server(('', 20000)) # 客户端连接服务器的交互 from socket import socket, AF_INET, SOCK_STREAM import ssl s = socket(AF_INET, SOCK_STREAM) s_ssl = ssl.wrap_socket(s, cert_reqs=ssl.CERT_REQUIRED, ca_certs = 'server_cert.pem') s_ssl.connect(('localhost', 20000)) s_ssl.send(b'Hello World?') s_ssl.recv(8192) # 直接处理底层socket方式有个问题就是不能很好的跟标准库中已存在的网络服务兼容。
例如,绝大部分服务器代码(HTTP、XML-RPC等)实际上是基于 socketserver 库的。
客户端代码在一个较高层上实现。需要另外一种稍微不同的方式来将SSL添加到已存在的服务中
# 服务器而言,使用一个mixin类来添加SSL: import ssl from xmlrpc.server import SimpleXMLRPCServer class SSLMixin: """ Mixin类,该类向基于SSL的现有服务器添加对SSL的支持在socketserver模块上。 """ def __init__(self, *args, keyfile=None, certfile=None, ca_certs=None, cert_reqs=ssl.CERT_NONE, **kwargs): self._keyfile = keyfile # 服务端密码文件 self._certfile = certfile # 客户端密钥文件 self._ca_certs = ca_certs self._cert_reqs = cert_reqs super().__init__(*args, **kwargs) def get_request(self): client, addr = super().get_request() client_ssl = ssl.wrap_socket(client, keyfile=self._keyfile, certfile=self._certfile, ca_certs=self._ca_certs, cert_reqs=self._cert_reqs, server_side=True) return client_ssl, addr # 使用这个mixin类,可以将它跟其他服务器类混合 # 定义一个基于SSL的XML-RPC服务器例子 class SSLSimpleXMLRPCServer(SSLMixin, SimpleXMLRPCServer): pass class KeyValueServer: _rpc_methods_ = ['get', 'set', 'delete', 'exists', 'keys'] def __init__(self, *args, **kwargs): self._data = {} self._serv = SSLSimpleXMLRPCServer(*args, allow_none=True, **kwargs) for name in self._rpc_methods_: self._serv.register_function(getattr(self, name)) def get(self, name): return self._data[name] def set(self, name, value): self._data[name] = value def delete(self, name): del self._data[name] def exists(self, name): return name in self._data def keys(self): return list(self._data) def serve_forever(self): self._serv.serve_forever() KEYFILE = 'server_key.pem' CERTFILE = 'server_cert.pem' kvserv = KeyValueServer(('', 15000), keyfile=KEYFILE, certfile=CERTFILE) kvserv.serve_forever() # 使用这个服务器时,使用普通的 xmlrpc.client 模块来连接它。 只需要在URL中指定 https: 即可 from xmlrpc.client import ServerProxy s = ServerProxy('https://localhost:15000', allow_none=True) s.set('foo','bar') s.set('spam', [1, 2, 3]) s.keys() # ['spam', 'foo'] s.get('foo') # 'bar' s.get('spam') # [1, 2, 3] s.delete('spam') s.exists('spam') # False
# 本质上重构一个新的类SSLSimpleXMLRPCServer继承了SSLMixin和SimpleXMLRPCServer
# 重构了SSLMixin内重构了__init__函数,然后重构了get_request函数,让这个SSLSimpleXMLRPCServer
# 类实例化的时候调用SSLMixin的__init__和get_request,调用get_request,内部先调用super().get_request()
# 调用上层的get_request()方法生成client套接字和addr,然后把client这个tcp连接的套接字调用ssl.wrap_socket函数
# 生成一个新的client_ssl,client_ssl是ssl加密认证后的套接字,就是相当于在原来的套接字client加一个加密外壳生成新套接字client_ssl
# SSL客户端来讲一个比较复杂的问题是如何确认服务器证书或为服务器提供客户端认证(比如客户端证书)。
暂时还没有一个标准方法来解决这个问题,需要自己去研究。
# 用来建立一个安全的XML-RPC连接来确认服务器证书 from xmlrpc.client import SafeTransport, ServerProxy import ssl class VerifyCertSafeTransport(SafeTransport): def __init__(self, cafile, certfile=None, keyfile=None): SafeTransport.__init__(self) self._ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) self._ssl_context.load_verify_locations(cafile) if certfile: self._ssl_context.load_cert_chain(certfile, keyfile) self._ssl_context.verify_mode = ssl.CERT_REQUIRED def make_connection(self, host): # 传递的字典中的项作为关键字传递 # http.client.HTTPSConnection()构造函数的参数。 # 上下文参数允许ssl.SSLContext实例 # 将传递有关SSL配置的信息 s = super().make_connection((host, {'context': self._ssl_context})) return s # 创建客户端代理 s = ServerProxy('https://localhost:15000', transport=VerifyCertSafeTransport('server_cert.pem'), allow_none=True) # 服务器将证书发送给客户端,客户端来确认它的合法性。这种确认可以是相互的。
如果服务器想要确认客户端,可以将服务器启动代码修改 if __name__ == '__main__': KEYFILE='server_key.pem' # 服务器的私钥 CERTFILE='server_cert.pem' # 服务器证书 CA_CERTS='client_cert.pem' # 认可客户的证明书 kvserv = KeyValueServer(('', 15000), keyfile=KEYFILE, certfile=CERTFILE, ca_certs=CA_CERTS, cert_reqs=ssl.CERT_REQUIRED, ) kvserv.serve_forever() # 让XML-RPC客户端发送证书,修改 ServerProxy 的初始化代码 # 创建客户端代理 s = ServerProxy('https://localhost:15000', transport=VerifyCertSafeTransport('server_cert.pem', 'client_cert.pem', 'client_key.pem'), allow_none=True)
每一个SSL连接终端一般都会有一个私钥和一个签名证书文件。
这个证书包含了公钥并在每一次连接的时候都会发送给对方。
对于公共服务器,它们的证书通常是被权威证书机构比如Verisign、Equifax或其他类似机构(需要付费的)签名过的。
为了确认服务器签名,客户端会保存一份包含了信任授权机构的证书列表文件。
例如,web浏览器保存了主要的认证机构的证书,并使用它来为每一个HTTPS连接确认证书的合法性。
目前只是为了测试,可以创建自签名的证书,下面是主要步骤: :: bash % openssl req -new -x509 -days 365 -nodes -out server_cert.pem -keyout server_key.pem Generating a 1024 bit RSA private key ……………………………………++++++ …++++++ writing new private key to ‘server_key.pem’ You are about to be asked to enter information that will be incorporated into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank For some fields there will be a default value,
If you enter ‘.’, the field will be left blank. Country Name (2 letter code) [AU]:US State or Province Name (full name)
[Some-State]:Illinois Locality Name (eg, city) []:Chicago Organization Name (eg, company)
[Internet Widgits Pty Ltd]:Dabeaz, LLC Organizational Unit Name (eg, section)
[]: Common Name (eg, YOUR name) []:localhost Email Address []: bash % 创建证书的时候,各个值的设定可以是任意的,但是”Common Name“的值通常要包含服务器的DNS主机名。
如果只是在本机测试,那么就使用”localhost“,否则使用服务器的域名。 :: —–BEGIN RSA PRIVATE KEY—– MIICXQIBAAKBgQCZrCNLoEyAKF+f9UNcFaz5Osa6jf7q
kbUl8si5xQrY3ZYC7juu nL1dZLn/VbEFIITaUOgvBtPv1qUWTJGwga62VSG1oFE0OD
Ix3g2Nh4sRf+rySsx2 L4442nx0z4O5vJQ7k6eRNHAZUUnCL50+YvjyLyt7ryLSjSu
KhCcJsbZgPwIDAQAB AoGAB5evrr7eyL4160tM5rHTeATlaLY3UBOe5Z8XN8Z6gLiB
/ucSX9AysviVD/6F 3oD6z2aL8jbeJc1vHqjt0dC2dwwm32vVl8mRdyoAsQpWmiqXrk
vP4Bsl04VpBeHw Qt8xNSW9SFhceL3LEvw9M8i9MV39viih1ILyH8OuHdvJyFECQQ
DLEjl2d2ppxND9 PoLqVFAirDfX2JnLTdWbc+M11a9Jdn3hKF8TcxfEnFVs5Gav1
MusicY5KB0ylYPb YbTvqKc7AkEAwbnRBO2VYEZsJZp2X0IZqP9ovWokkpYx+PE4+c6M
ySDgaMcigL7v WDIHJG1CHudD09GbqENasDzyb2HAIW4CzQJBAKDdkv+xoW6gJx42Auc
2WzTcUHCA eXR/+BLpPrhKykzbvOQ8YvS5W764SUO1u1LWs3G+wnRMvrRvlMCZKgggBjk
CQQCG Jewto2+a+WkOKQXrNNScCDE5aPTmZQc5waCYq4UmCZQcOjkUOiN3ST1U5iuxRqfb
V/yX6fw0qh+fLWtkOs/JAkA+okMSxZwqRtfgOFGBfwQ8/iKrnizeanTQ3L6scFXI
CHZXdJ3XQ6qUmNxNn7iJ7S/LDawo1QfWkCfD9FYoxBlg —–END RSA PRIVATE KEY—–
服务器证书文件server_cert.pem内容类似下面这样: :: —–BEGIN CERTIFICATE—– MIIC+DCCAmGgAwIBAgIJAPMd+vi45js3MA0GCSqGSIb
3DQEBBQUAMFwxCzAJBgNV BAYTAlVTMREwDwYDVQQIEwhJbGxpbm9pczEQMA4GA1U
EBxMHQ2hpY2FnbzEUMBIG A1UEChMLRGFiZWF6LCBMTEMxEjAQBgNVBAMTCWxvY2F
saG9zdDAeFw0xMzAxMTEx ODQyMjdaFw0xNDAxMTExODQyMjdaMFwxCzAJBgNVBAY
TAlVTMREwDwYDVQQIEwhJ bGxpbm9pczEQMA4GA1UEBxMHQ2hpY2FnbzEUMBIGA1UE
ChMLRGFiZWF6LCBMTEMx EjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BA
QEFAAOBjQAwgYkCgYEA mawjS6BMgChfn/VDXBWs+TrGuo3+6pG1JfLIucUK2N2WAu4
7rpy9XWS5/1WxBSCE 2lDoLwbT79alFkyRsIGutlUhtaBRNDgyMd4NjYeLEX/q8krMdi+OONp8dM+DubyU O5OnkTRwGVFJwi+dPmL48i8re68i0o0rioQnCbG2YD8CAwEAAaOBwTCBvjAdBgNV
HQ4EFgQUrtoLHHgXiDZTr26NMmgKJLJLFtIwgY4GA1UdIwSBhjCBg4AUrtoLHHgX
iDZTr26NMmgKJLJLFtKhYKReMFwxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhJbGxp
bm9pczEQMA4GA1UEBxMHQ2hpY2FnbzEUMBIGA1UEChMLRGFiZWF6LCBMTEMxEjAQ
BgNVBAMTCWxvY2FsaG9zdIIJAPMd+vi45js3MAwGA1UdEwQFMAMBAf8wDQYJKoZI
hvcNAQEFBQADgYEAFci+dqvMG4xF8UTnbGVvZJPIzJDRee6Nbt6AHQo9pOdAIMAu
WsGCplSOaDNdKKzl+b2UT2Zp3AIW4Qd51bouSNnR4M/gnr9ZD1ZctFd3jS+C5XRp
D3vvcW5lAnCCC80P6rXy7d7hTeFu5EYKtRGXNvVNd/06NALGDflrrOwxF3Y= —–END CERTIFICATE—– 在服务器端代码中,私钥和证书文件会被传给SSL相关的包装函数。
证书来自于客户端, 私钥应该在保存在服务器中,并加以安全保护。 在客户端代码中,需要保存一个合法证书授权文件来确认服务器证书。
如果没有这个文件,你可以在客户端复制一份服务器的证书并使用它来确认。
连接建立后,服务器会提供它的证书,然后你就能使用已经保存的证书来确认它是否正确。 服务器也能选择是否要确认客户端的身份。如果要这样做的话,客户端需要有自己的私钥和认证文件。
服务器也需要保存一个被信任证书授权文件来确认客户端证书。
单向加密:服务端有公钥私钥,服务端使用私钥生成签名证书(任意字段都可),服务端把公钥和正式传递给客户端,
私钥生成的证书公钥验证,公钥加密的文件私钥解密,客户端有了公钥和证书了能够验证证书和的准确性
证书没问题那么客户端使用公钥非对称加密来加密一段对称加密的传输密钥给服务端,服务端拿到二进制文件
然后使用私钥解密数据得到传输密钥,然后客户端和服务端使用这个对称的传输密钥进行文件传输
双向加密:客户端也有私钥和一个签名证书文件,
https详解:https://zhuanlan.zhihu.com/p/27395037
RSA加密:https://www.cnblogs.com/yadongliang/p/11738479.html
http和https的区别:https://www.cnblogs.com/wqhwe/p/5407468.html
26:进程间传递Socket文件描述符
# 多个Python解释器进程在同时运行,想将某个打开的文件描述符从一个解释器传递给另外一个。 # 比如,假设有个服务器进程相应连接请求,但是实际的相应逻辑是在另一个解释器中执行的 # 多个进程中传递文件描述符,首先需要将它们连接到一起。 # Unix机器上,需要使用Unix域套接字, # windows上面需要使用命名管道。 # 不需真操作这些底层, 通常使用 multiprocessing 模块来创建这样的连接会更容易一些 # 一旦一个连接被创建,可以使用 multiprocessing.reduction 中的 send_handle() 和 recv_handle() 函数在不同的处理器直接传递文件描述符 import multiprocessing from multiprocessing.reduction import recv_handle, send_handle import socket def worker(in_p, out_p): out_p.close() while True: fd = recv_handle(in_p) print('CHILD: GOT FD', fd) with socket.socket(socket.AF_INET, socket.SOCK_STREAM, fileno=fd) as s: while True: msg = s.recv(1024) if not msg: break print('CHILD: RECV {!r}'.format(msg)) s.send(msg) def server(address, in_p, out_p, worker_pid): in_p.close() s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) s.bind(address) s.listen(1) while True: client, addr = s.accept() print('SERVER: Got connection from', addr) send_handle(out_p, client.fileno(), worker_pid) client.close() if __name__ == '__main__': c1, c2 = multiprocessing.Pipe() # 创建一个进程间通信的管道
# 启动进程1运行worker函数, worker函数阻塞在recv_handle这里等待传递进来一个fd才会运行下面的代码
# 如果有fd传递进来,那么就使用fd生成一个一模一样的套接字,用来收发消息 worker_p = multiprocessing.Process(target=worker, args=(c1,c2)) worker_p.start() # 启动进程2运行server函数,server生成一个socket,然后接收客户端得连接生成一个套接字
# 生成了套接字把套接字的fileno传给线程1的worker函数接收生成新的套接字去运行收发逻辑 server_p = multiprocessing.Process(target=server, args=(('', 15000), c1, c2, worker_p.pid)) server_p.start() c1.close() c2.close()
# 这里主进程生成管道c1,c2,管道c1,c2复制给子进程1和进程2各一份,所以有三对管道
# 可以使用管道进行进程间的通信
multiprocessing.Pipe([duplex]):创建一个管道
返回2个连接对象(conn1, conn2),代表管道的两端,默认是双向通信.
如果duplex=False,conn1只能用来接收消息,conn2只能用来发送消息.
不同于os.open之处在于os.pipe()返回2个文件描述符(r, w),表示可读的和可写的
# 两个进程被创建并通过一个multiprocessing
管道连接起来。 服务器进程打开一个socket并等待客户端连接请求。
工作进程仅仅使用recv_handle()
在管道上面等待接收一个文件描述符。 当服务器接收到一个连接,
它将产生的socket文件描述符通过send_handle()
传递给工作进程。 工作进程接收到socket后向客户端回应数据,然后此次连接关闭
# 可以使用Telnet或类似工具连接到服务器
bash % python3 passfd.py SERVER: Got connection from (‘127.0.0.1’, 55543) CHILD: GOT FD 7 CHILD: RECV b’Hellorn’ CHILD: RECV b’Worldrn’
# 当前脚本服务器接收到的客户端socket实际上被另外一个不同的进程处理。服务器仅仅只是将其转手并关闭此连接,然后等待下一个连接
# 不同进程之间传递文件描述符好像没什么必要,有时候它是构建一个可扩展系统的很有用的工具,
在一个多核机器上面, 可以有多个Python解释器实例,将文件描述符传递给其它解释器来实现负载均衡 # send_handle() 和 recv_handle() 函数只能够用于 multiprocessing 连接。
使用它们来代替管道的使用(参考11.7节),只要你使用的是Unix域套接字或Windows管道。 # 可以让服务器和工作者各自以单独的程序来启动 # 服务端代码逻辑
from multiprocessing.connection import Listener from multiprocessing.reduction import send_handle import socket def server(work_address, port): # 等待woker连接进来 work_serv = Listener(work_address, authkey=b'peekaboo') worker = work_serv.accept() # 接收套接字连接 worker_pid = worker.recv() # 套接字接收数据 # 现在运行TCP/IP服务器并将客户端发送给worker s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) s.bind(('', port)) s.listen(1) while True: client, addr = s.accept() print('SERVER: Got connection from', addr) send_handle(worker, client.fileno(), worker_pid) client.close() if__name__ == '__main__': import sys if len(sys.argv) != 3: print('Usage: server.py server_address port', file=sys.stderr) raise SystemExit(1) server(sys.argv[1], int(sys.argv[2])) # 运行这个服务器,只需要执行 python3 servermp.py /tmp/servconn 15000 # 工作者代码:
from multiprocessing.connection import Client from multiprocessing.reduction import recv_handle import os from socket import socket, AF_INET, SOCK_STREAM def worker(server_address): serv = Client(server_address, authkey=b'peekaboo') serv.send(os.getpid()) while True: fd = recv_handle(serv) print('WORKER: GOT FD', fd) with socket(AF_INET, SOCK_STREAM, fileno=fd) as client: while True: msg = client.recv(1024) ifnot msg: breakprint('WORKER: RECV {!r}'.format(msg)) client.send(msg) if__name__ == '__main__': import sys if len(sys.argv) != 2: print('Usage: worker.py server_address', file=sys.stderr) raise SystemExit(1) worker(sys.argv[1]) # 运行工作者,执行执行命令 python3 workermp.py /tmp/servconn .
效果跟使用Pipe()例子是完全一样的。 文件描述符的传递会涉及到UNIX域套接字的创建和套接字的 sendmsg() 方法。
# 这种技术并不常见
上面代码逻辑:
server运行,阻塞在work_serv.accept()等待连接进来并且recv接受一次数据,接受到worker端的进程id
然后生成socket套接字,如果有客户连接进来那么就使用send_handle函数+worker把套接字发送给worker
然后阻塞在accept,等待下次有人连接进来,只要有人进来就把套接字发送给worker
worker运行,send发送进程(os.getpid())id给sever端,然后while循环里recv_handle等待server端发送数据过来
recv_handle接受到server发来的套接字运行收发逻辑
server管理客户端连接进来,有连接进来传递socket给worker,worker运行收发逻辑,这个使用connection模块和使用
pip管道有差别,这个能支持跨主机,管道只能在一台机器
# 使用套接字来传递描述符的另外一种实现 # 服务端代码: import socket import struct def send_fd(sock, fd): # 发送一个文件描述符 sock.sendmsg([b'x'], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, struct.pack('i', fd))]) ack = sock.recv(2) assert ack == b'OK' def server(work_address, port): # 等待worker的连接 work_serv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) work_serv.bind(work_address) work_serv.listen(1) worker, addr = work_serv.accept() # 上面创建一个套接字等待工作进程进来生成一个worker套接字,
# 工作进程连进来后又打开一个socket,绑定地址阻塞等待客户端连接进来
# 客户端连接进来后调用send_fd函数,使用woker套接字传递client套接字得fileno(文件描述符)
# 现在运行TCP/IP服务器并将客户端发送给worker s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) s.bind(('',port)) s.listen(1) while True: client, addr = s.accept() print('SERVER: Got connection from', addr) send_fd(worker, client.fileno()) client.close() if __name__ == '__main__': import sys if len(sys.argv) != 3: print('Usage: server.py server_address port', file=sys.stderr) raise SystemExit(1) server(sys.argv[1], int(sys.argv[2])) # 工作者代码 import socket import struct def recv_fd(sock): # 接收单个文件描述符 msg, ancdata, flags, addr = sock.recvmsg(1, socket.CMSG_LEN(struct.calcsize('i'))) cmsg_level, cmsg_type, cmsg_data = ancdata[0] assert cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS sock.sendall(b'OK') return struct.unpack('i', cmsg_data)[0] def worker(server_address): serv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) serv.connect(server_address) while True: fd = recv_fd(serv) # 得到文件句柄 print('WORKER: GOT FD', fd) with socket.socket(socket.AF_INET, socket.SOCK_STREAM, fileno=fd) as client: while True: msg = client.recv(1024) if not msg: break print('WORKER: RECV {!r}'.format(msg)) client.send(msg) if __name__ == '__main__': import sys if len(sys.argv) != 2: print('Usage: worker.py server_address', file=sys.stderr) raise SystemExit(1) worker(sys.argv[1]) # 想在程序中传递文件描述符,参考其他一些更加高级的文档,
Unix Network Programming by W. Richard Stevens (Prentice Hall, 1990) .
在Windows上传递文件描述符跟Unix是不一样的,研究下 multiprocessing.reduction 中的源代码看看其工作原理
# socket:sendto、sendmsg函数详解
27:理解事件驱动的IO 基于事件驱动或异步I/O的包
# 事件驱动I/O本质上来讲就是将基本I/O操作(比如读和写)转化为你程序需要处理的事件 # 当数据在某个socket上被接受后,它会转换成一个 receive 事件,然后被你定义的回调方法或函数来处理。
作为一个可能的起始点,一个事件驱动的框架可能会以一个实现了一系列基本事件处理器方法的基类开始 class EventHandler: def fileno(self): # 返回关联的文件描述符 raise NotImplemented('must implement') def wants_to_receive(self): # 如果允许接收,则返回True return False def handle_receive(self): # 执行接收操作 pass def wants_to_send(self): # 如果请求发送,则返回True return False def handle_send(self): # 发送传出数据 pass # 上面类的实例作为插件被放入类似下面这样的事件循环中: def event_loop(handlers): while True: wants_recv = [h for h in handlers if h.wants_to_receive()] wants_send = [h for h in handlers if h.wants_to_send()] can_recv, can_send, _ = select.select(wants_recv, wants_send, []) for h in can_recv: h.handle_receive() for h in can_send: h.handle_send() # 事件循环的关键部分是 select() 调用,它会不断轮询文件描述符从而激活它。
在调用 select() 之前,事件循环会询问所有的处理器来决定哪一个想接受或发生。
然后它将结果列表提供给 select() 。然后 select() 返回准备接受或发送的对象组成的列表。
然后相应的 handle_receive() 或 handle_send() 方法被触发
# 编写应用程序的时候,EventHandler 的实例会被创建 # 下面两个简单的基于UDP网络服务的处理器
import socket
import time
import select
def event_loop(handlers):
while True:
wants_recv = [h for h in handlers if h.wants_to_receive()]
wants_send = [h for h in handlers if h.wants_to_send()]
can_recv, can_send, _ = select.select(wants_recv, wants_send, [])
for h in can_recv:
h.handle_receive()
for h in can_send:
h.handle_send()
class EventHandler:
def fileno(self):
raise NotImplemented('must implement')
def wants_to_receive(self):
return False
def handle_receive(self):
pass
def wants_to_send(self):
return False
def handle_send(self):
pass
class UDPServer(EventHandler):
def __init__(self, address):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.bind(address)
def fileno(self):
return self.sock.fileno()
def wants_to_receive(self):
return True
class UDPTimeServer(UDPServer):
def handle_receive(self):
msg, addr = self.sock.recvfrom(1)
self.sock.sendto(time.ctime().encode('ascii'), addr)
class UDPEchoServer(UDPServer):
def handle_receive(self):
msg, addr = self.sock.recvfrom(8192)
self.sock.sendto(msg, addr)
if __name__ == '__main__':
handlers = [UDPTimeServer(('', 14000)), UDPEchoServer(('', 15000))]
event_loop(handlers)
# 测试这段代码,从另外一个Python解释器连接它
from socket import *
s = socket(AF_INET, SOCK_DGRAM)
s.sendto(b'',('localhost',14000))
s.recvfrom(128) #(b'Tue Sep 18 14:29:23 2012', ('127.0.0.1', 14000))
s.sendto(b'Hello',('localhost',15000))
s.recvfrom(128) # (b'Hello', ('127.0.0.1', 15000))
两个Udp服务端,重构handle处理逻辑+fileno+wants_to_receive
handle是程序处理逻辑,select函数接受的参数是列表里面存放套接字,如果存放的不是套接字存放类也需要实现这个类的fileno
fileno表示文件句柄表示(非负整数)标识套接字的,如果传递的参数是套接字本质上select也是根据fileno来识别每个套接字
重构wants_to_receive让他返回True表示这个套接字想接受外部来的数据,把他放在select的参数的第一个
当外部有数据进来那么select监听这个套接字触发了来这个事件那么select就会返回一个列表,第一个列表是能够收的socket集合,
第二个列表是能够发的socket集合,第三个列表是报错的socket集合
拿到这些能够收发的套接字后去调用各自套接字的收发函数
Python事件驱动模型与IO多路复用:https://python3-cookbook.readthedocs.io/zh_CN/latest/c11/p12_understanding_event_driven_io.html
廖雪峰官网IO和协程:https://www.liaoxuefeng.com/wiki/1016959663602400/1017968846697824
python事件驱动与异步IO:https://blog.csdn.net/runner668/article/details/80200373
事件驱动模型详解:https://www.cnblogs.com/sunlong88/articles/9033143.html
# 事件驱动实现一个TCP服务器,每一个客户端都要初始化一个新的处理器对象 import socket import select def event_loop(handlers): while True: wants_recv = [h for h in handlers if h.wants_to_receive()] wants_send = [h for h in handlers if h.wants_to_send()] can_recv, can_send, _ = select.select(wants_recv, wants_send, []) for h in can_recv: h.handle_receive() for h in can_send: h.handle_send() class EventHandler: def fileno(self): raise NotImplemented('must implement') def wants_to_receive(self): return False def handle_receive(self): pass def wants_to_send(self): return False def handle_send(self): pass class TCPServer(EventHandler): def __init__(self, address, client_handler, handler_list): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) self.sock.bind(address) self.sock.listen(1) # 设置内核应该为相应套接字排队的最大连接个数1,那就是最多连接2个 self.client_handler = client_handler # client_handler:TCPEchoClient这个类 self.handler_list = handler_list # handle:存放TCPServer类和TCPEchoClient类的列表,也就是select事件驱动检测的I/O文件 def fileno(self): return self.sock.fileno() def wants_to_receive(self): return True def handle_receive(self): client, addr = self.sock.accept() # 将客户端添加到事件循环的处理程序列表中 self.handler_list.append(self.client_handler(client, self.handler_list)) # 处理逻辑:先实例化一个TCPServer类,添加到事件循环的处理程序列表handlers中,这个TCPServer I/O只能accept接受连接 # 所以改写这个TCPServer类,wants_to_receive表示愿意接受连接进来数据handle_receive编写连接进来的的处理逻辑 # TCP和udp不同,tcp创建一个socket后需要等待客户端连接进来,每连接进来一个客户端就生成一个小的套接字,
这个小的套接字client也是一个阻塞I/O,把生成的小的client套接字也放入到select事件驱动检测的I/O列表中进行监测,
这个client小的套接字愿意进行读+写两个I/O操作 class TCPClient(EventHandler): def __init__(self, sock, handler_list): self.sock = sock self.handler_list = handler_list self.outgoing = bytearray() def fileno(self): return self.sock.fileno() def close(self): self.sock.close() # 从事件循环的处理程序列表中删除我自己 self.handler_list.remove(self) def wants_to_send(self): return True if self.outgoing else False def handle_send(self): nsent = self.sock.send(self.outgoing) self.outgoing = self.outgoing[nsent:] class TCPEchoClient(TCPClient): def wants_to_receive(self): return True def handle_receive(self): data = self.sock.recv(8192) if not data: self.close() else: self.outgoing.extend(data) if __name__ == '__main__': handlers = [] handlers.append(TCPServer(('', 16000), TCPEchoClient, handlers)) event_loop(handlers)
解析:
can_recv, can_send, _ = select.select(wants_recv, wants_send, [])
select.select()
参数1wantes_recv:表示想要收的套接字列表,参数2wants_send:表示想要发的套接字
返回1can_recv:表示目前能够进行recv的套接字,返回2:can_send:表情目前能够进行send的套接字
就是监控上面想要收发的套接字,select能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,
就添加到can_recv, can_send两个列表里,然后遍历can_recv, can_send两个列表来执行相应的handle_receive和handle_send操作
对于监听的socket,如果外部传来了消息,消息写道系统内核的缓存区,那么表示这个套接字收的动作可以执行了,就把这个I/O放到can_recv列表
如果写的数据准备好了就可以触发写的动作写到can_send列表里,
event_loop函数是外层是一个while死循环,一直调用select.select(wants_recv, wants_send, [])监听所有套接字,有一个套接字或者几个活跃就走一次while循环
运行一次里面的逻辑,遍历can_recv和can_send两个内部是套接字的列表去调用相关的handle_receive和handle_send函数,执行相应的收发操作后又
进入while循环,select去监听所有的套接字,
TCP关键点是从处理器中列表增加和删除客户端的操作。 对每一个连接,一个新的处理器被创建并加到列表中。
当连接被关闭后,每个客户端负责将其从列表中删除。 如果你运行程序并试着用Telnet或类似工具连接,
它会将你发送的消息回显给你。并且它能很轻松的处理多客户端连接
# 事件驱动框架原理差不多就是上面模型,最核心的部分,都会有一个轮询的循环来检查活动socket,并执行响应操作 # 事件驱动I/O的一个可能好处是它能处理非常大的并发连接,而不需要使用多线程或多进程。 # select() 调用(或其他等效的)能监听大量的socket并响应它们中任何一个产生事件的。 再循环中一次处理一个事件,并不需要其他的并发机制 # 事件驱动I/O的缺点是没有真正的同步机制。 如果任何事件处理器方法阻塞或执行一个耗时计算,它会阻塞所有的处理进程。 调用那些并不是事件驱动风格的库函数也会有问题,同样要是某些库函数调用会阻塞,那么也会导致整个事件循环停止 # 对于阻塞或耗时计算的问题可以通过将事件发送个其他单独的线程或进程来处理 # 在事件循环中引入多线程和多进程是比较棘手的, 可以使用 concurrent.futures 模块来实现 from concurrent.futures import ThreadPoolExecutor import select import os import socket class EventHandler: def fileno(self): raise NotImplemented('must implement') def wants_to_receive(self): return False def handle_receive(self): pass def wants_to_send(self): return False def handle_send(self): pass class ThreadPoolHandler(EventHandler): def __init__(self, nworkers): # os.name返回当前操作系统的类型,当前只注册了3个值:分别是posix , nt , java, 对应linux/windows/java虚拟机 if os.name == 'posix': # 如果是linux系统,创建两个连接的套接字 signal_done_sock和done_sock # socket.socketpair()函数仅返回两个已经连接的套接字对象,只能linux里面使用 self.signal_done_sock, self.done_sock = socket.socketpair() # signal_done_sock和done_sock else: # 如果不是linux系统需要自己创建一对套接字连接 # 这对套接字是 ignal_done_sock和done_sock server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('127.0.0.1', 0)) server.listen(1) self.signal_done_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.signal_done_sock.connect(server.getsockname()) self.done_sock, _ = server.accept() # self.done_sock是小套接字, server.close() self.pending = [] # 存放回调和结果的列表 self.pool = ThreadPoolExecutor(nworkers) # 创建一个最大容纳数量为nworkers的线程池 def fileno(self): return self.done_sock.fileno() # self.done_sock是小套接字,相当于服务端等待信号的到来 # 线程完成时执行的回调函数 def _complete(self, callback, r): self.pending.append((callback, r.result())) # callback回调函数,r.result()任务执行的结果 self.signal_done_sock.send(b'x') # 在线程池中运行函数 def run(self, func, args=(), kwargs={}, *, callback): r = self.pool.submit(func, *args, **kwargs) # 线程池提交任务,立即返回r结果对象,r.result()默认为空None,真正完成后返回才是任务执行的结果 r.add_done_callback(lambda r: self._complete(callback, r)) # r这个结果对象添加回调函数,当线程任务执行完成后再执行回调函数 def wants_to_receive(self): return True # 运行已完成工作的回调函数 def handle_receive(self): # 调用所有挂起的回调函数 for callback, result in self.pending: callback(result) self.done_sock.recv(1) self.pending = [] def fib(n): if n < 2: return 1 else: return fib(n - 1) + fib(n - 2) class UDPServer(EventHandler): def __init__(self, address): self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.bind(address) def fileno(self): return self.sock.fileno() def wants_to_receive(self): return True class UDPFibServer(UDPServer): def handle_receive(self): msg, addr = self.sock.recvfrom(128) n = int(msg) pool.run(fib, (n,), callback=lambda r: self.respond(r, addr)) def respond(self, result, addr): self.sock.sendto(str(result).encode('ascii'), addr) def event_loop(handlers): while True: wants_recv = [h for h in handlers if h.wants_to_receive()] wants_send = [h for h in handlers if h.wants_to_send()] can_recv, can_send, _ = select.select(wants_recv, wants_send, []) for h in can_recv: h.handle_receive() for h in can_send: h.handle_send() if __name__ == '__main__': pool = ThreadPoolHandler(16) # 这个ThreadPoolHandler里面创建一个16个线程的线程池 # 被select监听的I/O地址池,被监听的都是fileno这个文件 handlers = [pool, UDPFibServer(('', 16000))] event_loop(handlers) # run() 方法被用来将工作提交给回调函数池,处理完成后被激发。 实际工作被提交给 ThreadPoolExecutor 实例。 不过一个难点是协调计算结果和事件循环,为了解决它,我们创建了一对socket并将其作为某种信号量机制来使用。 当线程池完成工作后,它会执行类中的 _complete() 方法。 这个方法再某个socket上写入字节之前会讲挂起的回调函数和结果放入队列中。 fileno() 方法返回另外的那个socket。 因此,这个字节被写入时,它会通知事件循环,
然后 handle_receive() 方法被激活并为所有之前提交的工作执行回调函数
# select事件驱动+ThreadPoolExecutor多线程高速处理事件
# 使用线程池来处理耗时的计算,select主线程只监听事件,然后把逻辑都丢给线程池计算
from concurrent.futures import ThreadPoolExecutor import select import os import socket class EventHandler: def fileno(self): raise NotImplemented('must implement') def wants_to_receive(self): return False def handle_receive(self): pass def wants_to_send(self): return False def handle_send(self): pass class ThreadPoolHandler(EventHandler): def __init__(self, nworkers): # os.name返回当前操作系统的类型,当前只注册了3个值:分别是posix , nt , java, 对应linux/windows/java虚拟机 if os.name == 'posix': # 如果是linux系统 # socket.socketpair()函数返回两个已经连接的套接字对象,只能在linux里面使用 self.signal_done_sock, self.done_sock = socket.socketpair() # signal_done_sock和done_sock else: # 如果不是linux系统需要自己创建一对套接字连接 # 这对套接字是 ignal_done_sock和done_sock server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('127.0.0.1', 0)) server.listen(1) self.signal_done_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.signal_done_sock.connect(server.getsockname()) self.done_sock, _ = server.accept() # self.done_sock是小套接字 server.close() self.pending = [] # 存放回调和结果的列表 self.pool = ThreadPoolExecutor(nworkers) # 创建一个最大容纳数量为nworkers的线程池 def fileno(self): return self.done_sock.fileno() # self.done_sock是小套接字 # 线程完成时执行的回调函数 def _complete(self, callback, r): self.pending.append((callback, r.result())) # callback回调函数,r.result()任务执行的结果 self.signal_done_sock.send(b'x') # 在线程池中运行函数 def run(self, func, args=(), kwargs={}, *, callback): r = self.pool.submit(func, *args, **kwargs) # 线程池提交任务,立即返回r结果对象,r.result()默认为空None,真正完成后返回才是任务执行的结果 r.add_done_callback(lambda r: self._complete(callback, r)) # r这个结果对象添加回调函数,当线程任务执行完成后再执行回调函数 def wants_to_receive(self): return True # 运行已完成工作的回调函数 def handle_receive(self): # 调用所有挂起的回调函数 for callback, result in self.pending: callback(result) self.done_sock.recv(1) self.pending = [] def fib(n): if n < 2: return 1 else: return fib(n - 1) + fib(n - 2) class UDPServer(EventHandler): def __init__(self, address): self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.bind(address) def fileno(self): return self.sock.fileno() def wants_to_receive(self): return True class UDPFibServer(UDPServer): def handle_receive(self): msg, addr = self.sock.recvfrom(128) n = int(msg) pool.run(fib, (n,), callback=lambda r: self.respond(r, addr)) def respond(self, result, addr): self.sock.sendto(str(result).encode('ascii'), addr) def event_loop(handlers): while True: wants_recv = [h for h in handlers if h.wants_to_receive()] wants_send = [h for h in handlers if h.wants_to_send()] can_recv, can_send, _ = select.select(wants_recv, wants_send, []) print(can_recv, can_send) for h in can_recv: h.handle_receive() for h in can_send: h.handle_send() if __name__ == '__main__': pool = ThreadPoolHandler(16) # 这个ThreadPoolHandler里面创建一个16个线程的线程池 # 被select监听的I/O地址池,被监听的都是fileno这个文件 handlers = [pool, UDPFibServer(('', 16000))] event_loop(handlers) # 监听列表里UDPFibServer收到外面的UDP消息,成功循环一个select,运行UDPFibServer里面的handle_receive # 收到 msg和addr把msg转化成整数,比如10,然后调用pool.run(fib, (n,), callback=lambda r: self.respond(r, addr)) # pool = ThreadPoolHandler(16),就是调用ThreadPoolHandler的run方法 # func = fib args=(n,) callback=lambda r: self.respond(r, addr) # 调用pool.submit(fib, (n,))就会向线程池提交任务去执行fib函数,立即返回r结果对象,r.result()默认为空None,真正完成后返回才是任务执行的结果 # r.add_done_callback(lambda r: self._complete(callback, r)) r这个结果对象添加回调函数,当线程任务执行完成后再执行回调函数 # fib函数执行完后拿到执行结果r.result后执行回调函数_complete # pending.append((callback, r.result()))把回调函数和fib函数的执行结果添加到pending列表 # 然后调用signal_done_sock.send(b'x')发送一个tcp数据给done_sock,此时select这个事件驱动监测到done_sock这个I/O变化 # 马上调用ThreadPoolHandler这个类的handle_receive这个方法 # 遍历pending这个列表然后使用callback(result)函数 # callback=lambda r: self.respond(r, addr)) result= fib函数运行的结果 # 也就是调用UDPFibServer这个类的respond函数,传递参数result # 也就是调用respond(result, addr) # 调用sock.sendto(str(result).encode('ascii'), addr)把消息发送给连接进来的客户端
总体逻辑:
外部传来消息激发事件驱动监听的UDPFibServer这个I/O运行相应的代码,然后调用ThreadPoolHandler这个类的run方法
往线程池提交任务去运行,得到运行结果后执行回调函数_complete,把最终传递结果的回调函数callback传递给pending列表
,再使用signal_done_sock发送一个字节数据,再次触发select事件驱动监听的done_sock这个I/O的相关处理函数handle_receive
执行最终的callback回调函数把相应的处理结果传递给客户端
# 上面服务端的代码运行 # 为了校验服务端代码,运行下面客户端相关代码 from socket import * sock = socket(AF_INET, SOCK_DGRAM) # udp套接字,传递整数数据,先把整数转化字符串然后转化成bytes类型 for x in range(40): sock.sendto(str(x).encode('ascii'), ('localhost', 16000)) resp = sock.recvfrom(8192) print(resp[0])
# 可在不同的窗口重复的执行这个客户端且不会影响到其他程序,尽管当数字越来越大时候它会变得越来越慢
28:发送与接收大型数组 通过网络连接发送和接受连续数据的大型数组,并尽量减少数据的复制操作 memoryviews
# 利用 memoryviews 来发送和接受大数组 # 服务端 from socket import * import numpy def send_from(arr, dest): view = memoryview(arr).cast('B')
# 生成一个对象,len(view) = 400000000,表示这个数组有4个e的数据量,4个e的字节
# 下面while循环去发送这这个数组,
# 下面sock.send(view)直接就把这4e数据一次性全部发送出去了,就循环了一次就结束了
while len(view): nsent = dest.send(view) view = view[nsent:] def recv_into(arr, source): view = memoryview(arr).cast('B') while len(view): nrecv = source.recv_into(view) view = view[nrecv:] s = socket(AF_INET, SOCK_STREAM) s.bind(('', 25000)) s.listen(1) c, a = s.accept() # 通过连接传输一个超大数组,可以使用 array 模块或 numpy模块 a = numpy.arange(0.0, 50000000.0) # [0, 4.9999999e+07]一个数组 send_from(a, c)
# c是小套接字,a是大型数组
# 客户端 from socket import * import numpy def send_from(arr, dest): view = memoryview(arr).cast('B')
while len(view): nsent = dest.send(view) view = view[nsent:] def recv_into(arr, source): view = memoryview(arr).cast('B') while len(view): nrecv = source.recv_into(view) view = view[nrecv:] c = socket(AF_INET, SOCK_STREAM) c.connect(('localhost', 25000)) a = numpy.zeros(shape=50000000, dtype=float) # 表示5千万数据的数组 print(a[0:10]) # [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] recv_into(a, c) # 接受5千万长度的数组 print(a[0:10]) # [0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
# 接收的数据到了a这个对象里面,nrecv = source.recv_into(view)这个nrecv返回的是长度
400000000
# 使用内存视图展示了一些魔法操作。 本质上,一个内存视图就是一个已存在数组的覆盖层。 # 内存视图还能以不同的方式转换成不同类型来表现数据。 view = memoryview(arr).cast('B') # 接受一个数组 arr并将其转换为一个无符号字节的内存视图。这个视图能被传递给socket相关函数,
比如 socket.send() 或 send.recv_into() 。 在内部,这些方法能够直接操作这个内存区域。
例如,sock.send() 直接从内存中发送数据而不需要复制。 send.recv_into() 使用这个内存区域作为接受操作的输入缓冲区 # socket函数可能只操作部分数据。 通常来讲,我们得使用很多不同的 send() 和 recv_into() 来传输整个数组。
不用担心,每次操作后,视图会通过发送或接受字节数量被切割成新的视图。 新的视图同样也是内存覆盖层。因此,还是没有任何的复制操作 # 这里有个问题就是接受者必须事先知道有多少数据要被发送, 以便它能预分配一个数组或者确保它能将接受的数据放入一个已经存在的数组中。
如果没办法知道的话,发送者就得先将数据大小发送过来,然后再发送实际的数组数据
# 生成一个对象,len(view) = 400000000,表示这个数组有4个e的数据量,4个e的字节
# 下面while循环去发送这这个数组,
# 下面sock.send(view)直接就把这4e数据一次性全部发送出去了,就循环了一次就结束了