- 之前我们搭建了一个极简的WSGI服务器, 可以处理基本的HTTP请求,但是由于我们建立的服务一次只能处理一个客户端请求,在当前的请求处理完成之前,它不能接受新的连接。
服务器的代码
- 我们将处理请求的这块逻辑抽出来,代码如下:
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()
仔细看,当handle_request方法还没有结束的时候,循环会阻塞在这里,无法监听后续的请求。
客户端与服务端之间的通信
- 为了让两个程序通过网络彼此通讯,我们用到了socket,那么socket是什么呢?
socket
- socket是一个通信终端的抽象概念,它允许程序通过文件描述符与另一个程序通信
- TCP连接的socket对是一个拥有4个值的tuple,用来标识TCP连接的两个端点: 本地IP地址、本地端口、外部IP地址、外部端口。
- socket对是网络上每个tcp连接的唯一标识。这两个成对的值标识各自端点,一个IP地址和一个端口号,通常被称为一个socket。
- 例子
socket pair
- tuple {10.10.10.2:49152, 12.12.12.3:8888} 是客户端上一个唯一标识两个TCP连接终端的socket, {12.12.12.3:8888, 10.10.10.2:49152} 是服务端上一个唯一标识相同的两个TCP连接终端的socket。IP地址12.12.12.3和端口8888在TCP连接中用来识别服务器端点(同样适用于客户端)。
Python建立socket链接
-
服务器创建一个TCP/IP socket链接
- 新建socket连接
# AF_INET:服务器之间网络通信。 SOCK_STREAM: 流式socket , for TCP listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- 设置一些socket选项(这是可选的)
# SOL_SOCKET: 想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET # SO_REUSEADDR: 打开或关闭地址复用功能。当值不等于0时,打开,否则,关闭。 listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- 服务器绑定地址
# 在TCP中,调用bind允许你指定端口号,IP地址,要么两个都有,要么就都没有。 listen_socket.bind(SERVER_ADDRESS)
- 开始监听
# listen方法只供服务器调用。它告诉内核应该接受给这个socket传入的连接请求 # REQUEST_QUEUE_SIZE代表连接请求队列的长度 listen_socket.listen(REQUEST_QUEUE_SIZE)
- 上述步骤完成后,服务器开始逐个接受客户端连接。当一个连接可用accept返回要连接的客户端socket。然后服务器读从客户端socket取请求数据,打印出响应标准输出然后给客户端socket传回消息。然后服务器关闭客户端连接,准备接受一个新的客户端连接。
-
客户端连接服务器
- 和服务端0建立连接类似
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())
- 客户端只需提供服务器的远程地址或是主机名和远程端口号来连接。
- 客户端没有调用bind和accept。其原因是客户端不关心本地IP地址和端口号。客户端调用connect时内核中的TCP/IP socket会自动分配本地IP地址和端口号。本地端口被称为临时端口。
文件描述符
- 当一个进程打开现有的文件,创建一个新的文件,或者创建一个新的socket连接的时候,内核返回给它的一个非负整数。
- 在UNIX中,一切都是文件,内核通过文件描述符指向一个打开的文件。当你需要读写文件的时候,就是用文件描述符来识别的。
- UNINS shell默认分配文件描述符0给标准输入进程,1是标准输出,2是标准错误
怎么保证你的服务器能同时处理多个请求?或者换个说法,如何编写并发服务器?
- 在UNIX下,最简单的方式是用一个fork()系统调用
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()
- 运行多个curl命令之后,即使子进程处理一个进程之后会休眠60秒,但是这并不会影响处理其它的客户端请求,因为它们是完全独立的进程了。
- 在调用一次fork()之后,新进程会是原进程的子进程,原进程称为父进程。 子进程会复制父进程的数据信息。而后程序就分两个进程继续运行了。在子进程内,这个方法会返回0;在父进程内,这个方法会返回子进程的编号PID。可以使用PID来区分两个进程。
- 在上面的代码中,父进程关闭了客户端的连接,那么子进程是怎么继续读取客户端的socket连接的呢?
- 父进程fork出一个子进程之后,这个子进程得到了一个父进程文件描述符。
- 内核根据文件描述符的值来决定是否关闭连接socket,只有其值为0才会关闭。
- 服务器产生一个子进程,子进程拷贝父进程文件描述符,内核增加引用描述符的值。在一个父进程一个子进程的例子中,描述符引用值就是2,当父进程关闭连接socket,它只会把引用值减为1,不会小到让内核关闭socket。
- 子进程也关闭了父进程监听socket的重复拷贝,是因为它不关心接受新的客户端连接,而只在乎处理已连接客户端的响应:
listen_socket.close() # close child copy
小结
- 服务器socket创建过程(socket,bind,listen,accept)
- 客户端socket创建过程(socket,connect)
- fork()函数的意义:创建子进程,复制父进程的数据信息。而后程序就分两个进程继续运行
- 在UNIX下写并发服务器最简单的方法是用fork()系统调用。一个进程fork出一个新进程,它就变成新进程的父进程。
- 调用fork后,父进程和子进程公用同样的文件描述符。内核用文件描述符应用值来决定关闭或打开文件/socket。
- 服务器父进程的角色:从客户端接受新的连接,fork一个子进程去处理请求,继续接受新的连接。
网友评论