作为客户端与HTTP 服务交互
问题:
你需要通过HTTP 协议以客户端的方式访问多种服务。例如,下载数据或者与基于REST 的API 进行交互
解决方案:
对于简单的事情来说,通常使用urllib.request 模块就够了。例如,发送一个简单的HTTP GET 请求到远程的服务上,可以这样做:
1 from os import linesep 2 from pprint import pprint 3 from urllib import request, parse 4 from ssl import SSLContext, PROTOCOL_SSLv23 5 6 7 #要查询的主页 8 url = 'https://httpbin.org/get' 9 10 #查询的参数,也就是url中get请求的参数 11 parms = { 12 'name1' : 'value1', 13 'name2' : 'value2' 14 } 15 16 #浏览器头部 17 header = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)' 18 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36' 19 } 20 21 #把字典转换成get请求的字符串 22 querystring = parse.urlencode(parms) 23 24 #使用ssl的协议 25 gcontext = SSLContext(PROTOCOL_SSLv23) 26 27 #创建一个get请求 28 req = request.Request(url + '?' + querystring) 29 30 #获取get请求响应的内容 31 response = request.urlopen(req, context=gcontext).read().decode('utf-8') 32 33 #打印返回的json字符 34 print(response)
以上代码执行返回的结果为:
{ "args": { "name1": "value1", "name2": "value2" }, "headers": { "Accept-Encoding": "identity", "Connection": "close", "Host": "httpbin.org", "User-Agent": "Python-urllib/3.6" }, "origin": "114.241.48.46", "url": "https://httpbin.org/get?name1=value1&name2=value2" }
如果你需要使用POST 方法在请求主体中发送查询参数,可以将参数编码后作为可选参数提供给urlopen() 函数,就像这样:
1 from urllib import request, parse 2 from ssl import SSLContext, PROTOCOL_SSLv23 3 4 5 #要查询的主页 6 url = 'https://httpbin.org/post' 7 8 #查询的参数,也就是url中get请求的参数 9 parms = { 10 'name1' : 'value1', 11 'name2' : 'value2' 12 } 13 14 #浏览器头部 15 header = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)' 16 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36' 17 } 18 19 #使用ssl的协议 20 gcontext = SSLContext(PROTOCOL_SSLv23) 21 22 #构建post的查询字符串 23 querystring = parse.urlencode(parms) 24 25 #构建post请求 26 req = request.Request(url, querystring.encode('ascii'), headers=header) 27 28 #获取post请求响应的内容 29 response = request.urlopen(req, context=gcontext).read().decode('utf-8') 30 31 #打印返回的json字符 32 print(response)
以上代码执行返回的结果为:
{ "args": {}, "data": "", "files": {}, "form": { "name1": "value1", "name2": "value2" }, "headers": { "Accept-Encoding": "identity", "Connection": "close", "Content-Length": "25", "Content-Type": "application/x-www-form-urlencoded", "Host": "httpbin.org", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36" }, "json": null, "origin": "114.241.48.46", "url": "https://httpbin.org/post" }
如果需要交互的服务比上面的例子都要复杂, 也许应该去看看requests 库(https://pypi.python.org/pypi/requests)。例如,下面这个示例采用requests 库重新实现了上面的操作:
1 import requests 2 from ssl import SSLContext, PROTOCOL_SSLv23 3 4 5 #需要访问的url主页 6 url = 'https://httpbin.org/post' 7 8 parms = { 9 'name1' : 'value1', 10 'name2' : 'value2' 11 } 12 13 #浏览器头部 14 header = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)' 15 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36' 16 } 17 18 #使用ssl的协议 19 gcontext = SSLContext(PROTOCOL_SSLv23) 20 21 #构建post请求 22 req = requests.post(url=url, data=parms, headers=header) 23 24 #获取post请求响应的内容 25 response = req.text 26 27 #打印返回的json字符 28 print(response)
以上代码执行返回的结果为:
{ "args": {}, "data": "", "files": {}, "form": { "name1": "value1", "name2": "value2" }, "headers": { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Content-Length": "25", "Content-Type": "application/x-www-form-urlencoded", "Host": "httpbin.org", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36" }, "json": null, "origin": "114.241.48.46", "url": "https://httpbin.org/post" }
下面这个示例利用requests 库发起一个HEAD 请求,并从响应中提取出一些HTTP 头数据的字段:
1 #获取请求头的信息 2 import requests 3 4 5 resp = requests.head('https://www.jd.com') 6 7 status = resp.status_code 8 content_type = resp.headers['content-type'] 9 content_length = resp.headers['content-length'] 10 content_expires = resp.headers['expires'] 11 12 for k, v in resp.headers.items(): 13 print("{:<25}:{:}".format(k, v)) 14 15 16 #使用cookie请求 17 url = 'https://www.jd.com' 18 resp1 = requests.get(url) 19 resp2 = requests.get(url, cookies=resp1.cookies) 20 21 22 #上传二进制文件 23 url = 'http://httpbin.org/post' 24 files = { 'file': ('data.csv', open('data.csv', 'rb')) } 25 r = requests.post(url, files=files)
创建TCP 服务器
问题:
你想实现一个服务器,通过TCP 协议和客户端通信
解决方案:
创建一个TCP 服务器的一个简单方法是使用socketserver 库。例如,下面是一个简单的应答服务器:
1 from socketserver import BaseRequestHandler, TCPServer 2 3 class EchoHandler(BaseRequestHandler): 4 5 def handle(self): 6 print('Got connection from', self.client_address) 7 while True: 8 msg = self.request.recv(8192) 9 if not msg: 10 break 11 self.request.send(msg) 12 13 if __name__ == "__main__": 14 15 server = TCPServer(('', 20000), EchoHandler) 16 server.serve_forever()
在这段代码中,你定义了一个特殊的处理类,实现了一个handle() 方法,用来为客户端连接服务。request 属性是客户端socket,client address 有客户端地址。为了测试这个服务器,运行它并打开另外一个Python 进程连接这个服务器:
1 from socket import socket, AF_INET, SOCK_STREAM 2 3 4 sock = socket(AF_INET, SOCK_STREAM) 5 server = ('', 20000) 6 sock.connect(server) 7 sock.send('Golang大战Python'.encode('utf-8')) 8 data = sock.recv(8192) 9 print(data.decode('utf-8'))
很多时候, 可以很容易的定义一个不同的处理器。下面是一个使用StreamRequestHandler 基类将一个类文件接口放置在底层socket 上的例子:
1 from socketserver import StreamRequestHandler, TCPServer 2 3 4 class EchoHandler(StreamRequestHandler): 5 6 def handle(self): 7 print('Got connection from', self.client_address) 8 9 for line in self.rfile: 10 self.wfile.write(line) 11 12 if __name__ == "__main__": 13 server = TCPServer(('', 20000)) 14 server.serve_forever()
socketserver 可以让我们很容易的创建简单的TCP 服务器。但是,你需要注意的是,默认情况下这种服务器是单线程的,一次只能为一个客户端连接服务。如果你想处理多个客户端,可以初始化一个ForkingTCPServer 或者是ThreadingTCPServer 对象。例如:
1 from socketserver import ThreadingTCPServer 2 if __name__ == '__main__': 3 serv = ThreadingTCPServer(('', 20000), EchoHandler) 4 serv.serve_forever()
直接使用socket 库来实现服务器也并不是很难。下面是一个使用socket 直接编程实现的一个服务器简单例子:
1 from socket import socket, AF_INET, SOCK_STREAM 2 3 4 def echo_handle(address, client_sock): 5 print('Got connection from {}'.format(address)) 6 while True: 7 msg = client_sock.recv(8192) 8 if not msg: 9 break 10 client_sock.sendall(msg) 11 client_sock.close() 12 13 14 def echo_server(address, backlog=5): 15 sock = socket(AF_INET, SOCK_STREAM) 16 sock.bind(address) 17 sock.listen(backlog) 18 while True: 19 client_sock, client_address = sock.accept() 20 echo_handle(client_address, client_sock) 21 22 23 if __name__ == '__main__': 24 echo_server(('', 20000))
创建UDP 服务器
问题:
你想实现一个基于UDP 协议的服务器来与客户端通信
解决方案:
跟TCP 一样,UDP 服务器也可以通过使用socketserver 库很容易的被创建。例如,下面是一个简单的时间服务器:
1 from socketserver import BaseRequestHandler, UDPServer 2 import time 3 4 5 class TimeHandler(BaseRequestHandler): 6 def handle(self): 7 print('Got connection from', self.client_address) 8 msg, sock = self.request 9 resp = time.ctime() 10 sock.sendto(resp.encode('utf-8'), self.client_address) 11 12 if __name__ == '__main__': 13 server = UDPServer(('', 20000), TimeHandler) 14 server.serve_forever()
我们来测试下这个服务器,首先运行它,然后打开另外一个Python 进程向服务器发送消息:
1 from socket import socket, AF_INET, SOCK_DGRAM 2 3 sock = socket(AF_INET, SOCK_DGRAM) 4 sock.sendto(b'', ('', 20000)) 5 data = sock.recvfrom(8192) 6 print(data)
UDPServer 类是单线程的,也就是说一次只能为一个客户端连接服务。实际使用中,这个无论是对于UDP 还是TCP 都不是什么大问题。如果你想要并发操作,可以实例化一ForkingUDPServer 或ThreadingUDPServer 对象:
1 from socketserver import ThreadingUDPServer 2 if __name__ == '__main__': 3 serv = ThreadingUDPServer(('',20000), TimeHandler) 4 serv.serve_forever()
直接使用socket 来是想一个UDP 服务器也不难,下面是一个例子:
1 from socket import socket, AF_INET, SOCK_DGRAM 2 import time 3 4 5 def time_server(address): 6 sock = socket(AF_INET, SOCK_DGRAM) 7 sock.bind(address) 8 while True: 9 msg, addr = sock.recvfrom(8192) 10 print('Got message from', addr) 11 resp = time.ctime() 12 sock.sendto(resp.encode('utf-8'), addr) 13 14 if __name__ == "__main__": 15 time_server(('', 20001))
通过CIDR 地址生成对应的IP 地址集
问题:
你有一个CIDR 网络地址比如“123.45.67.89/27”,你想将其转换成它所代表的所有IP (比如,“123.45.67.64”, “123.45.67.65”, …, “123.45.67.95”))
解决方案:
可以使用ipaddress 模块很容易的实现这样的计算。例如:
1 import ipaddress 2 3 4 net = ipaddress.ip_network('123.45.67.64/27') 5 6 for i in net: 7 print(i) 8 9 10 net6 = ipaddress.ip_network('12:3456:78:90ab:cd:ef01:23:30/125') 11 12 for a in net6: 13 print(a)
以上代码执行返回的结果为:
123.45.67.64
123.45.67.65
123.45.67.66
123.45.67.67
123.45.67.68
123.45.67.69
123.45.67.70
..................
..................
123.45.67.95
12:3456:78:90ab:cd:ef01:23:30
12:3456:78:90ab:cd:ef01:23:31
12:3456:78:90ab:cd:ef01:23:32
12:3456:78:90ab:cd:ef01:23:33
12:3456:78:90ab:cd:ef01:23:34
12:3456:78:90ab:cd:ef01:23:35
12:3456:78:90ab:cd:ef01:23:36
12:3456:78:90ab:cd:ef01:23:37
创建一个简单的REST 接口
问题:
你想使用一个简单的REST 接口通过网络远程控制或访问你的应用程序,但是你又不想自己去安装一个完整的web 框架
解决方案:
构建一个REST 风格的接口最简单的方法是创建一个基于WSGI 标准(PEP3333)的很小的库,下面是一个例子:
1 import cgi 2 3 4 def notfound_404(environ, start_response): 5 start_response('404 Not Found', [ ('Content-type', 'text/plain') ]) 6 return [b'Not Found'] 7 8 9 class PathDispatcher: 10 def __init__(self): 11 self.pathmap = {} 12 13 def __call__(self, environ, start_response): 14 path = environ['PATH_INFO'] 15 params = cgi.FieldStorage(environ['wsgi.input'], environ=environ) 16 17 method = environ['REQUEST_METHOD'].lower() 18 environ['params'] = {key: params.getvalue(key) for key in params} 19 handler = self.pathmap.get((method, path), notfound_404) 20 return handler(environ, start_response) 21 22 def register(self, method, path, function): 23 self.pathmap[method.lower(), path] = function 24 return function
为了使用这个调度器,你只需要编写不同的处理器,就像下面这样:
1 import time 2 _hello_resp = ''' 3 <html> 4 <head> 5 <title>Hello {name}</title> 6 </head> 7 <body> 8 <h1>Hello {name}!</h1> 9 </body> 10 </html>''' 11 12 13 def hello_world(environ, start_response): 14 start_response('200 OK', [ ('Content-type','text/html')]) 15 params = environ['params'] 16 resp = _hello_resp.format(name=params.get('name')) 17 yield resp.encode('utf-8') 18 19 20 _localtime_resp = ''' 21 <?xml version="1.0"?> 22 <time> 23 <year>{t.tm_year}</year> 24 <month>{t.tm_mon}</month> 25 <day>{t.tm_mday}</day> 26 <hour>{t.tm_hour}</hour> 27 <minute>{t.tm_min}</minute> 28 <second>{t.tm_sec}</second> 29 </time>''' 30 31 32 def localtime(environ, start_response): 33 start_response('200 OK', [ ('Content-type', 'application/xml') ]) 34 resp = _localtime_resp.format(t=time.localtime()) 35 yield resp.encode('utf-8') 36 37 38 if __name__ == '__main__': 39 from resty import PathDispatcher 40 from wsgiref.simple_server import make_server 41 42 # Create the dispatcher and register functions 43 dispatcher = PathDispatcher() 44 dispatcher.register('GET', '/hello', hello_world) 45 dispatcher.register('GET', '/localtime', localtime) 46 47 # Launch a basic server 48 httpd = make_server('', 8080, dispatcher) 49 print('Serving on port 8080...') 50 httpd.serve_forever()
要测试下这个服务器,你可以使用一个浏览器或urllib 和它交互。例如:
1 from urllib.request import urlopen 2 3 4 req = urlopen('http://localhost:8080/hello?name=Guido') 5 resp = req.read().decode('utf-8') 6 print(resp) 7 8 req1 = urlopen('http://localhost:8080/localtime') 9 resp1 = req1.read().decode('utf-8') 10 print(resp1)
通过XML-RPC 实现简单的远程调用
问题:
你想找到一个简单的方式去执行运行在远程机器上面的Python 程序中的函数或方法
解决方案:
实现一个远程方法调用的最简单方式是使用XML-RPC。下面我们演示一下一个实现了键-值存储功能的简单服务器:
1 from xmlrpc.server import SimpleXMLRPCServer 2 3 4 class KeyValueServer: 5 _rpc_methods_ = ['get', 'set', 'delete', 'exists', 'keys'] 6 7 def __init__(self, address): 8 self._data = {} 9 self._serv = SimpleXMLRPCServer(address, allow_none=True) 10 for name in self._rpc_methods_: 11 self._serv.register_function(getattr(self, name)) 12 13 def get(self, name): 14 return self._data[name] 15 16 def set(self, name, value): 17 self._serv[name] = value 18 19 def delete(self, name): 20 del self._data[name] 21 22 def exists(self, name): 23 return name in self._data 24 25 def keys(self): 26 return list(self._data) 27 28 def serve_forever(self): 29 self._serv.serve_forever() 30 31 if __name__ == "__main__": 32 kvserver = KeyValueServer(('', 30000)) 33 kvserver.serve_forever()
下面我们从一个客户端机器上面来访问服务器:
1 from xmlrpc.client import ServerProxy 2 3 4 s = ServerProxy('http://localhost:30000', allow_none=True) 5 s.set('foo', 'bar') 6 s.get('foo')
简单的客户端认证
问题:
你想在分布式系统中实现一个简单的客户端连接认证功能,又不想像SSL 那样的复杂
解决方案:
可以利用hmac 模块实现一个连接握手,从而实现一个简单而高效的认证过程。下面是代码示例:
1 import hmac 2 import os 3 4 5 def client_authenticate(connection, secret_key): 6 7 message = connection.recv(32) 8 hash = hmac.new(secret_key, message) 9 digest = hash.digest() 10 connection.send(digest) 11 12 13 def server_authenticate(connection, secret_key): 14 15 message = os.urandom(32) 16 connection.send(message) 17 hash = hmac.new(secret_key, message) 18 digest = hash.digest() 19 response = connection.recv(len(digest)) 20 21 return hmac.compare_digest(digest, response)
基本原理是当连接建立后,服务器给客户端发送一个随机的字节消息(这里例子中使用了os.urandom() 返回值)。客户端和服务器同时利用hmac 和一个只有双方知道的密钥来计算出一个加密哈希值。然后客户端将它计算出的摘要发送给服务器,服务器通过比较这个值和自己计算的是否一致来决定接受或拒绝连接。摘要的比较需要使用hmac.compare digest() 函数。使用这个函数可以避免遭到时间分析攻击,不要用简单的比较操作符(==)。为了使用这些函数,你需要将它集成到已有的网络或消息代码中。例如,对于sockets,服务器代码应该类似下面:
1 import hmac 2 import os 3 4 5 def client_authenticate(connection, secret_key): 6 7 message = connection.recv(32) 8 hash = hmac.new(secret_key, message) 9 digest = hash.digest() 10 connection.send(digest) 11 12 13 def server_authenticate(connection, secret_key): 14 15 message = os.urandom(32) 16 connection.send(message) 17 hash = hmac.new(secret_key, message) 18 digest = hash.digest() 19 response = connection.recv(len(digest)) 20 21 return hmac.compare_digest(digest, response) 22 23 #服务端 24 25 from socket import socket, AF_INET, SOCK_STREAM 26 27 28 secret_key = b'golang fight python' 29 30 def echo_handler(client_sock): 31 if not server_authenticate(client_sock, secret_key): 32 client_sock.close() 33 return 34 35 while True: 36 msg = client_sock.recv(8192) 37 if not msg: 38 break 39 client_sock.send(msg) 40 41 def echo_server(address): 42 s = socket(AF_INET, SOCK_STREAM) 43 s.bind(address) 44 s.listen(5) 45 46 while True: 47 c, a = s.accept() 48 echo_handler(c) 49 50 echo_server(('', 30050)) 51 52 53 #客户端 54 55 from socket import socket, AF_INET, SOCK_STREAM 56 57 secret_key = b'golang fight python' 58 59 s = socket(AF_INET, SOCK_STREAM) 60 s.connect(('', 30050)) 61 client_authenticate(s, secret_key) 62 s.send(b'Hello World') 63 resp = s.recv(8192)
在网络服务中加入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' # Private key of the server CERTFILE = 'server_cert.pem' # Server certificate (given to client) def echo_client(s): while True: data = s.recv(8192) if data == b'': break s.send(data) s.close() print('Connection closed') def echo_server(address): s = socket(AF_INET, SOCK_STREAM) s.bind(address) s.listen(5) 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) except Exception as e: print('{}: {}'.format(e.__class__.__name__, e)) echo_server(('', 20010)) #客户端 from socket import socket, AF_INET, SOCK_STREAM import ssl s = socket(AF_INET, SOCK_STREAM) s_ssl = ssl.wrap_socket(s, ca_certs='server_cert.pem') s_ssl.connect(('', 20010)) s_ssl.send(b'Hello world') data = s_ssl.recv(8192) print(data)