第十一章 网络与web编程
1 作为客户端与http服务交互
示例代码如下:
服务端使用django,部分代码如下:
# 处理get请求 if request.method == 'GET': # TODO 测试get请求参数 print('receive the get request') print('==' * 20) print("param1", request.GET.get('name1')) print("param2", request.GET.get('name2')) print('==' * 20)# 处理post请求 if request.method == 'POST': # TODO 测试post请求参数 print('receive the post request...') param1 = request.POST.get('name1', None) param2 = request.POST.get('name2', None) print('==' * 20) print('param1', param1) print('param2', param2)
服务端输出结果如下:
receive the get request
========================================
param1 value1
param2 value2
========================================
客记端代码如下:
from urllib import request, parse # 使用本地工程进行测试 url = 'http://localhost:8000/jkx/index/' parms = {'name1': 'value1', 'name2': 'value2' } queryString = parse.urlencode(parms) # 发送get请求 #u = request.urlopen(url + '?' + queryString) # 发送post请求 u2 = request.urlopen(url, queryString.encode('ascii')) #resp = u.read() #print(resp) resp2 = u2.read() print(resp2) #服务端返回内容 # b'<!DOCTYPE html>\n<html>\n<head>\n <meta charset="utf-8">\n ...
2 创建TCP服务器
服务器端
from socketserver import BaseRequestHandler, \ TCPServer, \ StreamRequestHandler, \ ThreadingTCPServer, \ ForkingTCPServer import socket 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 print('msg:', msg) self.request.send(b'hello') # 方式二 class EchoHandler2(StreamRequestHandler): timeout = 5 rbufsize = -1 wbufsize = 0 disable_nagle_algorithm = False def handle(self): print('Got connection from', self.client_address) try: for line in self.rfile: self.wfile.write(line) except socket.timeout: print('Timed out!') def thread_pool(): NWROKERS = 16 serv = TCPServer(('', 20000), EchoHandler) for n in range(NWROKERS): t = Thread(target=serv.serve_forever()) t.daemon = True t.start() if __name__ == '__main__': # 单线程的tcp服务器 # 可以在TCPServer初始化前设置(允许服务器重新绑定一个之前使用过的端口号) # TCPServer.allow_reuse_address = True # bind_and_activate可以在初始化完成后,调整socket serv = TCPServer(('', 20000), EchoHandler, bind_and_activate=False) # 允许服务器重新绑定一个之前使用过的端口号 serv.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) serv.server_bind() serv.server_activate() # 处理多个客户端请求, # 1 可以合用 # serv = ThreadingTCPServer(('', 20000), EchoHandler) # 2 为每个客端连接创建一个新的进程或线程 # serv = ForkingTCPServer(('', 20000), EchoHandler) # 3 也可以使用线程池的方式 # thread_pool() serv.serve_forever()
客户端
from socket import socket, AF_INET, SOCK_STREAM # 创建套接字 s = socket(AF_INET, SOCK_STREAM) # 连接服务器 s.connect(('localhost', # 服务器ip 20000)) # 端口号 # 发送消息 print(s.send(b'hi retacn')) # 返回信息 print(s.recv(8192))
使用socket实现的简单服务器
from socket import socket, AF_INET, SOCK_STREAM def echo_handler(address, client_sock): print('Go 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)
3 创建UDP服务器
服务器端
from socketserver import BaseRequestHandler, UDPServer, ThreadingUDPServer import time class TimeHandler(BaseRequestHandler): 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) print('msg', msg) sock.sendto(b'hello', self.client_address) # udp无连接,消息可能丢失,应用场景流媒体/游戏 if __name__ == '__main__': serv = UDPServer(('', 20000), TimeHandler) # 并发操作可以使用 # serv = ThreadingUDPServer(('', 20000), TimeHandler) serv.serve_forever()
客户端
from socket import socket, AF_INET, SOCK_DGRAM s = socket(AF_INET, SOCK_DGRAM) print(s.sendto(b'hi retacn', ('localhost', 20000))) print(s.recvfrom(8192))
使用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: msg, addr = sock.recvfrom(8192) print('Got message from ', addr) resp = time.ctime() sock.sendto(resp.encode('asscii'), addr) if __name__ == '__main__': time_server(('', 20000))
4 通过CIDR地址生成对应的ip地址
import ipaddress def ipv4(): net = ipaddress.ip_network('123.45.67.64/27') print('net', net) # 可以按索引取值 print('the first ip: ', net[0], type(net)) # 成员检查 a = ipaddress.ip_network('123.45.67.69') print(a in net) for a in net: print(a) def ipv6(): net6 = ipaddress.ip_network('12:3456:78:90ab:cd:ef01:23:30/125') print('net6', net6) for a in net6: print(a) if __name__ == '__main__': ipv4() # 运行结果为: # net 123.45.67.64/27 # 123.45.67.64 # ...` # 123.45.67.95 # ipv6() # 运行结查为: # net6 12:3456:78:90ab:cd:ef01:23:30 / 125 # 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
5 创建一个简单的REST接口
调度器
#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2016/4/17 12:41 # @Author : Retacn # @Site : 简单的rest接口(一个简单的调度器), # 不提供一些高级特性(认证,cookies,重定向等), # 实现更多支持,可以使用webob和paste等第三方库 # @File : rest_demo.py # @Software: PyCharm __author__ = "retacn" __copyright__ = "property of mankind." __license__ = "CN" __version__ = "0.0.1" __maintainer__ = "retacn" __email__ = "zhenhuayue@sina.com" __status__ = "Development" 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, # 包含web服务器提供的CGI接口中取得的值 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>''' # 一个WSGI程序必须要返回一个字符串序列 def hello_world(environ, # 包含web服务器提供的CGI接口中取得的值 start_response): # 初始化一个请求对象而必须调用的函数 start_response('200 OK', # http状态值 [('Content-type', 'text/html')]) # http请求头 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') if __name__ == '__main__': from Eleven.rest.resty import PathDispatcher from wsgiref.simple_server import make_server 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() # 可以使用浏览器测试,也可以编写代码进行测试
测试程序
from urllib import request u = request.urlopen('http://localhost:8080/hello?name=Retacn') print(u.read().decode('utf-8')) # 运行结果如下: # <html> # <head> # <title>Hello Retacn</title> # </head> # <body> # <h1>Hello Retacn!</h1> # </body> # </html> u = request.urlopen('http://localhost:8080/localtime') print(u.read().decode('utf-8')) # 运行结果如下: # <?xml version="1.0"?> # <time> # <year>2016</year> # <month>4</month> # <day>17</day> # <hour>13</hour> # <minute>28</minute> # <second>39</second> # </time>
6 通过xml-rpc实现简单的远程序用
服务端程序
from xmlrpc.server import SimpleXMLRPCServer class KeyValueServer: _rpc_methods_ = ['get', 'set', 'delete', 'exists', 'keys'] def __init__(self, address): self._data = {} # 实例化rpc服务,单线程的 self._serv = SimpleXMLRPCServer(address, allow_none=True) # 注册rpc方法 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]) print(s.keys()) print(s.get('foo')) print(s.get('spam')) print(s.delete('spam')) print(s.exists('spam')) #运行结果如下: # ['foo', 'spam'] # bar # [1, 2, 3] # None # False
7 在不同的python解释器之间交互
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() if __name__ == "__main__": echo_server(('', 25000), authkey=b'retacn')
from multiprocessing.connection import Client c = Client(('localhost', 25000), authkey=b'retacn') c.send('hello') print(c.recv()) c.send(42) print(c.recv()) c.send([1, 2, 3, 4, 5, 6]) print(c.recv()) #运行结果如下: # hello # 42 # [1, 2, 3, 4, 5, 6]
8 实现远程方法调用
RPC处理器
import pickle 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: # 使用json格式的序列化消息 # func_name,args,kwargs=json.loads(connection.recv()) func_name, args, kwargs = pickle.loads(connection.recv()) try: r = self._functions[func_name](*args, **kwargs) # connection.send(json.dumps(r)) connection.send(pickle.dumps(r)) except Exception as e: # connection.send(json.dumps(e)) connection.send(pickle.dumps(e)) except EOFError: pass
消息服务器
from multiprocessing.connection import Listener from threading import Thread from Eleven.msg_rpc.msg_rpc_server import RPCHandler 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 = RPCHandler() handler.register_function(add) handler.register_function(sub) # 启动消息服务器 rpc_server(handler, ('localhost', 17000), authkey=b'retacn')
客户端用来传送请求的RPC代理类
import pickle import json class RPCProxy: def __init__(self, connection): self._connection = connection def __getattr__(self, item): def do_rpc(*args, **kwargs): # 使用json格式的序列化消息 # self._connection.send(json.dumps((item, args, kwargs))) self._connection.send(pickle.dumps((item, args, kwargs))) # result = json.loads(self._connection.recv()) result = pickle.loads(self._connection.recv()) # TODO 语句含义 if isinstance(result, Exception): raise result return result return do_rpc
消息层远程调用客户端
from multiprocessing.connection import Client from Eleven.msg_rpc.msg_rpc_client_proxy import RPCProxy c = Client(('localhost', 17000), authkey=b'retacn') proxy = RPCProxy(c) print(proxy.add(2, 3)) print(proxy.sub(2, 3)) print(proxy.sub([1, 2], 4)) #运行结果如下 # 5 # -1 # Traceback (most recent call last): # File "D:/workspace_pycharm/python3_cookbook/Eleven/msg_rpc/msg_rpc_client.py", line 25, in <module> # print(proxy.sub([1, 2], 4)) # File "D:\workspace_pycharm\python3_cookbook\Eleven\msg_rpc\msg_rpc_client_proxy.py", line 29, in do_rpc # raise result # TypeError: unsupported operand type(s) for -: 'list' and 'int'
9 简单的客户端认证
认证过程
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 from Eleven.auth.auth_hmac import server_authenticate secret_key = b'retacn' 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) if __name__ == '__main__': echo_server(('', 18000))
客户端
from socket import socket, AF_INET, SOCK_STREAM from Eleven.auth.auth_hmac import client_authenticate secret_key = b'retacn' 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) # 运行结果: # b'Hello World'
10 在网络服务中加入ssl
下载openssl安装略
生成自签名的证书,便于测试
D:\workspace_pycharm\python3_cookbook\Eleven\ssl>opensslreq -new -x509 -days 365 -nodes -out server_cert.pem -keyout server_key.pem
Loading 'screen' into random state - done
Generating a 1024 bit RSA private key
.......................................................................++++++
.............++++++
writing new private key to 'server_key.pem'
-----
You are about to be asked to enterinformation that will be incorporated
into your certificate request.
What you are about to enter is what iscalled a Distinguished Name or a DN.
There are quite a few fields but you canleave some blank
For some fields there will be a defaultvalue,
If you enter '.', the field will be leftblank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name)[Some-State]:SD
Locality Name (eg, city) []:ZB
Organization Name (eg, company) [InternetWidgits Pty Ltd]:retacn
Organizational Unit Name (eg, section)[]:retacn
#如果使用本机测试,可以填写localhost,否则使用服务器的域名
Common Name (e.g. server FQDN or YOUR name)[]:localhost
Email Address []:zhenhuayue@sina.com
D:\workspace_pycharm\python3_cookbook\Eleven\ssl>ls
server_cert.pem server_key.pem ssl_client.py ssl_server.py
服务器端
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('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)) if __name__ == '__main__': echo_server(('', 20000)) #运行结果: # Got connection <ssl.SSLSocket fd=444, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 20000), raddr=('127.0.0.1', 6273)> ('127.0.0.1', 6273) # Connection closed
客户端
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)) print(s_ssl.send(b'Hello World')) print(s_ssl.recv(8192)) #运行结果如下: # 11 # b'Hello World'
以上程序存在的问题是不能与标准库中存在的网络服务兼容,可以通过以下方式实现
未作测试
11 进程间传递socket文件描述符
应用场景:服务器响应连接请求,但实际的响应逻辑在另一个解释器中执行
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() worker_p = multiprocessing.Process(target=worker, args=(c1, c2)) worker_p.start() server_p = multiprocessing.Process(target=server, args=(('', 15000), c1, c2, worker_p.pid)) server_p.start() c1.close() c2.close() # 可以使用secureCRT与服务器进行连接 # 运行结果如下: # SERVER:GOT connection from ('127.0.0.1', 25309) # CHILD: GOT FD 220 # CHILD:RECV b'hello retacn\r\n'
服务器和工作者以单独的程序来运行
from multiprocessing.connection import Listener from multiprocessing.reduction import send_handle import socket def server(work_address, port): # 等待工作者来连接 work_serv = Listener(work_address, authkey=b'retacn') worker = work_serv.accept() worker_pid = worker.recv() 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])) print(sys.argv[1], sys.argv[2])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'retacn') 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])
使用套接字来完成进程间传递socket文件描述符
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]))
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])