Python多线程Web服务器

66

我正在尝试用Python创建一个多线程的Web服务器,但它每次只能响应一个请求,我无法弄清楚原因。你能帮我吗?

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

from SocketServer import ThreadingMixIn
from  BaseHTTPServer import HTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
from time import sleep

class ThreadingServer(ThreadingMixIn, HTTPServer):
    pass

class RequestHandler(SimpleHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/plain')
        sleep(5)
        response = 'Slept for 5 seconds..'
        self.send_header('Content-length', len(response))
        self.end_headers()
        self.wfile.write(response)

ThreadingServer(('', 8000), RequestHandler).serve_forever()

@shiplu.mokadd.im,你能否发布答案?非常感谢您的帮助。 - Deepak Ingole
@Pilot 这里需要两个东西:select() 和非阻塞式。Python 有一个socket 库。IBM 有一些关于使用 select() 进行 socket 编程的好文章 - Shiplu Mokaddim
@shiplu.mokadd 感谢您的有用评论,大师。 - Deepak Ingole
BaseHTTPServer 一次只能处理一个连接。 ThreadingMixIngunicorn(即使是 gevent 版本,也很遗憾)只是收集线程的结果,并将它们返回到一次连接中,这完全破坏了流式传输。幸运的是,您可以在 BaseHTTPServer 中更改一个简单的设置来解决此问题。请参见下面的答案。 - personal_cloud
@shiplu,如果你想使用状态机编程,那么非阻塞是可以的。考虑到Python线程调度器的糟糕表现,这样做可能会更快。但是开发时间会增加2倍,那么为什么不使用C++中的线程(然后你将快100倍),而要选择Python呢? - personal_cloud
显示剩余2条评论
6个回答

86
检查来自Doug Hellmann博客的this帖子。
以下代码与Python 2兼容。有关Python 3,请参阅其他答案。
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from SocketServer import ThreadingMixIn
import threading

class Handler(BaseHTTPRequestHandler):
    
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        message =  threading.currentThread().getName()
        self.wfile.write(message)
        self.wfile.write('\n')
        return

class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """Handle requests in a separate thread."""

if __name__ == '__main__':
    server = ThreadedHTTPServer(('localhost', 8080), Handler)
    print 'Starting server, use <Ctrl-C> to stop'
    server.serve_forever()

14
请注意,在超类列表中必须先使用 ThreadingMixIn,然后才能使用 HTTPServer,否则它将不起作用。 - Michael Mrozek
更详细的示例请参见Python3文档 - Eido95
这个无法流式传输。更好的方法是使用 BaseHTTPServer,请参考此链接:https://dev59.com/nlYO5IYBdhLWcg3wL-vY - personal_cloud
1
我无法发现此答案中的代码与问题之间有任何本质区别。有吗? - Leon
2
Python 3.7出现问题:无法工作,ModuleNotFoundError: No module named 'BaseHTTPServer'。 - recolic
显示剩余2条评论

18
在Python3中,你可以使用以下代码(https或http):
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import threading

USE_HTTPS = True

class Handler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'Hello world\t' + threading.currentThread().getName().encode() + b'\t' + str(threading.active_count()).encode() + b'\n')


class ThreadingSimpleServer(ThreadingMixIn, HTTPServer):
    pass

def run():
    server = ThreadingSimpleServer(('0.0.0.0', 4444), Handler)
    if USE_HTTPS:
        import ssl
        server.socket = ssl.wrap_socket(server.socket, keyfile='./key.pem', certfile='./cert.pem', server_side=True)
    server.serve_forever()


if __name__ == '__main__':
    run()

你会发现这段代码将为每个请求创建一个新的线程来处理。
以下命令用于生成自签名证书:
openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365

如果你正在使用Flask,这篇博客非常好。

1
这应该是一个注释。在Python3中,它是socketserver(小写)。 - jberryman
简洁的代码在Python3中运行得非常好,谢谢。 - meadlai
我认为不需要ThreadingSimpleServer,'http'包里有ThreadingHTTPServer,它和你在这里做的几乎是一样的。 - Tamás Sajti

18

我开发了一个名为ComplexHTTPServer的PIP工具,它是SimpleHTTPServer的多线程版本。

安装它只需做以下操作:

pip install ComplexHTTPServer

使用它非常简单:

python -m ComplexHTTPServer [PORT]

(默认情况下,端口为8000。)


我正在点赞你的答案,因为在不需要流式处理的情况下,它最终与其他任何答案一样有效。 - personal_cloud
你的回答非常简洁! - personal_cloud

9
如果未来可能需要流式传输,那么ThreadingMixIn和gunicorn就不适用了,因为它们只会在最后将响应作为一个整体收集起来并写入(如果您的流是无限的,则实际上什么也不做)。
BaseHTTPServer与线程结合的基本方法是正确的。但是默认的BaseHTTPServer设置会在每个侦听器上重新绑定新套接字,如果所有侦听器都在同一端口上,则在Linux中无法工作。在serve_forever()调用之前更改这些设置。(就像您必须在线程上设置self.daemon = True以停止禁用ctrl-C一样。)
以下示例在同一端口上启动100个处理程序线程,并通过BaseHTTPServer启动每个处理程序。
import time, threading, socket, SocketServer, BaseHTTPServer

class Handler(BaseHTTPServer.BaseHTTPRequestHandler):

    def do_GET(self):
        if self.path != '/':
            self.send_error(404, "Object not found")
            return
        self.send_response(200)
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.end_headers()

        # serve up an infinite stream
        i = 0
        while True:
            self.wfile.write("%i " % i)
            time.sleep(0.1)
            i += 1

# Create ONE socket.
addr = ('', 8000)
sock = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(addr)
sock.listen(5)

# Launch 100 listener threads.
class Thread(threading.Thread):
    def __init__(self, i):
        threading.Thread.__init__(self)
        self.i = i
        self.daemon = True
        self.start()
    def run(self):
        httpd = BaseHTTPServer.HTTPServer(addr, Handler, False)

        # Prevent the HTTP server from re-binding every handler.
        # https://dev59.com/nlYO5IYBdhLWcg3wL-vY
        httpd.socket = sock
        httpd.server_bind = self.server_close = lambda self: None

        httpd.serve_forever()
[Thread(i) for i in range(100)]
time.sleep(9e9)

2
这种情况下,难道不应该使用Websockets吗? - Sirmabus
2
基于这段代码的服务器对我来说非常有效,因为响应需要准备长达2分钟。能够立即返回“正在工作…”有所帮助。不幸的是,Chrome可以直接使用流式传输,而Internet Explorer v11在1-2分钟后返回整个页面。还不知道服务器是否需要其他东西或IE是否无法使用流式传输。 - Adrian Rosoga
@Adrian,我有个想法,你可以尝试使用分块传输编码。也许如果你有一个包含要立即显示的部分的内容长度的块头,浏览器可能会更快地“接受”它?但我自己没有尝试过。当然,如果这行不通,你总是可以提供一个拉取其余内容的<script>标签(这很标准)。 - personal_cloud
你的服务器是否适用于“同时处理数十个请求,每个请求可能需要长达二十秒的时间,并且涉及远程数据库调用和计算混合”这种使用情况?换句话说,每个工作任务都有足够的“空闲时间”(例如等待复杂的远程数据库操作完成),因此需要比系统中CPU数量更多的活动线程。 - WestCoastProjects
@StephenBoesch 是的,我在比那更苛刻的条件下使用过它。如果不需要那么多线程,请尽管使用较少的线程。 - personal_cloud

3
Python 3.7带有一个ThreadingHTTPServer
"""Custom response code server by Cees Timmerman, 2023-07-11.
Run and visit http://localhost:4444/300 for example."""

from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler


class Handler(BaseHTTPRequestHandler):

    def do_GET(self):
        try:
            response_code = int(self.path[1:])
        except:
            response_code = 500
        self.send_response(response_code)
        self.end_headers()
        self.wfile.write(f'Hello world! This response has code {response_code}.\n'.encode('utf8'))


def run():
    server = ThreadingHTTPServer(('0.0.0.0', 4444), Handler)
    server.serve_forever()


if __name__ == '__main__':
    run()

看起来是2023年最佳答案。 - undefined
@Pavel 虽然了解到Python3有更好的解决方案是件好事,但这个回答并不完全切题。问题明显是关于Python2的(在2023年仍然广泛使用)。 - undefined
@personal_cloud "我们决定2020年1月1日是我们停止支持Python 2的日子。这意味着在那天之后,即使有人发现其中存在安全问题,我们也不会再对其进行改进。您应尽快升级到Python 3。" - https://www.python.org/doc/sunset-python-2/ - undefined
@Cees 有很多编程语言已经不再得到最初的维护者支持,但仍然有大量的代码库在使用。至于现有语言的新版本:在某些情况下,比如C++,会努力保持与现有代码的兼容性。不幸的是,Python 3与大多数现有的Python 2代码不兼容。因此,升级对每个项目都需要进行成本效益分析。 - undefined

3
在Python3.7中实现的多线程https服务器。
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
import threading
import ssl

hostName = "localhost"
serverPort = 8080


class MyServer(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()
        self.wfile.write(bytes("<html><head><title>https://pythonbasics.org</title></head>", "utf-8"))
        self.wfile.write(bytes("<p>Request: %s</p>" % self.path, "utf-8"))
        self.wfile.write(bytes("<p>Thread: %s</p>" % threading.currentThread().getName(), "utf-8"))
        self.wfile.write(bytes("<p>Thread Count: %s</p>" % threading.active_count(), "utf-8"))
        self.wfile.write(bytes("<body>", "utf-8"))
        self.wfile.write(bytes("<p>This is an example web server.</p>", "utf-8"))
        self.wfile.write(bytes("</body></html>", "utf-8"))


class ThreadingSimpleServer(ThreadingMixIn,HTTPServer):
    pass

if __name__ == "__main__":
    webServer = ThreadingSimpleServer((hostName, serverPort), MyServer)
    webServer.socket = ssl.wrap_socket(webServer.socket, keyfile='./privkey.pem',certfile='./certificate.pem', server_side=True)
    print("Server started http://%s:%s" % (hostName, serverPort))

    try:
        webServer.serve_forever()
    except KeyboardInterrupt:
        pass

    webServer.server_close()
    print("Server stopped.")

你可以在浏览器中测试它:https://localhost:8080 运行结果如下: 输入图像描述
输入图像描述 请注意,你可以使用以下命令生成自己的密钥文件和证书:

$openssl req -newkey rsa:2048  -keyout privkey.pem -x509 -days 36500 -out certificate.pem

学习使用openssl创建自签名证书的详细信息:https://www.devdungeon.com/content/creating-self-signed-ssl-certificates-openssl


这个示例非常好,感谢分享。这实际上是在使用线程池吗?如果是的话,我如何控制这个池的大小? - rustyfinger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接