下面我们使用Python来实现并发的Web Server,其中采用了多进程、多线程、协程、单进程单线程非阻塞的方式。

一、使用子进程来实现并发Web Server

参照 https://www.cnblogs.com/leokale-zz/p/11949208.html 中的代码,我们将其修改为支持并发的简单Web Server:

import socket
import re
import multiprocessing def handle_request(new_socket):
# 接收请求
recv_msg = ""
recv_msg = new_socket.recv(1024).decode("utf-8")
if recv_msg == "":
print("recv null")
new_socket.close()
return # 从请求中解析出URI
recv_lines = recv_msg.splitlines()
print(recv_lines.__len__())
# 使用正则表达式提取出URI
ret = re.match(r"[^/]+(/[^ ]*)", recv_lines[0])
if ret:
# 获取URI字符串
file_name = ret.group(1)
# 如果URI是/,则默认返回index.html的内容
if file_name == "/":
file_name = "/index.html" try:
# 根据请求的URI,读取相应的文件
fp = open("." + file_name, "rb")
except:
# 找不到文件,响应404
response_msg = "HTTP/1.1 404 NOT FOUND\r\n"
response_msg += "\r\n"
response_msg += "<h1>----file not found----</h1>"
new_socket.send(response_msg.encode("utf-8"))
else:
html_content = fp.read()
fp.close()
# 响应正确 200 OK
response_msg = "HTTP/1.1 200 OK\r\n"
response_msg += "\r\n" # 返回响应头
new_socket.send(response_msg.encode("utf-8"))
# 返回响应体
new_socket.send(html_content) # 关闭该次socket连接
new_socket.close() def main():
# 创建TCP SOCKET实例
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# # 设置重用地址
# tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定地址(默认本机IP)和端口
tcp_server_socket.bind(("", 7890))
# 监听
tcp_server_socket.listen(128)
# 循环接收客户端连接
while True:
new_socket, client_addr = tcp_server_socket.accept()
# 启动一个子进程来处理客户端的请求
sub_p = multiprocessing.Process(target=handle_request, args=(new_socket,))
sub_p.start()
# 这里要关闭父进程中的new_socket,因为创建子进程会复制一份new_socket给子进程
new_socket.close() # 关闭整个SOCKET
tcp_server_socket.close() if __name__ == "__main__":
main()

我们使用进程来实现并发的Web Server,也就是将Accept到new_socket传递给子进程去处理,处理函数还是handle_request。

但是这里注意,子进程会从父进程中将所有的变量进行拷贝,也就是说父进程和子进程中各有一份new_socket,而在Linux下,socket对应的也是一个文件描述符,而这两个new_socket实际上是指向同一个fd的。所以我们将new_socket交给子进程后,父进程就可以马上关闭自己的new_socket了,当子进程服务完毕后,关闭子进程中的new_socket,这样对应的FD才会正真关闭,此时才会触发四次挥手。所以父进程代码中蓝色部分new_socket.close()非常重要。

二、使用线程来实现并发Web Server

在第一节中,我们使用进程来实现并发,但是进程对资源消耗很大,一般不推荐使用。所以这里我们使用线程来实现并发,很简单,我们将 multiprocessing.Process 替换为 threaing.Thread就可以了:

import socket
import re
import threading def handle_request(new_socket):
# 接收请求
recv_msg = ""
recv_msg = new_socket.recv(1024).decode("utf-8")
if recv_msg == "":
print("recv null")
new_socket.close()
return # 从请求中解析出URI
recv_lines = recv_msg.splitlines()
print(recv_lines.__len__())
# 使用正则表达式提取出URI
ret = re.match(r"[^/]+(/[^ ]*)", recv_lines[0])
if ret:
# 获取URI字符串
file_name = ret.group(1)
# 如果URI是/,则默认返回index.html的内容
if file_name == "/":
file_name = "/index.html" try:
# 根据请求的URI,读取相应的文件
fp = open("." + file_name, "rb")
except:
# 找不到文件,响应404
response_msg = "HTTP/1.1 404 NOT FOUND\r\n"
response_msg += "\r\n"
response_msg += "<h1>----file not found----</h1>"
new_socket.send(response_msg.encode("utf-8"))
else:
html_content = fp.read()
fp.close()
# 响应正确 200 OK
response_msg = "HTTP/1.1 200 OK\r\n"
response_msg += "\r\n" # 返回响应头
new_socket.send(response_msg.encode("utf-8"))
# 返回响应体
new_socket.send(html_content) # 关闭该次socket连接
new_socket.close() def main():
# 创建TCP SOCKET实例
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# # 设置重用地址
# tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定地址(默认本机IP)和端口
tcp_server_socket.bind(("", 7890))
# 监听
tcp_server_socket.listen(128)
# 循环接收客户端连接
while True:
new_socket, client_addr = tcp_server_socket.accept()
# 启动一个线程来处理客户端的请求
t = threading.Thread(target=handle_request, args=(new_socket,))
t.start() # 关闭整个SOCKET
tcp_server_socket.close() if __name__ == "__main__":
main()

我们发现,除了将子进程的创建过程替换成了线程的创建过程,后面的new_socket.close()也被删除了,这是因为线程是公用进程资源的,new_socket不会被复制,所以socket对应的FD,只有一个new_socket指向他。

如果此时我们仍然在这里关闭new_socket,那么在线程再使用new_socket就会报错。如下信息:

Exception in thread Thread-5:
Traceback (most recent call last):
File "D:\Anaconda3_530\lib\threading.py", line 917, in _bootstrap_inner
self.run()
File "D:\Anaconda3_530\lib\threading.py", line 865, in run
self._target(*self._args, **self._kwargs)
File "D:/pycharm_project/leo1127/thread_web_server.py", line 44, in handle_request
new_socket.send(response_msg.encode("utf-8"))
OSError: [WinError 10038] 在一个非套接字上尝试了一个操作。

三、使用协程来实现并发Web Server

使用进程和线程来实现的并发Web Server,当并发访问量很大时,资源消耗都很高。所以这里使用协程来实现并发服务器。

import socket
import re
import gevent
from gevent import monkey
monkey.patch_all() def handle_request(new_socket):
# 接收请求
recv_msg = ""
recv_msg = new_socket.recv(1024).decode("utf-8")
if recv_msg == "":
print("recv null")
new_socket.close()
return # 从请求中解析出URI
recv_lines = recv_msg.splitlines()
print(recv_lines.__len__())
# 使用正则表达式提取出URI
ret = re.match(r"[^/]+(/[^ ]*)", recv_lines[0])
if ret:
# 获取URI字符串
file_name = ret.group(1)
# 如果URI是/,则默认返回index.html的内容
if file_name == "/":
file_name = "/index.html" try:
# 根据请求的URI,读取相应的文件
fp = open("." + file_name, "rb")
except:
# 找不到文件,响应404
response_msg = "HTTP/1.1 404 NOT FOUND\r\n"
response_msg += "\r\n"
response_msg += "<h1>----file not found----</h1>"
new_socket.send(response_msg.encode("utf-8"))
else:
html_content = fp.read()
fp.close()
# 响应正确 200 OK
response_msg = "HTTP/1.1 200 OK\r\n"
response_msg += "\r\n" # 返回响应头
new_socket.send(response_msg.encode("utf-8"))
# 返回响应体
new_socket.send(html_content) # 关闭该次socket连接
new_socket.close() def main():
# 创建TCP SOCKET实例
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# # 设置重用地址
# tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定地址(默认本机IP)和端口
tcp_server_socket.bind(("", 7890))
# 监听
tcp_server_socket.listen(128)
# 循环接收客户端连接
while True:
new_socket, client_addr = tcp_server_socket.accept()
# 启动一个协程来处理客户端的请求
gevent.spawn(handle_request, new_socket) # 关闭整个SOCKET
tcp_server_socket.close() if __name__ == "__main__":
main()

使用gevent来实现协程,并发处理请求。

四、使用单进程单线程非阻塞模拟并发(非并行)

前面我们使用的多进程和多线程来处理并发,是因为socket.recv()是阻塞的,每次accept一个连接,就需要交给一个新的进程或线程去处理,从而不影响下一个socket连接。

但是我们可以通过单进程单线程和非阻塞的方式来完成并发socket的处理:

import socket

def main():
# 创建TCP SOCKET实例
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# # 设置重用地址
# tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定地址(默认本机IP)和端口
tcp_server_socket.bind(("", 7890))
# 监听
tcp_server_socket.listen(128) # 将accept设置为非阻塞,这里设置一次,后面不管调多少次accept都是非阻塞的
tcp_server_socket.setblocking(False)
# 定义一个列表,将每次连接的socket加入该列表
client_socket_list = list() # 循环接收客户端连接
while True:
try:
new_socket, client_addr = tcp_server_socket.accept()
except Exception as ret:
# 当没有客户端链接的时候,抛出异常
pass
else:
# 当有客户端链接的时候
# 将new_socket.recv()设置为非阻塞的
new_socket.setblocking(False)
# 将new_socket加入列表
client_socket_list.append(new_socket) # 遍历socket列表,检查每一个socket是否有数据到达,或者客户端是否断开
for client_socket in client_socket_list:
try:
recv_content = client_socket.recv(1024)
except Exception as ret:
# 异常,表示该客户端没有发数据过来
pass
else:
# 正常,表示客户端发了数据,或者客户端断开连接(断开连接会导致recv正常返回)
if recv_content:
# 有数据,调用请求处理代码
print("处理请求")
else:
# 客户端断开时,服务器也关闭连接
client_socket.close()
# 将已关闭的链接提出列表
client_socket_list.remove(client_socket) # 关闭整个SOCKET
tcp_server_socket.close()

上面代码只有主干部分(省略了请求处理部分),主要是说明在单进程单线程情况下,如何将accept和recv分开,并且都用非阻塞的方式来处理,这样每次查看是否有客户端链接进来的时候,都会去检查所有已链接的socket是否有数据发送过来。

注意:socket.recv(1024)一定要给参数,读取的字节数。否则会一直报异常。

在这种方式中,我们使用单进程单线程模拟了并发处理socket连接的功能,但这些socket连接的处理不是并行的。当一个socket处理数据时间比较长时,也会造成整个程序的等待。

五、短连接和长连接

在第四节中,我们使用单进程单线程非阻塞的形式实现了并发处理socket连接。其中省略了实际处理的部分,我们将其补充上:

import socket
import time
import re def handle_request(new_socket, recv_msg):
# 从请求中解析出URI
recv_lines = recv_msg.splitlines() # 使用正则表达式提取出URI
ret = re.match(r"[^/]+(/[^ ]*)", recv_lines[0]) if ret:
# 获取URI字符串
file_name = ret.group(1)
# 如果URI是/,则默认返回index.html的内容
if file_name == "/":
file_name = "/index.html" try:
# 根据请求的URI,读取相应的文件
fp = open("." + file_name, "rb")
except:
# 找不到文件,响应404
response_msg = "HTTP/1.1 404 NOT FOUND\r\n"
response_msg += "\r\n"
response_msg += "<h1>----file not found----</h1>"
new_socket.send(response_msg.encode("utf-8"))
else:
html_content = fp.read()
fp.close() response_body = html_content # 响应正确 200 OK
response_header = "HTTP/1.1 200 OK\r\n"
response_header += "Content-Length:%d\r\n" % len(response_body)
response_header += "\r\n" response = response_header.encode("utf-8") + response_body # 返回响应数据
new_socket.send(response) def main():
# 创建TCP SOCKET实例
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# # 设置重用地址
# tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定地址(默认本机IP)和端口
tcp_server_socket.bind(("", 7890))
# 监听
tcp_server_socket.listen(128) # 将accept设置为非阻塞,这里设置一次,后面不管调多少次accept都是非阻塞的
tcp_server_socket.setblocking(False)
# 定义一个列表,将每次连接的socket加入该列表
client_socket_list = list() # 循环接收客户端连接
while True:
time.sleep(0.5) try:
new_socket, client_addr = tcp_server_socket.accept()
except Exception as ret:
# 当没有客户端链接的时候,抛出异常
pass
else:
print("一个新的客户端连接。。。。")
# 当有客户端链接的时候
# 将new_socket.recv()设置为非阻塞的
new_socket.setblocking(False)
# 将new_socket加入列表
client_socket_list.append(new_socket)
print(client_socket_list.__len__()) # 遍历socket列表,检查每一个socket是否有数据到达,或者客户端是否断开
for client_socket in client_socket_list:
try:
recv_content = client_socket.recv(1024).decode("utf-8")
except Exception as ret:
# 异常,表示该客户端没有发数据过来
pass
else:
# 正常,表示客户端发了数据,或者客户端断开连接(断开连接会导致recv正常返回)
if recv_content:
# 有数据,调用请求处理代码
handle_request(client_socket, recv_content)
else:
# recv正常返回,且数据为空,表示客户端断开了链接
# 将该socket踢出列表
client_socket_list.remove(client_socket)
# 服务器也关闭连接
client_socket.close()
# 关闭整个SOCKET
tcp_server_socket.close() if __name__ == "__main__":
main()

特别注意的是,在请求处理函数handle_request中,我们将请求内容作为参数一并传递进去。然后在返回200 OK的时候,在响应头中添加了Content-Length字段,这个字段用于告诉客户端,此次发送的响应体有多大。当客户端收完指定大小的数据,就认为这次服务器发送的数据已经发送完毕。他就可以继续发送下一个新的请求。

在handle_request中可以看到,new_socket.close()已经被删除,也就是说服务器不会自动关闭连接,而直到客户端断开连接之前,服务器都保持长连接。断开连接由客户端来发起。

[Python之路] 多种方式实现并发Web Server的更多相关文章

  1. Python之路 - Socketserver实现多并发

    Python之路 - Socketserver实现多并发 阅读指引

  2. [Python之路] 使用装饰器给Web框架添加路由功能(静态、动态、伪静态URL)

    一.观察以下代码 以下来自 Python实现简易HTTP服务器与MINI WEB框架(利用WSGI实现服务器与框架解耦) 中的mini_frame最后版本的代码: import time def in ...

  3. 【Python之路】特别篇--微信Web网页版通信的全过程分析

    文章所使用Python版本为py3.5 1.微信服务器返回一个会话ID 微信Web版本不使用用户名和密码直接登录,而是采用二维码登录,所以服务器需要首先分配一个唯一的会话ID,用来标识当前的一次登录. ...

  4. python创建字典多种方式

    1.创建空字典 >>> dic = {} >>> type(dic) <type 'dict'> 2.直接赋值创建 >>> dic = ...

  5. [Python之路] 使用epoll实现高并发HTTP服务器

    什么是epoll 我们在  Python多种方式实现并发的Web Server 的最后使用单进程+单线程+非阻塞+长连接实现了一个可并发处理客户端连接的服务器.他的原理可以用以下的图来描述: 解释: ...

  6. [Python之路] 实现简单Web服务器(TCP3次握手4次挥手解释)

    一.如何使用Python实现一个返回固定页面的Web Server 1.使用socket创建一个TCP Server 2.接受来自浏览器的TCP链接,并接收HTTP请求 3.返回固定响应数据给浏览器 ...

  7. Python之路【第十八篇】:Web框架们

    Python之路[第十八篇]:Web框架们   Python的WEB框架 Bottle Bottle是一个快速.简洁.轻量级的基于WSIG的微型Web框架,此框架只由一个 .py 文件,除了Pytho ...

  8. Python之路,Day18 - 开发一个WEB聊天来撩妹吧

    Python之路,Day18 - 开发一个WEB聊天来撩妹吧   本节内容: 项目实战:开发一个WEB聊天室 功能需求: 用户可以与好友一对一聊天 可以搜索.添加某人为好友 用户可以搜索和添加群 每个 ...

  9. Python之路【第十五篇】WEB框架

    WEB框架本质 Python的WEB框架分为两类: 1.自己写socket,自己处理请求 2.基于wsgi(Web Server Gateway Interface WEB服务网关接口),自己处理请求 ...

随机推荐

  1. java持续添加内容至本地文件

    package com.lcc.commons; import com.lcc.commons.dto.FileLogDTO; import java.io.*; import java.util.A ...

  2. Linux磁盘挂载、分区、扩容操作

    本文最早发布于 Rootrl's blog 注:以下操作系统环境为CentOS7 基本概念 在操作前,首先要了解一些基本概念 磁盘 在Linux系统中所有的设备都会以文件的形式存储.设备一般保存在/d ...

  3. Java EE javax.servlet中的ServletRequest接口

    ServletRequest接口 public interface ServletRequest 子接口:HttpServletRequest 实现类:HttpServletRequestWrappe ...

  4. O054、Attach Volume 操作(Part II)

    参考https://www.cnblogs.com/CloudMan6/p/5631328.html     计算节点作为iSCSI initiator 访问存储节点 iSCSI Target 上的v ...

  5. Express bodyParser中间件使用方式

    bodyParser中间件用来解析http请求体,是express默认使用的中间件之一. 1.这个模块提供以下解析器 (1) JSON body parser (2) Raw body parser ...

  6. 如何源码编译安装并控制nginx

    安装nginx 注意 Linux操作系统需要2.6及其以上的内核(支持epoll) 使用nginx的必备软件 gcc编辑器 yum -y install gcc gcc-c++ pcre库(支持正则表 ...

  7. 第十一章、特性property

    目录 第十一章.特性property 一.property是一种特殊的属性,访问它时会执行一段功能(函数)然后返回值 二.为什么要用property 三.封装与拓展性 第十一章.特性property ...

  8. java_day07_异常

    第七章: 异常 1.异常概述 在我们日常生活中,有时会出现各种各样的异常,例如:职工小王开车去上班,在正常情况下,小王会准时到达单位.但是天有不测风云,在小王去上班时,可能会遇到一些异常情况,比如小王 ...

  9. mysql 中的 tinyint 字段

    只能存储  -128 ~ 127  之间的数字

  10. export命令用来设置环境变量。

    sh-# export export CONSOLE="/dev/console" export HOME="/" export LD_LIBRARY_PATH ...