현상
HTTP Post 메소드를 사용하는 경우 서버 어플리케이션은 ASP 의 BinaryRead 에 해당하는 ISAPI 함수인 ReadClient 를 이용하여 클라이언트로부터 HTTP 본문에 있는 데이터를 읽는다. 그런데, POST 의 사이즈가 큰 경우 클라이언트 사용자가 upload 를 취소하게 되면 ReadClient 혹은 BinaryRead 를 호출한 서버 프로그램이 blocking 되는 경우가 있다.
원인
ReadClient 기술을 읽어보면 다음과 같은 문장이 있다.
If the size specified in lpdwSize is greater than the number of bytes available to be read, it is possible ReadClient will block until the data arrives.ReadClient 는 일단 호출이 되고나면 한바이트의 데이터라도 도착하지 않으면 block 된다. 예외의 경우에 대한 다음 문장이 역시 기술되어 있다.
If the socket on which the server is listening to the client is closed, ReadClient will return TRUE, but with zero bytes read.소켓이 끊기는 경우에 한하여 Readclient 는 반환된다. 문제는 IE 와 IIS 가 사용하고 있는 HTTP 프로토콜에 Keep Alive 기능이 추가된데 있다. Keep Alive 는 연속되는 HTTP Request/Response 가 동일한 socket 연결을 계속 사용한다는 것이다. 이로 인해 client 가 POST 를 취소해도 socket 은 끊지 않는 경우가 발생한다.
해결 방법
이 문제는 ASP BinaryRead 의 메모리 누수 hotfix 에 의해 문제가 해결되지 않습니다. 하지만 다음과 같이 코딩을 하여 문제를 해결할 수 있을 것입니다. 주의할 점은 ASP Request 오브젝트의 BinaryRead 를 사용하는 경우 IIS 는 일정량의 데이터가 도착하기 전에는 데이터를 서버 어플리케이션에 전달하지 않는다는 점이다. 따라서 클라이언트가 이에 미치지 않는 소량의 데이터를 upload 하는 중간에 작업을 취소한 경우 서버가 역시 block 될 수 있다는 것이다. 이는 일정량의 데이터를 upload 하기 전에는 upload 를 취소하지 못하도록 하여 문제를 막을 수 있다.
client 와 서버가 모두 고객이 커스터마이징한 코드가 적용가능한 경우, HTTP 상에 다음과 같은 프로토콜을 적용하여 문제를 피해갈 수 있다.
client 와 서버가 모두 고객이 커스터마이징한 코드가 적용가능한 경우, HTTP 상에 다음과 같은 프로토콜을 적용하여 문제를 피해갈 수 있다.
전송 순서:
Client Server ------------------------------------------------------------------- HTTP Request header ReadClient 4 byte size indicator of HTTP Body part 1/n ReadClient if size == 0 then break HTTP Body part 1/n ReadClient 4 byte size indicator of HTTP Body part 2/n ReadClient if size == 0 then break HTTP Body part 2/n ReadClient 4 byte size indicator of HTTP Body part 3/n ReadClient if size == 0 then break HTTP Body part 3/n ReadClient 4 byte size indicator of HTTP Body part 4/n ReadClient if size == 0 then break HTTP Body part 4/n ReadClient ... 4 byte size indicator of HTTP Body part 5/n ReadClient if size == 0 then break HTTP Body part 5/n ReadClient
서버 샘플코드
STDMETHODIMP CRead::BinaryRead() { // TODO: Add your implementation code here VARIANT varCount; VARIANT varData; HRESULT hr; while(1) { varCount.vt = VT_I4; varCount.lVal = 4; hr = m_piRequest->BinaryRead(&varCount, &varData); if ( FAILED(hr) ) { #ifdef _DEBUG FILE *fp; if ( (fp = fopen(LOG_FILE, "a")) != NULL ) { fprintf(fp, "BinaryRead:Error for BinaryRead, hr = %x", hr); fflush(fp); fclose(fp); } #endif return E_FAIL; } if ( varData.vt != (VT_ARRAY | VT_UI1) ) { #ifdef _DEBUG FILE *fp; if ( (fp = fopen(LOG_FILE, "a")) != NULL ) { fprintf(fp, "BinaryRead:Error varData.vt != VT_ARRAY | VT_UI1"); fflush(fp); fclose(fp); } #endif return E_FAIL; } SAFEARRAY *pSafeArray; pSafeArray = varData.parray; BYTE *byteArray; hr = SafeArrayAccessData(pSafeArray, (void**)&byteArray); if ( FAILED(hr) ) { #ifdef _DEBUG FILE *fp; if ( (fp = fopen(LOG_FILE, "a")) != NULL ) { fprintf(fp, "BinaryRead:Error for SafeArrayAccessData, hr=%x\n", hr); fflush(fp); fclose(fp); } #endif SafeArrayUnaccessData(pSafeArray); return E_FAIL; } // cElements = pSafeArray->rgsabound->cElements; char size[5]; size[0] = byteArray[0]; size[1] = byteArray[1]; size[2] = byteArray[2]; size[3] = byteArray[3]; size[4] = 0; int nSize = atoi(size); #ifdef _DEBUG FILE *fp; if ( (fp = fopen(LOG_FILE, "a")) != NULL ) { fprintf(fp, "BinaryRead:nSize = %d\n", nSize); fflush(fp); fclose(fp); } #endif SafeArrayUnaccessData(pSafeArray); if ( nSize == 0 ) { #ifdef _DEBUG FILE *fp; if ( (fp = fopen(LOG_FILE, "a")) != NULL ) { fprintf(fp, "BinaryRead:nSize is 0, so break this loop\n"); fflush(fp); fclose(fp); } #endif break; } varCount.vt = VT_I4; varCount.lVal = nSize; hr = m_piRequest->BinaryRead(&varCount, &varData); if ( FAILED(hr) ) { #ifdef _DEBUG FILE *fp; if ( (fp = fopen(LOG_FILE, "a")) != NULL ) { fprintf(fp, "BinaryRead:Failed for BinaryRead, hr = %x\n", hr); fflush(fp); fclose(fp); } #endif } } return S_OK; }
클라이언트 샘플코드
클라이언트는 WinInet 을 사용하므로 wininet.lib 를 링크한다.#include <Windows.h> #include <WinINet.h> #include <stdio.h> BOOL UseHttpSendReqEx(HINTERNET hRequest, DWORD dwPostSize); #define BUFFSIZE 500 void main( int argc, char **argv ) { DWORD dwPostSize; if (argc < 4) { printf("Usage: Bigpost <Size> <Server> <Path>\n"); printf("<Size> is the number of KB to POST\n"); printf("<Server> is the server to POST to\n"); printf("<Path> is the virtual path to POST to\n"); exit(0); } if ( ((dwPostSize = strtoul(argv[1],NULL,10)) == 0) || (dwPostSize >= 2047999) ) { printf("%s is invalid size. Valid sizes are from 1 to 2047999\n", argv[1]); exit(0); } printf( "Test of POSTing %luKB with WinInet\n", dwPostSize); dwPostSize *= 1024; // Convert KB to bytes HINTERNET hSession = InternetOpen( "HttpSendRequestEx", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if(!hSession) { printf("Failed to open session\n"); exit(0); } ////ORIGINAL SOURCE CODES BEGIN//////////////////////////////////////////////////////// HINTERNET hConnect = InternetConnect(hSession, argv[2], INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP,NULL, NULL); if (!hConnect) printf( "Failed to connect\n" ); else { HINTERNET hRequest = HttpOpenRequest(hConnect, "POST", argv[3], NULL, NULL, NULL, INTERNET_FLAG_NO_CACHE_WRITE, 0); if (!hRequest) printf( "Failed to open request handle\n" ); else { if(UseHttpSendReqEx(hRequest, dwPostSize)) { char pcBuffer[BUFFSIZE]; DWORD dwBytesRead; printf("\nThe following was returned by the server:\n"); do { dwBytesRead=0; if(InternetReadFile(hRequest, pcBuffer, BUFFSIZE-1, &dwBytesRead)) { pcBuffer[dwBytesRead]=0x00; // Null-terminate buffer printf("%s", pcBuffer); } else printf("\nInternetReadFile failed"); }while(dwBytesRead>0); printf("\n"); } if (!InternetCloseHandle(hRequest)) printf( "Failed to close Request handle\n" ); else printf( "Succeeded to close Request handle\n" ); } /* if(!InternetCloseHandle(hConnect)) printf("Failed to close Connect handle\n"); else printf("Succeded to close Connect handle\n"); */ } printf( "\nFirst sending Finished....\n" ); ////ORIGINAL SOURCE CODES ENDS//////////////////////////////////////////////////////// /* if( InternetCloseHandle( hSession ) == FALSE ) printf( "Failed to close Session handle\n" ); hSession = InternetOpen( "HttpSendRequestEx", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if(!hSession) { printf("Failed to open session\n"); exit(0); } */ ////DUPLICATED SOURCE CODES BEGIN//////////////////////////////////////////////////////// //HINTERNET /* hConnect = InternetConnect(hSession, argv[2], INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP,NULL, NULL); if (!hConnect) printf( "Failed to connect\n" ); else */ { HINTERNET hRequest = HttpOpenRequest(hConnect, "POST", argv[3], NULL, NULL, NULL, INTERNET_FLAG_NO_CACHE_WRITE, 0); if (!hRequest) printf( "Failed to open request handle\n" ); else { if(UseHttpSendReqEx(hRequest, dwPostSize)) { char pcBuffer[BUFFSIZE]; DWORD dwBytesRead; printf("\nThe following was returned by the server:\n"); do { dwBytesRead=0; if(InternetReadFile(hRequest, pcBuffer, BUFFSIZE-1, &dwBytesRead)) { pcBuffer[dwBytesRead]=0x00; // Null-terminate buffer printf("%s", pcBuffer); } else printf("\nInternetReadFile failed"); }while(dwBytesRead>0); printf("\n"); } if (!InternetCloseHandle(hRequest)) printf( "Failed to close Request handle\n" ); else printf( "Succeded to close Request handle\n" ); } if(!InternetCloseHandle(hConnect)) printf("Failed to close Connect handle\n"); else printf("Succeeded to close Connect handle\n"); } printf( "\nSecond sending Finished....\n" ); ////DUPLICATED SOURCE CODES ENDS//////////////////////////////////////////////////////// if( InternetCloseHandle( hSession ) == FALSE ) printf( "Failed to close Session handle\n" ); printf( "\nFinished.\n" ); } BOOL UseHttpSendReqEx(HINTERNET hRequest, DWORD dwPostDataSize) { INTERNET_BUFFERS BufferIn; DWORD dwBytesWritten; int n; BYTE pBuffer[1024]; BOOL bRet; DWORD dwPostSize; dwPostSize = dwPostDataSize + (dwPostDataSize/1024)*4; dwPostSize += 4; // for the last data length = 0 BufferIn.dwStructSize = sizeof( INTERNET_BUFFERS ); // Must be set or error will occur BufferIn.Next = NULL; BufferIn.lpcszHeader = NULL; BufferIn.dwHeadersLength = 0; BufferIn.dwHeadersTotal = 0; BufferIn.lpvBuffer = NULL; BufferIn.dwBufferLength = 0; BufferIn.dwBufferTotal = dwPostSize; // This is the only member used other than dwStructSize BufferIn.dwOffsetLow = 0; BufferIn.dwOffsetHigh = 0; if(!HttpSendRequestEx( hRequest, &BufferIn, NULL, 0, 0)) { printf( "Error on HttpSendRequestEx %d\n",GetLastError() ); return FALSE; } FillMemory(pBuffer, 1024, 'D'); // Fill buffer with data bRet=TRUE; //Changed by YOO char pSize[4]; pSize[0] = '1'; pSize[1] = '0'; pSize[2] = '2'; pSize[3] = '4'; for(n=1; n<=(int)10 && bRet; n++)//(int)dwPostSize/1024 && bRet; n++) { if(!InternetWriteFile( hRequest, &pSize, 4, &dwBytesWritten)) printf("\nerror for writeing size"); if(bRet=InternetWriteFile( hRequest, pBuffer, 1024, &dwBytesWritten)) printf( "\r%d bytes sent.", n*1024); } pSize[0] = '0'; pSize[1] = '0'; pSize[2] = '0'; pSize[3] = '0'; if (InternetWriteFile( hRequest, &pSize, 4, &dwBytesWritten)) printf("\nwrote nSize = 0"); printf("\nafter InternetWriteFile loop, bRet = %d", bRet); if(!bRet) { printf( "\nError on InternetWriteFile %lu\n",GetLastError() ); return FALSE; } printf("\nWe gonna do HttpEndRequest"); //Changed by YOO // if(!HttpEndRequest(hRequest, NULL, 0, 0)) // if(!HttpEndRequest(hRequest, NULL, HSR_SYNC, 0)) // { // printf( "Error on HttpEndRequest %lu \n", GetLastError()); // return FALSE; // } printf("\nOK I'll return TRUE"); return TRUE; }