Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)
(시리즈 글이 2개 있습니다.)
.NET Framework: 1009. .NET 5에서의 네트워크 라이브러리 개선 (1) - HTTP 관련
; https://www.sysnet.pe.kr/2/0/12493

.NET Framework: 1017. .NET 5에서의 네트워크 라이브러리 개선 (2) - HTTP/2, HTTP/3 관련
; https://www.sysnet.pe.kr/2/0/12504




.NET 5에서의 네트워크 라이브러리 개선 (2) - HTTP/2, HTTP/3 관련

지난 글에 이어,

.NET 5에서의 네트워크 라이브러리 개선 (1) - HTTP 관련
; https://www.sysnet.pe.kr/2/0/12493

다음의 내용을 정리한 두 번째 글입니다. ^^

.NET 5 Networking Improvements
; https://devblogs.microsoft.com/dotnet/net-5-new-networking-improvements/





#1 HTTP/2: 버전 선택

휴~~~ 이 내용을 알기 쉽게 정리하기 위해 선행 작업이 필요했고, ^^;

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

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

아래의 글들도, 예제와 함께 환경 구성을 하다 보니 정리 차원에서 쓰게 되었습니다. ^^;

WSL2 인스턴스와 호스트 측의 Hyper-V에 운영 중인 VM과 네트워크 연결을 하는 방법
; https://www.sysnet.pe.kr/2/0/12494

.NET Core Kestrel 호스팅 - 비주얼 스튜디오의 Kestrel/IIS Express 프로파일 설정
; https://www.sysnet.pe.kr/2/0/12498

.NET Core Kestrel 호스팅 - 포트 변경, non-localhost 접속 지원 및 https 등의 설정 변경
; https://www.sysnet.pe.kr/2/0/12499

그래도 내용이 길어 정작 주제와 관련된 버전 선택은 별도의 글로 정리했으니 참고하세요. ^^

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

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





#2 HTTP/2: 다중 접속

HTTP/2 스펙에서는 서버에 대해 하나의 TCP 연결만 허용합니다. 일반적으로 클라이언트가 웹 브라우저라면 HTTP/2 프로토콜이 FRAME 단위의 처리를 하므로 HTTP 수준에서 발생하는 HOL(Head of line) 현상 없이 다중 요청을 처리할 수 있어 단일 TCP 연결이 오히려 권장되고 있는데요, 하지만 클라이언트가 서비스 측 코드라면 문제가 좀 달라집니다. 제한된 수의 클라이언트 측 코드라면 HTTP/2 서버로의 다중 연결 개체를 성능을 높이기 위한 방법으로 써도 상관없기 때문입니다.

HttpClient는 이렇게 Service-to-Service 환경에서의 성능을 올리는 방법으로 다중 연결을 SocketsHttpHandler.EnableMultipleHttp2Connections 속성을 통해 지원합니다.

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

class Program
{
    static int _created;

    private static readonly HttpClient _client = new HttpClient(new SocketsHttpHandler()
    {
        // Enable multiple HTTP/2 connections.
        EnableMultipleHttp2Connections = true,

        // Log each newly created connection and create the connection the same way as it would be without the callback.
        ConnectCallback = async (context, token) =>
        {
            Interlocked.Increment(ref _created);
            Console.WriteLine(
                $"New connection to {context.DnsEndPoint} with request:{Environment.NewLine}{context.InitialRequestMessage}");

            var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
            await socket.ConnectAsync(context.DnsEndPoint, token).ConfigureAwait(false);
            return new NetworkStream(socket, ownsSocket: true);
        },
    })
    {
        // Allow only HTTP/2, no downgrades or upgrades.
        DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact,
        DefaultRequestVersion = HttpVersion.Version20
    };

    static async Task Main()
    {
        // Burst send 2000 requests in parallel.
        var tasks = new Task[2000];
        for (int i = 0; i < tasks.Length; ++i)
        {
            tasks[i] = _client.GetAsync("http://localhost:15000/");
        }
        await Task.WhenAll(tasks);

        Console.WriteLine("Connection Created: " + _created);
    }
}
/*
...[생략]...
Connection Created: 18
*/

위의 코드를 실행하면 (상황에 따라 다르겠지만 제 테스트 환경에서는) 보통 18개 정도의 HTTP/2 연결이 생성되지만, EnableMultipleHttp2Connections = true 코드를 주석 처리하고 실행하면 1이 나옵니다.





#3 HTTP/2: SocketsHttpHandler의 Ping 제어

HTTP/2 스펙에는 PING 프레임도 정의되어 있군요. ^^ (기존의 HTTP에서는 이것이 없어 TCP에서 제공하는 KEEP-ALIVE를 사용했었습니다.)

.NET 5의 경우, internval, timeout, active stream 여부에 따라 ping을 보낼지를 결정할 수 있게 SocketsHttpHandler를 개선했습니다. (아래의 코드는 기본값과 함께 해당 속성들을 보여줍니다.)

public class SocketsHttpHandler
{
    ...

    // The client will send PING frames to the server if it hasn't receive any frame on the connection for this period of time.
    public TimeSpan KeepAlivePingDelay { get; set; } = Timeout.InfiniteTimeSpan;

    // The client will close the connection if it doesn't receive PING ACK frame within the timeout.
    public TimeSpan KeepAlivePingTimeout { get; set; } = TimeSpan.FromSeconds(20);

    // Whether the client will send PING frames only if there are any active streams on the connection or even if it's idle.
    public HttpKeepAlivePingPolicy KeepAlivePingPolicy { get; set; } = HttpKeepAlivePingPolicy.Always;

    ...
}
public enum HttpKeepAlivePingPolicy
{
    // PING frames are sent only if there are active streams on the connection.
    WithActiveRequests,

    // PING frames are sent regardless if there are any active streams or not.
    Always
}

KeepAlivePingDelay의 기본값이 Infinite이므로 Ping 동작도 기본적으로는 비활성화되어 있어 서버로의 PING 프레임은 자동적으로 전송되지 않습니다. (그리고 이 값에 상관없이, 서버 측에서 오는 PING 프레임에 대한 자동 응답은 동작하며 이것을 끌 수는 없습니다.)

만약 KeepAlivePingDelay 값을 1분으로 설정하면,

class Program
{
    private static readonly HttpClient _client = new HttpClient(new SocketsHttpHandler()
    {
        KeepAlivePingDelay = TimeSpan.FromSeconds(60)
    });
}

서버로부터 마지막 프레임을 수신하지 60초가 지나면 SocketsHttpHandler는 서버로 PING 프레임을 전달합니다. 그리고 (기본값이 20초인) KeepAlivePingTimeout 내에 서버로부터 PING 응답 프레임을 받지 못하면 SocketsHttpHandler는 접속을 종료해 버립니다. (KeepAlivePingDelay, KeepAlivePingTimeout 값은 최소 1초 이상이어야 하고 그 미만으로 설정하면 예외가 발생합니다.)

그런데, KeepAlivePingTimeout을 체크하는 주기가 재미있습니다.

체크 주기 = min(KeepAlivePingDelay, KeepAlivePingTimeout) / 4

가령, KeepAlivePingDelay = 15초, KeepAlivePingTimeout = 7.5라면 체크 주기는 1.875초가 됩니다. 따라서 PING 프레임이 서버로 전달한 후 1.875초마다 KeepAlivePingTimeout이 지났는지 체크하게 되는데 그런 경우 대략 7.5초 ~ 9.5초 사이에 timeout이 발생한다고 합니다. (그런데, 왜 7.5 ~ 9.375 사이가 아니고 9.5일까요? ^^ 혹시 아시는 분은 덧글 부탁드립니다.) 참고로, 이러한 주기 계산은 향후 바뀔 수 있다는 점에 유의해야 합니다.





#4 HTTP/3

HTTP/3과 그것의 전송 계층인 QUIC은 현재 표준화의 막바지 단계에 접어들었다고 합니다. QUIC은 UDP를 기반으로 하기 때문에 기존 TCP 기반의 연결이 제공할 수 없던 몇 가지 이점을 가집니다. 가령, TLS로 보안 연결을 하는 경우 더 빠르게 협상을 할 수 있고, (패킷 유실로 발생하는 TCP 레벨에서의 HOLB 문제가 없으므로) 단일 연결에서의 다중 요청에 대한 더 나은 다중화 처리를 할 수 있습니다. 게다가 요즘같은 모바일 시대에 Wi-Fi와 LTE 망을 넘나드는 경우에도 UDP의 특성 덕분에 부드러운 망 전환을 가능케 합니다.

물론, 이렇게 하려면 UDP 통신의 신뢰성 제어는 프로그래머의 몫이 됩니다. 하지만 그게 문제가 되지 않는 것이, 특별한 경우를 제외하면 저같은 일반 개발자는 "그 프로그래머"에 속하지 않기 때문입니다. ^^ (그렇습니다, 마이크로소프트의 개발자들이 모든 고생을 다 해줄 것입니다.)

일단, .NET 5에서는 HTTP/3 지원이 오픈 소스인 MsQuic 라이브러리를 기반으로 작성되었으며 시험적으로만 포함되어 있는 상태입니다. 따라서 업무 환경에서 사용하는 것을 권장하지 않습니다. MsQuic에 대한 좀 더 자세한 설명은 System.Net.Experimental.MsQuic을 참고하시고, 일단 현재 상태에서 HTTP/3를 사용해 보려면 다음의 조건을 갖춰야 합니다.

  • 현재 Windows Insider로만 배포되고 있는 버전 2004 (OS 빌드 20145.1000) 이후의 운영체제에서만 실행 가능(해당 운영체제에 있는 Schannel만이 QUIC 지원을 포함하며 리눅스의 경우 .NET 6부터 지원 예정)
  • 별도의 피드에서 배포 중인 System.Net.Experimental.MsQuic 패키지를 참조 추가(정식 Nuget에는 아직 미등록)

하지만 대부분은 System.Net.Experimental.MsQuic 패키지를 직접 사용하기보다는 HttpClient를 통해 HTTP/3 서비스를 접근하게 될 텐데, 이런 경우 다음과 같은 2가지 사전 작업이 요구됩니다.

// 1. AppContext로 지정하거나, 환경 변수에서 DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP3DRAFTSUPPORT 값을 설정한 후,
AppContext.SetSwitch("System.Net.SocketsHttpHandler.Http3DraftSupport", true);

// 2. 코드에서 3.0 프로토콜을 요구
new HttpRequestMessage
{
    // Request HTTP/3 version.
    Version = new Version(3, 0),
    // Only HTTP/3 is allowed, no version downgrades should happen.
    VersionPolicy = HttpVersionPolicy.RequestVersionExact
}

위의 코드에서는 RequestVersionExact 옵션을 사용했지만, 서버 측에서 HTTP/3 서비스를 제공한다고 알리는 Alt-Svc 헤더를 사용하는 것도 가능하므로 그런 경우에는 RequestVersionOrHigher 옵션을 설정해야 할 것입니다.

(반대로, 서비스를 제공하는 ASP.NET Core 웹 애플리케이션 측에서 HTTP/3를 제공하고 싶다면 Usage for ASP.NET Core 글을 참고합니다.)





#5 다양한 취소 방법 제공

근래의 비동기 API들이 모두 Task 기반으로 작성되고 있는데요, 그렇다면 일반적으로 CancellationToken을 이용한 작업 취소가 가능해야 하지만 네트워크 라이브러리에서는 그동안 이에 대한 지원이 부족했던 것이 사실입니다. 마이크로소프트도 이를 인지하고 꾸준히 간극을 메우려고 작업을 한끝에 이번 .NET 5에서는 취소 가능한 메서드를 기존 Socket에 확장 메서드로 추가를 했습니다.

// .NET 6 버전에서는 확장 메서드가 아닌 Socket 타입 자체에 포함될 예정
public static class SocketTaskExtensions
{
    public static ValueTask ConnectAsync(this Socket socket, EndPoint remoteEP, CancellationToken cancellationToken);
    public static ValueTask ConnectAsync(this Socket socket, IPAddress address, int port, CancellationToken cancellationToken);
    public static ValueTask ConnectAsync(this Socket socket, IPAddress[] addresses, int port, CancellationToken cancellationToken);
    public static ValueTask ConnectAsync(this Socket socket, string host, int port, CancellationToken cancellationToken);
}

또한 이런 메서드를 활용해 HttpClient와 TcpClient/HttpContent에도 마찬가지로 취소 가능한 메서드를 제공합니다.

public class TcpClient
{
    public ValueTask ConnectAsync(IPAddress address, int port, CancellationToken cancellationToken);
    public ValueTask ConnectAsync(IPAddress[] addresses, int port, CancellationToken cancellationToken);
    public ValueTask ConnectAsync(string host, int port, CancellationToken cancellationToken);
}

class HttpClient
{
    public Task<byte[]> GetByteArrayAsync(string requestUri, CancellationToken cancellationToken);
    public Task<byte[]> GetByteArrayAsync(Uri requestUri, CancellationToken cancellationToken);

    public Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken);
    public Task<Stream> GetStreamAsync(Uri requestUri, CancellationToken cancellationToken);

    public Task<string> GetStringAsync(string requestUri, CancellationToken cancellationToken);
    public Task<string> GetStringAsync(Uri requestUri, CancellationToken cancellationToken);
}

class HttpContent
{
    public Task<byte[]> ReadAsByteArrayAsync(CancellationToken cancellationToken);
    public Task<Stream> ReadAsStreamAsync(CancellationToken cancellationToken);
    public Task<string> ReadAsStringAsync(CancellationToken cancellationToken);

    protected virtual Task<Stream> CreateContentReadStreamAsync(CancellationToken cancellationToken);

    protected virtual Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken);

    public Task CopyToAsync(Stream stream, CancellationToken cancellationToken);
    public Task CopyToAsync(Stream stream, TransportContext context, CancellationToken cancellationToken);
}

한 가지 아쉬운 점을 언급하고 있는데요, 만약 HttpContent를 지금 시기에 만드는 것이었다면 CreateContentReadStreamAsync, SerializeToStreamAsync 2개의 메서드를 virtual이 아닌 abstract로 했을 것이라고 합니다. 하지만 이미 만들어진 상태이고 하위 호환성을 위해 어쩔 수 없이 virtual로 넣을 수밖에 없었다고!





#5 성능 측정 개선

성능 정보를 EventSource를 기반으로 하면서 (기존 윈도우에서는 ETW 제어를 위해 logman 등을 이용했지만) dotnet-trace, dotnet-counters 등을 통한 외부 프로세스에서도 성능 정보를 구할 수 있는 한편, .NET 2.2부터는 프로세스 내부에서도 이벤트 수신이 가능해졌습니다.

C# - (.NET Core 2.2부터 가능한) 프로세스 내부에서 CLR ETW 이벤트 수신
; https://www.sysnet.pe.kr/2/0/12474

이를 기반으로, .NET 5부터는 네트워크 라이브러리에 점점 더 많은 성능 관련 이벤트 소스를 제공하기 시작했는데요, 다음의 예제를 보면 "System.Net.Http", "System.Net.Sockets", "System.Net.Security", "System.Net.NameResolution" 관련 정보가 추가된 것을 확인할 수 있습니다.

class Program
{
    private static readonly HttpClient _client = new HttpClient();

    static async Task Main()
    {
        // Instantiate the listener which subscribes to the events. 
        using var listener = new HttpEventListener();

        // Send an HTTP request.
        using var response = await _client.GetAsync("https://github.com/runtime");
    }
}

internal sealed class HttpEventListener : EventListener
{
    // Constant necessary for attaching ActivityId to the events.
    public const EventKeywords TasksFlowActivityIds = (EventKeywords)0x80;

    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        // List of event source names provided by networking in .NET 5.
        if (eventSource.Name == "System.Net.Http" ||
            eventSource.Name == "System.Net.Sockets" ||
            eventSource.Name == "System.Net.Security" ||
            eventSource.Name == "System.Net.NameResolution")
        {
            EnableEvents(eventSource, EventLevel.LogAlways);
        }
        // Turn on ActivityId.
        else if (eventSource.Name == "System.Threading.Tasks.TplEventSource")
        {
            // Attach ActivityId to the events.
            EnableEvents(eventSource, EventLevel.LogAlways, TasksFlowActivityIds);
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        var sb = new StringBuilder().Append($"{eventData.TimeStamp:HH:mm:ss.fffffff}  {eventData.ActivityId}.{eventData.RelatedActivityId}  {eventData.EventSource.Name}.{eventData.EventName}(");
        for (int i = 0; i < eventData.Payload?.Count; i++)
        {
            sb.Append(eventData.PayloadNames?[i]).Append(": ").Append(eventData.Payload[i]);
            if (i < eventData.Payload?.Count - 1)
            {
                sb.Append(", ");
            }
        }

        sb.Append(")");
        Console.WriteLine(sb.ToString());
    }
}

/* 출력 결과
21:30:55.5872980  00000011-0000-0000-0000-00005ec19d59.00000000-0000-0000-0000-000000000000  System.Net.Http.RequestStart(scheme: https, host: github.com, port: 443, pathAndQuery: /runtime, versionMajor: 1, versionMinor: 1, versionPolicy: 0)
21:30:55.8877721  00001011-0000-0000-0000-00005ef19d59.00000011-0000-0000-0000-00005ec19d59  System.Net.NameResolution.ResolutionStart(hostNameOrAddress: )
21:30:55.8921599  00001011-0000-0000-0000-00005ef19d59.00000000-0000-0000-0000-000000000000  System.Net.NameResolution.ResolutionStop()
21:30:55.9134031  00002011-0000-0000-0000-00005ee19d59.00000011-0000-0000-0000-00005ec19d59  System.Net.NameResolution.ResolutionStart(hostNameOrAddress: github.com)
21:30:55.9454881  00002011-0000-0000-0000-00005ee19d59.00000000-0000-0000-0000-000000000000  System.Net.NameResolution.ResolutionStop()
21:30:55.9538667  00003011-0000-0000-0000-00005e919d59.00000001-0001-0000-e058-0000ffdcd7b5  System.Net.Sockets.ConnectStart(address: InterNetworkV6:28:{1,187,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,52,78,231,108,0,0,0,0})
21:30:55.9719179  00003011-0000-0000-0000-00005e919d59.00000000-0000-0000-0000-000000000000  System.Net.Sockets.ConnectStop()
21:30:55.9791334  00004011-0000-0000-0000-00005e819d59.00000005-0001-0000-e058-0000ffdcd7b5  System.Net.Security.HandshakeStart(isServer: False, targetHost: github.com)
21:30:58.5819420  00004011-0000-0000-0000-00005e819d59.00000000-0000-0000-0000-000000000000  System.Net.Security.HandshakeStop(protocol: 3072)
21:30:58.5849868  00000006-0001-0000-e058-0000ffdcd7b5.00000000-0000-0000-0000-000000000000  System.Net.Http.ConnectionEstablished(versionMajor: 1, versionMinor: 1)
21:30:58.5905578  00005011-0000-0000-0000-00005eb19d59.00000008-0001-0000-e058-0000ffdcd7b5  System.Net.Http.RequestHeadersStart()
21:30:58.5917764  00005011-0000-0000-0000-00005eb19d59.00000000-0000-0000-0000-000000000000  System.Net.Http.RequestHeadersStop()
21:30:59.6648194  00006011-0000-0000-0000-00005ea19d59.00000017-0001-0000-e058-0000ffdcd7b5  System.Net.Http.ResponseHeadersStart()
21:30:59.6693371  00006011-0000-0000-0000-00005ea19d59.00000000-0000-0000-0000-000000000000  System.Net.Http.ResponseHeadersStop()
21:30:59.7349590  00007011-0000-0000-0000-00005e519e59.0000000b-0001-0000-e058-0000ffdcd7b5  System.Net.Http.ResponseContentStart()
21:30:59.7396094  00007011-0000-0000-0000-00005e519e59.00000000-0000-0000-0000-000000000000  System.Net.Http.ResponseContentStop()
21:30:59.7397539  00000011-0000-0000-0000-00005ec19d59.00000000-0000-0000-0000-000000000000  System.Net.Http.RequestStop()
*/

보는 바와 같이, *Start 메서드와 *Stop 메서드는 쌍을 이루고 동일한 ActivityId를 가지므로 해당 메서드의 실행 시간과 횟수를 측정하는 것도 가능합니다. 또한 RelatedActivityId를 이용하면 호출 관계까지 파악할 수 있습니다.

(본문에는 dotnet-trace를 이용해 외부 프로세스에의 이벤트 수신을 다루는 방법도 소개하니 관심 있는 분들은 읽어보시고. ^^)





#6 TLS 1.3 지원

.NET의 보안 계층은 OS와 그것의 구성 요소에 의존하는데 TLS 1.3 지원도 마찬가지입니다. 따라서 플랫폼 별로 나뉘게 되는데요.

우선 윈도우의 경우에는 1903 버전 이후의 환경에서만 사용할 수 있습니다. 하지만 아직 테스트 목적으로만 지원하고 있어 현업에서의 사용은 권장하지 않으며 게다가 이를 사용하려면 명시적인 레지스트리 설정이 필요합니다. 그나마 반가운 소식이라면, 현재 빌드 버전 20170부터의 Windows Insider에서는 TLS 1.3이 기본적으로 활성화되어 있고 새로운 Schannel API가 이를 지원합니다. 또한 .NET 5의 경우 SslStream 타입에서 새로운 Schannel API를 사용하도록 변경되었으므로 접근성이 더 쉬워졌습니다.

반면, 리눅스의 경우에는 TLS 1.3을 지원하는 1.1.1 버전 이후의 OpenSSL에 의존합니다. 리눅스를 위한 .NET의 주요 기능 변경이라면, 원래는 기본 암호화 지원 목록에 시스템 환경 설정을 반영하지 않았지만 .NET 5.부터 openssl.cnf의 암호화 설정을 반영하도록 SslStream 타입을 변경한 것이 있습니다. 따라서, 다음과 같은 식으로 openssl.cnf에 추가적인 암호화 방식을 설정하는 것이 가능합니다.

[default_conf]
ssl_conf = ssl_sect

[ssl_sect]
system_default = system_default_sect

[system_default_sect]
CipherString = ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256

만약 특별한 암호화 설정이 없다면 기본적으로 다음의 지원 명세를 포함하는데,

  • TLS 1.3 cipher suites
  • TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
  • TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
  • TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
  • TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
  • TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
  • TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
  • TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
  • TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256

이러한 기본 지원을 바꾸고 싶다면 다음과 같이 CipherSuitesPolicy 속성을 통해 SslStream에 명시할 수 있습니다.

var sslStream = new SslStream(networkStream);
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions()
{
    CipherSuitesPolicy = new CipherSuitesPolicy(new[]
    {
        TlsCipherSuite.TLS_AES_256_GCM_SHA384,
        TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256,
        TlsCipherSuite.TLS_AES_128_GCM_SHA256,
        TlsCipherSuite.TLS_AES_128_CCM_8_SHA256,
        TlsCipherSuite.TLS_AES_128_CCM_SHA256
    }),
});

혹은, HttpClient를 사용하는 경우라면 SocketsHttpHandler.SslOptions 속성을 통해 설정할 수 있고.

private static readonly HttpClient _client = new HttpClient(new SocketsHttpHandler()
{
    SslOptions = new SslClientAuthenticationOptions()
    {
        CipherSuitesPolicy = new CipherSuitesPolicy(new []
        {
            TlsCipherSuite.TLS_AES_256_GCM_SHA384,
            TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256,
            TlsCipherSuite.TLS_AES_128_GCM_SHA256,
            TlsCipherSuite.TLS_AES_128_CCM_8_SHA256,
            TlsCipherSuite.TLS_AES_128_CCM_SHA256
        })
    }
});




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

[연관 글]






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

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

비밀번호

댓글 작성자
 



2021-04-16 10시15분
정성태

1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13446정성태11/16/20232416닷넷: 2161. .NET Conf 2023 - Day 1 Blazor 개요 정리
13445정성태11/15/20232679Linux: 62. 리눅스/WSL에서 CA 인증서를 저장하는 방법
13444정성태11/15/20232440닷넷: 2160. C# 12 - Experimental 특성 지원
13443정성태11/14/20232489개발 환경 구성: 687. OpenSSL로 생성한 사용자 인증서를 ASP.NET Core 웹 사이트에 적용하는 방법
13442정성태11/13/20232304개발 환경 구성: 686. 비주얼 스튜디오로 실행한 ASP.NET Core 사이트를 WSL 2 인스턴스에서 https로 접속하는 방법
13441정성태11/12/20232644닷넷: 2159. C# - ASP.NET Core 프로젝트에서 서버 Socket을 직접 생성하는 방법파일 다운로드1
13440정성태11/11/20232349Windows: 253. 소켓 Listen 시 방화벽의 Public/Private 제어 기능이 비활성화된 경우
13439정성태11/10/20232835닷넷: 2158. C# - 소켓 포트를 미리 시스템에 등록/예약해 사용하는 방법(Port Exclusion Ranges)파일 다운로드1
13438정성태11/9/20232444닷넷: 2157. C# - WinRT 기능을 이용해 윈도우에서 실행 중인 Media App 제어
13437정성태11/8/20232638닷넷: 2156. .NET 7 이상의 콘솔 프로그램을 (dockerfile 없이) 로컬 docker에 배포하는 방법
13436정성태11/7/20232861닷넷: 2155. C# - .NET 8 런타임부터 (Reflection 없이) 특성을 이용해 public이 아닌 멤버 호출 가능
13435정성태11/6/20232794닷넷: 2154. C# - 네이티브 자원을 포함한 관리 개체(예: 스레드)의 GC 정리
13434정성태11/1/20232606스크립트: 62. 파이썬 - class의 정적 함수를 동적으로 교체
13433정성태11/1/20232340스크립트: 61. 파이썬 - 함수 오버로딩 미지원
13432정성태10/31/20232371오류 유형: 878. 탐색기의 WSL 디렉터리 접근 시 "Attempt to access invalid address." 오류 발생
13431정성태10/31/20232696스크립트: 60. 파이썬 - 비동기 FastAPI 앱을 gunicorn으로 호스팅
13430정성태10/30/20232596닷넷: 2153. C# - 사용자가 빌드한 ICU dll 파일을 사용하는 방법
13429정성태10/27/20232849닷넷: 2152. Win32 Interop - C/C++ DLL로부터 이중 포인터 버퍼를 C#으로 받는 예제파일 다운로드1
13428정성태10/25/20232896닷넷: 2151. C# 12 - ref readonly 매개변수
13427정성태10/18/20233079닷넷: 2150. C# 12 - 정적 문맥에서 인스턴스 멤버에 대한 nameof 접근 허용(Allow nameof to always access instance members from static context)
13426정성태10/13/20233258스크립트: 59. 파이썬 - 비동기 호출 함수(run_until_complete, run_in_executor, create_task, run_in_threadpool)
13425정성태10/11/20233084닷넷: 2149. C# - PLinq의 Partitioner<T>를 이용한 사용자 정의 분할파일 다운로드1
13423정성태10/6/20233059스크립트: 58. 파이썬 - async/await 기본 사용법
13422정성태10/5/20233199닷넷: 2148. C# - async 유무에 따른 awaitable 메서드의 병렬 및 예외 처리
13421정성태10/4/20233237닷넷: 2147. C# - 비동기 메서드의 async 예약어 유무에 따른 차이
13420정성태9/26/20235245스크립트: 57. 파이썬 - UnboundLocalError: cannot access local variable '...' where it is not associated with a value
1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...