Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 5개 있습니다.)
개발 환경 구성: 361. Azure Web App(App Service)의 HTTP/2 프로토콜 지원
; https://www.sysnet.pe.kr/2/0/11493

웹: 40. IIS의 HTTP/2 지원 여부 - h2, h2c
; https://www.sysnet.pe.kr/2/0/12495

.NET Framework: 1014. ASP.NET Core(Kestrel)의 HTTP/2 지원 여부
; https://www.sysnet.pe.kr/2/0/12500

.NET Framework: 1015. .NET 5부터 HTTP/1.1, 2.0 선택을 위한 HttpVersionPolicy 동작 방식
; https://www.sysnet.pe.kr/2/0/12501

.NET Framework: 1016. .NET Core HttpClient의 HTTP/2 지원
; https://www.sysnet.pe.kr/2/0/12502




.NET 5부터 HTTP/1.1, 2.0 선택을 위한 HttpVersionPolicy 동작 방식

이해를 돕기 위해, 우선 다음의 예제로 시작해 보겠습니다.

// .NET 5 프로젝트

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    private static readonly HttpClient _client;

    static Program()
    {
        _client = new HttpClient();
    }

    static async Task Main(string[] args)
    {
        {
            HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, "http://nghttp2.org/");
            using var response = await _client.SendAsync(hrm);
            Console.WriteLine(response.Version);
        }

        {
            HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, "https://nghttp2.org/");
            using var response = await _client.SendAsync(hrm);
            Console.WriteLine(response.Version);
        }
    }
}

/* 출력 결과
1.1
1.1
*/

(나중에 설명하지만 HttpVersionPolicy의 기본값이 RequestVersionOrLower이고) HttpRequestMessage.Version의 기본값이 1.1이기 때문에 http/https 모두 1.1 통신을 합니다. 그리고 .NET 5의 HTTP/2 통신을 위한 지원을 설명하고 있는 다음의 문서를 보면,

HTTP/2 - Version Selection
; https://devblogs.microsoft.com/dotnet/net-5-new-networking-improvements/#version-selection

HttpVersionPolicy에 대한 설명이 나옵니다. 그러니까, 결국 .NET Core 3.0에서 임시 처리한 "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport" 옵션을 대체하면서 기능이 확장된 경우인데요, .NET 5부터는 이 옵션을 이용해야 HTTP/2 통신을 하는 것이 가능합니다.

현재 HttpVersionPolicy는 3가지 값이 허용되는데,

  • RequestVersionExact(2)- Only use the requested version.
  • RequestVersionOrHigher(1) - Use the highest available version, downgrading only to the requested version but not below.
  • RequestVersionOrLower(0) - Use the requested version or downgrade to a lower one. This is the default behavior.

이 중에서 가장 직관적인 RequestVersionExact부터 살펴보겠습니다. 이 값을 다음과 같이 이용하면 http와 https에서 모두 HTTP/2 통신을 하도록 만들 수 있습니다.

{
    HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, "http://nghttp2.org/")
    {
        VersionPolicy = HttpVersionPolicy.RequestVersionExact,
        Version = HttpVersion.Version20,
    };

    using var response = await _client.SendAsync(hrm);
    Console.WriteLine(response.Version);
}

{
    HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, "https://nghttp2.org/")
    {
        VersionPolicy = HttpVersionPolicy.RequestVersionExact,
        Version = HttpVersion.Version20,
    };

    using var response = await _client.SendAsync(hrm);
    Console.WriteLine(response.Version);
}

curl의 동작과 비교하면 마치 --http2-prior-knowledge 옵션을 주고 실행한 것과 같습니다. 유의해야 할 점은, HttpClient의 동작에는 h2c 협상 같은 것은 없습니다. 모두 처음부터 HTTP/2로 요청하든지, HTTP/1.1로 요청하든지 둘 중의 하나입니다. 반면 h2는 HTTP/2로 동작할 것을 요구하는 상황에 한해 TLS 협상이 이뤄집니다. (이 글의 첫 번째 예제 코드에서도 나오지만, https 통신조차도 버전 결정이 1.1인 경우라면 TLS에 h2 협상을 얹지 않습니다.)




그나저나, HttpVersionPolicy의 다른 옵션들은 직관적으로 잘 이해가 안 됩니다. 우선 RequestVersionOrHigher를 보면,

HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, "http://nghttp2.org/")
{
    VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
    Version = HttpVersion.Version11,
}; // ==> HTTP/1.1 통신

HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, "https://nghttp2.org/")
{
    VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
    Version = HttpVersion.Version11,
}; // ==> HTTP/2 통신

(서버가 HTTP/2를 지원했을 때) https의 통신 결과는 상식적이지만, http의 통신 결과는 그렇지 않습니다. h2c 협상 절차를 보면 당연히 HTTP/2 통신으로 넘어가야 하는데 그렇지 못한 것입니다. (위에서도 언급했지만) 실제로 저 상태에서의 http 요청 패킷을 보면 h2c 협상을 위한 Upgrade, HTTP2-Settings 헤더가 없습니다.

즉, VersionPolicy에 의한 통신 버전 결정은 서버와의 협상을 통해 진행하는 것이 아니라, 그 단계 이전에 내부적인 규칙에 의해 결정됩니다. 그리고 이에 대한 상세한 규칙은 다음의 글에 나옵니다.

HTTP Version Selection #39201
; https://github.com/dotnet/runtime/pull/39201

RequestVersionOrLower -- current behavior. (기본값)
    If HTTP/1 and no TLS, use HTTP/1.
    If HTTP/1 and TLS, use HTTP/1.
    If HTTP/2 and no TLS, use HTTP/1.
    If HTTP/2 and TLS, use ALPN to negotiate between HTTP/1 and HTTP/2.

RequestVersionOrHigher -- start at the user's version.
    If HTTP/1 and no TLS, use HTTP/1.
    If HTTP/1 and TLS, use ALPN to negotiate between HTTP/1 and HTTP/2.
    If HTTP/2 and no TLS, use HTTP/2 cleartext (H2C)
        if the server doesn't support H2 (only recognizes H1) we get protocol error (observed with loopback server)
        it's expected behavior as-is now; discussion: #31132 (comment), code:
    If HTTP/2 and TLS, use ALPN and fail if server returns error.
        we leave the original exception as-is now, because we don't know exactly what to look for to be able to map it to a better one

RequestVersionExact -- exactly what the user asked for.
    If HTTP/1 and no TLS, use HTTP/1.
    If HTTP/1 and TLS, use HTTP/1.
    If HTTP/2 and no TLS, use HTTP/2 cleartext (H2C)
    If HTTP/2 and TLS, use ALPN and fail if server returns error.

위의 "if HTTP/X"는 사용자가 요청한 Version을 의미합니다. (요청하지 않은 경우는 기본값 1.1) 일례로, 사용자가 요청한 Version이 HttpVersion.Version11이고 "no TLS"라면 - 달리 말해 http 통신이라면 RequestVersionOrLower에서는 HTTP/1.1이 선택되는 것입니다.




그런데, 규칙을 외우는 것이 좀 힘듭니다. ^^; 그래서 쉽게 다음과 같이 기억할 수 있습니다.

일단, http는 1.1 버전이, https는 2.0 버전이 기본 비교 대상에 포함시키고, 그것을 사용자가 요구한 Version에 대해 RequestVersionOrLower 정책에서는 MIN 연산을, RequestVersionOrHigher 정책에서는 MAX 연산을 하는 식입니다.

따라서, 위의 규칙에서 RequestVersionOrLower를 다시 표현해 보면 아래와 같이 해석됩니다.

RequestVersionOrLower
    MIN(1.1, 1.1) // 사용자 요구 버전 1.1, http의 기본 버전 1.1
    MIN(1.1, 2.0) // 사용자 요구 버전 1.1, https의 기본 버전 2.0
    MIN(2.0, 1.1) // 사용자 요구 버전 2.0, http의 기본 버전 1.1
    MIN(2.0, 2.0) // 사용자 요구 버전 2.0, https의 기본 버잔 2.0




저렇게 표현해도 헷갈릴 테니 RequestVersionOrHigher의 설명에 나오는,

RequestVersionOrHigher(1) - Use the highest available version, downgrading only to the requested version but not below.

"available version"이 내부적으로 http는 1.1, https는 2.0으로 미리 포함되어 있다고 가정해도 됩니다.

그에 따라, http에 대해 Version11로 RequestVersionOrHigher 정책이 적용되면 MAX(1.1, 1.1)을 처리하는 것이므로 결국 1.1로 결정되고, https에 대해 Version11을 RequestVersionOrHigher 정책에 적용하면 MAX(1.1, 2.0)을 처리하는 것이므로 2.0이 결정되는 식입니다.

위의 가정으로 진행하면 다음과 같이 코드를 변경해 실행한 결과에도 수긍이 갑니다.

HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, "http://nghttp2.org/")
{
    VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
    Version = HttpVersion.Version20,  /* 2.0과 1.1 중에 2.0 선택 */
}; // ==> HTTP/2 통신

HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, "https://nghttp2.org/")
{
    VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
    Version = HttpVersion.Version20,  /* 2.0과 2.0 중에 2.0 선택 */
}; // ==> HTTP/2 통신




위의 "available version"에 대한 규칙을 이제 (HttpRequestMessage.VersionPolicy 속성의 기본값인) RequestVersionOrLower의 동작 방식에도 적용할 수 있습니다.

HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, "http://nghttp2.org/")
{
    VersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
    Version = HttpVersion.Version20,  /* 2.0과 1.1 중에 1.1 선택 */
}; // ==> HTTP/1.1 통신

HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, "https://nghttp2.org/")
{
    VersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
    Version = HttpVersion.Version20,  /* 2.0과 2.0 중에 2.0 선택 */
}; // ==> HTTP/2 통신

HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, "http://nghttp2.org/")
{
    VersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
    Version = HttpVersion.Version11,  /* 1.1과 1.1 중에 1.1 선택 */
}; // ==> HTTP/1.1 통신

HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Get, "https://nghttp2.org/")
{
    VersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
    Version = HttpVersion.Version11,  /* 1.1과 2.0 중에 1.1 선택 */
}; // ==> HTTP/1.1 통신

좀 이해가 되시나요?!!! ^^

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




참고로, 개별 요청에 대해 HttpRequestMessage를 설정하는 것은 이것을 인자로 받아들이는 SendAsync에 대해서만 가능하므로, 만약 GetAsync 같은 메서드에 대해서도 버전 정책을 지정하고 싶다면 HttpClient 수준에서 설정해야만 합니다.

private static readonly HttpClient _client;

static Program()
{
    _client = new HttpClient()
    {
        // http, https 요청에 대해 모두 HTTP/2 통신 시도
        DefaultRequestVersion = HttpVersion.Version20,
        DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact,
    };
}

또한, 이 글에서는 설명의 편의를 위해 HTTP/1.0과 HTTP/3 옵션을 언급하지 않았지만, 위의 규칙은 그것들에 대해서도 동일하게 적용할 수 있습니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/28/2023]

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

비밀번호

댓글 작성자
 




... 106  107  108  [109]  110  111  112  113  114  115  116  117  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11200정성태5/16/201719472오류 유형: 385. WPF - 폰트가 없어 System.IO.FileNotFoundException 예외가 발생하는 경우
11199정성태5/16/201721245.NET Framework: 657. CultureInfo.GetCultures가 반환하는 값
11198정성태5/10/201722702.NET Framework: 656. Windows Forms의 오류(Exception) 처리 방법에 대한 차이점 설명
11197정성태5/8/201719639개발 환경 구성: 315. VHD 파일의 최소 크기파일 다운로드1
11196정성태5/4/201720821오류 유형: 384. Msvm_ImageManagementService WMI 객체를 사용할 때 오류 상황 정리 [1]
11195정성태5/3/201721172.NET Framework: 655. .NET Framework 4.7 릴리스
11194정성태5/3/201723291오류 유형: 383. net use 명령어로 네트워크 드라이브 연결 시 "System error 67 has occurred." 오류 발생
11193정성태5/3/201721592Windows: 141. 설치된 Windows로부터 설치 이미지를 만드는 방법
11192정성태5/2/201722158Windows: 140. unattended.xml/autounattend.xml 파일을 마련하는 방법
11191정성태5/2/201722911Windows: 139. Dell Venue 8 Pro 태블릿에 USB를 이용한 윈도우 운영체제 설치 방법
11190정성태5/2/201728269Windows: 138. Windows 운영체제의 ISO 설치 파일에 미리 Device driver를 준비하는 방법
11189정성태5/2/201720241Windows: 137. Windows 7 USB/DVD DOWNLOAD TOOL로 98%에서 실패하는 경우
11188정성태4/27/201722744VC++: 118. Win32 HANDLE 자료형의 이모저모 [1]
11187정성태4/26/201723307개발 환경 구성: 314. C# - PowerPoint 확장 Add-in 만드는 방법 [1]파일 다운로드1
11186정성태4/24/201721084VS.NET IDE: 117. Visual Studio 확장(VSIX)을 이용해 사용자 매크로를 추가하는 방법 [1]파일 다운로드1
11185정성태4/22/201719027VS.NET IDE: 116. Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법 (2) - 동적 메뉴 구성파일 다운로드1
11184정성태4/21/201720664VS.NET IDE: 115. Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법파일 다운로드1
11183정성태4/19/201719516.NET Framework: 654. UWP 앱에서 FolderPicker 사용 시 유의 사항파일 다운로드1
11182정성태4/19/201723547개발 환경 구성: 313. Nuget Facebook 라이브러리를 이용해 ASP.NET 웹 폼과 로그인 연동하는 방법
11181정성태4/18/201720423개발 환경 구성: 312. Azure Web Role의 AppPool 실행 권한을 Local System으로 바꾸는 방법
11180정성태4/16/201723475Java: 18. Java의 Memory Mapped File 자원 반환이 안 되는 문제
11179정성태4/13/201716622기타: 64. SVG Converter 스토어 앱 개인정보 보호 정책 안내
11178정성태4/10/201718823개발 환경 구성: 311. COM+ 관리자의 DCOM 구성에 나오는 기준
11177정성태4/7/201719225.NET Framework: 653. C# 7 새로운 문법(1) - 더욱 편리해진 Out 변수 사용파일 다운로드1
11176정성태4/5/201716296VC++: 117. Visual Studio - ATL COM 개체를 단위 테스트 하는 방법
11175정성태4/5/201725927.NET Framework: 652. C# 개발자를 위한 C++ COM 객체의 기본 구현 방식 설명파일 다운로드1
... 106  107  108  [109]  110  111  112  113  114  115  116  117  118  119  120  ...