project:downloadmonitor
차이
문서의 선택한 두 판 사이의 차이를 보여줍니다.
project:downloadmonitor [2012/12/23 04:56] – [웹서버와 통합하기] 127.0.0.1 | project:downloadmonitor [2014/10/09 21:24] (현재) – 바깥 편집 127.0.0.1 | ||
---|---|---|---|
줄 1: | 줄 1: | ||
+ | ====== 웹페이지를 통해 내 PC의 프로그램 상태 모니터링하기 ====== | ||
+ | 자잘한 파이썬 레시피나 초간단 웹서버 만들기 같은 작은 주제로 글을 쓰려다 보니 이미 저런 내용은 간단히 검색해도 찾을 수 있을 것 같더라구요. 그냥 한데 묶어서 뭔가를 해보는 것이 나을 거 같아서 이런 주제를 정했습니다. | ||
+ | |||
+ | ===== 프로그램의 장단점 ===== | ||
+ | ==== 장점 ==== | ||
+ | - 개인 취향이지만, | ||
+ | - RDP나 VNC 같은 원격접속 프로토콜과 해당 툴들은 여러 플랫폼에서 구할 수 있고, 심지어 스마트폰에서도 쓸 수 있는 앱이 수두룩하다. 하지만, | ||
+ | - 원거리의 경우 인터넷 속도에 크게 좌우된다. | ||
+ | - 주고받는 데이터량이 작업 내용에 비해 많은 편이다. 그러므로 스마트폰의 경우 무제한 요금이 아닌 한 많이 사용하면 요금이 문제가 될 수 있다. | ||
+ | - 반면 이 방법은 간단한 웹페이지므로 다른 피씨, 스마트폰에서도 쉽게 확인 가능하다. 인터넷 상태는 웹페이지 한 번만 로딩할 정도의 상태면 충분하다. 요금 걱정도 크게 줄어들 것이다. | ||
+ | - 다른 PC에 기록될 내 PC에 대한 정보가 최소한으로 줄어든다. 사용 후 웹브라우저에서 방문 기록을 삭제하거나, | ||
+ | |||
+ | ==== 단점 ==== | ||
+ | - 원격접속같이 PC 전반을 건드리거나, | ||
+ | - 보안상의 문제가 생길 수 있다. 웹서버를 만들어서 돌린다는 말은 내 PC에 아무나 들어올 수 있는 길을 하나 터놓는다는 것과 마찬가지다. 우리가 만드는 웹서버는 완전 장난감 웹서버이다. 십중팔구 보안에는 매우 취약할 것이다. 그렇다고, | ||
+ | |||
+ | ===== 활용 예 ====== | ||
+ | 사실 다음 상황을 시나리오로 그려 보았습니다. 예를 들어 대량의 자료를 다운로드 받고 있다고 가정하지요. PC를 켜둔 상태로 외출했습니다. 은근히 진행 상태가 궁금합니다. 속도는 얼마나 나오고 있는지, 앞으로 얼마나 더 받아야 하는지, 혹시 문제가 생기지는 않았는지... | ||
+ | |||
+ | 인터넷이 빠르고 안정적인 곳이라면 사실 이런 걸 만들 필요가 없을지도 모르겠습니다. 그런데 하루 종일 쉼없이 다운로드 받아봐야 한국에서는 고작 30분에서 1시간 정도면 받아지는 양 밖에 안되는, 인터넷 속도가 매우 느린 곳에 있다고 하면? | ||
+ | |||
+ | 바깥에서도 쉽게 다운로드 상태를 모니터링하면 좋겠는데 말이죠. 뭔가 알고 계신 분들은 스마트폰으로 집 PC에 원격 접속을 할 수 있도록 세팅을 해 두셨을 겁니다. 훌륭하십니다. 그런데 네트워크 품질이 영 좋지 않은 곳에서는 원격 접속도 쉽지 않습니다. 더구나 스마트폰 요금이라는 현실적인 제약도 있구요. 이럴 때 이 방법은 조금 수고스럽긴 해도 나름 요긴할 수도 있지 않을까 생각이 들더군요. | ||
+ | |||
+ | 해외 교민분들 중 불행히도 인터넷 속도가 많이 느린 지역에 거주하고 있으신 분들께는 혹여 도움이 될 수도 있을지 모르겠네요 ;-) | ||
+ | 피씨를 오래 켜두면서 특정 어플리케이션에 대해 지속적인 모니터링이 필요하신 분 또한 그럭저럭 눈여겨 볼 만한 문서가 될 수도 있을 것 같습니다. | ||
+ | |||
+ | ===== 프로그램 구상 ===== | ||
+ | 피씨에 아주 작은 웹서버를 돌릴 것입니다. 이 웹서버는 특정 주소에 대해 반응할 겁니다. 사용자가 특정 주소를 입력하면, | ||
+ | |||
+ | {{ : | ||
+ | |||
+ | 위 그림은 우리가 만들 프로그램에 대한 구상을 그려 본 것입니다. 우리가 짤 파이썬 스크립트는 두 가지입니다. | ||
+ | - 프로그램에서 정보를 추출하는 역할을 맡은 스크립트. 사실 이것은 ' | ||
+ | - 두번째 스크립트는 아주 간단한 웹서버입니다. 물론 웹서버에 대한 지식이 있으신 분은 아예 잘 만들어진 웹서버(nginx, | ||
+ | - 웹서버는 사용자가 보낸 특정 주소 요청에 응답해 프로그램의 상태를 전달합니다. | ||
+ | - 사용자가 보낸 특정 주소 문자열은 첫번째 스크립트에게 특정 파라미터를 전달합니다. | ||
+ | |||
+ | ===== 기반 지식 ===== | ||
+ | 이런 말을 할 자격이 될까 고민이 되지만 살짝 잡설을 늘어놓겠습니다. 개인적으로 한 언어를 능숙하게 쓰는 것보다 더 중요한 것은 언어로 표현하고 싶은 것이 무엇인지를 잘 아는 것이라고 생각합니다. | ||
+ | 코드를 능숙하게 짜기 전에 어떤 것이 문제이고, | ||
+ | |||
+ | 그러나 웹, 혹은 웹서버에 대해서는 해야 할 말이 좀 많긴 합니다. 파이썬이 워낙 간결하고 기본 라이브러리가 잘 갖춰져 있어 그냥 코딩 몇 줄이면 후닥닥 해결되는 경우가 많아서 코드에 근간이 되는 숨겨진 개념들을 너무 쉽게 지나치기 쉽습니다. 물론 재미로 하는 우리의 장난감 프로그램을 위해 너무 심각해질 필요는 없지만 너무 주마간산으로 지나치기에는 아쉽다는 생각이 드는군요. 한번쯤은 차근차근 이 문서를 읽어보기를 바랍니다. | ||
+ | |||
+ | ==== 네트워크에 대해 약간만 ==== | ||
+ | OSI 모델부터, | ||
+ | |||
+ | === IP 주소와 port에 대해 === | ||
+ | == IP 주소 == | ||
+ | 최근 공유기를 많이 사용하게 되면서 'IP 주소' | ||
+ | |||
+ | IP 주소는 네트워크 장비(쉽게 말해 랜카드 등)가 가지고 있는 식별 주소입니다. IPv4, IPv6가 있습니다. IPv6가 새롭게 제안된 프로토콜이지만, | ||
+ | |||
+ | 여러분의 PC가 ' | ||
+ | |||
+ | == Port == | ||
+ | IP 주소가 각 PC에 정보를 전달하기 위한 식별 수단이지만, | ||
+ | |||
+ | 비유를 하자면 이렇습니다. 'A 회사' | ||
+ | |||
+ | 실제로 우리의 PC안에는 여러 프로그램이 동시에 네트워크를 사용중입니다. 웹브라우저로 파일을 전송 받으면서도 메신저로 대화를 할 수 있고 웹서핑도 동시에 가능합니다. 눈에 보이지 않지만 윈도우 시스템은 그 와중에 나름대로 업데이트 서버로부터 업데이트 내용을 알아보고 있을 수도 있구요. | ||
+ | |||
+ | 포트 번호의 범위는 0에서부터 65, | ||
+ | |||
+ | 웹브라우저의 주소를 입력할 때, 예를 들어 ' | ||
+ | |||
+ | 일반적으로 한 PC에서 서로 다른 프로그램이 동시에 하나의 포트 번호를 사용할 수 없습니다. 만약 어떤 프로그램이 이미 사용중인 포트에 접근하려고 한다면 아마 운영 체제 수준에서 에러가 날 겁니다. ' | ||
+ | |||
+ | === DNS 에 대해 === | ||
+ | 각 PC를 구분하기 위해 IP 주소를 사용하는데, | ||
+ | |||
+ | 이건 이렇게 비유할 수 있지요. 기본적으로 전화를 걸 때는 전화 번호를 하나하나 적어서 걸어야 하지요. 하지만 전화기가 조금 좋아지면서 자주 쓰는 번호는 단축 다이얼에 넣을 수 있게 되었습니다. 이보다 훨씬 좋아진 전화기들은 주소록으로 연락처를 관리합니다. 주소록을 이용하면 이름, 집 전화, 직장 전화, 개인 휴대전화, | ||
+ | |||
+ | 전화번호와 도메인 이름의 개념은 이처럼 거의 같습니다. 그러나 전화번호의 경우 지극히 개인적인 사항이므로 주소록을 각자의 폰에 각자가 관리합니다. 그러나 도메인 이름은 모두가 공통적으로 사용하니 한 데 모아서 관리하는 것이 유용하고, | ||
+ | |||
+ | 눈에 보이지 않지만 사실 ' | ||
+ | |||
+ | 참고로 여기서 관리상의 이점이란 쉽게 말해 이렇습니다. 새로운 서버를 도입하면서 IP 주소가 변경되는 일이 피치 못해 생겼습니다. IP 주소를 그대로 쓰는 경우였다면 IP 주소가 바뀌었다는 사실을 동네방네 알려야 할 겁니다. 그러나 도메인 이름을 사용한다면 도메인 이름에 대응되는 IP 주소 기록만 살짝 변경해주면 그만입니다. 도메인 이름이 변경되지 않는 이상 접속하는 사람들은 IP 주소에 대해 아예 신경 쓸 필요도 없이 계속 서비스를 이용할 수 있겠죠. | ||
+ | |||
+ | 명령 프롬프트에서 ' | ||
+ | |||
+ | 각 인터넷 업체마다 각각 DNS 서버는 다를 수 있습니다. 대표적으로 KT의 DNS 서버 주소는 ' | ||
+ | |||
+ | ==== 웹페이지에 대해 약간만 ==== | ||
+ | 이 장에서는 웹페이지에 대한 주변 지식을 간단히 정리해보도록 하겠습니다. 어째서 웹페이지가 서버로부터 내 PC에 전달이 되는지, 어째서 웹브라우저에서 내가 클릭한 항목에 대해 페이지 이동이 가능한지에 대해 한 번쯤 의문을 가져 보셨다면 본 문서가 약간 도움이 될 것입니다. | ||
+ | |||
+ | === HTTP Request, response 메시지 === | ||
+ | HTTP 문서가 PC에서 보여지기 위해서는 먼저 서버에 ' | ||
+ | |||
+ | 서버와 클라이언트는 이 요청과 응답을 아주 단순한 방법으로 수행하고 있습니다. 서버와 클라이언트 둘 다 각각 보내는 메시지에 ' | ||
+ | |||
+ | 자, 그렇다면 보이지 않던 헤더를 적접 눈으로 확인해 보도록 하겠습니다. [[http:// | ||
+ | |||
+ | 다음은 Web-Sniffer가 출력한 요청 헤더입니다. | ||
+ | < | ||
+ | GET / HTTP/ | ||
+ | Host: www.google.com[CRLF] | ||
+ | Connection: close[CRLF] | ||
+ | User-Agent: Web-sniffer/ | ||
+ | Accept-Encoding: | ||
+ | Accept: text/ | ||
+ | Accept-Language: | ||
+ | Accept-Charset: | ||
+ | Cache-Control: | ||
+ | Referer: http:// | ||
+ | [CRLF] | ||
+ | </ | ||
+ | < | ||
+ | |||
+ | 다음은 서버에서 보낸 응답 헤더입니다. 첫 줄의 ' | ||
+ | ^헤더이름 | ||
+ | |Status: %%HTTP/1.1 200 OK%% || | ||
+ | |Date: | ||
+ | |Expires: | ||
+ | |Cache-Control: | ||
+ | |Content-Type: | ||
+ | |Set-Cookie: | ||
+ | |Set-Cookie: | ||
+ | |P3P: | ||
+ | |Server: | ||
+ | |X-XSS-Protection: | ||
+ | |X-Frame-Options: | ||
+ | |Connection: | ||
+ | |||
+ | Content 영역은 아래와 같이 출력됩니다. | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | Content 영역은 우리가 웹브라우저에서 ' | ||
+ | |||
+ | 위 헤더의 내용을 다 알 필요는 없습니다. 말씀드리고자 하는 바는 서버와 클라이언트 사이에 지금까지 눈에 보이지는 않았지만 이렇게 약속된 메시지를 주고 받는다는 사실이며, | ||
+ | |||
+ | 웹브라우저가 화면에 보이는 것처럼 웹페이지를 만들기 위해서는 우선 HTML 문서를 분석해서 컴퓨터가 HTML 문서를 이해하는 작업이 필요합니다. 이를 흔히 'HTML 파싱(parsing)' | ||
+ | |||
+ | 이후 위와 유사한 요청/ | ||
+ | |||
+ | 최근 웹브라우저들은 이렇게 HTML 파일을 얼마나 빠르게, 잘 표현하는지로 첨예한 경쟁을 벌이고 있습니다. | ||
+ | |||
+ | === Form과 POST === | ||
+ | 서버에서 클라이언트로 정보(HTML 문서)를 보내는 형태가 대부분이지만, | ||
+ | |||
+ | 이렇게 사용자가 좀더 손쉽고 실수 없이 서버에 필요한 정보를 입력할 수 있도록 한 장치를 ' | ||
+ | |||
+ | 하지만 때로는 웹브라우저의 주소창에 깨끗하게(? | ||
+ | |||
+ | 이렇게 주소창에 표시되지 않고 서버로 넘어가는 방식을 일컬어 'POST 방식' | ||
+ | |||
+ | 이 기능을 통해 서버로 보내는 요청 메시지를 살짝 엿보도록 하겠습니다. 다음은 제가 [[http:// | ||
+ | < | ||
+ | (Request-Line) POST / | ||
+ | Host www.clien.net | ||
+ | User-Agent Mozilla/ | ||
+ | Accept text/ | ||
+ | Accept-Language ko-kr, | ||
+ | Accept-Encoding gzip, | ||
+ | Connection keep-alive | ||
+ | Referer http:// | ||
+ | Content-Type application/ | ||
+ | Content-Length 71 | ||
+ | </ | ||
+ | 여기서 다음 두 라인을 주목해야 합니다. | ||
+ | POST / | ||
+ | |||
+ | Content-Type application/ | ||
+ | 로그인 할 때 POST 방식으로 데이터를 전송한다는 것을 첫 번째 줄이 확실히 보여줍니다. 두번째 줄은 클라이언트가 서버로 어떤 내용(콘텐츠)를 보낸다는 의미입니다. 그 내용이 다음과 같습니다. | ||
+ | url=%252F%253Fnowlogin%253D1& | ||
+ | 제 아이디와 비밀번호는 가렸습니다. 하마터면 제 아이디와 비밀번호가 그대로 노출될 수도 있겠네요. 누군가 제 통신 내용을 엿본다면 클리앙의 아이디와 비밀번호는 고스란히 그 사람의 것이 될 겁니다. | ||
+ | |||
+ | 현재 많은 사이트들이 이런 방식의 로그인을 사용중입니다. 이러한 방식은 사용자가 보안에 주의해야 합니다. 클리앙의 로그인 방식에 대해 왈가왈부하고자 하는 의도는 일절 없으며, 단지 POST 방식으로 전달되는 폼 양식은 이렇게 전송되는 메시지 본문(내용)에 있다는 것을 보여주고자 예를 들었습니다. | ||
+ | |||
+ | 한편 수많은 이들이 사용하는 대형 포털 사이트의 로그인에는 이러한 일을 막기 위해 ' | ||
+ | |||
+ | === Server-side Script === | ||
+ | 서버에서는 클라이언트로 입력받은 정보를 처리한다거나, | ||
+ | |||
+ | === Client-side Script === | ||
+ | 서버측 스크립트의 반대 개념으로, | ||
+ | |||
+ | 일반적으로 자바스크립트는 사용자의 PC의 접근에 제한을 받습니다. 악의를 가진 자바스크립트가 멋대로 사용자의 PC를 헤집고 다닐 수 없도록 말이죠. | ||
+ | |||
+ | 하지만 때로는 사용자의 PC의 파일에 대해 읽고 쓰기를 허용해야 할 때가 있는데, 이를 위해 플러그인이 이용됩니다. 플러그인의 대표적인 예로는 ' | ||
+ | |||
+ | === Basic Access Authentication === | ||
+ | 자 이제 마지막으로 ' | ||
+ | |||
+ | {{ : | ||
+ | |||
+ | 이 방식을 이용하면 서버와 클라이언트 사이에 오가는 헤더 정보만으로 간단하게 인증을 할 수 있어 매우 편리합니다. 별다른 플러그인이나 장치를 필요로 하지도 않아서 심플하기도 합니다. 혹여 웹페이지의 정보가 쉽게 노출되는 것이 꺼려진다면 이것으로 나름대로 보호할 수 있습니다. 그러나 이 방식 또한 보통 로그인처럼 아이디와 비밀번호가 노출되므로 주의해야 합니다. 더 강력한 보호를 위해서는 더 많은 보안 시스템을 도입해야 합니다. | ||
+ | |||
+ | 인증은 이렇게 이루어집니다. 클라이언트가 인증되지 않은 채로 서버에 접근하면, | ||
+ | WWW-Authenticate: | ||
+ | |||
+ | 그러면 웹브라우저에서는 위 그림과 같은 인증 창이 나옵니다. 사용자 이름(아이디)와 비밀번호를 입력하고 전송하면 클라이언트에서는 서버로 다음과 같은 헤더를 보냅니다. | ||
+ | Authorization: | ||
+ | 예를 들어 클라이언트가 보내는 사용자 이름이 ' | ||
+ | |||
+ | 서버는 클라이언트가 받은 ' | ||
+ | ===== 웹서버 스크립트 만들기 ===== | ||
+ | ==== 웹서버 동작시키기 ==== | ||
+ | 이제 파이썬 스크립트를 직접 제작하여 실습을 하는 시간입니다. | ||
+ | |||
+ | === 기본 웹서버 동작시켜 보기 === | ||
+ | 파이썬에서 웹서버를 만드는 것은 정말 식은 죽 먹기입니다. 파이썬에서 웹서버는 언제든 사용할 수 있도록 기본 라이브러리에서 제공이됩니다. 그럼 당장 파이썬의 내장 웹서버를 동작해볼까요? | ||
+ | python -m SimpleHTTPServer 8000 | ||
+ | 이 한 줄로 웹서버가 동작합니다. 개인적으로 urllib으로 웹페이지 소스를 읽어올 때만큼 충격적인 한 줄이었지요 :-) \\ | ||
+ | 파이썬 기본 웹서버는 파이썬이 실행된 경로의 파일과 디렉토리의 목록을 출력합니다. 실행 후 웹브라우저에서 ' | ||
+ | |||
+ | === 기본 웹서버를 상속받아 수정하기 === | ||
+ | 기본 웹서버는 우리가 원하는 동작을 수행하지 않습니다. 우리는 특정 문자를 입력받아서 프로그램의 상태를 보여 주는 서버를 만들어야 합니다. 그러므로 기본 웹서버 클래스를 상속받아 우리만의 웹서버로 만들어야 합니다. 일단 가장 기본적인 기능만 동작시켜 보도록 하지요. | ||
+ | |||
+ | <code python MonitorServer_01.py> | ||
+ | # -*- coding: | ||
+ | from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler | ||
+ | import sys | ||
+ | |||
+ | class MyRequestHandler(BaseHTTPRequestHandler): | ||
+ | def do_GET(self): | ||
+ | try: | ||
+ | addr, port = self.client_address | ||
+ | print " | ||
+ | print " | ||
+ | print self.headers | ||
+ | | ||
+ | urlpath = self.path | ||
+ | if urlpath == '/ | ||
+ | self.send_response(200) | ||
+ | self.end_headers() | ||
+ | f = open(urlpath[1: | ||
+ | self.wfile.write(f.read()) | ||
+ | f.close() | ||
+ | else: | ||
+ | raise IOError | ||
+ | |||
+ | except IOError: | ||
+ | self.send_error(404, | ||
+ | |||
+ | def log_message(self, | ||
+ | pass # 로그 메시지 끄고 조용히 하기 위함 | ||
+ | | ||
+ | # 상태 보고 서버를 시작함 | ||
+ | def StartStatusServer(port): | ||
+ | httpd = HTTPServer(('', | ||
+ | | ||
+ | try: | ||
+ | print " | ||
+ | httpd.serve_forever() | ||
+ | | ||
+ | except KeyboardInterrupt: | ||
+ | print " | ||
+ | | ||
+ | | ||
+ | def main(argv) : | ||
+ | if len(argv) != 2: | ||
+ | print >> sys.stderr, " | ||
+ | return 1 | ||
+ | | ||
+ | port = int(argv[1]) | ||
+ | StartStatusServer(port) | ||
+ | return 0 | ||
+ | | ||
+ | if __name__ == ' | ||
+ | sys.exit(main(sys.argv)) | ||
+ | </ | ||
+ | |||
+ | <code html status.html> | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | 두 파일을 같은 경로에 놓고 실행을 합니다. 스크립트는 포트 번호를 인자로 받습니다. 적당한 포트 번호를 입력하고 서버를 동작시키면, | ||
+ | |||
+ | ==== 웹서버에 사용자 인증 기능 추가하기 ==== | ||
+ | 웹페이지의 내용을 보호하하기 위해 기본 인증 방식을 이용해서 ' | ||
+ | |||
+ | 여기서 미리 한가지 문제를 미리 제기하고 시작하겠습니다. 기본 인증 방식은 ' | ||
+ | |||
+ | 다음은 초라하나마 사용자 인증 기능을 추가하여 완성한 우리의 서버입니다. | ||
+ | <code python MonitorServer_02.py> | ||
+ | # -*- coding: | ||
+ | from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler | ||
+ | import base64 | ||
+ | import sys | ||
+ | |||
+ | class MyRequestHandler(BaseHTTPRequestHandler): | ||
+ | def do_GET(self): | ||
+ | try: | ||
+ | addr, port = self.client_address | ||
+ | print " | ||
+ | print " | ||
+ | print self.headers | ||
+ | | ||
+ | urlpath = self.path | ||
+ | if urlpath == '/ | ||
+ | if self.login_process() == True: | ||
+ | self.send_response(200) | ||
+ | self.send_header(' | ||
+ | self.send_header(' | ||
+ | self.end_headers() | ||
+ | f = file(' | ||
+ | self.wfile.write(f.read()) | ||
+ | f.close() | ||
+ | else: | ||
+ | self.response401() | ||
+ | self.wfile.write('< | ||
+ | else: | ||
+ | raise IOError() | ||
+ | |||
+ | except IOError: | ||
+ | self.send_error(404, | ||
+ | |||
+ | def log_message(self, | ||
+ | pass # 로그 메시지 끄고 조용히 하기 위함 | ||
+ | |||
+ | def login_process(self): | ||
+ | if self.headers.has_key(' | ||
+ | authm, val = self.headers[' | ||
+ | userid, passwd = base64.b64decode(val).split(':' | ||
+ | # print userid, passwd | ||
+ | if userid == ' | ||
+ | return True | ||
+ | return False | ||
+ | | ||
+ | def response401(self): | ||
+ | self.send_response(401) | ||
+ | self.send_header(' | ||
+ | self.send_header(' | ||
+ | self.send_header(' | ||
+ | self.end_headers() | ||
+ | |||
+ | # 상태 보고 서버를 시작함 | ||
+ | def StartStatusServer(port): | ||
+ | httpd = HTTPServer(('', | ||
+ | | ||
+ | try: | ||
+ | print " | ||
+ | httpd.serve_forever() | ||
+ | | ||
+ | except KeyboardInterrupt: | ||
+ | print " | ||
+ | | ||
+ | | ||
+ | def main(argv) : | ||
+ | if len(argv) != 2: | ||
+ | print >> sys.stderr, " | ||
+ | return 1 | ||
+ | | ||
+ | port = int(argv[1]) | ||
+ | StartStatusServer(port) | ||
+ | return 0 | ||
+ | | ||
+ | if __name__ == ' | ||
+ | sys.exit(main(sys.argv)) | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== 타겟 윈도우 정보 추출 프로그램 만들기 ===== | ||
+ | ==== 타겟 프로그램 정하기 ==== | ||
+ | 타겟 프로그램은 어떤 것이라도 상관없습니다만, | ||
+ | |||
+ | ==== 타겟 프로그램의 윈도우 정보를 출력하기 ==== | ||
+ | 타겟 프로그램의 생김새는 다음 그림과 같습니다. | ||
+ | {{ : | ||
+ | |||
+ | 타겟 프로그램을 작업관리자에서 확인하여 프로그램에 위치한 컨트롤의 리스트를 출력해봅니다. 저는 그다지 수정할 것이 없군요. | ||
+ | 이 프로그램에서 가장 의미있는 부분은 파일의 리스트가 나오는 부분입니다. 이 부분은 5개의 열로 되어 있으므로 5개 열 각각을 리스트로 모두 가져오도록 하겠습니다. | ||
+ | |||
+ | enumlistwindow.py를 기반으로 해서 새로운 프로그램을 만들어 보겠습니다. 얼개는 같습니다만, | ||
+ | <code python report_status.py> | ||
+ | # -*- coding: | ||
+ | # TeraCopy 내부의 모든 윈도우 객체를 나열합니다. | ||
+ | import win32gui | ||
+ | import win32con | ||
+ | import commctrl | ||
+ | import pywintypes | ||
+ | import struct, array | ||
+ | import sys | ||
+ | from ListViewItems import GetListViewItems | ||
+ | |||
+ | # 부모 윈도우의 핸들을 검사합니다. | ||
+ | class WindowFinder: | ||
+ | def __init__(self, | ||
+ | try: | ||
+ | win32gui.EnumWindows(self.__EnumWindowsHandler, | ||
+ | except pywintypes.error as e: | ||
+ | # 발생된 예외 중 e[0]가 0이면 callback이 멈춘 정상 케이스 | ||
+ | if e[0] == 0: pass | ||
+ | | ||
+ | def __EnumWindowsHandler(self, | ||
+ | wintext = win32gui.GetWindowText(hwnd) | ||
+ | if wintext.find(extra) != -1: | ||
+ | self.__hwnd = hwnd | ||
+ | return pywintypes.FALSE # FALSE는 예외를 발생시킵니다. | ||
+ | |||
+ | def GetHwnd(self): | ||
+ | return self.__hwnd | ||
+ | |||
+ | __hwnd = 0 | ||
+ | |||
+ | # 자식 윈도우의 핸들 리스트를 검사합니다. | ||
+ | class ChildWindowFinder: | ||
+ | def __init__(self, | ||
+ | try: | ||
+ | win32gui.EnumChildWindows(parentwnd, | ||
+ | except pywintypes.error as e: | ||
+ | if e[0] == 0: pass | ||
+ | |||
+ | def __EnumChildWindowsHandler(self, | ||
+ | self.__childwnds.append(hwnd) | ||
+ | |||
+ | def GetChildrenList(self): | ||
+ | return self.__childwnds | ||
+ | |||
+ | __childwnds = [] | ||
+ | |||
+ | # teracopy의 보고 기능 | ||
+ | class teracopy_reporter: | ||
+ | def __init__(self, | ||
+ | self.__GetChildWindows(windowname) | ||
+ | |||
+ | # windowname을 가진 윈도우의 모든 자식 윈도우 리스트를 얻어낸다. | ||
+ | def __GetChildWindows(self, | ||
+ | # TeraCopy의 window handle을 검사한다. | ||
+ | self.__hwnd = WindowFinder(windowname).GetHwnd() | ||
+ | |||
+ | # Teracopy의 모든 child window handle을 검색한다. | ||
+ | self.__childwnds = ChildWindowFinder(self.__hwnd).GetChildrenList() | ||
+ | | ||
+ | # 리스트 뷰 컨트롤의 텍스트를 파일로 저장합니다. | ||
+ | def report_status(self, | ||
+ | listviewCtrl = 0 | ||
+ | for child in self.__childwnds: | ||
+ | wnd_clas = win32gui.GetClassName(child) | ||
+ | if wnd_clas == ' | ||
+ | listviewCtrl = child | ||
+ | break | ||
+ | |||
+ | item_grid = [] | ||
+ | for i in range(5): | ||
+ | column = GetListViewItems(listviewCtrl, | ||
+ | item_grid.append(column) | ||
+ | | ||
+ | # html로 쓰기 | ||
+ | html = '' | ||
+ | with open(save_as, | ||
+ | html += "< | ||
+ | html += "< | ||
+ | html += " | ||
+ | html += "< | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | for r in range(len(item_grid[0])): | ||
+ | html += " | ||
+ | html += " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | | ||
+ | item_grid[2][r], | ||
+ | item_grid[4][r]) | ||
+ | html += " | ||
+ | html += " | ||
+ | html += "</ | ||
+ | |||
+ | f.write(html) | ||
+ | | ||
+ | __hwnd = 0 | ||
+ | __childwnds = 0 | ||
+ | | ||
+ | | ||
+ | if __name__ == ' | ||
+ | tr = teracopy_reporter(' | ||
+ | tr.report_status(' | ||
+ | </ | ||
+ | |||
+ | 출력 결과의 일부입니다. | ||
+ | {{ : | ||
+ | |||
+ | ==== 타겟 프로그램의 이벤트 메시지 분석 ==== | ||
+ | Spy++에는 이벤트 메시지를 분석하는 기능도 있습니다. ' | ||
+ | |||
+ | 일단 클릭해보니 '더 보기/ | ||
+ | |||
+ | 그럼 수많은 메시지 중 원하는 것만 선택해 보기로 합니다. ' | ||
+ | |||
+ | 이제 버튼을 클릭해 보면 ' | ||
+ | |||
+ | ==== 스크립트로 발생시킨 이벤트로 타겟 프로그램을 동작시키기 === | ||
+ | 이제 report_status.py 프로그램을 조금 더 수정하도록 합니다. 다음 함수를 report_status 클래스에 추가해 봅니다. | ||
+ | <code python> | ||
+ | def toggle_listview(self): | ||
+ | btnwnd = 0 | ||
+ | for child in self.__childwnds: | ||
+ | wnd_clas = win32gui.GetClassName(child) | ||
+ | wnd_text = win32gui.GetWindowText(child) | ||
+ | if wnd_clas == ' | ||
+ | btnwnd = child | ||
+ | break | ||
+ | | ||
+ | win32gui.SendMessage(btnwnd, | ||
+ | win32gui.SendMessage(btnwnd, | ||
+ | </ | ||
+ | win32gui.SendMessage()의 3, 4번째 인자는 각각 wParam, lParam 이라고 하여 메시지 코드에 부가적으로 전달되는 데이터들입니다. 이 또한 Spy++에서 확인할 수 있습니다. 메시지 한 항목을 선택하고 " | ||
+ | |||
+ | ==== 웹서버와 통합하기 ==== | ||
+ | 웹서버는 클라이언트가 접속할 때까지 대기하다가, | ||
+ | |||
+ | 그리고 우리는 타겟 프로그램을 웹페이지에서 조작하기를 원합니다. 웹페이지에도 더 보기/ | ||
+ | |||
+ | 클라이언트에서 서버로 데이터를 전송하기 위해 HTML 코드에 폼(Form) 요소를 추가할 것입니다. report_status.py 코드에 살짝 추가하도록 하지요 | ||
+ | <code python report_status_form.py> | ||
+ | # -*- coding: | ||
+ | # TeraCopy 내부의 모든 윈도우 객체를 나열합니다. | ||
+ | import win32gui | ||
+ | import win32con | ||
+ | import commctrl | ||
+ | import pywintypes | ||
+ | import struct, array | ||
+ | import sys | ||
+ | from ListViewItems import GetListViewItems | ||
+ | |||
+ | # 부모 윈도우의 핸들을 검사합니다. | ||
+ | class WindowFinder: | ||
+ | def __init__(self, | ||
+ | try: | ||
+ | win32gui.EnumWindows(self.__EnumWindowsHandler, | ||
+ | except pywintypes.error as e: | ||
+ | # 발생된 예외 중 e[0]가 0이면 callback이 멈춘 정상 케이스 | ||
+ | if e[0] == 0: pass | ||
+ | | ||
+ | def __EnumWindowsHandler(self, | ||
+ | wintext = win32gui.GetWindowText(hwnd) | ||
+ | if wintext.find(extra) != -1: | ||
+ | self.__hwnd = hwnd | ||
+ | return pywintypes.FALSE # FALSE는 예외를 발생시킵니다. | ||
+ | |||
+ | def GetHwnd(self): | ||
+ | return self.__hwnd | ||
+ | |||
+ | __hwnd = 0 | ||
+ | |||
+ | # 자식 윈도우의 핸들 리스트를 검사합니다. | ||
+ | class ChildWindowFinder: | ||
+ | def __init__(self, | ||
+ | try: | ||
+ | win32gui.EnumChildWindows(parentwnd, | ||
+ | except pywintypes.error as e: | ||
+ | if e[0] == 0: pass | ||
+ | |||
+ | def __EnumChildWindowsHandler(self, | ||
+ | self.__childwnds.append(hwnd) | ||
+ | |||
+ | def GetChildrenList(self): | ||
+ | return self.__childwnds | ||
+ | |||
+ | __childwnds = [] | ||
+ | |||
+ | # teracopy의 보고 기능 | ||
+ | class teracopy_reporter: | ||
+ | def __init__(self, | ||
+ | self.__GetChildWindows(windowname) | ||
+ | |||
+ | # windowname을 가진 윈도우의 모든 자식 윈도우 리스트를 얻어낸다. | ||
+ | def __GetChildWindows(self, | ||
+ | # TeraCopy의 window handle을 검사한다. | ||
+ | self.__hwnd = WindowFinder(windowname).GetHwnd() | ||
+ | |||
+ | # Teracopy의 모든 child window handle을 검색한다. | ||
+ | self.__childwnds = ChildWindowFinder(self.__hwnd).GetChildrenList() | ||
+ | | ||
+ | # 리스트 뷰 컨트롤의 텍스트를 파일로 저장합니다. | ||
+ | def report_status(self, | ||
+ | listviewCtrl = 0 | ||
+ | for child in self.__childwnds: | ||
+ | wnd_clas = win32gui.GetClassName(child) | ||
+ | if wnd_clas == ' | ||
+ | listviewCtrl = child | ||
+ | break | ||
+ | |||
+ | item_grid = [] | ||
+ | for i in range(5): | ||
+ | column = GetListViewItems(listviewCtrl, | ||
+ | item_grid.append(column) | ||
+ | | ||
+ | # html로 쓰기 | ||
+ | html = '' | ||
+ | with open(save_as, | ||
+ | html += "< | ||
+ | html += "< | ||
+ | html += " | ||
+ | html += "< | ||
+ | # 폼 요소가 추가되었습니다. ######################################################## | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | ################################################################################# | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | html += " | ||
+ | for r in range(len(item_grid[0])): | ||
+ | html += " | ||
+ | html += " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | | ||
+ | item_grid[2][r], | ||
+ | item_grid[4][r]) | ||
+ | html += " | ||
+ | html += " | ||
+ | html += "</ | ||
+ | |||
+ | f.write(html) | ||
+ | |||
+ | def toggle_listview(self): | ||
+ | btnwnd = 0 | ||
+ | for child in self.__childwnds: | ||
+ | wnd_clas = win32gui.GetClassName(child) | ||
+ | wnd_text = win32gui.GetWindowText(child) | ||
+ | if wnd_clas == ' | ||
+ | btnwnd = child | ||
+ | break | ||
+ | | ||
+ | win32gui.SendMessage(btnwnd, | ||
+ | win32gui.SendMessage(btnwnd, | ||
+ | | ||
+ | __hwnd = 0 | ||
+ | __childwnds = 0 | ||
+ | | ||
+ | | ||
+ | if __name__ == ' | ||
+ | tr = teracopy_reporter(' | ||
+ | tr.toggle_listview() | ||
+ | </ | ||
+ | |||
+ | 클라이언트에서 데이터를 보낼 준비가 끝났으니, | ||
+ | <code python MonitorServer_03.py> | ||
+ | # -*- coding: | ||
+ | from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler | ||
+ | import urlparse, base64 | ||
+ | import sys | ||
+ | from report_status import teracopy_reporter | ||
+ | |||
+ | class MyRequestHandler(BaseHTTPRequestHandler): | ||
+ | def do_GET(self): | ||
+ | try: | ||
+ | addr, port = self.client_address | ||
+ | print " | ||
+ | print " | ||
+ | print self.headers | ||
+ | | ||
+ | urlparsed = urlparse.urlparse(self.path) | ||
+ | urlpath | ||
+ | query = urlparse.parse_qs(urlparsed.query) | ||
+ | |||
+ | # print urlparsed | ||
+ | # print urlpath | ||
+ | # print query | ||
+ | | ||
+ | if urlpath == '/ | ||
+ | if self.login_process() == True: | ||
+ | self.response200(query) | ||
+ | else: | ||
+ | self.response401() | ||
+ | self.wfile.write('< | ||
+ | else: | ||
+ | raise IOError() | ||
+ | |||
+ | except IOError: | ||
+ | self.send_error(404, | ||
+ | |||
+ | def log_message(self, | ||
+ | pass # 로그 메시지 끄고 조용히 하기 위함 | ||
+ | |||
+ | def login_process(self): | ||
+ | if self.headers.has_key(' | ||
+ | authm, val = self.headers[' | ||
+ | userid, passwd = base64.b64decode(val).split(':' | ||
+ | # print userid, passwd | ||
+ | if userid == ' | ||
+ | return True | ||
+ | return False | ||
+ | | ||
+ | # 응답 코드 200 | ||
+ | def response200(self, | ||
+ | self.send_response(200) | ||
+ | self.send_header(' | ||
+ | self.send_header(' | ||
+ | self.end_headers() | ||
+ | | ||
+ | # TeraCopy의 정보를 HTML로 출력 | ||
+ | tr = teracopy_reporter(' | ||
+ | | ||
+ | # togglelist라는 변수를 받을 경우 listview를 열고 닫는다. | ||
+ | print query | ||
+ | if query.has_key(' | ||
+ | tr.toggle_listview() | ||
+ | | ||
+ | # HTML 문서를 새롭게 만들도록 지시한다. | ||
+ | tr.report_status(' | ||
+ | |||
+ | # 만들어진 문서를 읽어서 클라이언트로 보낸다. | ||
+ | f = file(' | ||
+ | self.wfile.write(f.read()) | ||
+ | f.close() | ||
+ | | ||
+ | # 응답 코드 401 (인증 필요) | ||
+ | def response401(self): | ||
+ | self.send_response(401) | ||
+ | self.send_header(' | ||
+ | self.send_header(' | ||
+ | self.send_header(' | ||
+ | self.end_headers() | ||
+ | |||
+ | # 상태 보고 서버를 시작함 | ||
+ | def StartStatusServer(port): | ||
+ | httpd = HTTPServer(('', | ||
+ | | ||
+ | try: | ||
+ | print " | ||
+ | httpd.serve_forever() | ||
+ | | ||
+ | except KeyboardInterrupt: | ||
+ | print " | ||
+ | | ||
+ | | ||
+ | def main(argv) : | ||
+ | if len(argv) != 2: | ||
+ | print >> sys.stderr, " | ||
+ | return 1 | ||
+ | | ||
+ | port = int(argv[1]) | ||
+ | StartStatusServer(port) | ||
+ | return 0 | ||
+ | | ||
+ | if __name__ == ' | ||
+ | sys.exit(main(sys.argv)) | ||
+ | </ | ||
+ | |||
+ | 웹페이지를 통해 TeraCopy의 진행 상황이 보고됩니다. 버튼을 누르면 파일 목록이 나타났다 사라졌다 합니다. 완성입니다! | ||
+ | |||
+ | ===== 마치며 ===== | ||
+ | 파이썬으로 아주 간단한 웹서버를 구현해 보았습니다. 이 웹서버는 이전 문서를 응용하여 서버의 어떤 프로그램을 모니터링합니다. 사용자는 간단하지만 인증을 해야 접근할 수 있도록 해보기도 하였습니다. | ||
+ | |||
+ | 또한 웹서버는 클라이언트로부터 메시지를 전송받습니다. 웹서버는 간단한 문자열 작업을 통해 이 메시지를 해석하며, | ||
+ | |||
+ | 여러 모듈이 뭉치니까 분량이 상당해졌네요. 그래도 파이썬이니까 이만큼 간결하게 되었다고 생각합니다. | ||
+ | 질문이 있으시면 제 이메일(cs.chwnam@gmail.com)로 보내주세요. 그럼 이만 문서를 마칩니다. | ||