从urllib.request向HTTPServer

时间:2016-03-18 03:36:14

标签: python python-3.x urllib python-multithreading httpserver

我正在尝试执行this Matasano crypto challenge,其中涉及对具有人为减慢的字符串比较功能的服务器进行定时攻击。它说使用"您选择的网络框架",但我不想安装Web框架,所以我决定使用HTTPServer class内置的http.server }模块。

我想出了一些有用的东西,但它非常慢,所以我尝试使用multiprocessing.dummy中内置的(记录不完整的)线程池加快速度。它的速度要快得多,但我注意到一些奇怪的事情:如果我同时发出8个或更少的请求,它就可以了。如果我有更多,它会工作一段时间,并在看似随机的时间给我错误。错误似乎不一致,但并不总是相同,但它们通常包含Connection refused, invalid argumentOSError: [Errno 22] Invalid argumenturllib.error.URLError: <urlopen error [Errno 22] Invalid argument>BrokenPipeError: [Errno 32] Broken pipeurllib.error.URLError: <urlopen error [Errno 61] Connection refused>。< / p>

服务器可以处理的连接数有限制吗?我不认为线程本身的数量是问题,因为我编写了一个简单的函数,它在没有运行Web服务器的情况下进行了减慢的字符串比较,并使用500个并发线程调用它,并且它工作正常。我不认为只是从那么多线程发出请求就是问题所在,因为我已经使用了超过100个线程的爬虫(所有线程同时向同一个网站发出请求)并且它们工作正常。看起来HTTPServer可能无法可靠地托管获得大量流量的生产网站,但我很惊讶它很容易让它崩溃。

我尝试逐渐从我的代码中删除看起来与问题无关的内容,就像我通常在诊断这样的神秘错误时所做的那样,但在这种情况下这并不是很有帮助。看起来我正在删除看似无关的代码,服务器可以处理的连接数量逐渐增加,但没有明确的崩溃原因。

是否有人知道如何增加我一次可以提出的请求数量,或者至少为什么会这样做?

我的代码很复杂,但我想出了一个简单的程序来演示这个问题:

#!/usr/bin/env python3

import os
import random

from http.server import BaseHTTPRequestHandler, HTTPServer
from multiprocessing.dummy import Pool as ThreadPool
from socketserver import ForkingMixIn, ThreadingMixIn
from threading import Thread
from time import sleep
from urllib.error import HTTPError
from urllib.request import urlopen


class FancyHTTPServer(ThreadingMixIn, HTTPServer):
    pass


class MyRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        sleep(random.uniform(0, 2))
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"foo")

    def log_request(self, code=None, size=None):
        pass

def request_is_ok(number):
    try:
        urlopen("http://localhost:31415/test" + str(number))
    except HTTPError:
        return False
    else:
        return True


server = FancyHTTPServer(("localhost", 31415), MyRequestHandler)
try:
    Thread(target=server.serve_forever).start()
    with ThreadPool(200) as pool:
        for i in range(10):
            numbers = [random.randint(0, 99999) for j in range(20000)]
            for j, result in enumerate(pool.imap(request_is_ok, numbers)):
                if j % 20 == 0:
                    print(i, j)
finally:
    server.shutdown()
    server.server_close()
    print("done testing server")

出于某种原因,上面的程序工作正常,除非它有超过100个线程左右,但我的真正的挑战代码只能处理8个线程。如果我用9运行它,我通常会遇到连接错误,而对于10,我总是会遇到连接错误。我尝试使用concurrent.futures.ThreadPoolExecutorconcurrent.futures.ProcessPoolExecutormultiprocessing.pool代替multiprocessing.dummy.pool,但这些似乎都没有帮助。我尝试使用普通的HTTPServer对象(没有ThreadingMixIn)并且只是让事情运行得很慢并且没有解决问题。我尝试使用ForkingMixIn并且没有修复它。

我该怎么办呢?我在2013年末运行OS X 10.11.3的MacBook Pro上运行Python 3.5.1。

编辑:我尝试了一些其他内容,包括在一个进程而不是一个线程中运行服务器,一个简单的HTTPServerForkingMixIn,以及ThreadingMixIn。没有人帮助过。

编辑:这个问题比我想象的更奇怪。我尝试用服务器创建一个脚本,另一个用很多线程发出请求,然后在终端的不同选项卡中运行它们。服务器的进程运行正常,但发出请求的进程崩溃了。例外情况是ConnectionResetError: [Errno 54] Connection reset by peerurllib.error.URLError: <urlopen error [Errno 54] Connection reset by peer>OSError: [Errno 41] Protocol wrong type for socketurllib.error.URLError: <urlopen error [Errno 41] Protocol wrong type for socket>urllib.error.URLError: <urlopen error [Errno 22] Invalid argument>的混合。

我尝试使用如上所述的虚拟服务器,如果我将并发请求的数量限制为5或更少,它工作正常,但有6个请求,客户端进程崩溃。服务器出现了一些错误,但它仍然存在。无论我是使用线程还是进程来发出请求,客户端都崩溃了。然后我尝试将减速功能放在服务器中,它能够处理60个并发请求,但它与70崩溃。这似乎与服务器问题的证据相矛盾。

编辑:我尝试了使用requests而不是urllib.request描述的大部分内容,并遇到了类似的问题。

编辑:我现在正在运行OS X 10.11.4并遇到同样的问题。

3 个答案:

答案 0 :(得分:9)

您正在使用默认的listen()积压值,这可能是导致很多错误的原因。这不是已建立连接的并发客户端数,而是在建立连接之前等待侦听队列的客户端数。将您的服务器类更改为:

class FancyHTTPServer(ThreadingMixIn, HTTPServer):
    def server_activate(self):
        self.socket.listen(128)

128是一个合理的限制。如果要进一步增加它,可能需要检查socket.SOMAXCONN或您的操作系统somaxconn。如果在重负载下仍然存在随机错误,则应检查ulimit设置并在需要时增加。

我用你的例子做了这个,我运行了1000多个线程,所以我认为这应该可以解决你的问题。

<强>更新

如果它有所改进,但它仍然会同时崩溃200个客户端,那么我很确定你的主要问题是积压大小。请注意,您的问题不是并发客户端的数量,而是并发连接请求的数量。简要解释这意味着什么,而不是深入到TCP内部。

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(BACKLOG)
while running:
    conn, addr = s.accept()
    do_something(conn, addr)

在此示例中,套接字现在接受给定端口上的连接,s.accept()调用将阻塞,直到客户端连接。您可以让许多客户端同时尝试连接,并且根据您的应用程序,您可能无法调用s.accept()并以客户端尝试连接的速度调度客户端连接。待处理的客户端排队,该队列的最大大小由BACKLOG值确定。如果队列已满,则客户端将因连接拒绝错误而失败。

线程没有帮助,因为ThreadingMixIn类所做的是在一个单独的线程中执行do_something(conn, addr)调用,因此服务器可以返回到mainloop和s.accept()调用。

您可以尝试进一步增加积压,但是有一点不会有帮助,因为如果队列变得太大,某些客户端会在服务器执行s.accept()调用之前超时。

所以,正如我上面所说,你的问题是同时连接尝试的次数,而不是同时客户端的数量。对于你的实际应用程序来说,128就足够了,但是你的测试会出现错误,因为你试图同时连接所有200个线程并充斥队列。

除非您收到ulimit错误,否则不要担心Too many open files,但如果您想将积压增加到128以上,请对socket.SOMAXCONN进行一些调查。这是一个好的开始:https://utcc.utoronto.ca/~cks/space/blog/python/AvoidSOMAXCONN

答案 1 :(得分:1)

我说你的问题与某些IO阻止有关,因为我已经在NodeJ上成功执行了你的代码。我还注意到服务器和客户端都无法单独工作。

但是可以通过一些修改来增加请求的数量:

  • 定义并发连接数:

    http.server.HTTPServer.request_queue_size = 500

  • 在不同的过程中运行服务器:

    server = multiprocessing.Process(target = RunHTTPServer)   server.start()

  • 使用客户端的连接池执行请求

  • 使用服务器端的线程池来处理请求

  • 通过设置架构并使用&#34; keep-alive&#34;允许在客户端重用连接。头

通过所有这些修改,我设法运行500个线程的代码,没有任何问题。所以如果你想尝试一下,这里是完整的代码:

import random
from time import sleep, clock
from http.server import BaseHTTPRequestHandler, HTTPServer
from multiprocessing import Process
from multiprocessing.pool import ThreadPool
from socketserver import ThreadingMixIn
from concurrent.futures import ThreadPoolExecutor
from urllib3 import HTTPConnectionPool
from urllib.error import HTTPError


class HTTPServerThreaded(HTTPServer):
    request_queue_size = 500
    allow_reuse_address = True

    def serve_forever(self):
        executor = ThreadPoolExecutor(max_workers=self.request_queue_size)

        while True:
          try:
              request, client_address = self.get_request()
              executor.submit(ThreadingMixIn.process_request_thread, self, request, client_address)
          except OSError:
              break

        self.server_close()


class MyRequestHandler(BaseHTTPRequestHandler):
    default_request_version = 'HTTP/1.1'

    def do_GET(self):
        sleep(random.uniform(0, 1) / 100.0)

        data = b"abcdef"
        self.send_response(200)
        self.send_header("Content-type", 'text/html')
        self.send_header("Content-length", len(data))
        self.end_headers()
        self.wfile.write(data)

    def log_request(self, code=None, size=None):
        pass


def RunHTTPServer():
    server = HTTPServerThreaded(('127.0.0.1', 5674), MyRequestHandler)
    server.serve_forever()


client_headers = { 
    'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)',
    'Content-Type': 'text/plain',
    'Connection': 'keep-alive'
}

client_pool = None

def request_is_ok(number):
    response = client_pool.request('GET', "/test" + str(number), headers=client_headers)
    return response.status == 200 and response.data == b"abcdef"


if __name__ == '__main__':

    # start the server in another process
    server = Process(target=RunHTTPServer)
    server.start()

    # start a connection pool for the clients
    client_pool = HTTPConnectionPool('127.0.0.1', 5674)

    # execute the requests
    with ThreadPool(500) as thread_pool:
        start = clock()

        for i in range(5):
            numbers = [random.randint(0, 99999) for j in range(20000)]
            for j, result in enumerate(thread_pool.imap(request_is_ok, numbers)):
                if j % 1000 == 0:
                    print(i, j, result)

        end = clock()
        print("execution time: %s" % (end-start,))

更新1:

增加request_queue_size只会为您提供更多空间来存储当时无法执行的请求,以便以后执行。 所以队列越长,响应时间的分散度越高,我相信这与你的目标相反。 至于ThreadingMixIn,它并不理想,因为它为每个请求创建和销毁一个线程,而且价格昂贵。减少等待队列的更好选择是使用可重用线程池来处理请求。

在另一个进程中运行服务器的原因是利用另一个CPU来减少执行时间。

对于使用HTTPConnectionPool的客户端来说,这是我发现保持持续请求流的唯一方法,因为在分析连接时我有一些奇怪的urlopen行为。

答案 2 :(得分:-1)

规范是仅使用与核心一样多的线程,因此需要8个线程(包括虚拟核心)。线程模型是最容易工作的,但它实际上是一种垃圾方式。处理多个连接的更好方法是使用异步方法。但这更难。

使用线程方法,您可以在退出程序后调查进程是否保持打开状态。这意味着您的线程没有关闭,显然会导致问题。

试试这个......

class FancyHTTPServer(ThreadingMixIn, HTTPServer):
    daemon_threads = True

这将确保您的线程正确关闭。它可能会在线程池中自动发生,但无论如何它都值得尝试。