2013년 12월 17일 화요일

[Python[ SMTP를 통한 SOAP 요청 송/수신

자료출처: http://www-903.ibm.com/developerworks/kr/webservices/library/ws-pyth12.html
Mike Olson, Principal Consultant, Fourthought, Inc.
Uche Ogbuji, Principal Consultant, Fourthought, Inc.

2003년 4월
대부분의 사람들이 SOAP에 대해 생각할 때에는 HTTP 프로토콜을 통해 XML 요청을 보내고 XML 응답을 얻는 것을 떠올린다. 하지만 항상 이럴 필요는 없다. 사실 SOAP 프로토콜은 SOAP 메시지의 전송 수단으로서 HTTP를 정의한 것 뿐이다. 이 글에서는 Simple Mail Transfer Protocol (SMTP)를 통한 SOAP 요청 송수신을 다룬다.
Introduction
많은 사람들이 HTTP를 통한 SOAP을 생각하는 데는 몇 가지 이유가 있다. 첫째, 이것은 SOAP 프로토콜의 가장 일반적인 전송수단이며 지금까지 웹 상에서 대부분의 서비스가 가능했다. 두 번째 이유로는 HTTP 프로토콜의 작동방법 때문인데, SOAP 송수신 구조와 매우 잘 맞는다. HTTP에서는, 메시지를 서버에 보낸다. HTTP 요청(request) 포맷은 충분히 유연성을 갖추었기 때문에 이 바디 안에 SOAP 요청을 삽입할 수 있다. 이 때 HTTP 프로토콜은 (모든 HTTP 요청에 대한) 응답을 지정한다. 이러한 이유때문에 매우 단순한 SOAP 서비스 구현이 가능했다.
SMTP 프로토콜의 경우는 다르다. SMTP를 사용할 때 요청 포맷은 충분히 유연하여 SOAP 요청을 어태치할 수 있다. 하지만 응답구조는 그렇게 유연하지 않다. SMTP 응답은 "O.K" 처럼 간단하다. SMTP Service Extensions (ESMTP) 스팩은 "Unknown User" 같은 응답에 약간의 정보를 추가하지만 응답 포맷 치고는 유연하지 않다. 전체 SOAP 응답을 넣을 수 없기 때문이다. SMTP로 응답을 보내는 유일한 방법은 또 다른 이메일 메시지를 사용하는 것이다.
이런 이유 때문에 SMTP SOAP 서비스 개발자는 추가 로직을 추가하여 인커밍 SOAP 요청을 트래킹한 다음 개별 SMTP 메시지를 통해 SOAP 응답을 수혜자에게 보낼 수 있었다. 때문에 SMTP를 사용하는 SOAP 서비스가 점점 복잡해졌다.
SMTP를 원하는 이유는? 가장 일반적인 대답은 "HTTP를 사용할 수 없기 때문" 이다. 또 다른 이유는 방화벽 때문이다. 방화벽 뒤에 있으면 HTTP 요청이 어디서 처리되는지에 대해 제어권을 잃는다. 하지만 이메일을 받을 때는 이 보다 좋은 것이 없다. 또 다른 이유는 요청/응답 메시지전송 모델이 애플리케이션에 맞는 권장 모델이 아니기 때문이다. SMTP는 퍼블리시/등록이나 일방(one-way) 메시징 모델에 적합하다. 마지막으로 여러분이 작성하는 서비스는 실시간이 아니다. 서비스가 복잡한 쿼리나 300 초가 넘게 걸릴 수 있는 복잡한 전산을 수행해야 한다면 SMTP 같은 비동기식 접근 방식이 필요하다.
SOAP 메시지 처리하기
SMTP를 통해 SOAP 요청과 응답을 처리할 수 있는 세 가지 방법이 있다. 첫 번째 접근방식은 SMTP 서버 기능과 관련이 있다. 이 접근방식을 통해 SOAP 프로세싱 애플리케이션을 작성하고 이를 이메일 주소와 연결한다. 대부분의 이메일 서버들은 특정 주소에서 받게되는 메시지를 애플리케이션에 파이핑(pipe)한다. 이렇게 되면 애플리케이션은 SMTP 메시지에서 읽고 요청을 처리하고 응답을 보낸다.
두 번째 접근방식은 메일박스 파일에 액세스하는 것이다. 주기적으로 SOAP 요청에 대한 메일 파일을 스캔하고 메일 파일에서 이 메시지들을 지운다. 그리고 요청을 처리하고 응답을 보낸다.
마지막 접근방식이자 이 글에서 활용할 방식은 SMTP Server를 작성하는 것이다. 포트에서 SMTP 요청을 리스닝하고 요청을 받을 때 처리하고 프로세싱이 끝날 때 응답을 보낸다.
요청과 응답 연결하기
SMTP를 통해 SOAP을 어떻게 핸들하든지간에 SOAP 요청과 응답을 링크시킬 메커니즘이 필요하다. SOAP experimental e-mail binding (참고자료)은 "Message-ID"와 "In-Reply-To" SMTP 헤더 사용을 권장한다. 클라이언트는 메시지가 전송되면 고유 식별자를 "Message-ID"에 넣고 서버는 응답 메시지에 "In-Reply-To" 헤더에 같은 식별자를 사용한다. 이로서 클라이언트는 수신된 메시지를 적절한 요청과 매치할 수 있다.
예제
이 글의 예제는 Python 2.2.1을 사용하여 작성되었고 새로운 서버 아키텍쳐를 주로 사용한다. 또한 ZSI를 사용하여 SOAP 메시지를 처리할 것이다. ZSI 설치는 표준 파이썬 distutils를 사용하기 때문에 매우 간단하다. (참고자료)
SOAP SMTP 서버
파이썬의 builtin smtpd 라이브러리를 사용하면 SMTP 서버 작성은 비교적 쉽다. 파이썬의 모든 서버 아키텍쳐의 경우와 마찬가지로, 이 경우 베이스 클래스인 SMTPd.SMTPServer (Listing 1)에서 상속받고 원하는 기능에 따라 특정 메소드를 오버라이딩 한다. 여기에서는 새로운 메시지가 도착할 때마다 호출되는 process_message 메소드를 오버라이드한다. 서버를 시작하기 위해서 Python 2.2의 새로운 asyncore를 사용하여 요청을 처리하기 시작한다.

    
class OurServer(SMTPd.SMTPServer):

    #A place to store the current ID of the message that we are processing.
    currentID = None

    #This is the callback from SMTPServer that all SMPTServers
    #must implement to handle messages
    def process_message(self, peer, mailfrom, rcpttos, data):
       #
       # Snip out code for process_message
       # It will be discussed below
       #

#This is the 2.2 way of running asynchronous servers
server = OurServer(("localhost", 8023),
                   (None, 0))
try:
    asyncore.loop()
except KeyboardInterrupt:
    pass
일단, 메시지를 받게되면 간단한 테스트를 하여 이것이 정말 SOAP 요청인지를 확인할 수 있다. 그렇다면 이를 ZSI에 전달하여 메시지를 내보낸다. ZSI _Dispatch 메소드가 추가 매개변수를 허용하지 않기 때문에 서버 인스턴스에 현재 메시지 ID를 저장해야한다. 물론 트래픽이 높은 서버에서는 이러한 솔루션은 결코 적용되지 않는다.
결과를 받으면 유효 응답이든 아니든, 이메일 메시지로 결과를 보낸다. 이메일 메시지를 보낼 때 일반 메소드인 common.SendMessage (Listing 2)를 사용하는 것에 주목하자. 이 헬퍼 함수는 다른 쓰레드를 사용하여 지정된 서버에 메시지를 보낸다. 송신 프로세스가 서버 연결, 네트워크 지연 등을 기다리면서 차단당하지 않기위해 이것이 필요하다. 실제 제품의 경우 이 함수는 좀더 복잡할 것이고 큐(que) 메커니즘을 사용하여 네트워크 장애, 서버 다운의 문제들을 처리한다.

    
    def process_message(self, peer, mailfrom, rcpttos, data):
        #Parse the message into an email.Message instance
        p = Parser.Parser()
        m = p.parsestr(data)
        print "Received Message"

        #See if it is a SOAP request
        if m.has_key('To') and m['To'] == 'calendar@localhost':
            self.process_soap(m)
        else :
            #In normal circumstances, this would probably
            #forward the email message to another SMTP Server
            print "Unknown Email message"
            print m

    def process_soap(self,message):

        #Parse the SOAP Message
        ps = parse.ParsedSoap(message.get_payload(decode=1))

        #Store the current ID
        self.currentID = message['Message-Id']
        print "Processing Message: " + self.currentID

        #Use ZSI's dispatcher to call the correct function based on the message.
        dispatch._Dispatch(ps,
                           [self],
                           self.send_xml,
                           self.send_fault)

    #ZSI Callback to send an SOAP(non-Fault) response.
    def send_xml(self,xml):
        self.return_soap(xml)

    #ZSI callback to send a fault.
    def send_fault(self,fault):
        sys.stderr.write("FAULT While processing request:\n");
        s = cStringIO.StringIO()
        fault.serialize(s)
        st = s.getvalue()
        print st
        #Serialize the fault and send it to the client
        self.return_soap(st)

    #Called by our code to send result XML.
    def return_soap(self,st):
        msg = MIMEText.MIMEText(st)

        msg['Subject'] = "Test Message"
        msg['To'] = 'calendar@localhost'
        msg['From'] = 'Mike.Olson@Fourthought.com'
        msg['Message-Id'] = "2"
        msg['In-Reply-To'] = self.currentID or 0

        print "Sending Reply"
        common.SendMessage("127.0.0.1",8024,"me@fourthought.com",
        ["Mike.Olson@Fourthought.com"],msg)
                         
    #Implementation of our SOAP Service.
    def getMonth(self,year,month):
        print "Request for %d,%d" % (year,month)
        return calendar.month(year, month)
SOAP SMTP 클라이언트
SMTP SOAP 클라이언트는 응답을 표현하는 인커밍 메일 메시지를 리스닝해야한다. 다시 smtpd.SMTPServer를 오버라이드하여 (다른 포트에) 리스너를 만들어 응답을 리스닝한다.(Listing 3). 서버와 클라이언트 리스너의 가장 큰 차이점은 개별 쓰레드에서 리스너를 시작한다는 것이다. 이로써 사용자 인풋을 핸들링하는 한 개의 쓰레드와 응답을 리스닝하고 처리하는 리스너를 갖게된다.
또 다른 차이점으로는 리스너가 "In-Reply-To" ID를 콜백(call back) 메소드로 매핑하는 "응답" 사전을 포함하고 있다. 이 접근 방식으로 주 인풋 쓰레드는 가능한 많은 요청을 보낼 수 있다.

    
class ClientServer(SMTPd.SMTPServer):
    #A simple server to receive our SOAP responses.
    #The responses dictionary is a mapping from
    #Message ID to a callback to handle the response.
    responses = {}

    #this is the method we must override in to handle SMTP messages
    def process_message(self, peer, mailfrom, rcpttos, data):
        #Parse the message into a email.Message instance
        p = Parser.Parser()
        m = p.parsestr(data)
        #See if this is a reply that we were waiting for.
        if m.has_key('In-Reply-To') and self.responses.has_key(m['In-Reply-To']):
            mID = m['In-Reply-To']
            #Invoke the response callback with the parsed SOAP.
            self.responses[mID](mID,parse.ParsedSoap(m.get_payload(decode=1)))
            del self.responses[mID]
        else:
            #In a product server, this would probably forward the message to another
            #SMTP Server.
            print "Unknown Email message"
            print m
            
    #method used to register that we are expecting a response from the server.
    def expectResponse(self,mId,callback):
        self.responses[str(mId)] = callback
사용자 인풋은 HandleInput 메소드에 모아진다. (Listing 4). 사용자에게 한 달 또는 일 년 안으로 입력할 것을 요청한다. 그런다음 SOAP 요청을 만들어 이를 서버에 보낸다. 또한 리스너와 함께 요청을 등록하여 리스너가 응답을 기대하고 있다는 것을 알도록 한다. 이 예제에서 모든 요청은 DisplayResults 콜백과 함께 등록된다. 이 메소드는 결과를 스크린에 디스플레이한다. 그런다음 SOAP 요청을 만들어 이를 서버에 보낸다. 또한 리스너와 함께 요청을 등록하여 리스너가 응답을 기다릴 수 있게 한다.

    
def DisplayResults(ID,ps):
    #This method is the generic callback used by all requests.
    #It uses the parsed SOAP to print out the results.
    print "\nResults for ID: " + ID
    tc = TC.String()
    data = _child_elements(ps.body_root)
    if len(data) == 0: print None
    print tc.parse(data[0], ps)

def HandleInput(server):
    #This method is used to query the user for a year and a month.
    #When one is received, then a new message is sent, and the server
    #is told to expect the results
    done = 0
    lastID = 1
    while not done:
        year = raw_input("Year of request(Return to exit): ")
        if not year: done = 1
        else:
            year = int(year)
            month = int(raw_input("Month of request: "))

            lastID += 1
            mID = lastID
            
            msg = MIMEText.MIMEText(BODY_TEMPLATE%(year,month))

            msg['Subject'] = "Test Message"
            msg['To'] = 'calendar@localhost'
            msg['From'] = 'Mike.Olson@Fourthought.com'
            msg['Message-Id'] = str(mID)

            server.expectResponse(mID,DisplayResults)
            print "Sending out message ID: " + str(mID)
            common.SendMessage("127.0.0.1",8023,"me@fourthought.com",
             ["Mike.Olson@Fourthought.com"],msg)

def StartServer():
    #Start up our response server in another thread.
    server = ClientServer(("localhost", 8024),
                          (None, 0))
    def run():
        try:
            asyncore.loop()
        except KeyboardInterrupt:
            pass
    print "Starting Client Server"
    t = threading.Thread(None,run)
    t.start()
    return server

if __name__ == '__main__':
    server = StartServer()
    HandleInput(server)
예제 실행하기
클라이언트 애플리케이션은 날짜와 달(month)을 사용자에게 요청한다. 원하는 만큼 많은 쿼리를 할 수 있다. 유효 응답이 리턴되면 스크린에 프린트된다. 보내진 것과 같은 순서로 모든 응답을 받을 수 없다는 것을 알게 될 것이다. 아웃풋은 Listing 5 이다.

    
[molson@penny src]$ python client.py
Starting Client Server
Year of request(Return to exit): 2003
Month of request: 1
Sending out message ID: 2
Year of request(Return to exit): 2004
Month of request: 1
Sending out message ID: 3
Year of request(Return to exit): 2005
Month of request: 1
Sending out message ID: 4
Year of request(Return to exit):
Results for ID: 2
January 2003
Mo Tu We Th Fr Sa Su
       1  2  3  4  5
 6  7  8  9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31

Results for ID: 3
January 2004
Mo Tu We Th Fr Sa Su
          1  2  3  4
 5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

Results for ID: 4
January 2005
Mo Tu We Th Fr Sa Su
                1  2
 3  4  5  6  7  8  9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
다음에는..
Google의 SOAP API와 인터랙팅에 무엇이 필요한지를 검토할 것이다.
참고자료

*SOAP (Simple Object Access Protocol)은 웹상의 객체들을 액세스하기 위한 마이크로소프트의 프로토콜이다. 이 프로토콜은 HTTP를 사용하여 인터넷에 텍스트 명령어를 보내기 위해 XML 구문을 쓴다. SOAP은 COM, DCOM, 인터넷 익스플로러, 마이크로소프트의 자바 이행 등 내에서 지원된다.

댓글 없음:

댓글 쓰기