. HTTP 열기

로컬 네트워크 지원은 공짜로 지원 받을 수 있으므로 코딩을 할 필요가 없고 그럼 이제 좀 더 넓은 세상으로 나가 보도록 하자. 외부 네트워크를 통해 파일을 주고 받을 수 있는 프로토콜에는 대표적으로 FTP가 있고 HTTP도 파일을 읽을 수는 있다. 이 두 프로토콜로 원격지의 파일을 편집하는 기능을 작성해보도록 하자.

FTP 같은 네트워크 프로토콜을 제대로 프로그래밍하려면 소켓 프로그래밍을 해야 한다. 그러나 이는 너무 어렵고 귀찮은 일이라 여기서는 윈도우즈가 제공하는 WinInet 고수준 라이브러리를 사용하여 네트워크 입출력을 하기로 한다. WinInet에 관한 사항은 API 정복 43장에 있으므로 참고하기 바란다. HTTP로 파일을 읽으려면 먼저 대상 파일의 정확한 위치를 입력받아야 한다. 단순한 문자열 입력 대화상자로 URL을 입력받기만 하면 된다. 이 대화상자는 리소스에 다음과 같이 이미 만들어져 있다.

다운로드만 받을 수 있으며 대소문자 구분이 정확해야 한다는 안내 메시지를 대화상자에 표시해놓았다. 웹 자체는 대소문자 구분을 하지 않지만 유닉스(리눅스 포함) 계열의 파일 시스템은 대소문자를 구분하기 때문에 실제 폴더, 파일명과 정확히 같게 적어야 한다. HTTP 프로토콜은 폴더 열거 기능을 제공하지 않기 때문에(지원하는 웹 서버도 있다) 찾아보기 기능은 사용할 수 없으며 사용자가 URL을 직접 기억해서 입력해야 한다. 이 대화상자의 프로시저를 다음과 같이 작성한다.

 

BOOL CALLBACK DGHttpProc(HWND hDlg,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

     static TCHAR *Path;

 

     switch(iMessage)

     {

     case WM_INITDIALOG:

          MoveToParentCenter(hDlg);

          SetDlgItemText(hDlg,IDC_HTTPFILE,"http://");

          MySetImeMode(GetDlgItem(hDlg,IDC_HTTPFILE),FALSE);

          Path=(TCHAR *)lParam;

          return TRUE;

     case WM_COMMAND:

          switch (LOWORD(wParam))

          {

          case IDOK:

              GetDlgItemText(hDlg,IDC_HTTPFILE,Path,MAX_PATH);

              EndDialog(hDlg,IDOK);

              return TRUE;

          case IDCANCEL:

              EndDialog(hDlg,IDCANCEL);

              return TRUE;

          }

          return FALSE;

     }

     return FALSE;

}

 

URL을 입력받아 lParam으로 전달된 버퍼에 넣어 리턴하는 기능밖에 없다. 파일열기 공통 대화상자가 파일의 이름을 얻어주기만 하는 것과 마찬가지다. OnCommand에서는 이 대화상자를 통해 URL을 입력받고 파일을 연다.

 

void OnCommand(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

     ....

     case IDM_FILE_HTTPOPEN:

          if (DialogBoxParam(g_hInst, MAKEINTRESOURCE(IDD_HTTP),

              g_hFrameWnd, DGHttpProc,(LPARAM)Path)==IDOK) {

              if (lstrlen(Path)) {

                   OpenFromFile(Path);

              }

          }

          break;

 

URL을 입력받으면 OpenFromFile 함수로 URL을 넘겨 준다. 네트워크에서 파일을 여는 것과 로컬에서 파일을 여는 것을 동일한 함수에서 수행해야 한다. 그렇지 않으면 잠시 후에 작성할 프로젝트 기능이 복잡해지며 이미 만들어져 있는 MRU 기능도 손상된다. 파일을 입출력하는 방법에 일관성이 있어야 관련 루틴들이 파일의 종류에 상관없이 파일을 관리할 수 있다.

HTTP 파일을 읽는 작업은 OpenFromFile과 그의 졸병 함수인 OpenFileToChild 함수가 직접 처리해야 한다. 다음과 같이 수정하되 코드 추가뿐만 아니라 전체적인 순서가 바뀌어야 한다.

 

BOOL OpenFileToChild(HWND hChild, TCHAR *Path)

{

     SInfo *pSi;

     HANDLE hFile;

     DWORD dwRead,dwSize;

     TCHAR *TextBuf;

 

     pSi=(SInfo *)GetWindowLong(hChild,0);

 

     SelectParser(pSi->Ae,Path);

     pSi->Ae.InitDoc();

 

    if (strnicmp(Path,"http",4) == 0) {

        dwSize=DgHttpDown(Path,TextBuf);

        if (dwSize==-1) {

           return FALSE;

        }

    } else {

          hFile=CreateFile(Path,GENERIC_READ,0,NULL,

              OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

          if (hFile==INVALID_HANDLE_VALUE) {

              return FALSE;

          }

          dwSize=GetFileSize(hFile,NULL);

          if (dwSize > 30*1048576) {

              if (MessageBox(g_hFrameWnd, "이 파일의 크기는 30M가 넘습니다. 정말로 여시겠습니까?",

                   "질문",MB_YESNO)==IDNO) {

                   CloseHandle(hFile);

                   return FALSE;

              }

          }

          TextBuf=(TCHAR *)malloc(dwSize+2);

          ReadFile(hFile,TextBuf,dwSize,&dwRead,NULL);

          TextBuf[dwRead]=0;

          TextBuf[dwRead+1]=0;

          CloseHandle(hFile);

    }

     pSi->Ae.SetText(TextBuf,dwSize);  

     free(TextBuf);

 

     pSi->Ae.SetModified(FALSE);

     lstrcpy(pSi->NowFile,Path);

     SetWindowText(hChild,Path);

 

     return TRUE;

}

 

인수로 전달된 Path의 첫 4글자가 http로 시작되면 이 파일은 HTTP 서버에 있으므로 다운로드받아서 열도록 했다. 파일 자체를 다운로드받는 것이 아니라 파일의 내용을 버퍼로 읽어들이도록 했으며 다운로드된 텍스트를 ApiEdit에 전달하였다. 다음 함수는 HTTP 서버에서 파일을 다운로드받아 버퍼에 복사한다. 이 함수는 다운로드 과정을 보여주고 중간에 취소할 수 있도록 하는데 이 기능은 별도의 대화상자가 처리하므로 잠시 후 따로 알아 보자.

 

int DgHttpDown(TCHAR *URL, TCHAR *&Text)

{

     HINTERNET hInternet, hURL;

     int len,extra;

     TCHAR *p;

     DWORD Size;

     DWORD dwRead, TotalRead;

     int toff;

     BOOL Result=FALSE;

     MSG msg;

     TCHAR Mes[400];

     HWND hDlgDown;

 

     bContDown=TRUE;

     hDlgDown=CreateDialog(g_hInst, MAKEINTRESOURCE(IDD_DOWNLOAD),

          g_hFrameWnd, (DLGPROC)DGDownProc);

     SetDlgItemText(hDlgDown,IDC_STDOWN1,"인터넷 접속중...");

     SetWindowText(hDlgDown,"HTTP 다운로드");

     ShowWindow(hDlgDown,SW_SHOW);

     UpdateWindow(hDlgDown);

     EnableWindow(g_hFrameWnd, FALSE);

 

     hInternet=InternetOpen("Dangeun", INTERNET_OPEN_TYPE_PRECONFIG,

          NULL, NULL, 0);

     if (hInternet == NULL) {

          MessageBox(g_hFrameWnd, "인터넷에 접속할 수 없습니다","알림",MB_OK);

          goto NetFail;

     }

 

     hURL=InternetOpenUrl(hInternet,URL,NULL,0,INTERNET_FLAG_RELOAD,0);

     if (hURL==NULL) {

          MessageBox(g_hFrameWnd, "URL을 찾을 수 없습니다","알림",MB_OK);

          goto EndDown;

     }

     wsprintf(Mes,"위치 : %s",URL);

     SetDlgItemText(hDlgDown,IDC_STDOWN1,Mes);

 

     len=10000;

     extra=10000;

     Text=(TCHAR *)malloc(len);

     memset(Text,0,len);

     p=Text;

     TotalRead=0;

 

     do {

          InternetQueryDataAvailable(hURL,&Size,0,0);

          if (extra < int(Size+2)) {

              len+=(Size+2+10000);

              extra+=(Size+2+10000);

              toff=p-Text;

              Text=(TCHAR *)realloc(Text,len);

              memset(Text+len-(Size+2+10000),0,(Size+2+10000));

              p=Text+toff;

          }

          Result=InternetReadFile(hURL,p,Size,&dwRead);

          if (Result==FALSE) {

              MessageBox(g_hFrameWnd, "HTTP 서버의 파일을 읽을 수 없습니다","알림",MB_OK);

              break;

          }

          if (bContDown==FALSE) {

              break;

          }

          p+=dwRead;

          TotalRead+=dwRead;

          extra-=dwRead;

          SendMessage(hDlgDown,WM_USER+1,(WPARAM)TotalRead,(LPARAM)-1);

          while (PeekMessage(&msg, NULL,0,0,PM_REMOVE)) {

              if (!IsDialogMessage(hDlgDown, &msg)) {

                   TranslateMessage(&msg);

                   DispatchMessage(&msg);

              }

          }

     } while (dwRead != 0);

 

     InternetCloseHandle(hURL);

EndDown:

     InternetCloseHandle(hInternet);

NetFail:

     EnableWindow(g_hFrameWnd, TRUE);

     DestroyWindow(hDlgDown);

     if (Result == TRUE && bContDown == TRUE) {

          return TotalRead;

     } else {

          return -1;

     }

}

 

원격지에 있는 파일을 다운로드받아야 하므로 인터넷에 먼저 접속 한다. 이 기능을 사용하려면 컴퓨터가 네트워크에 접속되어 있어야 하며 접속에 실패하면 파일을 다운로드받을 수 없다. 에러 발생시 에러 코드를 리턴하는 것이 아니라 이 함수가 직접 에러 메시지를 출력하도록 했는데 OpenFromFile이나 OpenFileToChild 함수들이 네트워크 에러까지 처리하기는 힘들기 때문이다.

인터넷에 무사히 접속했으면 파일을 열고 파일을 읽기 위한 버퍼를 할당한다. 원격지에 있는 파일의 크기를 미리 알 수 없기 때문에 일단 10KB를 할당해놓고 읽는 중에 서버가 요구하는 만큼 재할당하도록 했다. 호출원에서 미리 메모리를 할당할 수 없으므로 이 함수가 파일의 크기를 실시간으로 조사하여 필요한 만큼 메모리를 할당하여 리턴하도록 되어 있다. 그래서 Text인수는 포인터의 레퍼런스로 전달받는다.

파일을 완전히 다운로드받았으면 인터넷 접속을 해제하고 리턴한다. HTTP 프로토콜은 원래 연결을 계속 유지하지 않는 특성이 있으므로 다음 접속을 위해 계속 연결해놓을 필요는 없다. 리턴값은 실제로 읽은 파일 크기이다. 파일을 다운로드받는 작업을 이 함수와 OpenFileToChild가 하므로 OpenFromFile 함수는 거의 수정할 필요가 없으며 약간만 손질하면 된다.

 

BOOL OpenFromFile(TCHAR *Path,BOOL bReadOnly/*=FALSE*/,BOOL bBrowse/*=FALSE*/)

{

     ....

     if (OpenFileToChild(hChild,Path)==FALSE) {

          if (bNew) {

              SendMessage(g_hMDIClient,WM_MDIDESTROY,(WPARAM)hChild,0);

          }

        if (strnicmp(Path,"http",4) != 0 && strnicmp(Path,"ftp",3) != 0) {

              wsprintf(Mes,"%s 파일을 열 수 없습니다. "

                   "다른 프로그램이 사용중인지 확인하십시오.",Path);

              MessageBox(g_hFrameWnd, Mes,"알림",MB_OK);

        }

          return FALSE;

     }

    if (strnicmp(Path,"http",4)!=0 && strnicmp(Path,"ftp",3)!=0) {

          if (bReadOnly || (GetFileAttributes(Path) & FILE_ATTRIBUTE_READONLY)) {

              pSi->Ae.SetReadOnly(TRUE);

          }

    }

 

네트워크 입출력함수들이 직접 에러 메시지를 출력하므로 OpenFromFile 함수는 설사 에러가 발생했더라도 메시지를 출력하지 않도록 했다. FTP, HTTP 프로토콜로 읽은 파일은 GetFileAttributes 함수가 속성을 제대로 조사할 수 없으며 HTTP 파일은 항상 읽기전용으로 조사된다. 그렇다고 해서 파일 편집을 거부할 필요는 없으므로 네트워크 파일의 경우는 읽기전용 속성을 점검하지 않도록 하였다.

HTTP 열기 대화상자에서 URL만 제대로 입력하면 원격지의 파일을 읽어올 수 있으며 이 파일도 일반 파일처럼 MRU에 등록된다. MRU에서 HTTP 파일을 다시 선택하면 인터넷에 접속하여 파일을 읽어올 것이다. OpenFromFile 함수가 받아들이는 인수 Path가 로컬 경로에서 URL로 의미가 확장되었기 때문에 내 컴퓨터에 있지 않은 파일에 대해서도 MRU가 잘 동작하는 것이다. 다만 그래도 네트워크에 있는 파일은 로컬 파일과는 다른 점이 있으므로 다음 코드만 약간 손질하면 된다.

 

void OnCommand(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

     ....

     if (LOWORD(wParam) >= Mru.MenuID && LOWORD(wParam) < Mru.MenuID+Mru.MaxMru) {

          idx=LOWORD(wParam)-Mru.MenuID;

          Mru.GetFilePath(idx,Path);

        if (strnicmp(Path,"http",4) != 0 && strnicmp(Path,"ftp",3) != 0) {

              if (_access(Path,0) != 0) {

                   wsprintf(Mes,"%s 파일을 찾을 수 없습니다. 최근 파일 목록에서 "

                        "이 파일을 제거합니다.",Path);

                   MessageBox(g_hFrameWnd, Mes,"알림",MB_OK);

                   Mru.RemoveMRU(idx);

                   return;

              }

        }

          OpenFromFile(Path);

          return;

     }

 

존재하지 않는 파일을 MRU 목록에서 삭제하는 기능이 있는데 HTTP, FTP로 읽은 파일은 _access 함수로 존재 여부를 알 수 없으므로 이 테스트를 하지 않도록 한다. MRU 관리를 위해 인터넷에 먼저 접속을 해 볼 수도 있지만 이렇게 되면 이중으로 접속을 하는 셈이 되어 바람직하지 않다. 네트워크 파일에 대해서는 굳이 존재 여부를 점검하지 않더라도 별 문제는 없으며 설사 당장 파일을 읽을 수 없다 하더라도 일시적인 문제일 수도 있기 때문에 MRU의 목록까지 삭제할 필요는 없다.

HTTP 프로토콜은 다운로드만 지원하며 업로드는 지원하지 않는다. 그래서 클라이언트 입장에서는 읽기전용의 프로토콜이라 할 수 있다. HTTP 헤더에 첨부파일을 붙여서 보내는 기능이 있기는 하지만 이는 파일 업로드와는 다른 의미이다. 만약 HTTP로 다운로드받은 파일에 대해 저장 명령을 내리면 원래 읽었던 곳으로 업로드할 수 없으므로 에러 메시지를 확실하게 보여주어야 한다. 에러 메시지라기 보다는 일종의 안내 메시지라 할 수 있다.

 

BOOL SaveToFile(HWND hChild,TCHAR *Path)

{

     ....

     if (strnicmp(Path,"http",4)==0) {

          MessageBox(hChild, "HTTP 프로토콜은 업로드를 지원하지 않으므로 이 이름으로는 "

              "저장할 수 없습니다. 새 이름을 하드 디스크에 저장하십시오","알림",MB_OK);

          return FALSE;

     }

 

그렇다면 당근은 저장하지도 못하는 HTTP 파일에 대해 왜 읽기전용 플래그를 설정하지 않고 편집을 허용할까? 그 이유는 업로드는 할 수 없지만 새 이름으로 바꾸어 로컬 경로에 저장하는 것은 가능하기 때문이다. HTTP로 업로드가 안된다는 것은 대부분은 사용자들이 알고 있으므로 이 기능은 주로 파일 확인용으로 사용될 것이다. 만약 HTTP 파일을 사용자가 수정했다면 이는 새 이름으로 저장하려는 분명한 목적을 가지고 있다고 판단하였다.

HTTP는 업로드를 지원하지 않는다는 측면에서 사실 별로 실용성이 없으며 그래서 대부분의 편집기는 HTTP 파일열기를 지원하지 않는다. 당근이 이 기능을 지원하는 목적은 그냥 읽기전용으로 웹 서버에 있는 내용을 확인이나 하라는 뜻이며 이후 작성될 프로젝트에서 조금이라도 관련이 있는 파일을 한 묶음으로 관리할 수 있도록 하기 위해서이다. 원격지의 파일을 직접 편집하려면 HTTP보다는 바로 다음에 작성할 FTP가 훨씬 더 실용적이다. FTP 프로토콜은 네트워크 속도만 충분하다면 거의 로컬과 차이가 없을 정도로 편리하다.