Winsock 프로그래밍 연습 - 8 : WebMail POP3 Check
출처 주소
http://www.sockaddr.com/ExampleSourceCode.html
바로 받기
- 새로 친 코드
변경 내용 - 약간의 한글화 / CheckMail.h 헤더 부분 내장 (내용이 작아서 넣었음)
수행 내용
- TCP/IP - POP3 서버에 접속 및 통신하는 내용입니다.
- 수행화면
작업 로그
----시작(STARTUP)----
WSAStartup() 함수 성공
---- FINDHOST(호스트 찾기) ----
WSAAsyncGetHostByName() 함수를 써서 서버를 찾습니다.
SM_GETHOST 메시지를 받았습니다.
WSAAsyncGetHostByName() 함수 수행중 31 밀리초(ms)가 완료 하는데 소요되었습니다.
서버를 찾았습니다.
---- 호스트에 접속합니다.(Connect to host) ----
socket(AF_INET, SOCK_STREAM, 0); 을 호출 합니다.
WSAASyncSelete() 함수를 호출 합니다
getservbyname() 함수 수행으로 0밀리초(ms)가 소요 되었습니다.
connect() 함수를 호출합니다.
---- 처리 메시지(Process Messages) ----
FD_CONNECT 알림을 받았습니다.
FD_WRITE 알림을 받았습니다.
FD_READ 알림을 받았습니다.
recv() 함수를 호출합니다.
서버로 부터의 응답입니다 :
+OK NMQpopper NaverMail Team 2004 base Qpopper (version Naver1.0) at i4l043.nhncorp.com starting.
AppState = CONNECTING, senging USER
FD_READ 알림을 받았습니다.
recv() 함수를 호출합니다.
서버로 부터의 응답입니다 :
+OK Password required for cakel.
AppState = SEND, 패스워드를 전송합니다.
FD_READ 알림을 받았습니다.
recv() 함수를 호출합니다.
서버로 부터의 응답입니다 :
+OK cakel has 16 visible messages in 2312723 octets.
AppState = PASS, STAT 를 보냅니다.
FD_READ 알림을 받았습니다.
recv() 함수를 호출합니다.
서버로 부터의 응답입니다 :
+OK 16 2312723
AppState = STAT, 응답을 읽습니다.
---- 결과(Result)----
16 메시지 2312723 바이트
QUIT 명령를 전송합니다.
FD_READ 알림을 받았습니다.
recv() 함수를 호출합니다.
서버로 부터의 응답입니다 :
+OK Pop server at i4l043.nhncorp.com signing off.
호스트 QUIT OK
소켓을 닫습니다.
FD_CLOSE 통보를 받았습니다.
수행 방법
- 바로 수행되게 Link 부분을 수정했습니다.
#pragma comment (lib,"wsock32.lib") // wsock32.lib 를 링크하기
#pragma comment( linker, "/subsystem:windows" ) // WinMain 을 불러 쓰기. int main 을 쓰지 않습니다.
- Resouce 부분은 직접 만들어서 추가했습니다. 처음이라 그런지 X 버튼으로 종료가 되지 않습니다.
- 프로젝트에 Resource 을 추가하시고 같이 생성되는 recource.h 파일을 include 하시면 됩니다.
- 워크스페이스(workspace)에 넣어 두시면 많이 편합니다(엉뚱하게 있어도 오류가 나는 경우가 있음) 추가하는 법은 아래 파일 트리에 Add Files in Project 를 누르시던지 Project 메뉴에 Add to Project -> Files 를 누르시면 됩니다.
리소스 구조 입니다. 단순합니다;
- password 부분은 보이진 않지만 암호화 되어 전송되지 않기 때문에 외부 유출이 가능하므로 조심하시기 바랍니다.
- POP3 서버를 사용할수 있는 계정으로 접근 합니다.
- 만들어진 코드에서 문제가 시스템 명령 상으로 닫혀지지 않습니다. 아시는 분 댓글 부탁 드립니다. 해킹은 아닌데; 이상하게 X 버튼이 먹히지 않습니다. 그냥 닫기 컨트롤을 누르면 닫힙니다^^;
코드 보기(좀 깁니다.)
/* 저작권 공지 http://www.sockaddr.com/ExampleSourceCode.html http://cakel.tistory.com 교육용을 전제로 자유롭게 배포 가능합니다. winapi_checkmail 간단하게 POP3 를 이용한 통신을 통해 전자우편 서버와 통신을 하는 내용입니다. Windows API 를 쓰기 때문에 윈도우 상에서 교신 내용이 나옵니다. 아래 5개 파일로 구성되어 있습니다. resource.h - GUI 구성과 C++ 코드와 연동을 위한 헤더 파일 checkpop3.rc - GUI 실제 구성하는 틀(Frame) winapi_checkmail.dsw - MSDEV : Visual Studio 6용 작업 공간 winapi_checkmail.dsp - MSDEV : 작업용 Project 설정 File checkpop3.cpp - 구동을 맏는 핵심 C++ */ /* CheckMail.h Header 부분입니다. 내용이 작아서 checkpop3.cpp 에 내장(embedded) 시켰습니다. 전자우편을 확인하기 위해 POP3 서버를 기다립니다. */ // 16-bit 윈도우즈 에서 MAKEWORD 를 정의 합니다. // MAKEWORD (a, b) - 상위 8비트(BYTE) a OR 하위 8비트(BYTE) b = 16비트(WORD 형)로 받음 #include <windows.h> #include <string.h> #include <stdio.h> #include <winsock.h> #include "resource.h" #ifndef WIN32 #define MAKEWORD(a, b) ( (WORD) ( ((BYTE)(A)) | ( (WORD) ( (BYTE)(b)) ) << 8) ) #endif // Winsock 을 Link 시키고 윈도우즈용 시스템이라는 것을 명시 합니다. // 안 쓰면 컴파일러시 옵션을 주시 않으면 WinMain 링크 오류가 생깁니다. // 원인 모르는 Link 오류해결시 참고 하시기 바랍니다. #pragma comment (lib,"wsock32.lib") #pragma comment( linker, "/subsystem:windows" ) // WINAPI 관련 함수 정의는 아직 모르겠습니다. // 도우미 매크로 #define Display(s) SendDlgItemMessage(hwndDlg, IDC_EDIT1, EM_REPLACESEL, 0, (LPARAM) ((LPSTR)s) ) #define EnableOneButton(id, flag) EnableWindow( GetDlgItem(hwndDlg, id), flag) #define EnableButtons(flag) EnableOneButton(IDC_CHECKMAIL, flag) // 비동기 통지(Asynchronous notificatio)를 위한 메시지 정의 #define SM_GETHOST WM_USER+1 #define SM_ASYNC WM_USER+2 // 함수 프로토타입(Prototypes) BOOL CALLBACK MainDialogProc(HWND, UINT, WPARAM, LPARAM); void HandleGetHostMsg(HWND, WPARAM, LPARAM); void HandleAsyncMsg(HWND, WPARAM, LPARAM); void ProcessData(HWND, LPSTR, int); void ProcessData(HWND, LPSTR, int); void ProcessLine(HWND, LPSTR); void CloseSocket(SOCKET); // 전역(Global) 변수 부분 입니다. ///////////////////////////////// // POP3 호스트 이름 char gszServerName[255]; // 사용자 ID char gszUserID[80]; // 비밀번호 char gszPassword[80]; // WSAAsyncHostByName() 함수를 위한 핸들(Handle) - 자원을 접근(Access) 할수 있는 포인터(Pointer) HANDLE hndGetHost; // WSAAsyncGetHostByName() 함수를 위한 HostEnt 버퍼(Buffer) char bufHostEnt[MAXGETHOSTSTRUCT]; // 소켓 SOCKET socketPOP; // wsprintf() 함수를 위한 스크래치 버퍼(Scratch buffer - 임시 저장 공간) char gszTemp[255]; // 타이밍을 위한 변수들 // 이 예제에세만(For example only) 쓰입니다 -- 실제로는 필요하지 않습니다. DWORD gdwTicks; DWORD gdwElapsed; // recv() 데이터 버퍼 입니다. char gbufRecv[256]; // 응용프로그램 작동 상태(Application State)를 정의 합니다. int gnAppState; #define STATE_CONNECTING 1 // 접속중 #define STATE_USER 2 // 사용자 #define STATE_PASS 3 // 비밀번호 #define STATE_STAT 4 // 상태 #define STATE_QUIT 5 // 나가기 // WinMain() - 접근 점(Entry Point) int PASCAL WinMain ( HINSTANCE hinstCurrent, HINSTANCE hinstPrevious, LPSTR lpszCmdLine, int nCmdShow) { // 변수를 초기화 합니다. int nReturnCode; WSADATA wsaData; // 필요한 Winsock 의 Version 은 1.1 #define MAJOR_VERSION_REQUIRED 1 #define MINOR_VERSION_REQUIRED 1 WORD wVersionRequired = MAKEWORD(MAJOR_VERSION_REQUIRED, MINOR_VERSION_REQUIRED); // WinSock DLL 을 시작합니다. nReturnCode = WSAStartup(wVersionRequired, &wsaData); if (nReturnCode !=0) { MessageBox(NULL, "WSAStartUp()함수를 수행하는데 문제가 생겼습니다", "CheckMail - winapi_checkmail", MB_OK); return 1; } // 원하는 Version 의 Winsock 이 없다고 표시합니다. if (wsaData.wVersion != wVersionRequired) { // 필요한 Version 이 없습니다. MessageBox(NULL, "틀린 WinSock Version 입니다", "Checkmail - winapi_checkmail", MB_OK); WSACleanup(); // 생성된 WinSock Application 을 지웁니다. return 1; } // 기본 윈도우로 쓰인 대화 상자를 염니다. DialogBox( hinstCurrent, MAKEINTRESOURCE(IDD_DIALOG_MAIN), NULL, MainDialogProc); WSACleanup(); return 0; } // MainDialogProc() - 기본 윈도우 프로시저(Main Window Procedure) 입니다. BOOL CALLBACK MainDialogProc( HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam ) { BOOL fRet = FALSE; switch(msg) { case WM_INITDIALOG: // WSADATA 에서 오는 정보를 표시 합니다. Display("----시작(STARTUP)----\r\n"); Display("WSAStartup() 함수 성공\r\n\r\n"); break; case WM_COMMAND: switch(wParam) { // 메일 확인 버튼을 눌렀을때 case IDC_CHECKMAIL: // POP 정보를 받습니다. // 서버 이름, 사용자 ID 와 비밀번호 if(!GetDlgItemText( hwndDlg, IDC_SERVERNAME, gszServerName, sizeof(gszServerName) )) { MessageBox(hwndDlg, "서버 이름을 넣어 주세요", "POP 알림", MB_OK); break; } if (!GetDlgItemText(hwndDlg, IDC_USERID, gszUserID, sizeof(gszUserID)) ) { MessageBox(hwndDlg, "사용자 ID를 넣어 주세요", "POP 알림", MB_OK); break; } if (!GetDlgItemText( hwndDlg, IDC_PASSWORD, gszPassword, sizeof(gszPassword) )) { MessageBox(hwndDlg, "비밀번호를 넣어 주세요", "POP 알림", MB_OK); break; } // 비동기 Version 의 gethostbyname() 함수로 // 호스트 이름을 확인하는 요청을 합니다. // 작업이 완료 되면 SM_GETHOST 메시지가 표시 됩니다. Display("---- FINDHOST(호스트 찾기) ---- \r\n"); Display("WSAAsyncGetHostByName() 함수를 써서 " "서버를 찾습니다. \r\n"); // 이번 예제를 위해 WSAAsyncGetHostByName() 수행 시간을 잽니다. gdwTicks = GetTickCount(); hndGetHost = WSAAsyncGetHostByName( hwndDlg, SM_GETHOST, gszServerName, bufHostEnt, MAXGETHOSTSTRUCT); if(hndGetHost == 0) { MessageBox(hwndDlg, "시작하는데 오류가 생겼습니다" "WSAAsyncGetHostByName() 함수 수행", "CheckMail - winapi_checkmail", MB_OK); } else { EnableButtons(FALSE); gnAppState = 0; } fRet = TRUE; break; // 취소 퍼든을 누르면 기본 윈도우(Main window)에서 나가게 합니다. case IDC_CANCEL: if(gnAppState) CloseSocket(socketPOP); PostQuitMessage(0); fRet = TRUE; break; } break; // 비동기 gethostbyname() 함수의 되돌림(return) 값을 다룹니다. case SM_GETHOST: HandleGetHostMsg(hwndDlg, wParam, lParam); fRet = TRUE; break; // 비동기 메시지를 처리 합니다. case SM_ASYNC: HandleAsyncMsg(hwndDlg, wParam, lParam); fRet = TRUE; break; } return fRet; } // HandleGetHost() 함수 정의 // WSAASyncGetHostByName() 함수가 완료 되었을때 불리어 집니다. void HandleGetHostMsg( HWND hwndDlg, WPARAM wParam, LPARAM lParam) { SOCKADDR_IN saServ; // 인터넷을 위한 소켓 주소입니다. LPHOSTENT lpHostEnt; // 호스트 진입점(host entry) 을 가리키는 포인터 입니다. LPSERVENT lpServEnt; // 서버 진입점(sever entry) 을 가리키는 포인터 입니다. int nRet; // 되돌림 값(return code)입니다. Display("SM_GETHOST 메시지를 받았습니다.\r\n"); if( (HANDLE)wParam != hndGetHost) return; // 이번 예제에서 WSAGetHostByName() 함수의 // 수행 중인 시간을 표시 합니다. gdwElapsed = ( GetTickCount() - gdwTicks ); wsprintf( (LPSTR)gszTemp, (LPSTR) "WSAAsyncGetHostByName() 함수 수행중 %ld" " 밀리초(ms)가 완료 하는데 소요되었습니다.\r\n", gdwElapsed); Display(gszTemp); // 오류 코드를 확인합니다. nRet = WSAGETASYNCERROR(lParam); if (nRet) { wsprintf( (LPSTR)gszTemp, (LPSTR) "WSAAsyncGetHostByName() 함수 수행중 오류가 생겼습니다. : %d\r\n", nRet); Display(gszTemp); EnableButtons(FALSE); return; } // 서버를 찾았습니다(Server found OK). // bufHostEnt 에 서버의 정보를 담고 있습니다. Display("서버를 찾았습니다.\r\n\r\n"); Display("---- 호스트에 접속합니다.(Connect to host) ----\r\n\r\n"); // 소켓(Socket)을 생성합니다. Display("socket(AF_INET, SOCK_STREAM, 0); 을 호출 합니다.\r\n"); // socket(인터넷 주소, TCP 소켓 스트림, 플래그) socketPOP = socket(AF_INET, SOCK_STREAM, 0); if (socketPOP == INVALID_SOCKET) { Display("소켓을 생성하지 못했습니다.\r\n"); EnableButtons(TRUE); return; } // 비동기 통지(asynchronous notificatio)를 위해 소켓을 // non-blocking 으로 만들고 등록 합니다. Display("WSAASyncSelete() 함수를 호출 합니다\r\n"); if(WSAAsyncSelect(socketPOP, hwndDlg, SM_ASYNC, FD_CONNECT|FD_READ|FD_WRITE|FD_CLOSE)) { Display("WSAAsyncSelect() 함수 부르는 중에 문제가 생겼습니다.\r\n"); EnableButtons(TRUE); return; } // getservbyname() 함수로 포트 번호를 찾습니다. // 간단한 동기화 버전(synchronous version)에서 사용 되었기 때문에 // 거의 항상 빨리 이루어 집니다. // 이 예제를 위해 getgervbyname() 함수의 수행 시간을 확인합니다. gdwTicks = GetTickCount(); // getservbyname(서비스,프토토콜) lpServEnt = getservbyname("pop3", "tcp"); gdwElapsed = (GetTickCount() - gdwTicks); wsprintf( (LPSTR)gszTemp, (LPSTR) "getservbyname() 함수 수행으로 %ld" "밀리초(ms)가 소요 되었습니다.\r\n", gdwElapsed); Display(gszTemp); // 서비스 정보로 제공해주는 포트 번호 정보를 찾지 못했다면 if (lpServEnt == NULL) { // 잘 알려진 포트(well-known)로 채웁니다. saServ.sin_port = htons(110); Display("getservbyent() 함수가 성공하지 못해서 110번 포트를 사용합니다.\r\n"); } else { // 수행도우미(서번트 - servent)에서 받은 되돌림(return)값 saServ.sin_port = lpServEnt->s_port; } // 서버 주소 구조체를 완성합니다. saServ.sin_family = AF_INET; // 인터넷 주소 계열 lpHostEnt = (LPHOSTENT)bufHostEnt; saServ.sin_addr = *( (LPIN_ADDR)*lpHostEnt->h_addr_list); // 소켓에 접속합니다. Display("connect() 함수를 호출합니다.\r\n"); // connect(접속하여 연결될 소켓, 접속할 서버주소, 주소의 길이 nRet = connect(socketPOP, (LPSOCKADDR)&saServ, sizeof(SOCKADDR_IN) ); if(nRet == SOCKET_ERROR) { if(WSAGetLastError() != WSAEWOULDBLOCK) { Display("접속 중에 오류가 발생했습니다.\r\n"); EnableButtons(TRUE); return; } } gnAppState = STATE_CONNECTING; Display("\r\n ---- 처리 메시지(Process Messages) ---- \r\n"); } void HandleAsyncMsg( HWND hwndDlg, WPARAM wParam, LPARAM lParam) { int nBytesRead; switch(WSAGETSELECTEVENT(lParam)) { case FD_CONNECT: Display("FD_CONNECT 알림을 받았습니다.\r\n"); break; case FD_WRITE: Display("FD_WRITE 알림을 받았습니다.\r\n"); break; case FD_READ: Display("FD_READ 알림을 받았습니다.\r\n"); Display("recv() 함수를 호출합니다.\r\n"); // 대기 자료를 받습니다. nBytesRead = recv( socketPOP, // 소켓 gbufRecv, // 버퍼 sizeof(gbufRecv), // 최대로 받을 길이 0); // recv 함수의 플래그 값 // recv() 함수의 되돌림 값을 받습니다. if(nBytesRead == 0) { // 연결이 끝어 졌습니다. MessageBox( hwndDlg, "연결에 예상치 못하게 끊어 졌습니다.", "recv() 함수 오류", MB_OK); break; } if(nBytesRead == SOCKET_ERROR) { wsprintf( (LPSTR)gszTemp, "recv() 오류 : %d", nBytesRead); MessageBox( hwndDlg, gszTemp, "recv() 함후 오류", MB_OK); break; } // 버퍼에 종단 문자(terminate)를 넣습니다. gbufRecv[nBytesRead] ='\0'; // 해석 하기 위해서 넘겨 줍니다. ProcessData(hwndDlg, gbufRecv, nBytesRead); break; case FD_CLOSE: Display("FD_CLOSE 통보를 받았습니다.\r\n"); EnableButtons(TRUE); break; } } // ProcessData() 함수 // 들어오는 문서를 받아서 줄을 끊어서 저장합니다. void ProcessData(HWND hwndDlg, LPSTR lpBuf, int nBytesRead) { static char szResponse[512]; static int nLen = 0; char *cp; // 새 자료를 저장할 공간이 있을까요? if ( (nLen + nBytesRead) > sizeof(szResponse) ) { // 아니요~ , 공간이 없습니다. Display("!!!! 버퍼가 가득 찼습니다. 자료가 잘려(절삭)집니다.\r\n"); nLen = 0; szResponse[0] = '\0'; return; } // 공간 있어요~ // 버퍼에 새 줄을 추가 합니다. strcat(szResponse, lpBuf); nLen = strlen(szResponse); // 저체 줄을 작업을 합니다. while(1) { // 버퍼에 새줄 값을 담고 있나요? cp = strchr(szResponse, '\n'); if(cp == NULL) // 못 찾았습니다. break; // CR/LF (Carrage Return / Line Feed - 행갈이/줄끝) 쌍을 가지고 있습니다. // LF 을 NULL 값을 바꿉니다. *cp = '\0'; // PrcessLine() 함수에 넣습니다. ProcessLine(hwndDlg, szResponse); // 버퍼에 남겨진 데이터를 앞으로 당겨 옵니다. cp++; if(strlen(cp)) memmove(szResponse, cp, strlen(cp)+1); else szResponse[0] = '\0'; } } // ProcessLine() // 서버(Server)에게 명령줄에 대한 응답을 받고 다음에 뭘 할지 결정합니다. void ProcessLine(HWND hwndDlg, LPSTR lpStr) { int nRet; long lCount; long lSize; Display("서버로 부터의 응답입니다 : \r\n"); Display(lpStr); Display("\r\n"); // 오류에 대한 응답인지 확인합니다. if(lpStr[0] == '-') { Display("부정적인 응답입니다.(Negative response) :"); switch(gnAppState) { case STATE_CONNECTING: Display("접속이 거부 당했습니다.\r\n"); break; case STATE_USER: Display("알수 없는 사용자ID입니다.\r\n"); break; case STATE_PASS: Display("틀린 비밀번호입니다.\r\n"); break; case STATE_STAT: Display("STAT 명령이 지원하지 않습니다.\r\n"); break; case STATE_QUIT: Display("QUIT 명령이 지원하지 않습니다.\r\n"); break; } Display("Sending QUIT\r\n"); wsprintf(gszTemp, "QUIT\r\n"); Display(gszTemp); nRet = send( socketPOP, // 소켓 gszTemp, // 데이터 버퍼 strlen(gszTemp), // 데이터의 길이 0); // 플래그 gnAppState = STATE_QUIT; return; } // 긍정적인 응답을 받았습니다. switch(gnAppState) { case STATE_CONNECTING: // 로그인(LOGING) 부분에서 사용자(USER) 정보 부분을 전송하고 // 프로그램 상태를 수정합니다. Display("AppState = CONNECTING, " "senging USER\r\n"); wsprintf(gszTemp, "USER %s\r\n", gszUserID); nRet = send( socketPOP, // 소켓 gszTemp, // 데이터 버퍼 strlen(gszTemp), // 데이터의 길이 0); gnAppState = STATE_USER; break; case STATE_USER: // 로그인 부분에서 패스워드(PASSWORD) 부분을 전송하고 // 프로그램 상태를 수정합니다. Display("AppState = SEND, 패스워드를 전송합니다.\r\n"); wsprintf(gszTemp, "PASS %s\r\n", gszPassword); nRet = send( socketPOP, // 소켓 gszTemp, // 데이터 버퍼 strlen(gszTemp), // 데이터의 길이 0); gnAppState = STATE_PASS; break; case STATE_PASS: // STAT 명령어를 보내고, // 프로그램 상태를 수정합니다. Display("AppState = PASS, STAT 를 보냅니다.\r\n"); wsprintf(gszTemp, "STAT\r\n"); nRet = send( socketPOP, // 소켓 gszTemp, // 데이터 버퍼 strlen(gszTemp), // 데이터의 길이 0); gnAppState = STATE_STAT; break; case STATE_STAT: // STAT 응답을 읽습니다. // 결과를 출력합니다. Display("AppState = STAT, 응답을 읽습니다.\r\n"); sscanf(lpStr, "%s %ld %ld", gszTemp, &lCount, &lSize); Display("---- 결과(Result)----\r\n"); wsprintf(gszTemp, "%ld 메시지 %ld 바이트\r\n", lCount, lSize); Display(gszTemp); // QUIT 명령을 전송합니다. // 프로그램의 상태를 수정합니다. Display("QUIT 명령를 전송합니다.\r\n"); wsprintf(gszTemp, "QUIT\r\n"); nRet = send( socketPOP, // 소켓 gszTemp, // 데이터 버퍼 strlen(gszTemp), // 데이터의 길이 0); gnAppState = STATE_QUIT; break; case STATE_QUIT: Display("호스트 QUIT OK\r\n"); Display("소켓을 닫습니다.\r\n"); CloseSocket(socketPOP); } } // CloseSocket() // 프로토콜 스택에 있는 버퍼를 비우고 소켓을 닫습니다. void CloseSocket(SOCKET sock) { int nRet; char szBuf[255]; // 원격지에게 우리는 자료를 보내지 않을 거라는 정보를 전송합니다. shutdown(sock, 1); while(1) { // 자료 받기를 시도 합니다. // 이는 아직 프로토콜에 남아 있을지 모르는 자료를 지우기 위해서 입니다. nRet = recv( sock, // 소켓 szBuf, // 데이터 버퍼 sizeof(szBuf), // 버퍼의 길이 0); // revc 플래그 // 연결이 끝났거나 어떤 다른 문제라도 생기면 받는걸 중단합니다. if (nRet == 0 || nRet == SOCKET_ERROR) break; } // 상대편에게 우리는 자료를 더 받지도 않을 거라고 알리니다. shutdown(sock,2); // 소켓을 닫습니다. closesocket(sock); }