Microsoft MVP성태의 닷넷 이야기
.NET Framework: 2028. C# - HttpWebRequest의 POST 동작 방식 [링크 복사], [링크+제목 복사],
조회: 16790
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

C# - HttpWebRequest의 POST 동작 방식

아래와 같은 질문이 있는데,

HttpWebRequest POST 전송 관련해서 질문 드립니다.
; https://www.sysnet.pe.kr/2/0/1430

답변에 앞서... 그나저나, "최소한의 재현 코드"를 작성하는 것이 그렇게 어려운 요구 사항인가요? ^^; 그냥 다음과 같은 식으로 재현 코드를 정리하면 얼마나 좋습니까?

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

internal class Program
{
    static void Main(string[] args)
    {
        ThreadPool.QueueUserWorkItem(ServerProc);

        while (true)
        {
            ClientProc();

            Console.WriteLine("Press any key to send again....");
            Console.ReadLine();
        }
    }

    static void ClientProc()
    {
        HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create("http://127.0.0.1:9982/");
        httpWebRequest.Method = "POST";

        string data = new string('c', 51);

        byte[] sendBuf = Encoding.UTF8.GetBytes(data);
        using (Stream dataStream = httpWebRequest.GetRequestStream())
        {
            dataStream.Write(sendBuf, 0, sendBuf.Length);
        }

        try
        {
            using (HttpWebResponse resp = (HttpWebResponse)httpWebRequest.GetResponse())
            {
                using (StreamReader streamReader = new StreamReader(resp.GetResponseStream()))
                {
                    string responseText = streamReader.ReadToEnd();

                    if (resp.StatusCode == HttpStatusCode.OK)
                    {
                        Console.WriteLine($"[Client] {responseText}");
                    }
                }
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }

    static void ServerProc(object state)
    {
        // C# - IPv4, IPv6를 모두 지원하는 서버 소켓 생성 방법
        // https://www.sysnet.pe.kr/2/0/13091
        using (Socket serverSocket = new Socket(SocketType.Stream, ProtocolType.Tcp))
        {
            serverSocket.Bind(new IPEndPoint(IPAddress.IPv6Any, 9982));
            serverSocket.Listen(5);

            while (true)
            {
                Socket socket = serverSocket.Accept();
                ThreadPool.QueueUserWorkItem(processChildSocket, socket);
            }
        }
    }

    static void processChildSocket(object state)
    {
        using (Socket child = state as Socket)
        {
            byte[] buf = new byte[8192];
            int len = child.Receive(buf);

            string txt = Encoding.UTF8.GetString(buf, 0, len);
            Console.WriteLine($"[Server:{len}] {txt}");

            string body = "test";
            string header = "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n";
            byte[] responseBuf = Encoding.UTF8.GetBytes(header + body);

            child.Send(responseBuf);
        }
    }
}

하나의 프로젝트로, 깔끔하게 그냥 실행시키면 재현이 되니 답변하는 사람 입장에서도 오로지 문제 분석에만 신경 쓸 수 있고, 질문자 입장에서도 문제의 원인을 가능한 축소시켰기에 훨씬 더 상황을 잘 설명할 수 있습니다.




자, 그럼 재현 코드도 잘 정리했으니 원인 분석을 해볼까요? ^^

저 코드를 .NET Framework (제 경우 4.8) 환경에서 실행하면 클라이언트에서 다음과 같은 예외가 (경우에 따라) 발생합니다.

System.IO.IOException: Unable to read data from the transport connection: An established connection was aborted by the software in your host machine. ---> System.Net.Sockets.SocketException: An established connection was aborted by the software in your host machine
   at System.Net.Sockets.Socket.Receive(Byte[] buffer, Int32 offset, Int32 size, SocketFlags socketFlags)
   at System.Net.Sockets.NetworkStream.Read(Byte[] buffer, Int32 offset, Int32 size)
   --- End of inner exception stack trace ---
   at System.Net.ConnectStream.Read(Byte[] buffer, Int32 offset, Int32 size)
   at System.IO.StreamReader.ReadBuffer()
   at System.IO.StreamReader.ReadToEnd()
   at Program.ClientProc() in C:\...\Program.cs:line 44

그런데, 이 예외가 발생하는 공통적인 상황이 있습니다. 바로, 서버에서 HttpWebRequest 클라이언트가 보낸 데이터를 Socket.Receive로 받았을 때 다음과 같은 내용만 출력이 된다는 점입니다.

[Server:154] POST / HTTP/1.1
Host: 127.0.0.1:9982
Content-Length: 51
Expect: 100-continue
Connection: Keep-Alive

반면, 예외가 발생하지 않을 때는 이런 출력 결과가 나옵니다.

[Server:181] POST / HTTP/1.1
Host: 127.0.0.1:9982
Content-Length: 51
Expect: 100-continue

ccccccccccccccccccccccccccccccccccccccccccccccccccc

이 현상을 예전에 한 번 설명한 적이 있습니다.

모바일용 웹 사이트에서 발생하는 응답 시간 지연 현상
; https://www.sysnet.pe.kr/2/0/1430#header_body

HttpWebRequest는 Send(Header) + Send(Body)로 나눠서 전송을 하는 것입니다. 이것을 서버 소켓에서 읽을 때 확률적으로 Receive(All)이 될 수도 있고, Receive(Header) + Receive(Body)가 될 수도 있는 것입니다.

즉, 예외 없이 발생한 상황에서는 HttpWebRequest가 보낸 Send(Header) + Send(Body) 데이터를 TCP Stream의 특성상 Receive(All)로 받아버린 경우입니다. 반면, 예외가 발생한 상황에서는 Receive(Header)만을 읽었기 때문에 아직 HttpWebRequest가 보낸 Body 데이터를 읽지 않은 경우입니다.

그렇다면, 이제 후자의 상황, 즉 Header만을 읽은 상황에서 왜 예외가 발생하는 것일까요?

역시 이에 대해서도 예전에 한 번 설명한 적이 있습니다.

socket - shutdown 호출이 필요한 사례
; https://www.sysnet.pe.kr/2/0/11037

그러니까, 아직 읽어야 할 데이터가 있는 상황인데 서버에서 "child.Close"를 했으므로 TCP 연결을 닫는 동작이 RST 패킷 전송을 하게 된 것입니다. 결국, 클라이언트 측에서는 GetResponseStream을 통해 데이터를 읽으려고 시도하는 과정에서 RST 패킷이 도착했으므로 "An established connection was aborted by the software in your host machine" 오류 메시지를 내고 마는 것입니다.




이 문제에 대한 근본적인 원인은, 서버가 제대로 클라이언트가 보낸 모든 데이터를 읽지 않은 것입니다. 따라서, 서버의 Receive 코드를 다음과 같이 바꾸면 해결이 됩니다.

static void processChildSocket(object state)
{
    using (Socket child = state as Socket)
    {
        string txt = ReceiveAll(child);

        // ...[생략]...
    }
}

private static string ReceiveAll(Socket child)
{
    byte[] buf = new byte[8192];

    int contentLengthPos = -1;
    int bodyStart = -1;
    string text = "";
    string contentLengthHeader = "Content-Length";
    int readLen = 0;

    // 대개의 경우 한 번의 Read에 적어도 HTTP Header는 읽힘
    // 이 코드는 HTTP Header가 큰 경우는 가정하지 않음.
    {
        readLen = child.Receive(buf);

        text = Encoding.UTF8.GetString(buf, 0, readLen);
        contentLengthPos = text.IndexOf(contentLengthHeader, 0, StringComparison.OrdinalIgnoreCase);
        bodyStart = text.IndexOf("\r\n\r\n", contentLengthPos);
    }

    // HTTP Body를 보냈는지 확인하고,
    int contentLength = 0;

    if (contentLengthPos > 0)
    {
        int startPos = contentLengthPos + contentLengthHeader.Length;
        int endPos = text.IndexOf("\r\n", startPos);
        if (endPos != -1)
        {
            string contentLengthText = text.Substring(startPos, endPos - startPos).Trim(' ', ':');
            contentLength = int.Parse(contentLengthText);
        }
    }

    int remains = (bodyStart + 4 + contentLength) - readLen;

    if (remains == 0)
    {
        return text;
    }

    // 이 코드는 HTTP Body가 문자열로 이뤄졌다고 가정
    StringBuilder sb = new StringBuilder();

    while (remains > 0)
    {
        int len = child.Receive(buf);
        if (len <= 0)
        {
            break;
        }

        remains -= len;

        sb.Append(Encoding.UTF8.GetString(buf, 0, len));
    }

    return text + sb.ToString();
}

(첨부 파일은 이 글의 예제 코드를 포함합니다.)

참고로, 이런 식의 HTTP Header 처리가 번거롭다면 애당초 HttpListener 등을 이용하는 것이 더 좋은 선택입니다.

IIS의 80 포트를 공유하는 응용 프로그램 만드는 방법
; https://www.sysnet.pe.kr/2/0/1555

C# - HttpListener를 이용한 HTTPS 통신 방법
; https://www.sysnet.pe.kr/2/0/12012




위의 HttpWebRequest 동작은 .NET Core (제가 테스트한 버전은 .NET 6)에서 다소 바뀐 듯합니다. 동일한 예제를 .NET 6 환경에서 실행해 보면, HttpWebRequest가 보낸 데이터를 언제나 Receive(All) 유형으로만 받게 되는 것을 볼 수 있습니다.

아마도 HttpWebRequest가 Send(Header) + Send(Body)로 나눠서 보내는 것이 아닌, 한 번에 Send(Header + Body)를 보내는 방식으로 바뀐 듯합니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/4/2022]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 136  137  138  139  140  141  142  143  144  145  146  147  148  149  [150]  ...
NoWriterDateCnt.TitleFile(s)
1303정성태6/26/201227375개발 환경 구성: 152. sysnet DB를 SQL Azure 데이터베이스로 마이그레이션
1302정성태6/25/201229339개발 환경 구성: 151. Azure 웹 사이트에 사용자 도메인 네임 연결하는 방법
1301정성태6/20/201225755오류 유형: 156. KB2667402 윈도우 업데이트 실패 및 마이크로소프트 Answers 웹 사이트 대응
1300정성태6/20/201231736.NET Framework: 329. C# - Rabin-Miller 소수 생성방법을 이용하여 RSACryptoServiceProvider의 개인키를 직접 채워보자 [1]파일 다운로드2
1299정성태6/18/201232850제니퍼 .NET: 21. 제니퍼 닷넷 - Ninject DI 프레임워크의 성능 분석 [2]파일 다운로드2
1298정성태6/14/201234385VS.NET IDE: 72. Visual Studio에서 pfx 파일로 서명한 경우, 암호는 어디에 저장될까? [2]
1297정성태6/12/201231011VC++: 63. 다른 프로세스에 환경 변수 설정하는 방법파일 다운로드1
1296정성태6/5/201227667.NET Framework: 328. 해당 DLL이 Managed인지 / Unmanaged인지 확인하는 방법 - 두 번째 이야기 [4]파일 다운로드1
1295정성태6/5/201225070.NET Framework: 327. RSAParameters와 System.Numerics.BigInteger 이야기파일 다운로드1
1294정성태5/27/201248501.NET Framework: 326. 유니코드와 한글 - 유니코드와 닷넷을 이용한 한글 처리 [7]파일 다운로드2
1293정성태5/24/201229767.NET Framework: 325. System.Drawing.Bitmap 데이터를 Parallel.For로 처리하는 방법 [2]파일 다운로드1
1292정성태5/24/201223734.NET Framework: 324. First-chance exception에 대해 조건에 따라 디버거가 멈추게 할 수는 없을까? [1]파일 다운로드1
1291정성태5/23/201230254VC++: 62. 배열 초기화를 위한 기계어 코드 확인 [2]
1290정성태5/18/201235071.NET Framework: 323. 관리자 권한이 필요한 작업을 COM+에 대행 [7]파일 다운로드1
1289정성태5/17/201239217.NET Framework: 322. regsvcs.exe로 어셈블리 등록 시 시스템 변경 사항 [5]파일 다운로드2
1288정성태5/17/201226445.NET Framework: 321. regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (3) - Type Library파일 다운로드1
1287정성태5/17/201229268.NET Framework: 320. regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (2) - .NET 4.0 + .NET 2.0 [2]
1286정성태5/17/201238187.NET Framework: 319. regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (1) - .NET 2.0 + x86/x64/AnyCPU [5]
1285정성태5/16/201233255.NET Framework: 318. gacutil.exe로 어셈블리 등록 시 시스템 변경 사항파일 다운로드1
1284정성태5/15/201225680오류 유형: 155. Windows Phone 연결 상태에서 DRIVER POWER STATE FAILURE 블루 스크린 뜨는 현상
1283정성태5/12/201233294.NET Framework: 317. C# 관점에서의 Observer 패턴 구현 [1]파일 다운로드1
1282정성태5/12/201226090Phone: 6. Windows Phone 7 Silverlight에서 Google Map 사용하는 방법 [3]파일 다운로드1
1281정성태5/9/201233173.NET Framework: 316. WPF/Silverlight의 그래픽 단위와 Anti-aliasing 처리를 이해하자 [1]파일 다운로드1
1280정성태5/9/201226146오류 유형: 154. Could not load type 'System.ServiceModel.Activation.HttpModule' from assembly 'System.ServiceModel, ...'.
1279정성태5/9/201224906.NET Framework: 315. 해당 DLL이 Managed인지 / Unmanaged인지 확인하는 방법 [1]파일 다운로드1
1278정성태5/8/201226131오류 유형: 153. Visual Studio 디버깅 - Unable to break execution. This process is not currently executing the type of code that you selected to debug.
... 136  137  138  139  140  141  142  143  144  145  146  147  148  149  [150]  ...