SimpleHTTPServer Python, запущенный в потоке, не закроет порт

У меня есть следующий код:

import os
from ghost import Ghost
import urlparse, urllib

import SimpleHTTPServer
import SocketServer

import sys, traceback

from threading import Thread, Event
from time import sleep

please_die = Event() # this is my enemy

httpd = None
PORT = 8001
address = 'http://localhost:'+str(PORT)+'/'
search_dir = './category'

def main():
    """
      basic run script routine, 
      FIXME: is supossed to exits gracefully
    """
    thread = Thread(target = simpleServe)
    try:
      thread.start()
      run()
    except KeyboardInterrupt:
      print "Shutdown requested"
    except Exception:
      traceback.print_exc(file=sys.stdout)

    shutdown()
    sys.exit(0)

def shutdown():
  global httpd
  global please_die
  print "Shutting down"
  # A try - except for the shutdown routine
  try:
    please_die.wait() # how do you do? 

    httpd.shutdown() # Please! I whant to run you multiple times. 
    print "Have you died?"
  except Exception:
    traceback.print_exc(file=sys.stdout)

def path2url(path):
  """
  constructs an url from a relative path / concatenates the global address
  variable with the path given
  """
  global address
  return urlparse.urljoin(address, urllib.pathname2url(path))

def simpleServe():
  global httpd, PORT

  please_die.set() # Attaching the event to this thread

  # Start the service
  Handler = SimpleHTTPServer.SimpleHTTPRequestHandler

  httpd = SocketServer.TCPServer(("", PORT), Handler)

  print "serving at port", PORT
  # And loop infinetly in the hope that I can stop you later
  httpd.serve_forever()

def run():
  global search_dir;

  ghost = Ghost() # the webkit facade

  with ghost.start() as session:

    session.set_viewport_size(2560, 1600) # "retina" size

    for directory, subdirectories, files in os.walk(search_dir):
        for file in files:
            path = os.path.join(directory, file)
            urlPath = path2url(path)
            process(session, urlPath);

def process(session, urlPath):
  page, resources = session.open(urlPath)
  assert page.http_status == 200
  # ... other asserts here 


if __name__ == '__main__':
  main()

Идея состоит в том, чтобы создать скрипт, который запускает "простой http-сервер", выполнить несколько запросов и затем выйти.

Первый раз работает без проблем:

...
127.0.0.1 - - [31/Jul/2015 13:16:17] "GET /category/52003.html HTTP/1.1" 200 -
127.0.0.1 - - [31/Jul/2015 13:16:17] "GET /category/52003.html HTTP/1.1" 200 -
127.0.0.1 - - [31/Jul/2015 13:16:17] "GET /category/52003.html HTTP/1.1" 200 -
127.0.0.1 - - [31/Jul/2015 13:16:17] "GET /static/img/glyphicons-halflings.png HTTP/1.1" 200 -
Shutting down
Have you died?

Запуск его во второй раз дает сбой, говоря, что:

Адрес уже используется

Exception in thread Thread-1:
Traceback (most recent call last):
  File "/usr/lib/python2.7/threading.py", line 810, in __bootstrap_inner
    self.run()
  File "/usr/lib/python2.7/threading.py", line 763, in run
    self.__target(*self.__args, **self.__kwargs)
  File "download-images.py", line 51, in simpleServe
    httpd = SocketServer.TCPServer(("", PORT), Handler)
  File "/usr/lib/python2.7/SocketServer.py", line 420, in __init__
    self.server_bind()
  File "/usr/lib/python2.7/SocketServer.py", line 434, in server_bind
    self.socket.bind(self.server_address)
  File "/usr/lib/python2.7/socket.py", line 228, in meth
    return getattr(self._sock,name)(*args)
error: [Errno 98] Address already in use

Если я уничтожу все процессы Python, сценарий снова запустится, и поэтому я предполагаю, что я использовал поток неправильно, но не могу найти где.

Обновить

Забыл упомянуть,

моя ОС это:

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 15.04
Release:        15.04
Codename:       vivid

Питон, который я использую:

$ python --version
Python 2.7.9

$ netstat -putelan | grep 8001 печатает:

$ netstat -putelan | grep 8001
(Not all processes could be identified, non-owned process info
    cp        0      0 127.0.0.1:34691         127.0.0.1:8001          TIME_WAIT   0          0           -               
    tcp        0      0 127.0.0.1:8001          127.0.0.1:34866         TIME_WAIT   0          0           -               
    tcp        0      0 127.0.0.1:34798         127.0.0.1:8001          TIME_WAIT   0          0           -               
    tcp        0      0 127.0.0.1:8001          127.0.0.1:34588         TIME_WAIT   0          0           -               
    tcp        0      0 127.0.0.1:34647         127.0.0.1:8001          TIME_WAIT   0          0           -               
    tcp        0      0 127.0.0.1:34915         127.0.0.1:8001          TIME_WAIT   0          0           -               
    tcp        0      0 127.0.0.1:34674         127.0.0.1:8001          TIME_WAIT   0          0           -               
    tcp        0      0 127.0.0.1:34451         127.0.0.1:8001          TIME_WAIT   0          0           -               
    tcp        0      0 127.0.0.1:8001          127.0.0.1:34930         TIME_WAIT   0          0           -               
    tcp        0      0 127.0.0.1:8001          127.0.0.1:34606         TIME_WAIT   0          0           -               
    tcp        0      0 127.0.0.1:34505         127.0.0.1:8001          TIME_WAIT   0          0           -               
    tcp        0      0 127.0.0.1:34717         127.0.0.1:8001          TIME_WAIT   0          0           -               
    tcp        0      0 127.0.0.1:8001          127.0.0.1:34670         0      0 127.0.0.1:8001          127.0.0.1:34626         
...

Я не могу опубликовать всю последовательность (из-за ограничений на размещение стека). Остальное то же самое с портом 34***, смешанным с портом 8001 в единой последовательности.

3 ответа

Решение

Как говорит @LFJ, это, вероятно, связано с allow_reuse_address атрибут TCPServer,

httpd = SocketServer.TCPServer(("", PORT), Handler, bind_and_activate=False)
httpd.allow_reuse_address = True

try:
    httpd.server_bind()
    httpd.server_activate()
except:
    httpd.server_close()
    raise

Эквивалентный код:

SocketServer.TCPServer.allow_reuse_address = True
https = SocketServer.TCPServer(("", PORT), Handler)

Давайте немного объясним, почему.

Когда вы включаете TCPServer.allow_reuse_address, он добавляет опцию на сокете:

class TCPServer:
    [...]
    def server_bind(self):
        if self.allow_reuse_address:
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        [...]

Что такое socket.SO_REUSEADDR?

This socket option tells the kernel that even if this port is busy (in
the TIME_WAIT state), go ahead and reuse it anyway.  If it is busy,
but with another state, you will still get an address already in use
error.  It is useful if your server has been shut down, and then
restarted right away while sockets are still active on its port.  You
should be aware that if any unexpected data comes in, it may confuse
your server, but while this is possible, it is not likely.       

Фактически, это позволяет повторно использовать адрес привязки сокета вашего сокета. Если другой процесс попытается выполнить привязку, пока сокет не прослушивает, процессу будет разрешено использовать этот адрес привязки сокета.

Причина, по которой вам нужно включить это, заключается в том, что вы не TCPServer, Для того, чтобы закрыть его правильно, вы должны запустить shutdown метод, который закроет поток, запущенный server_forever а затем закройте гнездо правильно, вызвав server_close метод.

def shutdown():
    global httpd
    global please_die
    print "Shutting down"

    try:
        please_die.wait() # how do you do? 
        httpd.shutdown() # Stop the serve_forever
        httpd.server_close() # Close also the socket.
    except Exception:
        traceback.print_exc(file=sys.stdout)

Я видел исходный код TCPServer:

def server_bind(self):
        """Called by constructor to bind the socket.

        May be overridden.

        """
        if self.allow_reuse_address:
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(self.server_address)
    self.server_address = self.socket.getsockname()

allow_reuse_address должен быть установлен перед привязкой. так что попробуйте это:

SocketServer.TCPServer.allow_reuse_address=True
httpd = SocketServer.TCPServer(("", PORT), Handler)

Вы не очищаете сервер после его закрытия. Это означает, что вы оставляете ресурсы мертвых сокетов, которые ОС не очищает сразу после завершения процесса.

Вам нужно позвонить httpd.server_close() в блоке, наконец, после вашего звонка httpd.serve_forever(), Этот вызов сообщает ОС освободить любые ресурсы, которые могли быть связаны с данным экземпляром сервера.

try:
    httpd.serve_forever()
finally:
    httpd.server_close()
Другие вопросы по тегам