Python3 asyncoreで動作するhttp server

PyMailHog メール送信テスト用サーバー
このプログラムを作成した時、
・SMTP
・HTTP
2つのサーバーを同時に起動しなくてはいけませんでした。

smtpd.SMTPServerは「asyncore.loop()」
http.server.HTTPServerは「httpd.serve_forever」
で起動することになるのですが、なんとか1つにまとめられないか。


asyncore


SMTPサーバーを実装するのは大変そうなので、HTTPサーバーを
asyncore.loop()で起動できないか考えてみます。

調べてみるとそのものずばりのソースが。
SIMPLE HTTP SERVER BASED ON ASYNCORE/ASYNCHAT (PYTHON RECIPE)

Python2向けに記載されているソースなので、Python3向けに書き直しました。
https://bitbucket.org/snippets/symfo/kn9Rg9

こちらにもソースを記載しておきます。


  1. """Simple HTTP server based on the asyncore / asynchat framework
  2. Under asyncore, every time a socket is created it enters a table which is
  3. scanned through select calls by the asyncore.loop() function
  4. All events (a client connecting to a server socket, a client sending data,
  5. a server receiving data) is handled by the instances of classes derived
  6. from asyncore.dispatcher
  7. Here the server is represented by an instance of the Server class
  8. When a client connects to it, its handle_accept() method creates an
  9. instance of RequestHandler, one for each HTTP request. It is derived
  10. from asynchat.async_chat, a class where incoming data on the connection
  11. is processed when a "terminator" is received. The terminator can be :
  12. - a string : here we'll use the string \r\n\r\n to handle the HTTP request
  13. line and the HTTP headers
  14. - an integer (n) : the data is processed when n bytes have been read. This
  15. will be used for HTTP POST requests
  16. The data is processed by a method called found_terminator. In RequestHandler,
  17. found_terminator is first set to handle_request_line to handle the HTTP
  18. request line (including the decoding of the query string) and the headers.
  19. If the method is POST, terminator is set to the number of bytes to read
  20. (the content-length header), and found_terminator is set to handle_post_data
  21. After that, the handle_data() method is called and the connection is closed
  22. Subclasses of RequestHandler only have to override the handle_data() method
  23. """
  24. import asynchat
  25. import asyncore
  26. import socket
  27. import http.server
  28. import select
  29. import sys
  30. import cgi
  31. import io
  32. import os
  33. import traceback
  34. import shutil
  35. class SocketStream:
  36.     def __init__(self,sock):
  37.         """Initiate a socket (non-blocking) and a buffer"""
  38.         self.sock = sock
  39.         self.buffer = io.BytesIO()
  40.         self.closed = 1 # compatibility with SocketServer
  41.     
  42.     def write(self, data):
  43.         """Buffer the input, then send as many bytes as possible"""
  44.         self.buffer.write(data)
  45.         if self.writable():
  46.             buff = self.buffer.getvalue()
  47.             # next try/except clause suggested by Robert Brown
  48.             try:
  49.                     sent = self.sock.send(buff)
  50.             except:
  51.                     # Catch socket exceptions and abort
  52.                     # writing the buffer
  53.                     sent = len(data)
  54.                     print('exect')
  55.             # reset the buffer to the data that has not yet be sent
  56.             self.buffer=io.BytesIO()
  57.             self.buffer.write(buff[sent:])
  58.             
  59.     def finish(self):
  60.         """When all data has been received, send what remains
  61.         in the buffer"""
  62.         data = self.buffer.getvalue()
  63.         # send data
  64.         while len(data):
  65.             while not self.writable():
  66.                 pass
  67.             sent = self.sock.send(data)
  68.             data = data[sent:]
  69.     def writable(self):
  70.         """Used as a flag to know if something can be sent to the socket"""
  71.         return select.select([],[self.sock],[])[1]
  72. class RequestHandler(asynchat.async_chat,
  73.     http.server.SimpleHTTPRequestHandler):
  74.     protocol_version = "HTTP/1.1"
  75.     def __init__(self,conn,addr,server):
  76.         asynchat.async_chat.__init__(self,conn)
  77.         self.client_address = addr
  78.         self.connection = conn
  79.         self.server = server
  80.         # set the terminator : when it is received, this means that the
  81.         # http request is complete ; control will be passed to
  82.         # self.found_terminator
  83.         self.set_terminator ('\r\n\r\n'.encode())
  84.         self.rfile = io.BytesIO()
  85.         self.found_terminator = self.handle_request_line
  86.         self.request_version = "HTTP/1.1"
  87.         # buffer the response and headers to avoid several calls to select()
  88.         self.wfile = io.BytesIO()
  89.     def collect_incoming_data(self,data):
  90.         """Collect the data arriving on the connexion"""
  91.         self.rfile.write(data)
  92.     def prepare_POST(self):
  93.         """Prepare to read the request body"""
  94.         bytesToRead = int(self.headers.getheader('content-length'))
  95.         # set terminator to length (will read bytesToRead bytes)
  96.         self.set_terminator(bytesToRead)
  97.         self.rfile = io.BytesIO()
  98.         # control will be passed to a new found_terminator
  99.         self.found_terminator = self.handle_post_data
  100.     
  101.     def handle_post_data(self):
  102.         """Called when a POST request body has been read"""
  103.         self.rfile.seek(0)
  104.         self.do_POST()
  105.         self.finish()
  106.             
  107.     def do_GET(self):
  108.         """Begins serving a GET request"""
  109.         # nothing more to do before handle_data()
  110.         self.body = {}
  111.         self.handle_data()
  112.     def handle_data(self):
  113.         """Class to override"""
  114.         f = self.send_head()
  115.         if f:
  116.             self.copyfile(f, self.wfile)
  117.         
  118.     def do_POST(self):
  119.         """Begins serving a POST request. The request data must be readable
  120.         on a file-like object called self.rfile"""
  121.         ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
  122.         self.body = cgi.FieldStorage(fp=self.rfile,
  123.             headers=self.headers, environ = {'REQUEST_METHOD':'POST'},
  124.             keep_blank_values = 1)
  125.         self.handle_data()
  126.     def handle_request_line(self):
  127.         """Called when the http request line and headers have been received"""
  128.         # prepare attributes needed in parse_request()
  129.         self.rfile.seek(0)
  130.         self.raw_requestline = self.rfile.readline()
  131.         self.parse_request()
  132.         if self.command in ['GET','HEAD']:
  133.             # if method is GET or HEAD, call do_GET or do_HEAD and finish
  134.             method = "do_"+self.command
  135.             if hasattr(self,method):
  136.                 getattr(self,method)()
  137.                 self.finish()
  138.         elif self.command=="POST":
  139.             # if method is POST, call prepare_POST, don't finish yet
  140.             self.prepare_POST()
  141.         else:
  142.             self.send_error(501, "Unsupported method (%s)" %self.command)
  143.     def end_headers(self):
  144.         """Send the blank line ending the MIME headers, send the buffered
  145.         response and headers on the connection, then set self.wfile to
  146.         this connection
  147.         This is faster than sending the response line and each header
  148.         separately because of the calls to select() in SocketStream"""
  149.         super().end_headers()
  150.         if self.request_version != 'HTTP/0.9':
  151.             self.wfile.write("\r\n".encode())
  152.         self.start_resp = io.BytesIO(self.wfile.getvalue())
  153.         self.wfile = SocketStream(self.connection)
  154.         self.copyfile(self.start_resp, self.wfile)
  155.     def handle_error(self):
  156.         traceback.print_exc(sys.stderr)
  157.         self.close()
  158.     def copyfile(self, source, outputfile):
  159.         """Copy all data between two file objects
  160.         Set a big buffer size"""
  161.         shutil.copyfileobj(source, outputfile, length = 128*1024)
  162.     def finish(self):
  163.         """Send data, then close"""
  164.         try:
  165.             self.wfile.finish()
  166.         except AttributeError:
  167.             # if end_headers() wasn't called, wfile is a StringIO
  168.             # this happens for error 404 in self.send_head() for instance
  169.             self.wfile.seek(0)
  170.             self.copyfile(self.wfile, SocketStream(self.connection))
  171.         self.close()
  172. class Server(asyncore.dispatcher):
  173.     """Copied from http_server in medusa"""
  174.     def __init__ (self, ip, port,handler):
  175.         self.ip = ip
  176.         self.port = port
  177.         self.handler = handler
  178.         asyncore.dispatcher.__init__ (self)
  179.         self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
  180.         self.set_reuse_addr()
  181.         self.bind ((ip, port))
  182.         # lower this to 5 if your OS complains
  183.         self.listen (1024)
  184.     def handle_accept (self):
  185.         try:
  186.             conn, addr = self.accept()
  187.         except socket.error:
  188.             self.log_info ('warning: server accept() threw an exception', 'warning')
  189.             return
  190.         except TypeError:
  191.             self.log_info ('warning: server accept() threw EWOULDBLOCK', 'warning')
  192.             return
  193.         # creates an instance of the handler class to handle the request/response
  194.         # on the incoming connexion
  195.         self.handler(conn,addr,self)
  196. if __name__=="__main__":
  197.     # launch the server on the specified port
  198.     port = 8081
  199.     s=Server('',port,RequestHandler)
  200.     print("SimpleAsyncHTTPServer running on port %s" % port)
  201.     try:
  202.         asyncore.loop(timeout=2)
  203.     except KeyboardInterrupt:
  204.         print("Crtl+C pressed. Shutting down.")




これでSMTP、HTTP 2つのサービスがasyncore.loop()で同時に起動できました。
PyMailHogの実装も変更できそうです。


【参考URL】
SIMPLE HTTP SERVER BASED ON ASYNCORE/ASYNCHAT (PYTHON RECIPE)

関連記事

プロフィール

Author:symfo
blog形式だと探しにくいので、まとめサイト作成中です。
Symfoware まとめ

PR




検索フォーム

月別アーカイブ