.NET 10 네트워킹 개선 사항 | Máňa


#dotnet #networking #http #websockets #tls

.NET 10 네트워킹 개선사항

.NET 10에서는 HTTP 성능 최적화, 새로운 WebSocket API, OSX TLS 1.3 지원, 네트워킹 프리미티브 확장 등 다양한 네트워킹 개선이 이루어졌습니다. WinHttpHandler의 인증서 캐싱 기능과 새로운 QUERY HTTP 메서드가 추가되었으며, WebSocketStream을 통해 스트림 기반 WebSocket 작업이 크게 단순화됩니다. 보안 측면에서는 오랫동안 요청되어 온 OSX 클라이언트 측 TLS 1.3 지원이 추가되었습니다. 또한 Server-Sent Events 포매터, IP 주소 검증 메서드, URI 길이 제한 제거 등 네트워킹 프리미티브 영역에서도 중요한 개선이 포함됩니다.


HTTP 개선사항

.NET 10에서는 HTTP 영역에서 성능 최적화, 새로운 HTTP 동사, 쿠키 관련 변경사항이 포함됩니다.

WinHttpHandler 인증서 캐싱

WinHttpHandler에서 서버 인증서 유효성 검사의 성능 최적화가 이루어졌습니다. 사용자 코드가 ServerCertificateValidationCallback을 등록하면, 매 요청마다 콜백이 호출되어 성능 저하가 발생했습니다. 이를 해결하기 위해 서버 IP 주소별로 이미 유효성이 검증된 인증서의 캐시가 도입되었습니다.

  • 새 요청 전송 시 이전에 유효성이 검증된 인증서에 대해 전체 인증서 체인 구축 및 콜백 호출을 건너뜀
  • 각 새 연결은 해당 서버 IP에 대한 캐시된 인증서를 지워 재유효성 검사를 수행함
  • 보안 관점에서 opt-in 기능으로 AppContext 스위치로 활성화됨
AppContext.SetSwitch("System.Net.Http.UseWinHttpCertificateCaching", true);

using var client = new HttpClient(new WinHttpHandler()
{
    ServerCertificateValidationCallback = static (req, cert, chain, errors) =>
    {
        Console.WriteLine("Server certificate validation invoked");
        return errors == SslPolicyErrors.None;
    }
});

Console.WriteLine((await client.GetAsync("https://github.com")).StatusCode);
Console.WriteLine((await client.GetAsync("https://github.com")).StatusCode);
Console.WriteLine((await client.GetAsync("https://github.com")).StatusCode);

스위치 활성화 시 콜백이 한 번만 호출됨:

Server certificate validation invoked
OK
OK
OK

새로운 QUERY HTTP 메서드

새로운 HTTP 동사 QUERY가 추가되었습니다. 이 동사의 목적은 안전하고 멱등적인 요청을 유지하면서 요청 본문에 쿼리 세부 정보를 보낼 수 있게 하는 것입니다.

  • URI 길이 제한에 맞지 않는 쿼리 세부 정보가 있을 때 유용함
  • GET 요청 본문이 서버에서 지원되지 않는 경우에 활용 가능함
  • 현재 RFC 표준화 진행 중으로, 문자열 상수만 추가됨
using var client = new HttpClient();
var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Query, "https://api.example.com/resource"));

CookieException 생성자 공개

커뮤니티 기여자 @deeprobin에 의해 CookieException 생성자가 public으로 변경되었습니다. 이제 수동으로 예외를 throw할 수 있습니다.

throw new CookieException("쿠키 오류 메시지");

WebSockets 개선사항

.NET 10에서는 WebSocket 작업을 크게 단순화하는 WebSocketStream이 도입되었습니다. 기존의 raw WebSocket API는 버퍼링, 프레이밍, 인코딩, 스트림이나 채널 통합을 위한 커스텀 래퍼 등 저수준 세부 사항을 직접 다뤄야 했습니다.

WebSocketStream 주요 장점

  • 스트림 우선 설계: StreamReader, JsonSerializer, 압축기, 직렬화기 등과 원활하게 작동함
  • 수동 플러밍 제거: 메시지 프레이밍 및 잔여 데이터 처리가 불필요함
  • 다양한 시나리오 지원: JSON 기반 프로토콜, STOMP 같은 텍스트 기반 프로토콜, AMQP 같은 바이너리 프로토콜을 지원함

일반적인 사용 패턴

  1. 완전한 JSON 메시지 읽기 (텍스트)

    • CreateReadableMessageStreamJsonSerializer.DeserializeAsync<T>() 사용
    • 수동 수신 루프나 MemoryStream 버퍼 불필요함
    • 스트림이 메시지 경계에서 정확히 종료됨
  2. 텍스트 프로토콜 스트리밍 (STOMP 유사, 줄 지향)

    • Create로 전송 스트림을 얻고 StreamReader를 위에 계층화함
    • 스트림이 프레임 간에 열린 상태로 유지되며 줄 단위 파싱 가능함
  3. 단일 바이너리 메시지 쓰기 (AMQP 페이로드)

    • CreateWritableMessageStream으로 청크 단위 쓰기 후 Dispose가 EOM 전송함
    • 수동 EndOfMessage 처리 불필요함

Before vs After 비교 (JSON 예제)

이전: 수동 버퍼링

static async Task<AppMessage?> ReceiveJsonManualAsync(WebSocket ws, CancellationToken ct)
{
    var buffer = new byte[8192];
    using var mem = new MemoryStream();
    while (ws.State == WebSocketState.Open)
    {
        var result = await ws.ReceiveAsync(buffer, ct);
        if (result.MessageType == WebSocketMessageType.Close)
        {
            return null;
        }
        await mem.WriteAsync(buffer.AsMemory(0, result.Count), ct);
        if (result.EndOfMessage)
        {
            break;
        }
    }
    mem.Position = 0;
    return await JsonSerializer.DeserializeAsync<AppMessage>(mem, cancellationToken: ct);
}

이후: 메시지 스트림

static async Task<AppMessage?> ReceiveJsonAsync(WebSocket ws, CancellationToken ct)
{
    using Stream message = WebSocketStream.CreateReadableMessageStream(ws);
    return await JsonSerializer.DeserializeAsync<AppMessage>(message, cancellationToken: ct);
}

“이후” 버전은 Stream 추상화를 통해 버퍼링, 복사, EndOfMessage 검사를 제거합니다.

보안 개선사항

OSX 클라이언트 측 TLS 1.3

OSX에서의 TLS 1.3 지원은 여러 릴리스에 걸쳐 매우 요청이 많았던 기능입니다. 다른 네이티브 OSX API 세트로 전환해야 하는 비자명한 작업이었습니다.

  • 새 API는 TLS와 TCP를 결합하지만 .NET은 이 두 계층을 SslStreamSocket으로 독립적으로 노출함
  • 새 API는 TLS 1.2와 TLS 1.3만 지원하며, .NET은 다른 deprecated된 SslProtocols도 여전히 지원함
  • opt-in 기능으로 AppContext 스위치로 활성화됨

코드로 활성화:

AppContext.SetSwitch("System.Net.Security.UseNetworkFramework", true);

환경 변수로 활성화:

export DOTNET_SYSTEM_NET_SECURITY_USENETWORKFRAMEWORK=1

이 기능은 OSX에서 클라이언트 작업에 SslStream이 사용하는 백엔드를 전환하며, TLS 1.3과 TLS 1.2만 지원됩니다. 서버 측 SslStream 사용에는 영향이 없습니다.

협상된 암호 모음 (Negotiated Cipher Suite)

SslStream은 협상된 암호 모음에 대한 여러 속성(KeyExchangeAlgorithm, HashAlgorithm, CipherAlgorithm)을 제공했지만, 기본 열거형이 오랫동안 업데이트되지 않아 정확한 정보를 제공하지 못했습니다.

  • 열거형 확장 대신 deprecated 처리하고 NegotiatedCipherSuite만 유일한 정보 소스로 남김
  • 기본 열거형 TlsCipherSuite는 TLS Cipher Suites에 대한 IANA 사양을 따름
  • QuicConnection에도 동일한 NegotiatedCipherSuite 속성이 추가됨

네트워킹 프리미티브

Server-Sent Events 포매터

.NET 9에서 server-sent events 파싱 지원이 추가된 데 이어, .NET 10에서는 반대편인 포매터가 추가되었습니다.

간단한 string 타입 데이터 사용:

using var stream = new MemoryStream();

await SseFormatter.WriteAsync(GetStringItems(), stream);

static async IAsyncEnumerable<SseItem<string>> GetStringItems()
{
    yield return new SseItem<string>("data 1");
    yield return new SseItem<string>("data 2");
    yield return new SseItem<string>("data 3");
    yield return new SseItem<string>("data 4");
}

스트림 출력:

data: data 1

data: data 2

data: data 3

data: data 4

커스텀 데이터 타입 사용:

var stream = new MemoryStream();

await SseFormatter.WriteAsync<int>(GetItems(), stream, (item, writer) =>
{
    writer.Write(Encoding.UTF8.GetBytes(item.Data.ToString()));
});

static async IAsyncEnumerable<SseItem<int>> GetItems()
{
    yield return new SseItem<int>(1) { ReconnectionInterval = TimeSpan.FromSeconds(1) };
    yield return new SseItem<int>(2);
    yield return new SseItem<int>(3);
    yield return new SseItem<int>(4);
}

SseItem 새 속성:

  • EventId: id 필드 전송용
  • ReconnectionInterval: retry 필드 전송용

이 필드들은 연결 재수립 시 클라이언트 동작을 제어합니다.

IP 주소

IPAddress 클래스에 두 가지 새로운 추가가 있습니다.

문자열이 유효한 IP 주소인지 검증하는 정적 메서드:

if (IPAddress.IsValid("10.0.0.1"))
{ ... }
if (IPAddress.IsValid("::1"))
{ ... }
if (IPAddress.IsValidUtf8("::192.168.0.1"u8))
{ ... }
if (IPAddress.IsValidUtf8("fe80::9656:d028:8652:66b6"u8))
{ ... }

IUtf8SpanParsable 구현:

  • .NET 8에서 IUtf8SpanFormattable 추가에 후속하여 IPAddressIPNetwork 모두 IUtf8SpanParsable<T>를 구현함
  • 커뮤니티 기여자 @edwardneal에 의해 구현됨

기타 변경사항

Uri 길이 제한 제거:

  • RFC 2397에 지정된 data URI 스킴을 지원하기 위해 Uri의 길이 제한이 제거됨
  • data URI는 리소스를 링크하는 대신 리소스의 데이터를 직접 담음
  • 예: data:image/jpeg;base64,[base64 인코딩된 이미지 데이터]
  • 기존 약 64KB 제한으로는 많은 data URI에 충분하지 않았음

YAML 미디어 타입 추가:

  • yml 파일을 위한 새 미디어 타입 상수 MediaTypesName.Yaml 추가됨
  • 커뮤니티 기여자 @martincostello에 의해 구현됨

참고 자료

1개의 좋아요