Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 4개 있습니다.)
웹: 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

비밀번호

댓글 작성자
 




... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13031정성태4/15/20226823.NET Framework: 1194. C# - IdealProcessor와 ProcessorAffinity의 차이점
13030정성태4/15/20226509오류 유형: 804. 정규 표현식 오류 - Quantifier {x,y} following nothing.
13029정성태4/14/20226915Windows: 203. iisreset 후에도 이전에 설정한 전역 환경 변수가 w3wp.exe에 적용되는 문제
13028정성태4/13/20226830.NET Framework: 1193. (appsettings.json처럼) web.config의 Debug/Release에 따른 설정 적용
13027정성태4/12/20227115.NET Framework: 1192. C# - 환경 변수의 변화를 알리는 WM_SETTINGCHANGE Win32 메시지 사용법파일 다운로드1
13026정성태4/11/20228649.NET Framework: 1191. C 언어로 작성된 FFmpeg Examples의 C# 포팅 전체 소스 코드 [3]
13025정성태4/11/20228002.NET Framework: 1190. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 vaapi_encode.c, vaapi_transcode.c 예제 포팅
13024정성태4/7/20226497.NET Framework: 1189. C# - 런타임 환경에 따라 달라진 AppDomain.GetCurrentThreadId 메서드
13023정성태4/6/20226797.NET Framework: 1188. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 transcoding.c 예제 포팅 [3]
13022정성태3/31/20226710Windows: 202. 윈도우 11 업그레이드 - "PC Health Check"를 통과했지만 여전히 업그레이드가 안 되는 경우 해결책
13021정성태3/31/20226891Windows: 201. Windows - INF 파일을 이용한 장치 제거 방법
13020정성태3/30/20226634.NET Framework: 1187. RDP 접속 시 WPF UserControl의 Unloaded 이벤트 발생파일 다운로드1
13019정성태3/30/20226621.NET Framework: 1186. Win32 Message를 Code로부터 메시지 이름 자체를 구하고 싶다면?파일 다운로드1
13018정성태3/29/20227139.NET Framework: 1185. C# - Unsafe.AsPointer가 반환한 포인터는 pinning 상태일까요? [5]
13017정성태3/28/20226940.NET Framework: 1184. C# - GC Heap에 위치한 참조 개체의 주소를 알아내는 방법 - 두 번째 이야기 [3]
13016정성태3/27/20227797.NET Framework: 1183. C# 11에 추가된 ref 필드의 (우회) 구현 방법파일 다운로드1
13015정성태3/26/20229146.NET Framework: 1182. C# 11 - ref struct에 ref 필드를 허용 [1]
13014정성태3/23/20227711VC++: 155. CComPtr/CComQIPtr과 Conformance mode 옵션의 충돌 [1]
13013정성태3/22/20226035개발 환경 구성: 641. WSL 우분투 인스턴스에 파이썬 2.7 개발 환경 구성하는 방법
13012정성태3/21/20225369오류 유형: 803. C# - Local '...' or its members cannot have their address taken and be used inside an anonymous method or lambda expression
13011정성태3/21/20226855오류 유형: 802. 윈도우 운영체제에서 웹캠 카메라 인식이 안 되는 경우
13010정성태3/21/20225786오류 유형: 801. Oracle.ManagedDataAccess.Core - GetTypes 호출 시 "Could not load file or assembly 'System.DirectoryServices.Protocols...'" 오류
13009정성태3/20/20227401개발 환경 구성: 640. docker - ibmcom/db2 컨테이너 실행
13008정성태3/19/20226710VS.NET IDE: 176. 비주얼 스튜디오 - 솔루션 탐색기에서 프로젝트를 선택할 때 csproj 파일이 열리지 않도록 만드는 방법
13007정성태3/18/20226282.NET Framework: 1181. C# - Oracle.ManagedDataAccess의 Pool 및 그것의 연결 개체 수를 알아내는 방법파일 다운로드1
13006정성태3/17/20227371.NET Framework: 1180. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 remuxing.c 예제 포팅
... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...