看python标准库理解Web
Web基于HTTP协议,HTTP协议基于TCP协议,TCP的实现产物就是socket,因此从socket开始,层层向上可以更好的理解Web。
1. 基本socket编程
基本的socket编程,从最简单到逐步复杂,有3种实现形式:阻塞式,多线程形式,基于select
1.1 阻塞式
阻塞的socket一次只能服务一个客户端,前一个客户断开连接,后一个客户端才能连接。
# coding:utf-8
from socket import socket, AF_INET, SOCK_STREAM
host = '127.0.0.1'
port = 8080
socket_obj = socket(AF_INET, SOCK_STREAM)
socket_obj.bind((host, port))
socket_obj.listen(5)
conn, address = socket_obj.accept()
print('Tcp server connected by', address)
while True:
data = conn.recv(1024)
if not data:
break
print(data)
conn.close()
1.2 多线程式
多线程式可以同时接受多个客户端的连接,同时为多个客户端提供服务。利用的是操作系统可以并行的执行多个线程。
# coding:utf-8
from socket import socket, AF_INET, SOCK_STREAM
import threading
host = '127.0.0.1'
port = 8080
socket_obj = socket(AF_INET, SOCK_STREAM)
socket_obj.bind((host, port))
socket_obj.listen(5)
def serve(connection):
while True:
data = conn.recv(1024)
if not data:
break
print(data)
connection.close()
while True:
conn, address = socket_obj.accept()
print('Tcp server connected by', address)
threading.Thread(target=serve, args=(conn,)).start()
1.3 select
select即多路复用选择器,不需要进程或者线程,即可实现并行服务多个客户端。
# coding:utf-8
from socket import socket, AF_INET, SOCK_STREAM
from select import select
host = '127.0.0.1'
port = 8080
socket_obj = socket(AF_INET, SOCK_STREAM)
socket_obj.bind((host, port))
socket_obj.listen(5)
socket_obj.setblocking(False)
readable_conns = [socket_obj]
while True:
ready_read_conns = select(readable_conns, [], [])[0]
for c in ready_read_conns:
if c == socket_obj:
new_conn, client_ads = c.accept()
print('Tcp server connected by', client_ads)
readable_conns.append(new_conn)
else:
data = c.recv(8)
if not data:
readable_conns.remove(c)
break
print(data)
2. 网络编程框架
直接基于socket编写网络应用程序效率低下,任何事情都要亲历亲为,显然这只适合学习阶段。python标准库socketserver
提供了一个简单的网络编程框架。只需几行代码,即可完成一个简单的TCP服务器:
# coding:utf-8
from socketserver import TCPServer, BaseRequestHandler
class MyTcpHandler(BaseRequestHandler):
def handle(self):
data = self.request.recv(1024).strip()
print(data)
tcp_server = TCPServer(('localhost', 8080), MyTcpHandler)
tcp_server.serve_forever()
标准库socketserver
一共才700多行代码,其中还包括大量的文档和注释,总体代码量很小,还是比较好理解的。
只理解最简单TCP服务实现,只需需要看里面的4个类BaseServer
,TcpServer
,BaseRequestHandler
,StreamRequestHandler
BaseServer
是一个设计用于被继承的“抽象类”,定义了一个Server的基本逻辑的架子,TcpServer
继承了BaseServer
实现基于TCP的Server,BaseServer
中的逻辑规定:一个“Server”必须要要一个“Hander”来处理客户端socket连接。因此,提供了BaseRequestHandler
这个设计用于被继承的抽象类,StreamRequestHandler
继承了BaseRequestHandler
,TcpServer
的Handler如果不想自己从头写的话,就继承StreamRequestHandler
。
BaseServer
类的serve_forever
方法如下:
def serve_forever(self, poll_interval=0.5):
self.__is_shut_down.clear()
try:
with _ServerSelector() as selector:
selector.register(self, selectors.EVENT_READ)
while not self.__shutdown_request:
ready = selector.select(poll_interval)
if ready:
self._handle_request_noblock()
self.service_actions()
finally:
self.__shutdown_request = False
self.__is_shut_down.set()
重点是第10行,会调用_handle_request_noblock
方法处理每个请求,这个方法的代码如下:
def _handle_request_noblock(self):
try:
request, client_address = self.get_request()
except OSError:
return
if self.verify_request(request, client_address):
try:
self.process_request(request, client_address)
except:
self.handle_error(request, client_address)
self.shutdown_request(request)
else:
self.shutdown_request(request)
重点是第8行,会调用process_request
方法处理请求,这个方法的代码如下:
def process_request(self, request, client_address):
self.finish_request(request, client_address)
self.shutdown_request(request)
finish_request
方法的代码如下:
def finish_request(self, request, client_address):
self.RequestHandlerClass(request, client_address, self)
也就是说,请求被处理的核心业务逻辑是在构造“Server”类时提供的“Handler”中,并且就是其构造方法。
再看BaseRequestHandler
的代码:
class BaseRequestHandler:
def __init__(self, request, client_address, server):
self.request = request
self.client_address = client_address
self.server = server
self.setup()
try:
self.handle()
finally:
self.finish()
def setup(self):
pass
def handle(self):
pass
def finish(self):
pass
到这里就可以知道了,客户端的socket连接处理是在handler
方法中,并且还有setup
,finish
等方法可以选择重写,增加前置处理和后置处理。
回过头来在看本节最开始的代码,首先定义了一个继承自BaseRequestHandler
的”Handler“,重写了handler
方法,其第一个参数request,其实就是客户端的socket连接对象,使用该连接进行收发数据即可。然后实例化了一个TcpServer,并调用serve_forever
方法,程序就开始监听指定的socket地址,并能同时响应多个客户端的请求了。
继承自BaseRequestHandler
的MyTcpHandler
只能通过handle
方法的第一个参数-这个socket连接对象处理数据,能不能使用文件api进行数据处理呢?能!BaseRequestHandler
的子类StreamRequestHandler
为我们将socket连接对象进行了包装,继承自这个类的“Handler”就可以使用类似与文件api的方法处理数据。StreamRequestHandler
类在其setup
方法中包装了rfile
和wfile
属性分别用来读写数据,其代码如下:
class StreamRequestHandler(BaseRequestHandler):
rbufsize = -1
wbufsize = 0
timeout = None
disable_nagle_algorithm = False
def setup(self):
self.connection = self.request
if self.timeout is not None:
self.connection.settimeout(self.timeout)
if self.disable_nagle_algorithm:
self.connection.setsockopt(socket.IPPROTO_TCP,
socket.TCP_NODELAY, True)
self.rfile = self.connection.makefile('rb', self.rbufsize)
self.wfile = self.connection.makefile('wb', self.wbufsize)
def finish(self):
if not self.wfile.closed:
try:
self.wfile.flush()
except socket.error:
pass
self.wfile.close()
self.rfile.close()
使用继承StreamRequestHandler
的方式再简单实现TCP服务器,代码如下:
# coding:utf-8
from socketserver import TCPServer, StreamRequestHandler
class MyTcpHandler(StreamRequestHandler):
def handle(self):
data = self.rfile.readline().strip()
print(data)
tcp_server = TCPServer(('localhost', 8080), MyTcpHandler)
tcp_server.serve_forever()
3. HTTPServer
有了TCPServer,就可以在其之上构建简单的HTTPServer,pyton的标准库也基于前一节的网络编程框架实现了简单的HTTP服务器。位于标准库模块http.server
该模块中的HTTPServer
可以说与socketserver
模块中的TCPServer
基本没有区别,其代码只有几行:
class HTTPServer(socketserver.TCPServer):
allow_reuse_address = 1
def server_bind(self):
socketserver.TCPServer.server_bind(self)
host, port = self.socket.getsockname()[:2]
self.server_name = socket.getfqdn(host)
self.server_port = port
确实,对于TCPServer
和HTTPServer
,其socket连接部分并没有什么不同,差别只是在于HTTPServer
需要解析HTTP报文,因此,主要的区别在于Handler
的实现。http.server
模块提供了一个设计用于被继承的BaseHTTPRequestHandler
,以及其2个子类SimpleHTTPRequestHandler
和CGIHTTPRequestHandler
,CGIHTTPRequestHandler
是SimpleHTTPRequestHandler
的子类。
BaseHTTPRequestHandler
类的handler方法如下:
def handle(self):
self.close_connection = True
self.handle_one_request()
while not self.close_connection:
self.handle_one_request()
这个方法本身逻辑比较简单,因为核心HTTP协议处理相关的核心处理逻辑都在handle_one_request
中,该方法完成对一次HTTP请求的处理。只是当HTTP头中有keep-alive
时,这个方法里的close_connection
变量为False
,下次HTTP请求就会仍使用此条TCP连接。从这里可以看出这个HTTP协议中的保持长连接keep-alive
是如何实现的。
再看handle_one_request
方法的代码:
def handle_one_request(self):
try:
self.raw_requestline = self.rfile.readline(65537)
if len(self.raw_requestline) > 65536:
self.requestline = ''
self.request_version = ''
self.command = ''
self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG)
return
if not self.raw_requestline:
self.close_connection = True
return
if not self.parse_request():
# An error code has been sent, just exit
return
mname = 'do_' + self.command
if not hasattr(self, mname):
self.send_error(
HTTPStatus.NOT_IMPLEMENTED,
"Unsupported method (%r)" % self.command)
return
method = getattr(self, mname)
method()
self.wfile.flush() #actually send the response if not already done.
except socket.timeout as e:
#a read or a write timed out. Discard this connection
self.log_error("Request timed out: %r", e)
self.close_connection = True
return
这个方法首先解析出HTTP报文的第一行,也就是HTTP报文的请求行,如果过长(超过65537)就直接返回错误响应了。接下来调用了parse_request
方法,这个方法的调用栈就比较深了,因为按照HTTP协议的规范,解析HTTP请求报文还是很复杂的,这个方法就不深究了。这里暂时只需要知道这个方法执行完后,command
变量就是HTTP的请求方法了,假如HTTP请求为
GET /index HTTP/1.1
那么这个command
就等于’GET‘, 那么第16行的变量mname
就为do_GET
接下来的代码,会在当前的Handler中找到do_GET
方法并调用,如果没有这个方法,就会返回“Unsupported method”
基于以上的知识,实现一个简单的HTTP服务器,代码如下:
# coding:utf-8
from http.server import HTTPServer, BaseHTTPRequestHandler
class MyHTPPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.wfile.write(b'Hello World')
http_server = HTTPServer(('localhost', 8080), MyHTPPRequestHandler)
http_server.serve_forever()
使用浏览器访问localhost:8080
就能看到返回'Hello World',而且任何url都是返回‘Hello World’,不管是localhost:8080/index
还是`localhost:8080/123 ,因为后台没有根据url不同做任何逻辑。
notice:
火狐可以看到“Hello World”但是chrome不可以,因为这里返回的“Hello World”字节流不是一个完整的HTTP响应报文。而且这里要是
b‘Hello World’
不能是字符串‘Hello World’
,wfile的write方法要求参数为字节对象。
如果要根据url处理的话,就要用到BaseHTTPRequestHandler
的'self.path',其中存放这url路径,请求为localhost:8080/index
则self.path='/index'
,url路由的起点就在这里。
4. 静态文件服务器
有了BaseHTTPRequestHandler
,我们不需要操心解析HTTP请求报文的事情了,但是HTTP响应报文还是需要亲自编写,设置响应状态,设置响应头,以及返回响应体。这也是比较复杂的事情,需要知道HTTP协议的细节。因此,对于静态的文件,http.server
模块提供了简单的SimpleHTTPRequestHandler
实现静态文件服务。对于动态内容,提供了CGIHTTPRequestHandler
以实现基于CGI技术的动态网页内容。
SimpleHTTPRequestHandler
包含有do_GET
和do_HEAD
方法,可以响应‘GET’和‘HEAD’请求。其逻辑是根据url返回当前目录下的指定文件。
直接使用SimpleHTTPRequestHandler
几行代码就可以实现一个静态文件服务器:
simple_file_server.py
# coding:utf-8
from http.server import HTTPServer, SimpleHTTPRequestHandler
http_server = HTTPServer(('127.0.0.1', 8080), SimpleHTTPRequestHandler)
http_server.serve_forever()
假定这个脚本所在的目录为/home/test/www
,这个目录的结构为:
test@ubuntu:~$ tree www
www/
├── static
│ └── readme.txt
└── simple_file_server.py
在这个目录下执行python3 simple_file_server.py
启动程序。
在浏览器访问 localhost:8080/static/readme.txt
就能看到文件readme.txt
的内容。
访问 localhost:8080/static
就能看到页面列出了static
目录的文件列表。
SimpleHTTPRequestHandler
的do_GET
方法代码如下:
def do_GET(self):
f = self.send_head()
if f:
try:
self.copyfile(f, self.wfile)
finally:
f.close()
其中调用的send_head
方法,该方法返回一个文件流,代表了HTTP响应报文的body
,接下来的try代码中,将这个文件流复制到wfile
,返回给了客户端。并且,在send_head
方法内部,已经分析了文件的类型,文件的长度,填充好了HTTP响应报文的状态码和响应头部。整个do_GET
方法就返回给了客户端一个完整的HTTP响应报文。
send_head
方法的代码如下:
def send_head(self):
path = self.translate_path(self.path)
f = None
if os.path.isdir(path):
parts = urllib.parse.urlsplit(self.path)
if not parts.path.endswith('/'):
self.send_response(HTTPStatus.MOVED_PERMANENTLY)
new_parts = (parts[0], parts[1], parts[2] + '/',
parts[3], parts[4])
new_url = urllib.parse.urlunsplit(new_parts)
self.send_header("Location", new_url)
self.end_headers()
return None
for index in "index.html", "index.htm":
index = os.path.join(path, index)
if os.path.exists(index):
path = index
break
else:
return self.list_directory(path)
ctype = self.guess_type(path)
try:
f = open(path, 'rb')
except OSError:
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
return None
try:
self.send_response(HTTPStatus.OK)
self.send_header("Content-type", ctype)
fs = os.fstat(f.fileno())
self.send_header("Content-Length", str(fs[6]))
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
self.end_headers()
return f
except:
f.close()
raise
这个方法会先分析url的path,translate_path
方法完成了对url中的参数和锚点的剔除,进行特殊字符解码,并实现了防止通过含有/../
这样的字符实现对任一路径的访问。比如url的path为/static/readme.txt?a=1&b=2
在translate_path
方法返回后就是/static/readme.txt
了。如果分析是目录的话,会先看目录下有没有’index.html‘或者’index.htm‘,有的话就返回该文件。否则调用list_directory
方法,这个方法会分析目录,列出里面的文件,采用字符串拼接的方式拼一个HTML文档出来。如果是文件的话,分根据文件名的后缀,猜测出应该填在Content-type
中的值。
5. CGI
SimpleHTTPRequestHandler
只是处理‘GET’和‘HEAD’请求并返回静态的内容,CGIHTTPRequestHandler
继承了SimpleHTTPRequestHandler
,增加了do_POST
方法以响应‘POST’请求。
do_POST
的代码如下:
def do_POST(self):
if self.is_cgi():
self.run_cgi()
else:
self.send_error(
HTTPStatus.NOT_IMPLEMENTED,
"Can only POST to CGI scripts")
is_cgi
方法比较简单,只是完成了判断请求是不是请求一个cgi脚本,判断方法其实就是看url是不是以cgi-bin
或者htbin
开头。从这里可以之道这个CGIHTTPRequestHandler
要求把cgi建本防止在这2个目录中。
如果是cgi请求,则运行方法run_cgi
,这个方法实现了CGI协议,会为CGI脚本准备好CGI协议中要求的各项环境变量,例如SERVER_NAME
,PATH_INFO
,QUERY_STRING
等数十个环境变量,具体的可以看CGI协议。环境变量准备好后存在变量名env
中,接下来会判断操作系有无fork
系统调用,有则说明是Unix类系统,使用fork的方式启动子进程执行脚本,否则以subprocess的方式执行脚本。并获取脚本的输出。
一个简单的用python语言写的cgi脚本如下:
cgi-bin/test.py
#!/usr/bin/python
# coding:utf-8
print('Content-Type: text/plain\n')
print('hello cgi')
notice:
第一行的”#!/usr/bin/python“在cgi脚本是必须的,不然服务器不知道如何执行cgi脚本
将前一节的静态文件服务器修改为CGI服务器,代码如下:
simple_cgi_server.py
# coding:utf-8
from http.server import HTTPServer, CGIHTTPRequestHandler
http_server = HTTPServer(('127.0.0.1', 8080), CGIHTTPRequestHandler)
http_server.serve_forever()
www
目录的结构如下:
www/
├── cgi-bin
│ └── test.py
└── simple_cgi_server.py
在www
目录下执行pyhton3 simple_cgi_server
,访问localhost:8080/cgi-bin/test.py
就能看到输出‘hello cgi’。
6. wsgi
CGI技术是动态网页的鼻祖,使用CGI技术开发Web,服务器(比如Apache)部署好后,按照Apache的配置,在指定的目录放置编写的cgi脚本,脚本不需要关心tcp如何连接,HTTP请求报文如何解析,这一切的信息,都被上层服务器(比如Apache,比如前几节的HttpServer)放在了一个Map对象env中,cgi脚本可以从里面通过操作Map的方式拿到如请求参数等信息,然后根据这些信息生成不同的HTTP响应报文即可。
CGI技术在一定程度上提高了网页开发效率,但是它有严重的不足,就是每个请求都需要另外启动一个进程,而且HTTP响应报文需要自己在脚本通过编码的方式生成。因此更高级的WSGI协议营运而生(在python领域是WSGI协议,在Java则时Servelt规范,在不同的语言有不同的生成动态网页内容的技术)。
wsgi协议的定义先不说,这个可以看相关文档。这里我们只关心有了wsgi协议,如何简化Web开发,或者说wsgi为我们提供了什么。
在pyton标准库中提供了一个wsgi服务器,在标准库模块wsgiref
中,这个服务器完全不考虑性能,是一个wsgi协议的参考实现。利用这个模块提供的功能,编写一个简单的Web应用程序,代码如下:
# coding:utf-8
from wsgiref import simple_server
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return [b'<h1>Hello, web!</h1>']
simple_server.make_server('localhost', 8080, application).serve_forever()
建立一个HTTP服务器只要一行代码,即第9行。其第3个参数是一个函数,这个函数就是wsgi函数。
上面的application
函数就是符合WSGI标准的一个HTTP处理函数,它接收两个参数:
environ:一个包含所有HTTP请求信息的dict对象;
start_response:一个发送HTTP响应的函数。
在application()函数中,调用:
start_response('200 OK', [('Content-Type', 'text/html')])
就发送了HTTP响应的Header,注意Header只能发送一次,也就是只能调用一次start_response
函数。start_response
函数接收两个参数,一个是HTTP响应码,一个是一组list表示的HTTP Header,每个Header用一个包含两个str的tuple表示。
通常情况下,都应该把Content-Type头发送给浏览器。其他很多常用的HTTP Header也应该发送。
然后,函数的返回值b'<h1>Hello, web!</h1>'
将作为HTTP响应的Body发送给浏览器。
有了WSGI,我们关心的就是如何从environ这个dict对象拿到HTTP请求信息,然后构造HTML,通过start_response()发送Header,最后返回Body。
整个application
函数本身没有涉及到任何解析HTTP的部分,也就是说,底层代码不需要我们自己编写,我们只负责在更高层次上考虑如何响应请求就可以了。
wsgi协议就没有了cgi对每个请求都启动一个新的进程问题,每个请求都在一个进程中。现在我们要开发一个Web,选择一个wsgi服务器,比如apache+mod_wsgi, 比如uWSGI,通过配置把请求处理逻辑指定到我们编写的wsgi函数来处理。我们只需要写好这个函数即可,不需要关心底层。
但是,开发Web的效率仍有提高的空间,在wsgi函数里,我们要基于上层提供的env这个字典中存储的信息,自己实现session的概念,自己进行url路径解析和路由,自己用start_response函数返回HTTP响应头。因此Web框架就产生了,框架本身有一个复杂的wsgi函数,它解析env字典,为我们包装了request,response,session这些概念。并为我们提供了更上层的API,我们使用这些API来完成Web的开发。像Django,Flask等就是在WSGI之上的Web开发框架。我们自己也能在WSGi之上实现一个Web框架。而且我们不一定要从编写wsgi函数开始实现框架,有一些第三方库提供了丰富的编写WSGI程序的库,就如同JQuery之于Js,比如广泛使用的‘Wekzeug’,它包装好了request,reposne等Web概念,并提供了很多有用的函数来方便WSGI程序的开发,Web框架Falsk就是使用‘Wekzeug’来开发的。