Python实现web服务器之 单进程单线程非阻塞实现并发及其原理

  在Python实现web服务器入门学习多进程、多线程实现并发HTTP服务器中,我们知道可以分别通过多进程、多线程的方式实现并发服务器,那么,是否可以通过单进程单线程的程序实现类似功能呢?

  实际上,在Python多任务学习分别通过yield关键字、greenlet以及gevent实现多任务中,我们知道gevent可以通过协程的方式实现多任务,且相较于yield关键字和greenlet而言,gevent屏蔽了很多实现细节,使用起来简单方便。

  一、gevent实现并发HTTP服务器

  下面代码以gevent实现并发HTTP服务器(即一种单进程、单线程、非阻塞的方式):

  from gevent import monkey

  import gevent

  import socket

  import re

  monkey.patch_all()

  def serve_client(new_client_socket):

  """为这个客户端返回数据"""

  # 6.接收浏览器发送过来的http请求

  request = new_client_socket.recv(1024).decode("utf-8")

  # 7.将请求报文分割成字符串列表

  request_lines = request.splitlines()

  print(request_lines)

  # 8.通过正则表达式提取浏览器请求的文件名

  file_name = None

  ret = re.match(r"^[^/]+(/[^ ]*)", request_lines[0])

  if ret:

  file_name = ret.group(1)

  print("file_name:", file_name)

  if file_name == "/":

  file_name = "/index.html"

  # 9.返回http格式的应答数据给浏览器

  try:

  f = open("./Charisma" + file_name, "rb")

  except Exception:

  response = "HTTP/1.1 404 NOT FOUND\r\n"

  response += "\r\n"

  response += "-----file not found-----"

  new_client_socket.send(response.encode("utf-8"))

  else:

  # 9.1 读取发送给浏览器的数据-->body

  html_content = f.read()

  f.close()

  # 9.2 准备发送给浏览器的数据-->header

  response = "HTTP/1.1 200 OK\r\n"

  response += "\r\n"

  # 将response header发送给浏览器--先以utf-8格式编码

  new_client_socket.send(response.encode("utf-8"))

  # 将response body发送给浏览器--直接是以字节形式发送

  new_client_socket.send(html_content)

  # 10. 关闭此次服务的套接字

  new_client_socket.close()

  def main():

  """用来完成程序整体控制"""

  # 1.创建套接字

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

  # 通过设定套接字选项解决[Errno 98]错误

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

  # 2.绑定端口

  tcp_server_socket.bind(("", 7899))

  # 3.变为监听套接字

  tcp_server_socket.listen(128)

  while True:

  # 4.等待新客户端连接

  new_client_socket, client_addr = tcp_server_socket.accept()

  # 5.为连接上的客户端服务

  # 创建一个greenlet并不会导致其立即得到切换执行,

  # 还需要在其父greenlet(在哪个程序控制流中创建该greenlet,

  # 则这个程序控制流就是父greenlet)中遇到正确的阻塞延时类操作或调用greenlet对象的join()方法

  #(此处不需要使用join()函数,因为主程序由于死循环的缘故不会在greenlet执行结束前退出)

  greenlet = gevent.spawn(serve_client, new_client_socket)

  # 关闭监听套接字

  tcp_server_socket.close()

  if __name__ == "__main__":

  main()

  至此,本文和Python实现web服务器入门学习笔记(3)——多进程、多线程实现并发HTTP服务器给出了三种实现并发HTTP服务器的方式,对比之后,可以发现:

  对于进程、线程实现方式,服务器都是在客户端数量大于1时开辟新的程序执行控制流(分别叫子进程、子线程),且程序控制流之间一般相对独立,不会因为一个的阻塞而导致其他程序控制流无法执行,以避免在单进程且单线程的程序中,先建立请求的客户端因各种原因引起程序阻塞而导致其他客户端的请求得不到执行。如:上述通过进程和线程实现并发服务器程序中的accept()、recv()方法均为阻塞类操作。

  对于协程实现方式,服务器为实现并发,虽然也会开辟新的程序执行控制流(这里叫greenlet,程序执行控制流可以承担很多身份,比如:进程、线程、greenlet,关于greenlet的详细说明,具体请见Python多任务学习笔记(10)——分别通过yield关键字、greenlet以及gevent实现多任务),但是这些程序执行控制流之间通过确定的时序作相互间切换实现并发,切换时机为程序中所有延时阻塞类操作的地方,指定程序控制流切换执行时机的方式有两种:

  程序规模很小时,对于所有延时阻塞类操作,如:time.sleep(),socket.accept(),socket.recv(),使用gevent模块中的同名操作做替换,如:gevent.sleep(),gevent.accept(),gevent.recv();

  程序规模很大,且多处使用了涉及延时阻塞类操作时,不用挨个做模块替换,通过gevent.monkey模块中的patch_all()函数改变所有的阻塞类操作的行为,使得每当程序遇到阻塞类操作则切换至其他greenlet。

  对于协程实现方式,在主程序执行控制流中,通过gevent.spawn()这一类方法创建一个greenlet之后,主程序执行控制流自动成为该greenlet的父greenlet,如果程序中仅存在这两个greenlet,则程序也会在遇到正确的阻塞延时类操作时,在二者之间切换执行,请比较下面两段代码:

  1. 程序未正确指定阻塞延时类操作:

  import gevent

  import time

  def foo():

  print('Explicit context switch to foo!')

  gevent.sleep(0.0)

  print('Explicit context switch to foo again!')

  def main():

  greenlet = gevent.spawn(foo)

  print('Explicit execution in main!')

  time.sleep(0.0)

  print('Explicit context switch to main again!')

  time.sleep(0.0)

  print("The end of main!")

  # 确保主程序(即主greenlet)等待子greenlet执行完毕之后才退出

  greenlet.join()

  if __name__ == '__main__':

  main()

  上述代码的运行结果为:

  Explicit execution in main!

  Explicit context switch to main again!

  The end of main!

  Explicit context switch to foo!

  Explicit context switch to foo again!

  2. 程序正确指定了阻塞延时类操作:

  import gevent

  def foo():

  print('Explicit context switch to foo!')

  gevent.sleep(0.0)

  print('Explicit context switch to foo again!')

  def main():

  greenlet = gevent.spawn(foo)

  print('Explicit execution in main!')

  gevent.sleep(0.0)

  print('Explicit context switch to main again!')

  gevent.sleep(0.0)

  # greenlet.join()

  print("The end of main!")

  if __name__ == '__main__':

  main()

  上述代码运行结果为:

  Explicit execution in main!

  Explicit context switch to foo!

  Explicit context switch to main again!

  Explicit context switch to foo again!

  The end of main!

  对比上述两段代码,我们知道:

  创建一个greenlet并不会导致其立即得到切换执行,还需要在其父greenlet(在哪个程序控制流中创建该greenlet,则这个程序控制流就是父greenlet)中遇到正确的阻塞延时类操作或调用greenlet对象的join()方法;

  即使不调用greenlet对象的join()方法,只要使用正确的阻塞延时类操作,程序依然可以按照期望的顺序执行完毕。

  二、单进程单线程非阻塞实现并发原理

  实际上,从单进程、单线程、非阻塞这几个关键字就可以发现,要想通过单进程、单线程实现并发,首要是要解决单进程、单线程的程序可能面对的程序阻塞问题,因为这一般会无谓地耗费时间。郑州哪个人流医院好 http://www.csyhjlyy.com/

  那么,自然地,我们会想到:是否可以让原本阻塞的操作不阻塞?答案是肯定的:对于socket对象中的accept()、recv()等方法,其原本都是阻塞类操作,可以通过调用socket对象的setblocking()方法设置其为非阻塞模式。

  然而,问题在于:在将socket对象设置为非阻塞模式的情况下,在调用其accept()、recv()方法时,如果未能立刻正确返回,则程序会抛出异常。故此时需要进行异常捕捉和处理,保证程序不被中断。

  基于上述讨论,下面代码简单演示了单进程单线程非阻塞实现并发的原理:

  import socket

  import time

  def initialize(port):

  # 1.创建服务器端TCP协议socket

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

  # 2.绑定本地IP和端口

  tcp_server_sock.bind(("", port))

  # 3.设置套接字为监听状态

  tcp_server_sock.listen(128)

  # 4.设置套接字为非阻塞状态

  tcp_server_sock.setblocking(False)

  return tcp_server_sock

  def non_blocking_serve(tcp_server_sock):

  # 定义一个列表,用于存放已成功连接但是未完成数据发送的客户端

  client_sock_list = list()

  while True:

  try:

  new_client_sock, new_client_addr = tcp_server_sock.accept()

  except Exception as exception:

  print(exception)

  else:

  print("新客户端", new_client_sock, "已成功连接!")

  new_client_sock.setblocking(False)

  client_sock_list.append(new_client_sock)

  for client_sock in client_sock_list:

  try:

  recv_data = client_sock.recv(1024)

  except Exception as exception:

  print(exception)

  else:

  if recv_data:

  # 表明客户端发来了数据

  print(recv_data)

  else:

  # 客户端已调用close()方法,recv()返回为空

  client_sock_list.remove(client_sock)

  client_sock.close()

  print("客户端", client_sock, "已断开连接!")

  def main():

  tcp_server_sock = initialize(8888)

  non_blocking_serve(tcp_server_sock)

  if __name__ == '__main__':

  main()

  对于上述代码,需要说明的几点是:

  程序24行定义的列表client_sock_list用于存放已成功连接但是未完成数据发送的客户端对象;

  程序36行遍历列表client_sock_list,挨个通过recv()方法通过非阻塞方式接收数据,而recv()方法正确返回有两种情况:

  客户端将数据正确发送了过来,此时recv_data变量非空;

  客户端完成了此次请求,主动先断开了连接,此时recv_data为空。

  程序45行将已经完成请求的客户端移出列表client_sock_list,避免列表过长产生无效遍历,导致程序性能下降。

posted @ 2020-05-07 16:33  网管布吉岛  阅读(453)  评论(0编辑  收藏  举报