15 votes

Comment gérer l'asynchronisme dans une classe en python, sans bloquer quoi que ce soit ?

J'ai besoin de créer une classe qui peut recevoir et stocker des messages SMTP, c'est-à-dire des E-Mails. Pour ce faire, j'utilise asyncore selon un exemple affiché aquí . Cependant, asyncore.loop() est bloqué et je ne peux donc rien faire d'autre dans le code.

J'ai donc pensé à utiliser des fils. Voici un exemple de code qui montre ce que j'ai en tête :

class MyServer(smtpd.SMTPServer):
    # derive from the python server class

    def process_message(..):
        # overwrite a smtpd.SMTPServer method to be able to handle the received messages
        ...
        self.list_emails.append(this_email)

    def get_number_received_emails(self):
        """Return the current number of stored emails"""
        return len(self.list_emails)

    def start_receiving(self):
        """Start the actual server to listen on port 25"""

        self.thread =   threading.Thread(target=asyncore.loop)
        self.thread.start()     

    def stop(self):
        """Stop listening now to port 25"""
        # close the SMTPserver from itself
        self.close()
        self.thread.join()

J'espère que vous avez saisi l'image. La classe MyServer doit pouvoir commencer et arrêter d'écouter le port 25 de manière non bloquante, et être capable d'être interrogé pour des messages pendant l'écoute (ou non). Le site start lance la méthode asyncore.loop() qui, lors de la réception d'un courriel, l'ajoutent à une liste interne. De façon similaire, le stop devrait être en mesure d'arrêter ce serveur, comme suggéré aquí .

Malgré le fait que ce code ne fonctionne pas comme je l'espère (l'asynchronisme semble s'exécuter indéfiniment, même si j'appelle le code ci-dessus stop méthode. Le site error Je soulève est attrapé dans stop mais pas dans le cadre de la target fonction contenant asyncore.loop() ), je ne suis pas sûr que mon approche du problème soit judicieuse. Avez-vous des suggestions pour corriger le code ci-dessus ou proposer une mise en œuvre plus solide ( sans en utilisant un logiciel tiers), sont appréciés.

17voto

Alex Points 2686

La solution fournie n'est peut-être pas la plus sophistiquée, mais elle fonctionne raisonnablement et a été testée.

Tout d'abord, le problème avec asyncore.loop() est qu'il bloque jusqu'à ce que tous les asyncore sont fermés, car les utilisateurs Wessie souligné dans un commentaire précédent. En se référant à la exemple smtp mentionné plus tôt, il s'avère que smtpd.SMTPServer hérite de asyncore.dispatcher (tel que décrit sur le documentation smtpd ), ce qui répond à la question de savoir quel canal doit être fermé.

Par conséquent, la question originale peut être répondue avec l'exemple de code mis à jour suivant :

class CustomSMTPServer(smtpd.SMTPServer):
    # store the emails in any form inside the custom SMTP server
    emails = []
    # overwrite the method that is used to process the received 
    # emails, putting them into self.emails for example
    def process_message(self, peer, mailfrom, rcpttos, data):
        # email processing

class MyReceiver(object):
    def start(self):
        """Start the listening service"""
        # here I create an instance of the SMTP server, derived from  asyncore.dispatcher
        self.smtp = CustomSMTPServer(('0.0.0.0', 25), None)
        # and here I also start the asyncore loop, listening for SMTP connection, within a thread
        # timeout parameter is important, otherwise code will block 30 seconds after the smtp channel has been closed
        self.thread =  threading.Thread(target=asyncore.loop,kwargs = {'timeout':1} )
        self.thread.start()     

    def stop(self):
        """Stop listening now to port 25"""
        # close the SMTPserver to ensure no channels connect to asyncore
        self.smtp.close()
        # now it is save to wait for the thread to finish, i.e. for asyncore.loop() to exit
        self.thread.join()

    # now it finally it is possible to use an instance of this class to check for emails or whatever in a non-blocking way
    def count(self):
        """Return the number of emails received"""
        return len(self.smtp.emails)        
    def get(self):
        """Return all emails received so far"""
        return self.smtp.emails
    ....

Donc au final, j'ai un start et un stop pour démarrer et arrêter l'écoute sur le port 25 dans un environnement non bloquant.

4voto

Wessie Points 1799

Venant de l'autre question asyncore.loop ne se termine pas lorsqu'il n'y a plus de connexions

Je pense que vous pensez un peu trop à l'enfilage. En utilisant le code de l'autre question, vous pouvez démarrer un nouveau thread qui exécute la commande asyncore.loop par l'extrait de code suivant :

import threading

loop_thread = threading.Thread(target=asyncore.loop, name="Asyncore Loop")
# If you want to make the thread a daemon
# loop_thread.daemon = True
loop_thread.start()

Cela se fera dans un nouveau fil de discussion et se poursuivra jusqu'à ce que tout soit terminé. asyncore les canaux sont fermés.

3voto

Jean-Paul Calderone Points 27680

Vous devriez envisager d'utiliser Twisted, à la place. http://twistedmatrix.com/trac/browser/trunk/doc/mail/examples/emailserver.tac montre comment configurer un serveur SMTP avec un crochet de livraison personnalisable.

0voto

Paul Jacobs Points 400

La réponse d'Alex est la meilleure mais elle était incomplète pour mon cas d'utilisation. Je voulais tester le SMTP dans le cadre d'un test unitaire, ce qui impliquait la construction d'un faux serveur SMTP à l'intérieur de mes objets de test. Comme le serveur ne mettait pas fin au thread asyncio, j'ai dû ajouter une ligne pour le définir comme un thread démon afin de permettre au reste du test unitaire de se dérouler sans être bloqué par l'attente de l'arrivée du thread asyncio. J'ai également ajouté une journalisation complète de toutes les données de courrier électronique afin de pouvoir vérifier tout ce qui est envoyé par le SMTP.

Voici ma fausse classe SMTP :

class TestingSMTP(smtpd.SMTPServer):
    def __init__(self, *args, **kwargs):
        super(TestingSMTP, self).__init__(*args, **kwargs)
        self.emails = []

    def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
        msg = {'peer': peer,
               'mailfrom': mailfrom,
               'rcpttos': rcpttos,
               'data': data}
        msg.update(kwargs)
        self.emails.append(msg)

class TestingSMTP_Server(object):

    def __init__(self):
        self.smtp = TestingSMTP(('0.0.0.0', 25), None)
        self.thread = threading.Thread()

    def start(self):
        self.thread = threading.Thread(target=asyncore.loop, kwargs={'timeout': 1})
        self.thread.daemon = True
        self.thread.start()

    def stop(self):
        self.smtp.close()
        self.thread.join()

    def count(self):
        return len(self.smtp.emails)

    def get(self):
        return self.smtp.emails

Et voici comment il est appelé par les classes unittest :

smtp_server = TestingSMTP_Server()
smtp_server.start()

# send some emails

assertTrue(smtp_server.count() == 1) # or however many you intended to send
assertEqual(self.smtp_server.get()[0]['mailfrom'], 'first@fromaddress.com')

# stop it when done testing
smtp_server.stop()

0voto

duhaime Points 494

Au cas où quelqu'un d'autre aurait besoin d'un peu plus de détails, voici ce que j'ai fini par utiliser. Cela utilise smtpd pour le serveur de messagerie et smtpblib pour le client de messagerie, avec Flask comme serveur http [ Gist ] :

app.py

from flask import Flask, render_template
from smtp_client import send_email
from smtp_server import SMTPServer

app = Flask(__name__)

@app.route('/send_email')
def email():
  server = SMTPServer()
  server.start()
  try:
    send_email()
  finally:
    server.stop()
  return 'OK'

@app.route('/')
def index():
  return 'Woohoo'

if __name__ == '__main__':
  app.run(debug=True, host='0.0.0.0')

smtp_server.py

# smtp_server.py
import smtpd
import asyncore
import threading

class CustomSMTPServer(smtpd.SMTPServer):
  def process_message(self, peer, mailfrom, rcpttos, data):
    print('Receiving message from:', peer)
    print('Message addressed from:', mailfrom)
    print('Message addressed to:', rcpttos)
    print('Message length:', len(data))
    return

class SMTPServer():
  def __init__(self):
    self.port = 1025

  def start(self):
    '''Start listening on self.port'''
    # create an instance of the SMTP server, derived from  asyncore.dispatcher
    self.smtp = CustomSMTPServer(('0.0.0.0', self.port), None)
    # start the asyncore loop, listening for SMTP connection, within a thread
    # timeout parameter is important, otherwise code will block 30 seconds
    # after the smtp channel has been closed
    kwargs = {'timeout':1, 'use_poll': True}
    self.thread = threading.Thread(target=asyncore.loop, kwargs=kwargs)
    self.thread.start()

  def stop(self):
    '''Stop listening to self.port'''
    # close the SMTPserver to ensure no channels connect to asyncore
    self.smtp.close()
    # now it is safe to wait for asyncore.loop() to exit
    self.thread.join()

  # check for emails in a non-blocking way
  def get(self):
    '''Return all emails received so far'''
    return self.smtp.emails

if __name__ == '__main__':
  server = CustomSMTPServer(('0.0.0.0', 1025), None)
  asyncore.loop()

smtp_client.py

import smtplib
import email.utils
from email.mime.text import MIMEText

def send_email():
  sender='author@example.com'
  recipient='6142546977@tmomail.net'

  msg = MIMEText('This is the body of the message.')
  msg['To'] = email.utils.formataddr(('Recipient', recipient))
  msg['From'] = email.utils.formataddr(('Author', 'author@example.com'))
  msg['Subject'] = 'Simple test message'

  client = smtplib.SMTP('127.0.0.1', 1025)
  client.set_debuglevel(True) # show communication with the server
  try:
    client.sendmail('author@example.com', [recipient], msg.as_string())
  finally:
    client.quit()

Ensuite, démarrez le serveur avec python app.py et dans une autre demande, simuler une demande à /send_email con curl localhost:5000/send_email . Notez que pour envoyer réellement l'e-mail (ou le sms), vous devrez passer par d'autres étapes détaillées ici : https://blog.codinghorror.com/so-youd-like-to-send-some-email-through-code/ .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X