网络和Web编程
一、以客户端的形式同HTTP服务交互
(1)使用urllib.request模块发送HTTP GET请求
from urllib import request,parse url = 'http://httpbin.org/get' parms = { 'name1':'value1', 'name2':'value2', } querystring = parse.urlencode(parms) u = request.urlopen(url+'?'+querystring) resp = u.read()
(2)发送HTTP POST请求
url = 'http://httpbin.org/post' u = request.urlopen(url, querystring.encode('ascii')) resp = u.read()
(3)使用自定义HTTP头
headers = { 'User-agent':'none/ofyourbusiness', 'Spam':'Eggs' } req = request.Request(url, querystring.encode('ascii'), headers=headers) u = request.urlopen(req) resp = u.read()
(4)使用request库,以多种方式从请求中返回响应结果的内容
import requests resp = requests.post(url, data=parms, headers=headers) text = resp.text
resp.text 得到以Unicode解码的响应文本;resp.content 得到原始的二进制数据;resp.json 得到JSON格式的响应内容
(5)request库发起HEAD请求,提供HTTP头数据
requests.head('http://www.python.org/index.html') status = resp.status_code last_modified = resp.headers['last-modifued'] content_type = resp.headers['content-type'] content_length = resp.headers['content-length']
(6)request库将第一个请求得到的 http cookies传递给下一个请求
resp1 = requests.get(url)
requests.get(url, cookies=resp1.cookies)
(7)request库实现内容上传
files = {'file':('data.csv',open('data.csv','rb'))} requests.post(url, files=files)
(8)用底层http.client模块实现HEAD请求
from http.client import HTTPConnection c = HTTPConnection('www.python.org', 80) c.request('HEAD', '/index.html') resp = c.getresponse() print('Status',resp.status) for name, value in resp.getheaders(): print(name, value)
(9)http://httpbin.org 这个站点会接受发出的请求,然后以JSON的形式将响应信息回传回来。
import requests r = requests.get('http://httpbin.org/get?name=Dave&n=37', headers={'User-agent':'goaway/1.0'}) resp = r.json() print(resp['headers']) {'Accept': '*/*', 'User-Agent': 'goaway/1.0', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org'} print(resp['args']) {'name': 'Dave', 'n': '37'}
二、创建TCP服务器
利用socketserver库实现
from socketserver import BaseRequestHandler, TCPServer class EchoHandle(BaseRequestHandler, TCPServer): def handle(self): print(' Got connenction 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), EchoHandle) serv.serve_forever()
request属性就代表着底层的客户端socket,而client_address中包含了客户端的地址。
客户端测试代码:
>>> from socket import socket, AF_INET, SOCK_STREAM >>> s = socket(AF_INET, SOCK_STREAM) >>> s.connect(('localhost',20000)) >>> s.send(b'HEllo') 5 >>> s.recv(8192) b'HEllo'
使用StreamRequestHandler作为基类,给底层的socket加上了文件类型的接口:
from socketserver import StreamRequestHandler, TCPServer class EchoHandle(StreamRequestHandler, TCPServer): def handle(self): print(' Got connection from ', self.client_address) for line in self.rfile: self.wfile.write(line) if __name__ == '__main__': serv = TCPServer(('', 20000), EchoHandle) serv.serve_forever()
实例化ForkingTCPServer或者ThreadingTCPServer对象处理多个客户端
from socketserver import ThreadingTCPServer if __name__ == '__main__': serv = ThreadingTCPServer(('', 20000), EchoHandle) serv.serve_forever()
创建一个预先分配好的工作者线程或进程池。
if __name__ == '__main__': from threading import Thread NWORKERS = 16 serv = TCPServer(('', 20000), EchoHandle) for n in range(NWORKERS): t = Thread(target=serv.serve_forever) t.daemon = True t.start() serv.serve_forever()
TCPServer在实例化时会绑定并激活底层的socket。提供bind_and_activate参数调整底层socket的行为。
if __name__ == '__main__': serv = TCPServer(('',20000), EchoHandle, bind_and_activate=False) serv.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) serv.server_bind() serv.server_activate() serv.serve_forever()
设置允许服务器重新对之前使用过的端口号进行绑定。
if __name__ == '__main__': TCPServer.allow_reuse_address = True serv = TCPServer(('', 20000), EchoHandle) serv.serve_forever()
使用socket库实现服务器
from socket import socket, AF_INET, SOCK_STREAM def echo_handler(address, client_sock): print(' Got connection from {}'.format(address)) while True: msg = socket.recv(8192) if not msg: break socket.send(msg) socket.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))
三、创建一个UDP服务器
UDP通信底层不需要简历连接,因此UDP是不可靠连接。处理消息丢失要引入序列号、重传、超时以及其他的机制来确保传输的可靠性。
UDP常用在对可靠性传输要求不高的应用中,如多媒体流应用以及游戏中常用到UDP,因为应用中不会倒退回去试图重传某个丢失的数据包。
利用socketserver库创建出UDP服务器
from socketserver import BaseRequestHandler, UDPServer import time class TimeHandler(BaseRequestHandler, UDPServer): def handle(self): print(' GOt connection from ',self.client_address) msg, sock = self.request resp = time.ctime() sock.sendto(resp.encode('ascii'), self.client_address) if __name__ == '__main__': serv = UDPServer(('',20000), TimeHandler) serv.serve_forever()
测试客户端:
from socket import socket, AF_INET, SOCK_DGRAM s = socket(AF_INET, SOCK_DGRAM) s.sendto(b'', ('localhost',20000)) s.recvfrom(8192)
在UDP通信中应该使用sendto()和recvfrom()方法。
UDPServer类也是单线程的,可以实例化ForkingUDPServer或者ThreadingUDPServer
直接通过socket来实现UDP服务器:
from socket import socket, AF_INET, SOCK_DGRAM import time def time_server(address): sock = socket(AF_INET, SOCK_DGRAM) sock.bind(address) while True: sock.recvfrom(8192) print(' Got message from ',address) resp = time.ctime() sock.sendto(resp.encode('ascii'), address) if __name__ == '__main__': time_server(('', 20000))
四、从CIDR地址中生成IP地址范围
(1)利用ipaddress模块
>>> import ipaddress >>> net = ipaddress.ip_network('123.45.67.64/27') >>> net IPv4Network('123.45.67.64/27') >>> for a in net: ... print(a) ... 123.45.67.64 123.45.67.65 ... 123.45.67.95
(2)支持数组那样的索引操作
>>> net[0]
IPv4Address('123.45.67.64')
>>> net[-1]
IPv4Address('123.45.67.95')
(3)检查成员归属
>>> a = ipaddress.ip_address('123.45.67.69')
>>> a in net
True
(4)ip地址加上网络号可以用来指定一个ip接口interface
>>> inet = ipaddress.ip_interface('123.45.67.73/27')
>>> inet.network
IPv4Network('123.45.67.64/27')
>>> inet.ip
IPv4Address('123.45.67.73')
▲ ipaddress模块同其他网络相关的模块socket库交互是由局限性,需要通过str()转换为字符串。
五、基于REST风格的简单接口
根据WSGI规范,创建一个小型库
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 import time _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') if __name__ == '__main__': from wsgiref.simple_server import make_server dispatcher = PathDispatcher() dispatcher.register('GET', '/hello', hello_world) httpd = make_server('', 8080, dispatcher) print('Serving on port 8080 ....') httpd.serve_forever()
在WSGI中,应用程序是以一个接受如下调用形式的可调用对象来实现的:
import cgi def wsgi_app(environ, start_response): ...
参数environ是一个字典,其中需要包含的值参考了许多web服务器比如apache所提供的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是个函数,必须调用它才能发起响应。start_response的第一个参数是HTTP结果状态。第二个参数是一个元组序列,以(name,value)这样的形式组成响应的HTTP头。
def wsgi_app(environ, start_response): ... start_response('200 OK', [ ('Content-type','text/html')])
要返回数据,满足WSGI规范的应用程序必须返回字节串序列。
def wsgi_app(environ, start_response): ... start_response('200 OK', [ ('Content-type','text/html')]) resp = [] resp.append(b'Hello World\n') return resp
还可以使用yield作为替代方案:
def wsgi_app(environ, start_response): ... start_response('200 OK', [ ('Content-type','text/html')]) yield b'Hello World\n'
返回的结果必须使用字节串的形式。
尽管遵循WSGI规范的应用程序通常都被定义为函数,就如我们的示例那样,但是类实例同样也是可行的,只要它实现了合适的__cal__()方法即可。
class WSGIApplication: def __init__(self): ... def __call__(self, environ, start_response): ...
六、利用XML-RPC实现简单的远端过程调用
在远端机器上运行的Python程序中执行函数或者方法
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('spam') [1, 2, 3] >>> s.get('foo') 'bar' >>> s.delete('spam') >>> s.exists('spam') False
配置一个简单的远端过程调用服务器,可以用XML-RPC实现。
创建一个服务器实例,通过register_function()方法注册处理函数,然后通过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传递一个实例
class Point: def __init__(self, x, y): self.x = x self.y = y >>> p = Point(2, 3) >>> s.set('foo', p) >>> s.get('foo') {'x':2, 'y':3}
对二进制数据的处理:
>>> s.set('foo', b'Hello World') >>> s.get('foo') <xmlrpc.client.Binary object at 0xxxxx> >>> _.data b'Hello World'
XML-RPC的缺点在于它的性能。SimpleXMLRPCServer是以单线程来实现的,由于XML-RPC会将所有的数据序列化为XML格式,因此就会比其他的方法要慢一些。
七、在不同解释间进行通信
正在运行的多个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() echo_client(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') >>> c.recv() 'hello' >>> c.send(42) >>> c.recv() 42 >>> c.send([1,2,3,4,5]) >>> c.recv() [1, 2, 3, 4, 5]
这里所有的消息都是完整的,对象都是通过pickle来进行序列化的。
因此,任何同pickle兼容的对象都可以再连接之间传递和接收。
如果,知道解释器运行在同一台机器上,那么可以利用网络作为替代方案,比如UNIX域socket或者windows上的命名管道。
要通过UNIX域socket创建连接,只要简单地将地址改为文件名即可。
s = Listener('/tmp/myconn', authkey=b'peekboo') s = Listener(r'\\.\pipe\myconn', authkey=b'peekboo')
multiprocessing.connection模块最好适用于能长时间运行的连接,而不是大量的短连接。
八、实现远端过程调用
在Socket、mutiprocessing.connection或者ZeroMQ这样的消息传递层之上实现简单的远端过程调用(RPC)
import pickle class RPCHandle: def __init__(self): self._function = {} def register_functions(self, func): self._function[func.__name__] = func def handle_connection(self, connection): try: func_name, args, kwargs = pickle.loads(connection.recv()) try: result = self._function[func_name](*args,**kwargs) connection.send(pickle.dumps(result)) except Exception as e: connection.send(pickle.dumps(e)) except EOFError: pass 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() 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 = RPCHandle() handler.register_functions(add) handler.register_functions(sub) rpc_server(handler, ('localhost', 17000), authkey=b'peekaboo')
要在远端的客户端中访问这个服务器需要创建一个相应的RPC代理类来转发请求。
from multiprocessing.connection import Listener 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(2,3) 5
客户端想要调用一个远端函数,比如foo(1,2,z=3),代理类将创建出一个包含了函数名和参数的元组('foo',(1,2),{'z':3})。这个元组经pickle序列化处理后通过连接发送出去。
服务器端接收到消息后执行反序列化处理,然后检查函数名是否已经注册过了。如果是注册过的函数,就用给定的参数调用该函数。
把得到的结果(或者异常)进行pickle序列化处理然后再发送回去。
九、以简单的方式验证客户端身份
利用hmac模块实现一个我收连接来达到简单且搞笑的身份验证目的
import hmac import os def client_authenticate(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)
在发起连接时,服务器将一段由随机字节组成的消息发送给客户端。客户端和服务器通过hmac模块以及双方事先都知道的密钥计算出随机数据的加密hash。
客户端发送它计算出的摘要值(digest)给服务器,而服务器对摘要值进行比较,以此来决定是要接受还是拒绝这个连接。
对摘要值进行比较需要使用hmac.compare_digest()函数。
合并到socket服务器中使用
import hmac import os def client_authenticate(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) from socket import socket, AF_INET, SOCK_STREAM authkey = b'peekaboo' def echo_handler(client_sock): print(client_sock) if not server_authenticate(client_sock, authkey): 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) print(resp)
在内部消息系统以及进程间通信中常常会用到hmac来验证身份。如实现跨集群的多进程间通信,确保只有获得许可的进程才能互相通信。
在multiprocessing库中,当同子进程建立通信时在内部也是使用基于hmac的身份验证方式。
在经过验证的连接上后续的通信都是以明文发送的。
十、为网络服务增加SSL支持
通过socket实现一个网络服务,要求服务器端和客户端可以通过SSL实现身份验证,并且对传输的数据进行加密。
ssl模块为底层的socket连接添加对SSL的支持。ssl.wrap_socket()函数可接受一个已有的socket,并为其包装一个SSL层。
from socket import socket, AF_INET, SOCK_STREAM import ssl KEYFILE = 'server_key.pem' # private key of the server CERTFILE = 'server_cert.pem' # server certificate (given to client) def echo_client(s): while True: msg = s.recv(8192) if not msg: break s.sendall(msg) s.close() print('Connection closed ///') def echo_server(address): s = socket(AF_INET, SOCK_STREAM) s.bind(address) s.listen(1) s_ssl = ssl.wrap_socket(s, keyfile=KEYFILE, certfile=CERTFILE, server_side=True) while True: try: c,a = s_ssl.accept() print('Got connection', c, a) echo_client(c) except Exception as e: print('{} {}'.format(e.__class__.__name__, e)) echo_server(('', 17899))
客户端连接到服务器:客户端要求服务器初始自己的整数并完成验证。
>>> 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', 17899)) >>> s_ssl.send(b'Hello World !!!!') 16 >>> s_ssl.recv(8192) b'Hello World !!!!'
底层socket技巧带来的问题在于,无法和已经通过标准库实现的网络服务很好的结合在一起。
大部分的服务器端代码是基于socketserver 。
对服务器端可以通过混入类来添加对SSL的支持。
import ssl class SSLMixin: ''' Mixin class that adds support for SSL to existing servers based on the socketserver module. ''' 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 from xmlrpc.server import SimpleXMLRPCServer 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() if __name__ == '__main__': KEYFILE='server_key.pem' # Private key of the server CERTFILE='server_cert.pem' # Server certificate kvserv = KeyValueServer(('', 15001), keyfile=KEYFILE, certfile=CERTFILE) kvserv.serve_forever()
要使用这个服务器端,利用xmlrpc.client模块来完成连接。指定https:即可
>>> import ssl >>> context = ssl._create_unverified_context() >>> s = ServerProxy('https://localhost:15001', allow_none=True, context=context) >>> s.set('foo','bar') >>> s.get('foo') 'bar'
SSL客户端中问题在于如何执行额外的步骤来验证服务器证书,或者向服务器展示客户端的凭证。
(1)客户端验证 服务器端发来的证书:
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): # Items in the passed dictionary are passed as keyword # arguments to the http.client.HTTPSConnection() constructor. # The context argument allows an ssl.SSLContext instance to # be passed with information about the SSL configuration s = super().make_connection((host, {'context': self._ssl_context})) return s # Create the client proxy s = ServerProxy('https://localhost:15000', transport=VerifyCertSafeTransport('server_cert.pem'), allow_none=True)
(2)服务器端验证客户端证书:
if __name__ == '__main__': KEYFILE='server_key.pem' # Private key of the server CERTFILE='server_cert.pem' # Server certificate CA_CERTS='client_cert.pem' # Certificates of accepted clients kvserv = KeyValueServer(('', 15000), keyfile=KEYFILE, certfile=CERTFILE, ca_certs=CA_CERTS, cert_reqs=ssl.CERT_REQUIRED, ) kvserv.serve_forever()
客户端发出证书
# Create the client proxy s = ServerProxy('https://localhost:15000', transport=VerifyCertSafeTransport('server_cert.pem', 'client_cert.pem', 'client_key.pem'), allow_none=True)
需要先创建自签名的证书:
openssl req -new -x509 -days 365 -nodes -out server_cert.pem -keyout server_key.pem
十一、在进程间传递socket文件描述符
在进程间传递文件描述符,首选需要将进程连接在一起。
一旦进程间的连接建立起来了,就可以使用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() 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 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, address = s.accept() send_handle(out_p, client.fileno(), worker_pid) client.close() if __name__ == '__main__': c1, c2=multiprocessing.Pipe() worker_p = multiprocessing.Process(target=worker,args=(c1,c2)) worker_p.start() server_p = multiprocessing.Process(target=server,args=(('',20000), c1, c2, worker_p.pid)) server_p.start() c1.close() c2.close()
示例中生成了两个进程,并且利用multiprocessing模块的Pipe对象将它们连接在一起。
服务器进程打开一个socket并等待客户端的连接。
工作者进程只是通过recv_handle()在管道上等待接受文件描述符。
当服务器接收到一条连接时,会将得到的socket文件描述符通过send_handle()发送给工作者进程。
工作者进程接管这个socket并将数据回显给客户端直到连接关闭为止。
▲ 服务器端接收到的客户端socket实际上是有另一个进程去处理的。服务器仅仅只是将它转手出去,关闭它然后等待下一个连接。
将服务器和工作者进程实现为完全的分离:
# servermp.py from multiprocessing.connection import Listener from multiprocessing.reduction import send_handle import socket def server(work_address, port): # Wait for the worker to connect work_serv = Listener(work_address, authkey=b'peekaboo') worker = work_serv.accept() worker_pid = worker.recv() # Now run a TCP/IP server and send clients to 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 ,下面是相应的工作者代码:
# workermp.py 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) 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])
要运行工作者,执行执行命令 python3 workermp.py /tmp/servconn . 效果跟使用Pipe()例子是完全一样的。
文件描述符的传递会涉及到UNIX域套接字的创建和套接字的 sendmsg()
方法。 不过这种技术并不常见,下面是使用套接字来传递描述符的另外一种实现:
# server.py import socket import struct def send_fd(sock, fd): ''' Send a single file descriptor. ''' 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): # Wait for the worker to connect work_serv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) work_serv.bind(work_address) work_serv.listen(1) worker, addr = work_serv.accept() # Now run a TCP/IP server and send clients to 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]))
下面是使用套接字的工作者实现:
# worker.py import socket import struct def recv_fd(sock): ''' Receive a single file descriptor ''' 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])
十二、事件驱动型I/O
事件驱动型I/O是一种将基本的I/O操作(读和写)转换成事件的技术,而我们必须在程序中去处理这种事件。
如:当在socket上接受到数据时,这就称为一个接收事件,由我们提供的回调方法或者函数负责处理以此来响应这个事件。
一个事件驱动型框架可能会以一个基类作为其实点,实现一系列基本的事件处理方法。
class EventHandler: def fileno(self): 'Return the associated file descriptor' raise NotImplemented('must implement') def wants_to_receive(self): 'Return True if receiving is allowed' return False def handle_receive(self): 'Perform the receive operation' pass def wants_to_send(self): 'Return True if sending is requested' return False def handle_send(self): 'Send outgoing data' pass
把这个类的实例插入到一个事件循环中。
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()
事件循环的核心在于select()调用,它会轮询文件描述符检查他们是否处于活跃状态。
在调用select()之前,事件循环会简单地查询所有的处理方法,看它们是希望接收还是发送数据。然后把查询的结果以列表的方式提供给select()
结果就是,select()会返回已经在接受或发送事件上就绪的对象列表。对应的handle_receive()或者handle_send()方法就会被触发执行。
(1)基于UDP的网络服务:
import socket import time 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)
测试代码:
>>> from socket import * >>> s = socket(AF_INET, SOCK_DGRAM) >>> s.sendto(b'',('localhost',14000)) 0 >>> s.recvfrom(128) (b'Tue Sep 18 14:29:23 2012', ('127.0.0.1', 14000)) >>> s.sendto(b'Hello',('localhost',15000)) 5 >>> s.recvfrom(128) (b'Hello', ('127.0.0.1', 15000))
(2)基于TCP服务器:
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) self.client_handler = client_handler self.handler_list = handler_list def fileno(self): return self.sock.fileno() def wants_to_receive(self): return True def handle_receive(self): client, addr = self.sock.accept() # Add the client to the event loop's handler list self.handler_list.append(self.client_handler(client, self.handler_list)) 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() # Remove myself from the event loop's handler list 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)
TCP例子的关键点是从处理器中列表增加和删除客户端的操作。
对每一个连接,一个新的处理器被创建并加到列表中。当连接被关闭后,每个客户端负责将其从列表中删除。
事件驱动型I/O的核心部分都是有一个循环来轮询socket的活跃性并执行响应操作。
事件驱动型I/O优势在于:不使用线程和进程的条件下同时处理大量的连接。select()调用可以用来监视成百上千个socket,并且针对他们中间发生的事件做出响应。
缺点在于没有涉及真正的并发,如果任何一个事件处理方法阻塞了或者执行了一个耗时较长的计算,那么会阻塞整个程序的执行进程。
对于阻塞型或者需要长时间运行的计算,可以通过将任务发送给单独的线程或者进程来解决。
通过concurrent.futures模块来实现:
from concurrent.futures import ThreadPoolExecutor import os class ThreadPoolHandler(EventHandler): def __init__(self, nworkers): if os.name == 'posix': self.signal_done_sock, self.done_sock = socket.socketpair() else: 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() server.close() self.pending = [] self.pool = ThreadPoolExecutor(nworkers) def fileno(self): return self.done_sock.fileno() # Callback that executes when the thread is done def _complete(self, callback, r): self.pending.append((callback, r.result())) self.signal_done_sock.send(b'x') # Run a function in a thread pool def run(self, func, args=(), kwargs={},*,callback): r = self.pool.submit(func, *args, **kwargs) r.add_done_callback(lambda r: self._complete(callback, r)) def wants_to_receive(self): return True # Run callback functions of completed work def handle_receive(self): # Invoke all pending callback functions for callback, result in self.pending: callback(result) self.done_sock.recv(1) self.pending = []
在代码中,run()
方法被用来将工作提交给回调函数池,处理完成后被激发。
实际工作被提交给 ThreadPoolExecutor
实例。 不过一个难点是协调计算结果和事件循环,为了解决它,我们创建了一对socket并将其作为某种信号量机制来使用。
当线程池完成工作后,它会执行类中的 _complete()
方法。
这个方法再某个socket上写入字节之前会讲挂起的回调函数和结果放入队列中。 fileno()
方法返回另外的那个socket。
因此,这个字节被写入时,它会通知事件循环, 然后 handle_receive()
方法被激活并为所有之前提交的工作执行回调函数。
坦白讲,说了这么多连我自己都晕了。 下面是一个简单的服务器,演示了如何使用线程池来实现耗时的计算:
# A really bad Fibonacci implementation def fib(n): if n < 2: return 1 else: return fib(n - 1) + fib(n - 2) 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) if __name__ == '__main__': pool = ThreadPoolHandler(16) handlers = [ pool, UDPFibServer(('',16000))] event_loop(handlers)
运行这个服务器,然后试着用其它Python程序来测试它:
from socket import * sock = socket(AF_INET, SOCK_DGRAM) for x in range(40): sock.sendto(str(x).encode('ascii'), ('localhost', 16000)) resp = sock.recvfrom(8192) print(resp[0])
你应该能在不同窗口中重复的执行这个程序,并且不会影响到其他程序,尽管当数字便越来越大时候它会变得越来越慢。
十三、发送和接收大型数组
通过网络连接发送和接受连续数据的大型数组,并尽量减少数据的复制操作。
利用 memoryviews
来发送和接受大数组:
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:]
为了测试程序,首先创建一个通过socket连接的服务器和客户端程序:
>>> from socket import * >>> s = socket(AF_INET, SOCK_STREAM) >>> s.bind(('', 25000)) >>> s.listen(1) >>> c,a = s.accept() >>>
在客户端(另外一个解释器中):
>>> from socket import * >>> c = socket(AF_INET, SOCK_STREAM) >>> c.connect(('localhost', 25000))
本节的目标是你能通过连接传输一个超大数组。这种情况的话,可以通过 array
模块或 numpy
模块来创建数组:
# Server >>> import numpy >>> a = numpy.arange(0.0, 50000000.0) >>> send_from(a, c) >>> # Client >>> import numpy >>> a = numpy.zeros(shape=50000000, dtype=float) >>> a[0:10] array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) >>> recv_into(a, c) >>> a[0:10] array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]) >>>
在数据密集型分布式计算和平行计算程序中,自己写程序来实现发送/接受大量数据并不常见。
不过,要是你确实想这样做,你可能需要将你的数据转换成原始字节,以便给低层的网络函数使用。
你可能还需要将数据切割成多个块,因为大部分和网络相关的函数并不能一次性发送或接受超大数据块。
一种方法是使用某种机制序列化数据——可能将其转换成一个字节字符串。
不过,这样最终会创建数据的一个复制。 就算你只是零碎的做这些,你的代码最终还是会有大量的小型复制操作。
通过使用内存视图展示了一些魔法操作。
本质上,一个内存视图就是一个已存在数组的覆盖层。
不仅仅是那样, 内存视图还能以不同的方式转换成不同类型来表现数据。 这个就是下面这个语句的目的:
>>> view = memoryview(arr).cast('B')
它接受一个数组 arr并将其转换为一个无符号字节的内存视图。
这个视图能被传递给socket相关函数, 比如 socket.send()
或 send.recv_into()
。
在内部,这些方法能够直接操作这个内存区域。例如,sock.send()
直接从内存中发生数据而不需要复制。 send.recv_into()
使用这个内存区域作为接受操作的输入缓冲区。
剩下的一个难点就是socket函数可能只操作部分数据。
通常来讲,我们得使用很多不同的 send()
和 recv_into()
来传输整个数组。
不用担心,每次操作后,视图会通过发送或接受字节数量被切割成新的视图。 新的视图同样也是内存覆盖层。因此,还是没有任何的复制操作。
这里有个问题就是接受者必须事先知道有多少数据要被发送, 以便它能预分配一个数组或者确保它能将接受的数据放入一个已经存在的数组中。
如果没办法知道的话,发送者就得先将数据大小发送过来,然后再发送实际的数组数据。