看python标准库理解web

看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个类BaseServerTcpServerBaseRequestHandlerStreamRequestHandler

BaseServer是一个设计用于被继承的“抽象类”,定义了一个Server的基本逻辑的架子,TcpServer继承了BaseServer实现基于TCP的Server,BaseServer中的逻辑规定:一个“Server”必须要要一个“Hander”来处理客户端socket连接。因此,提供了BaseRequestHandler这个设计用于被继承的抽象类,StreamRequestHandler继承了BaseRequestHandlerTcpServer的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方法中,并且还有setupfinish等方法可以选择重写,增加前置处理和后置处理。

回过头来在看本节最开始的代码,首先定义了一个继承自BaseRequestHandler的”Handler“,重写了handler方法,其第一个参数request,其实就是客户端的socket连接对象,使用该连接进行收发数据即可。然后实例化了一个TcpServer,并调用serve_forever方法,程序就开始监听指定的socket地址,并能同时响应多个客户端的请求了。

继承自BaseRequestHandlerMyTcpHandler只能通过handle方法的第一个参数-这个socket连接对象处理数据,能不能使用文件api进行数据处理呢?能!BaseRequestHandler的子类StreamRequestHandler为我们将socket连接对象进行了包装,继承自这个类的“Handler”就可以使用类似与文件api的方法处理数据。StreamRequestHandler类在其setup方法中包装了rfilewfile属性分别用来读写数据,其代码如下:

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

确实,对于TCPServerHTTPServer,其socket连接部分并没有什么不同,差别只是在于HTTPServer需要解析HTTP报文,因此,主要的区别在于Handler的实现。http.server模块提供了一个设计用于被继承的BaseHTTPRequestHandler,以及其2个子类SimpleHTTPRequestHandlerCGIHTTPRequestHandlerCGIHTTPRequestHandlerSimpleHTTPRequestHandler的子类。

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/indexself.path='/index',url路由的起点就在这里。

4. 静态文件服务器

有了BaseHTTPRequestHandler,我们不需要操心解析HTTP请求报文的事情了,但是HTTP响应报文还是需要亲自编写,设置响应状态,设置响应头,以及返回响应体。这也是比较复杂的事情,需要知道HTTP协议的细节。因此,对于静态的文件,http.server模块提供了简单的SimpleHTTPRequestHandler实现静态文件服务。对于动态内容,提供了CGIHTTPRequestHandler以实现基于CGI技术的动态网页内容。

SimpleHTTPRequestHandler包含有do_GETdo_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目录的文件列表。

SimpleHTTPRequestHandlerdo_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=2translate_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_NAMEPATH_INFOQUERY_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’来开发的。

转载于:https://my.oschina.net/u/1393056/blog/1633677

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值