该内容已被发布者删除 该内容被自由微信恢复
文章于 2017年4月2日 被检测为删除。
查看原文
被用户删除
其他

一起写一个 Web 服务器(3)

2015-07-31 高世界 Python开发者

(点击上方公众号,可快速关注)


英文:ruslan spivak

译者:伯乐在线 - 高世界

译文:http://python.jobbole.com/81820/

点击“阅读原文”,查看原文网页版



忘了前两篇的同学,可以先回顾一下:





,你已经创造了一个可以处理基本的 HTTP GET 请求的 WSGI 服务器。我还问了你一个问题,“怎么让服务器在同一时间处理多个请求?”在本文中你将找到答案。那么,系好安全带加大马力。你马上就乘上快车啦。准备好Linux、Mac OS X(或任何类unix系统)和 Python。


首先咱们回忆下一个基本的Web服务器长什么样,要处理客户端请求它得做什么。你在创建的是一个迭代的服务器,每次处理一个客户端请求。除非已经处理了当前的客户端请求,否则它不能接受新的连接。有些客户端对此就不开心了,因为它们必须要排队等待,而且如果服务器繁忙的话,这个队伍会很长。



以下是迭代服务器webserver3a.py的代码:


#########################

terative server - webserver3a.py


# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #

#########################

import socket


SERVER_ADDRESS = (HOST, PORT) = '', 8888

REQUEST_QUEUE_SIZE = 5


def handle_request(client_connection):

request = client_connection.recv(1024)

print(request.decode())

http_response = b"""

HTTP/1.1 200 OK


Hello, World!

"""

client_connection.sendall(http_response)


def serve_forever():

listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

listen_socket.bind(SERVER_ADDRESS)

listen_socket.listen(REQUEST_QUEUE_SIZE)

print('Serving HTTP on port {port} ...'.format(port=PORT))


while True:

client_connection, client_address = listen_socket.accept()

handle_request(client_connection)

client_connection.close()


if __name__ == '__main__':

serve_forever()


要观察服务器同一时间只处理一个客户端请求,稍微修改一下服务器,在每次发送给客户端响应后添加一个60秒的延迟。添加这行代码就是告诉服务器睡眠60秒。



以下是睡眠版的服务器webserver3b.py代码:


#########################

Iterative server - webserver3b.py


Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X

- Server sleeps for 60 seconds after sending a response to a client


#########################

import socket

import time


SERVER_ADDRESS = (HOST, PORT) = '', 8888

REQUEST_QUEUE_SIZE = 5


def handle_request(client_connection):

request = client_connection.recv(1024)

print(request.decode())

http_response = b"""

HTTP/1.1 200 OK


Hello, World!

"""

client_connection.sendall(http_response)

time.sleep(60) # sleep and block the process for 60 seconds


def serve_forever():

listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

listen_socket.bind(SERVER_ADDRESS)

listen_socket.listen(REQUEST_QUEUE_SIZE)

print('Serving HTTP on port {port} ...'.format(port=PORT))


while True:

client_connection, client_address = listen_socket.accept()

handle_request(client_connection)

client_connection.close()


if __name__ == '__main__':

serve_forever()


启动服务器:


$ python webserver3b.py


现在打开一个新的控制台窗口,运行以下curl命令。你应该立即就会看到屏幕上打印出了“Hello, World!”字符串:


$ curl http://localhost:8888/hello

Hello, World!


And without delay open up a second terminal window and run the same curl command:


立刻再打开一个控制台窗口,然后运行相同的curl命令:


$ curl http://localhost:8888/hello


如果你是在60秒内做的,那么第二个curl应该不会立刻产生任何输出,而是挂起。而且服务器也不会在标准输出打印出新请求体。在我的Mac上看起来像这样(在右下角的黄色高亮窗口表示第二个curl命令正挂起,等待服务器接受这个连接):



当你等待足够长时间(大于60秒)后,你会看到第一个curl终止了,第二个curl在屏幕上打印出“Hello, World!”,然后挂起60秒,然后再终止:



它是这么工作的,服务器完成处理第一个curl客户端请求,然后睡眠60秒后开始处理第二个请求。这些都是顺序地,或者迭代地,一步一步地,或者,在我们例子中是一次一个客户端请求地,发生。


咱们讨论点客户端和服务器的通信吧。为了让两个程序能够网络通信,它们必须使用socket。你在已经见过socket了,但是,socket是什么呢?



socket就是通信终端的一种抽象,它允许你的程序使用文件描述符和别的程序通信。本文我将详细谈谈在Linux/Mac OS X上的TCP/IP socket。理解socket的一个重要的概念是TCP socket对。


TCP的socket对是一个4元组,标识着TCP连接的两个终端:本地IP地址、本地端口、远程IP地址、远程端口。一个socket对唯一地标识着网络上的TCP连接。标识着每个终端的两个值,IP地址和端口号,通常被称为socket。



所以,元组{10.10.10.2:49152, 12.12.12.3:8888}是客户端TCP连接的唯一标识着两个终端的socket对。元组{12.12.12.3:8888, 10.10.10.2:49152}是服务器TCP连接的唯一标识着两个终端的socket对。标识TCP连接中服务器终端的两个值,IP地址12.12.12.3和端口8888,在这里就是指socket(同样适用于客户端终端)。


服务器创建一个socket并开始接受客户端连接的标准流程经历通常如下:



服务器创建一个TCP/IP socket。在Python里使用下面的语句即可:


listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)


服务器可能会设置一些socket选项(这是可选的,上面的代码就设置了,为了在杀死或重启服务器后,立马就能再次重用相同的地址)。


listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)


然后,服务器绑定指定地址,bind函数分配一个本地地址给socket。在TCP中,调用bind可以指定一个端口号,一个IP地址,两者都,或者两者都不指定。


listen_socket.bind(SERVER_ADDRESS)


然后,服务器让这个socket成为监听socket。


listen_socket.listen(REQUEST_QUEUE_SIZE)


listen方法只会被服务器调用。它告诉内核它要接受这个socket上的到来的连接请求了。


做完这些后,服务器开始循环地一次接受一个客户端连接。当有连接到达时,aceept调用返回已连接的客户端socket。然后,服务器从这个socket读取请求数据,在标准输出上把数据打印出来,并回发一个消息给客户端。然后,服务器关闭客户端连接,准备好再次接受新的客户端连接。


下面是客户端使用TCP/IP和服务器通信要做的:



以下是客户端连接服务器,发送请求并打印响应的示例代码:


import socket


# create a socket and connect to a server

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock.connect(('localhost', 8888))


# send and receive some data

sock.sendall(b'test')

data = sock.recv(1024)

print(data.decode())


创建socket后,客户端需要连接服务器。这是通过connect调用做到的:


sock.connect(('localhost', 8888))


客户端仅需提供要连接的远程IP地址或主机名和远程端口号即可。


可能你注意到了,客户端不用调用bind和accept。客户端没必要调用bind,是因为客户端不关心本地IP地址和本地端口号。当客户端调用connect时内核的TCP/IP栈自动分配一个本地IP址地和本地端口。本地端口被称为暂时端口( ephemeral port),也就是,short-lived 端口。



服务器上标识着一个客户端连接的众所周知的服务的端口被称为well-known端口(举例来说,80就是HTTP,22就是SSH)。操起Python shell,创建个连接到本地服务器的客户端连接,看看内核分配给你创建的socket的暂时的端口是多少(在这之前启动webserver3a.py或webserver3b.py):


>>> import socket

>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

>>> sock.connect(('localhost', 8888))

>>> host, port = sock.getsockname()[:2]

>>> host, port

('127.0.0.1', 60589)


上面这个例子中,内核分配了60589这个暂时端口。


在我开始回答第二部分提出的问题前,我需要快速讲一下几个重要的概念。你很快就知道为什么重要了。两个概念是进程和文件描述符。


什么是进程?进程就是一个正在运行的程序的实例。比如,当服务器代码执行时,它被加载进内存,运行起来的程序实例被称为进程。内核记录了进程的一堆信息用于跟踪,进程ID就是一个例子。当你运行服务器 webserver3a.py 或 webserver3b.py 时,你就在运行一个进程了。



在控制台窗口运行webserver3b.py:


$ python webserver3b.py


在别的控制台窗口使用ps命令获取这个进程的信息:


$ ps | grep webserver3b | grep -v grep

7182 ttys003 0:00.04 python webserver3b.py


ps命令表示你确实运行了一个Python进程webserver3b。进程创建时,内核分配给它一个进程ID,也就是 PID。在UNIX里,每个用户进程都有个父进程,父进程也有它自己的进程ID,叫做父进程ID,或者简称PPID。假设默认你是在BASH shell里运行的服务器,那新进程的父进程ID就是BASH shell的进程ID。



自己试试,看看它是怎么工作的。再启动Python shell,这将创建一个新进程,使用 os.getpid() 和 os.getppid() 系统调用获取Python shell进程的ID和父进程ID(BASH shell的PID)。然后,在另一个控制台窗口运行ps命令,使用grep查找PPID(父进程ID,我的是3148)。在下面的截图你可以看到在我的Mac OS X上,子Python shell进程和父BASH shell进程的关系:



另一个要了解的重要概念是文件描述符。那么什么是文件描述符呢?文件描述符是当打开一个存在的文件,创建一个文件,或者创建一个socket时,内核返回的非负整数。你可能已经听过啦,在UNIX里一切皆文件。内核使用文件描述符来追踪进程打开的文件。当你需要读或写文件时,你就用文件描述符标识它好啦。Python给你包装成更高级别的对象来处理文件(和socket),你不必直接使用文件描述符来标识一个文件,但是,在底层,UNIX中是这样标识文件和socket的:通过它们的整数文件描述符。



默认情况下,UNIX shell分配文件描述符0给进程的标准输入,文件描述符1给进程的标准输出,文件描述符2给标准错误。



就像我前面说的,虽然Python给了你更高级别的文件或者类文件的对象,你仍然可以使用对象的fileno()方法来获取对应的文件描述符。回到Python shell来看看怎么做:


>>> import sys

>>> sys.stdin

<open file '<stdin>', mode 'r' at 0x102beb0c0>

>>> sys.stdin.fileno()

0

>>> sys.stdout.fileno()

1

>>> sys.stderr.fileno()

2


虽然在Python中处理文件和socket,通常使用高级的文件/socket对象,但有时候你需要直接使用文件描述符。下面这个例子告诉你如何使用write系统调用写一个字符串到标准输出,write使用整数文件描述符做为参数:


>>> import sys

>>> import os

>>> res = os.write(sys.stdout.fileno(), 'hellon')

hello


有趣的是——应该不会惊讶到你啦,因为你已经知道在UNIX里一切皆文件——socket也有一个分配给它的文件描述符。再说一遍,当你创建一个socket时,你得到的是一个对象而不是非负整数,但你也可以使用我前面提到的fileno()方法直接访问socket的文件描述符。


还有一件事我想说下:你注意到了吗?在第二个例子webserver3b.py中,当服务器进程在60秒的睡眠时你仍然可以用curl命令来连接。当然啦,curl没有立刻输出什么,它只是在那挂起。但为什么服务器不接受连接,客户端也不立刻被拒绝,而是能连接服务器呢?答案就是socket对象的listen方法和它的BACKLOG参数,我称它为 REQUEST_QUEUE_SIZE(请求队列长度)。BACKLOG参数决定了内核为进入的连接请求准备的队列长度。当服务器webser3b.py睡眠时,第二个curl命令可以连接到服务器,因为内核在服务器socket的进入连接请求队列上有足够的可用空间。


然而增加BACKLOG参数不会神奇地让服务器同时处理多个客户端请求,设置一个合理大点的backlog参数挺重要的,这样accept调用就不用等新连接建立起来,立刻就能从队列里获取新的连接,然后开始处理客户端请求啦。


吼吼!你已经了解了非常多的背景知识啦。咱们快速简要重述到目前为止你都学了什么(如果你都知道啦就温习一下吧)。



  • 迭代服务器

  • 服务器socket创建流程(socket, bind, listen, accept)

  • 客户端连接创建流程(socket, connect)

  • socket对

  • socket

  • 临时端口和众所周知端口

  • 进程

  • 进程ID(PID),父进程ID(PPID),父子关系。

  • 文件描述符

  • listen方法的BACKLOG参数的意义


现在我准备回答第二部分问题的答案了:“怎样才能让服务器同时处理多个请求?”或者换句话说,“怎样写一个并发服务器?”




在Unix上写一个并发服务器最简单的方法是使用fork()系统调用。


下面就是新的牛逼闪闪的并发服务器webserver3c.py的代码,它能同时处理多个客户端请求(和咱们迭代服务器例子webserver3b.py一样,每个子进程睡眠60秒):



#########################

# Concurrent server - webserver3c.py


# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X


# - Child process sleeps for 60 seconds after handling a client's request

# - Parent and child processes close duplicate descriptors


#########################

import os

import socket

import time


SERVER_ADDRESS = (HOST, PORT) = '', 8888

REQUEST_QUEUE_SIZE = 5


def handle_request(client_connection):

request = client_connection.recv(1024)

print(

'Child PID: {pid}. Parent PID {ppid}'.format(

pid=os.getpid(),

ppid=os.getppid(),

)

)

print(request.decode())

http_response = b"""

HTTP/1.1 200 OK


Hello, World!

"""

client_connection.sendall(http_response)

time.sleep(60)


def serve_forever():

listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

listen_socket.bind(SERVER_ADDRESS)

listen_socket.listen(REQUEST_QUEUE_SIZE)

print('Serving HTTP on port {port} ...'.format(port=PORT))

print('Parent PID (PPID): {pid}n'.format(pid=os.getpid()))


while True:

client_connection, client_address = listen_socket.accept()

pid = os.fork()

if pid == 0: # child

listen_socket.close() # close child copy

handle_request(client_connection)

client_connection.close()

os._exit(0) # child exits here

else: # parent

client_connection.close() # close parent copy and loop over


if __name__ == '__main__':

serve_forever()



【提示】:第三篇篇幅较长,已超过微信字数限制。分了上学两部分来发。


另外本文代码段也多,且微信无代码高亮插件。建议大家直接点击“阅读原文”,看我们的网页版,支持代码高亮。


当然了,你也可以继续看多图文第二条。



Python开发者

微信号:PythonCoder

可能是东半球最好的 Python 微信号

--------------------------------------

商务合作QQ:2302462408

招聘和猎头服务QQ:2302462408

投稿网址:top.jobbole.com


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存