Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions ProjectVG.Application/Models/Chat/ChatProcessResultMessage.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Text.Json.Serialization;
using System.Buffers;
using System.Buffers.Text;

namespace ProjectVG.Application.Models.Chat
{
Expand Down Expand Up @@ -42,9 +44,9 @@ public record ChatProcessResultMessage

public static ChatProcessResultMessage FromSegment(ChatSegment segment, string? requestId = null)
{
var audioData = segment.HasAudio ? Convert.ToBase64String(segment.AudioData!) : null;
var audioData = segment.HasAudio ? ConvertToBase64Optimized(segment.GetAudioSpan()) : null;
var audioFormat = segment.HasAudio ? segment.AudioContentType ?? "wav" : null;

return new ChatProcessResultMessage
{
RequestId = requestId,
Expand All @@ -64,13 +66,43 @@ public ChatProcessResultMessage WithAudioData(byte[]? audioBytes)
{
if (audioBytes != null && audioBytes.Length > 0)
{
return this with { AudioData = Convert.ToBase64String(audioBytes) };
return this with { AudioData = ConvertToBase64Optimized(new ReadOnlySpan<byte>(audioBytes)) };
}
else
{
return this with { AudioData = null };
}
}

/// <summary>
/// ArrayPool을 사용한 메모리 효율적인 Base64 인코딩 (LOH 방지)
/// </summary>
private static string? ConvertToBase64Optimized(ReadOnlySpan<byte> data)
{
if (data.IsEmpty) return null;

var arrayPool = ArrayPool<byte>.Shared;
var base64Length = Base64.GetMaxEncodedToUtf8Length(data.Length);
var buffer = arrayPool.Rent(base64Length);

try
{
if (Base64.EncodeToUtf8(data, buffer, out _, out var bytesWritten) == OperationStatus.Done)
{
// UTF8 바이트를 문자열로 변환
return System.Text.Encoding.UTF8.GetString(buffer, 0, bytesWritten);
}
else
{
// 폴백: 기존 방법 사용
return Convert.ToBase64String(data);
}
}
finally
{
arrayPool.Return(buffer);
}
}

public ChatProcessResultMessage WithCreditInfo(decimal? creditsUsed, decimal? creditsRemaining)
{
Expand Down
86 changes: 75 additions & 11 deletions ProjectVG.Application/Models/Chat/ChatSegment.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,50 @@
using System.Collections.Generic;
using System.Buffers;

namespace ProjectVG.Application.Models.Chat
{
public record ChatSegment
{

public string Content { get; init; } = string.Empty;

public int Order { get; init; }

public string? Emotion { get; init; }

public List<string>? Actions { get; init; }

public byte[]? AudioData { get; init; }
public string? AudioContentType { get; init; }
public float? AudioLength { get; init; }

// 스트림 기반 음성 데이터 처리를 위한 새로운 프로퍼티
public IMemoryOwner<byte>? AudioMemoryOwner { get; init; }
public int AudioDataSize { get; init; }



public bool HasContent => !string.IsNullOrEmpty(Content);
public bool HasAudio => AudioData != null && AudioData.Length > 0;
public bool HasAudio => (AudioData != null && AudioData.Length > 0) || (AudioMemoryOwner != null && AudioDataSize > 0);
public bool IsEmpty => !HasContent && !HasActions;
public bool HasEmotion => !string.IsNullOrEmpty(Emotion);
public bool HasActions => Actions != null && Actions.Any();

/// <summary>
/// 메모리 효율적인 방식으로 음성 데이터에 접근합니다
/// </summary>
public ReadOnlySpan<byte> GetAudioSpan()
{
if (AudioMemoryOwner != null && AudioDataSize > 0)
{
return AudioMemoryOwner.Memory.Span.Slice(0, AudioDataSize);
}
if (AudioData != null)
{
return new ReadOnlySpan<byte>(AudioData);
}
return ReadOnlySpan<byte>.Empty;
}
Comment on lines +33 to +47
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Span 접근 자체는 적절하나 크기 검증 필요

AudioMemoryOwner.Memory.Span.Slice(0, AudioDataSize)AudioDataSize가 실제 메모리 길이보다 클 경우 예외를 유발합니다. 방어적 체크를 추가하세요.

         public ReadOnlySpan<byte> GetAudioSpan()
         {
-            if (AudioMemoryOwner != null && AudioDataSize > 0)
+            if (AudioMemoryOwner != null && AudioDataSize > 0)
             {
-                return AudioMemoryOwner.Memory.Span.Slice(0, AudioDataSize);
+                var mem = AudioMemoryOwner.Memory;
+                var size = Math.Min(AudioDataSize, mem.Length);
+                return mem.Span.Slice(0, size);
             }
             if (AudioData != null)
             {
                 return new ReadOnlySpan<byte>(AudioData);
             }
             return ReadOnlySpan<byte>.Empty;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// <summary>
/// 메모리 효율적인 방식으로 음성 데이터에 접근합니다
/// </summary>
public ReadOnlySpan<byte> GetAudioSpan()
{
if (AudioMemoryOwner != null && AudioDataSize > 0)
{
return AudioMemoryOwner.Memory.Span.Slice(0, AudioDataSize);
}
if (AudioData != null)
{
return new ReadOnlySpan<byte>(AudioData);
}
return ReadOnlySpan<byte>.Empty;
}
/// <summary>
/// 메모리 효율적인 방식으로 음성 데이터에 접근합니다
/// </summary>
public ReadOnlySpan<byte> GetAudioSpan()
{
if (AudioMemoryOwner != null && AudioDataSize > 0)
{
var mem = AudioMemoryOwner.Memory;
var size = Math.Min(AudioDataSize, mem.Length);
return mem.Span.Slice(0, size);
}
if (AudioData != null)
{
return new ReadOnlySpan<byte>(AudioData);
}
return ReadOnlySpan<byte>.Empty;
}
🤖 Prompt for AI Agents
ProjectVG.Application/Models/Chat/ChatSegment.cs around lines 33-47: the current
GetAudioSpan() slices AudioMemoryOwner.Memory.Span with AudioDataSize without
validating that AudioDataSize is within the backing memory/span length (and
non-negative), which can throw; update the method to defensively clamp
AudioDataSize to a valid range before slicing (e.g., compute int validSize =
Math.Max(0, Math.Min(AudioDataSize, AudioMemoryOwner.Memory.Span.Length)) and
use that for Slice), and similarly when returning a ReadOnlySpan from AudioData
validate/clamp against AudioData.Length to avoid out-of-range issues; keep
behavior of returning ReadOnlySpan<byte>.Empty when size is zero or data absent.




Expand Down Expand Up @@ -51,12 +72,55 @@ public static ChatSegment CreateAction(string action, int order = 0)
// Method to add audio data (returns new record instance)
public ChatSegment WithAudioData(byte[] audioData, string audioContentType, float audioLength)
{
return this with
{
AudioData = audioData,
AudioContentType = audioContentType,
AudioLength = audioLength
return this with
{
AudioData = audioData,
AudioContentType = audioContentType,
AudioLength = audioLength
};
}

/// <summary>
/// 메모리 효율적인 방식으로 음성 데이터를 추가합니다 (LOH 방지)
/// </summary>
public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audioDataSize, string audioContentType, float audioLength)
{
return this with
{
AudioMemoryOwner = audioMemoryOwner,
AudioDataSize = audioDataSize,
AudioContentType = audioContentType,
AudioLength = audioLength,
// 기존 AudioData는 null로 설정하여 중복 저장 방지
AudioData = null
};
}
Comment on lines +86 to 97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

소유권 이전 시 크기/수명 검증 및 IDisposable 구현 권장

  • audioDataSize <= audioMemoryOwner.Memory.Length 검증이 없습니다.
  • 이 타입에 Dispose()가 있으나 IDisposable을 구현하지 않아 호출 누락 위험이 큽니다. 소유권을 이전받는 API라면 세멘틱을 강제해야 합니다.

아래 보강을 권장합니다.

-        public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audioDataSize, string audioContentType, float audioLength)
+        public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audioDataSize, string audioContentType, float audioLength)
         {
+            if (audioMemoryOwner is null) throw new ArgumentNullException(nameof(audioMemoryOwner));
+            if (audioDataSize < 0 || audioDataSize > audioMemoryOwner.Memory.Length)
+                throw new ArgumentOutOfRangeException(nameof(audioDataSize));
             return this with
             {
                 AudioMemoryOwner = audioMemoryOwner,
                 AudioDataSize = audioDataSize,
                 AudioContentType = audioContentType,
                 AudioLength = audioLength,
                 // 기존 AudioData는 null로 설정하여 중복 저장 방지
                 AudioData = null
             };
         }

또한, 레코드 선언부에 : IDisposable을 추가하고 Dispose()를 통해 메모리 해제를 보장하세요(선언부 변경은 별도 전체 코드 스니펫 제공 가능).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audioDataSize, string audioContentType, float audioLength)
{
return this with
{
AudioMemoryOwner = audioMemoryOwner,
AudioDataSize = audioDataSize,
AudioContentType = audioContentType,
AudioLength = audioLength,
// 기존 AudioData는 null로 설정하여 중복 저장 방지
AudioData = null
};
}
public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audioDataSize, string audioContentType, float audioLength)
{
if (audioMemoryOwner is null) throw new ArgumentNullException(nameof(audioMemoryOwner));
if (audioDataSize < 0 || audioDataSize > audioMemoryOwner.Memory.Length)
throw new ArgumentOutOfRangeException(nameof(audioDataSize));
return this with
{
AudioMemoryOwner = audioMemoryOwner,
AudioDataSize = audioDataSize,
AudioContentType = audioContentType,
AudioLength = audioLength,
// 기존 AudioData는 null로 설정하여 중복 저장 방지
AudioData = null
};
}
🤖 Prompt for AI Agents
In ProjectVG.Application/Models/Chat/ChatSegment.cs around lines 86-97, add
validation in WithAudioMemory to ensure audioMemoryOwner is not null and
audioDataSize is non-negative and does not exceed audioMemoryOwner.Memory.Length
(throw ArgumentNullException/ArgumentOutOfRangeException/ArgumentException as
appropriate) and validate audioLength is non-negative; also make the record
implement IDisposable and implement Dispose to release AudioMemoryOwner (call
Dispose on the owned IMemoryOwner<byte> and clear related fields) so ownership
transfer is explicit and memory is deterministically freed. Ensure
WithAudioMemory performs the checks before returning the new instance and does
not swallow exceptions, and update the type declaration to implement IDisposable
so callers can rely on the dispose semantic.


/// <summary>
/// 음성 데이터를 배열로 변환합니다 (필요한 경우에만 사용)
/// </summary>
public byte[]? GetAudioDataAsArray()
{
if (AudioData != null)
{
return AudioData;
}

if (AudioMemoryOwner != null && AudioDataSize > 0)
{
var span = AudioMemoryOwner.Memory.Span.Slice(0, AudioDataSize);
return span.ToArray();
}

return null;
}

/// <summary>
/// 리소스 해제 (IMemoryOwner 해제)
/// </summary>
public void Dispose()
{
AudioMemoryOwner?.Dispose();
}
Comment on lines +118 to +124
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

IDisposable 비구현 상태의 Dispose 메서드

메서드만 있고 인터페이스를 구현하지 않아 호출 누락 위험이 큽니다. IDisposable 구현 및 using 패턴 사용을 유도하세요.

// 선언부 예시 (파일 상단 선언부 수정 필요)
public record ChatSegment : IDisposable
{
    // ...
    public void Dispose() => AudioMemoryOwner?.Dispose();
}
🤖 Prompt for AI Agents
In ProjectVG.Application/Models/Chat/ChatSegment.cs around lines 118-124, the
Dispose method exists but the record doesn't implement IDisposable which risks
callers not disposing the AudioMemoryOwner; update the record declaration to
implement IDisposable (e.g., add ": IDisposable" to the record type) and keep
the existing public void Dispose() method so the compiler enforces the
interface; optionally call GC.SuppressFinalize(this) inside Dispose if you add a
finalizer, and update any callers to use using or using-declaration to ensure
deterministic disposal.

}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Buffers;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
Expand All @@ -9,6 +10,8 @@ public class TextToSpeechClient : ITextToSpeechClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<TextToSpeechClient> _logger;
private static readonly ArrayPool<byte> _arrayPool = ArrayPool<byte>.Shared;
private const int MaxPoolSize = 1024 * 1024; // 1MB max pooled size

public TextToSpeechClient(HttpClient httpClient, ILogger<TextToSpeechClient> logger)
{
Expand Down Expand Up @@ -47,7 +50,8 @@ public async Task<TextToSpeechResponse> TextToSpeechAsync(TextToSpeechRequest re
return voiceResponse;
}

voiceResponse.AudioData = await response.Content.ReadAsByteArrayAsync();
// 스트림 기반으로 음성 데이터 읽기 (LOH 방지)
voiceResponse.AudioData = await ReadAudioDataWithPoolAsync(response.Content);
voiceResponse.ContentType = response.Content.Headers.ContentType?.ToString();
Comment on lines +53 to 55
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

오디오 읽기 실패/빈 결과 처리 누락 및 LOH 목표 불충족

  • ReadAudioDataWithPoolAsyncnull(실패) 또는 길이 0을 반환해도 성공 경로로 진행합니다.
  • 현재 구현은 최종적으로 byte[]를 생성(ToArray)하므로 85KB+ 응답에서는 LOH 할당이 여전히 발생합니다. “LOH 방지” 목표와 불일치합니다.

아래처럼 실패 시 에러로 처리하고, 후속 PR에서 IMemoryOwner 기반 반환(또는 스트리밍 전달)로 전환을 검토하세요.

-                // 스트림 기반으로 음성 데이터 읽기 (LOH 방지)
-                voiceResponse.AudioData = await ReadAudioDataWithPoolAsync(response.Content);
+                // 스트림 기반으로 음성 데이터 읽기
+                voiceResponse.AudioData = await ReadAudioDataWithPoolAsync(response.Content);
+                if (voiceResponse.AudioData == null || voiceResponse.AudioData.Length == 0)
+                {
+                    voiceResponse.Success = false;
+                    voiceResponse.ErrorMessage = "오디오 데이터를 읽지 못했습니다.";
+                    return voiceResponse;
+                }

추가 제안(별도 변경): TextToSpeechResponse가 메모리 소유(IMemoryOwner)를 수용하도록 확장하면 최종 byte[] 생성을 피할 수 있습니다. 원하시면 스펙/코드 초안을 드리겠습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 스트림 기반으로 음성 데이터 읽기 (LOH 방지)
voiceResponse.AudioData = await ReadAudioDataWithPoolAsync(response.Content);
voiceResponse.ContentType = response.Content.Headers.ContentType?.ToString();
// 스트림 기반으로 음성 데이터 읽기
voiceResponse.AudioData = await ReadAudioDataWithPoolAsync(response.Content);
if (voiceResponse.AudioData == null || voiceResponse.AudioData.Length == 0)
{
voiceResponse.Success = false;
voiceResponse.ErrorMessage = "오디오 데이터를 읽지 못했습니다.";
return voiceResponse;
}
voiceResponse.ContentType = response.Content.Headers.ContentType?.ToString();
🤖 Prompt for AI Agents
In
ProjectVG.Infrastructure/Integrations/TextToSpeechClient/TextToSpeechClient.cs
around lines 53-55, the code currently assigns the result of
ReadAudioDataWithPoolAsync directly to voiceResponse.AudioData and then calls
ToArray which allows null/empty to be treated as success and forces LOH
allocations for large responses; update the flow to treat a null result or a
byte length of 0 as an error (throw or set a failure on voiceResponse and avoid
continuing the success path), and remove the immediate ToArray allocation by
returning/propagating pooled memory instead (plan a follow-up to change
ReadAudioDataWithPoolAsync to return IMemoryOwner<byte> or stream the data so
you never call ToArray() and thus avoid LOH allocations).


if (response.Headers.Contains("X-Audio-Length"))
Expand Down Expand Up @@ -75,6 +79,50 @@ public async Task<TextToSpeechResponse> TextToSpeechAsync(TextToSpeechRequest re
}
}

/// <summary>
/// ArrayPool을 사용하여 스트림 기반으로 음성 데이터를 읽습니다 (LOH 할당 방지)
/// </summary>
private async Task<byte[]?> ReadAudioDataWithPoolAsync(HttpContent content)
{
const int chunkSize = 32768; // 32KB 청크 크기
byte[]? buffer = null;
MemoryStream? memoryStream = null;

try
{
buffer = _arrayPool.Rent(chunkSize);
memoryStream = new MemoryStream();

using var stream = await content.ReadAsStreamAsync();
int bytesRead;

// 청크 단위로 데이터 읽어서 MemoryStream에 복사
while ((bytesRead = await stream.ReadAsync(buffer, 0, chunkSize)) > 0)
{
await memoryStream.WriteAsync(buffer, 0, bytesRead);
}

var result = memoryStream.ToArray();
_logger.LogDebug("[TTS][ArrayPool] 음성 데이터 읽기 완료: {Size} bytes, 청크 크기: {ChunkSize}",
result.Length, chunkSize);

return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "[TTS][ArrayPool] 음성 데이터 읽기 실패");
return null;
}
finally
{
if (buffer != null)
{
_arrayPool.Return(buffer);
}
memoryStream?.Dispose();
}
}

Comment on lines +82 to +125
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

HTTP 리소스/스트림 수명 및 버퍼 관리 개선

  • MemoryStream 사전 용량(capacity)을 Content-Length로 지정하면 재할당/복사를 줄일 수 있습니다.
  • 스트림/요청에 CancellationToken 전달이 없습니다.
  • ArrayPool 반환 시 민감 데이터 취급 시 clearArray: true 고려.

아래와 같이 보완을 제안합니다.

-        private async Task<byte[]?> ReadAudioDataWithPoolAsync(HttpContent content)
+        private async Task<byte[]?> ReadAudioDataWithPoolAsync(HttpContent content, CancellationToken ct = default)
         {
             const int chunkSize = 32768; // 32KB 청크 크기
             byte[]? buffer = null;
             MemoryStream? memoryStream = null;

             try
             {
                 buffer = _arrayPool.Rent(chunkSize);
-                memoryStream = new MemoryStream();
+                var capacity = content.Headers.ContentLength is long len && len <= int.MaxValue
+                    ? (int)len
+                    : 0;
+                memoryStream = capacity > 0 ? new MemoryStream(capacity) : new MemoryStream();

-                using var stream = await content.ReadAsStreamAsync();
+                using var stream = await content.ReadAsStreamAsync(ct);
                 int bytesRead;

                 // 청크 단위로 데이터 읽어서 MemoryStream에 복사
-                while ((bytesRead = await stream.ReadAsync(buffer, 0, chunkSize)) > 0)
+                while ((bytesRead = await stream.ReadAsync(buffer, 0, chunkSize, ct)) > 0)
                 {
-                    await memoryStream.WriteAsync(buffer, 0, bytesRead);
+                    await memoryStream.WriteAsync(buffer, 0, bytesRead, ct);
                 }
@@
             finally
             {
                 if (buffer != null)
                 {
-                    _arrayPool.Return(buffer);
+                    _arrayPool.Return(buffer, clearArray: false);
                 }
                 memoryStream?.Dispose();
             }
         }

또한, 상위 TextToSpeechAsync에도 HttpResponseMessage/StringContentusing var로 감싸 리소스 누수를 방지하세요(변경 범위가 넓어 별도 스니펫으로 제시 가능합니다).

🤖 Prompt for AI Agents
In
ProjectVG.Infrastructure/Integrations/TextToSpeechClient/TextToSpeechClient.cs
around lines 82-125, improve stream and buffer handling by: pre-sizing
MemoryStream with content.Headers.ContentLength when available to avoid
reallocations, pass a CancellationToken into ReadAsStreamAsync and into
ReadAsync/WriteAsync calls to allow cancellation, return the rented array with
_arrayPool.Return(buffer, clearArray: true) to avoid leaking sensitive audio
bytes, and ensure streams and HttpContent are disposed via using (e.g., using
var stream = await content.ReadAsStreamAsync(cancellationToken); using var
memoryStream = new MemoryStream((int?)content.Headers.ContentLength ?? 0)). Also
update the caller TextToSpeechAsync to wrap HttpResponseMessage and any
StringContent in using/using var to prevent resource leaks.

private string GetErrorMessageForStatusCode(int statusCode, string reasonPhrase)
{
return $"HTTP {statusCode}: {reasonPhrase}";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ProjectVG.Common.Models.Session;
using System.Buffers;
using System.Text;
using System.Net.WebSockets;

Expand All @@ -9,6 +10,9 @@ namespace ProjectVG.Infrastructure.Realtime.WebSocketConnection
/// </summary>
public class WebSocketClientConnection : IClientConnection
{
private static readonly ArrayPool<byte> _arrayPool = ArrayPool<byte>.Shared;
private static readonly Encoding _utf8Encoding = Encoding.UTF8;

public string UserId { get; set; } = string.Empty;
public DateTime ConnectedAt { get; set; } = DateTime.UtcNow;
public System.Net.WebSockets.WebSocket WebSocket { get; set; } = null!;
Expand All @@ -21,20 +25,61 @@ public WebSocketClientConnection(string userId, WebSocket socket)
}

/// <summary>
/// 텍스트 메시지를 전송합니다
/// 텍스트 메시지를 전송합니다 (ArrayPool 사용으로 LOH 할당 방지)
/// </summary>
public Task SendTextAsync(string message)
public async Task SendTextAsync(string message)
{
var buffer = Encoding.UTF8.GetBytes(message);
return WebSocket.SendAsync(new ArraySegment<byte>(buffer), System.Net.WebSockets.WebSocketMessageType.Text, true, CancellationToken.None);
byte[]? rentedBuffer = null;
try
{
// UTF8 인코딩에 필요한 최대 바이트 수 계산
var maxByteCount = _utf8Encoding.GetMaxByteCount(message.Length);
rentedBuffer = _arrayPool.Rent(maxByteCount);

// 실제 인코딩된 바이트 수
var actualByteCount = _utf8Encoding.GetBytes(message, 0, message.Length, rentedBuffer, 0);

// ArraySegment 생성하여 실제 사용된 부분만 전송
var segment = new ArraySegment<byte>(rentedBuffer, 0, actualByteCount);
await WebSocket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None);
}
finally
{
if (rentedBuffer != null)
{
_arrayPool.Return(rentedBuffer);
}
}
}

/// <summary>
/// 바이너리 메시지를 전송합니다
/// </summary>
public Task SendBinaryAsync(byte[] data)
{
return WebSocket.SendAsync(new ArraySegment<byte>(data), System.Net.WebSockets.WebSocketMessageType.Binary, true, CancellationToken.None);
return WebSocket.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Binary, true, CancellationToken.None);
}

/// <summary>
/// 청크 방식으로 대용량 바이너리 데이터를 전송합니다 (LOH 방지)
/// </summary>
public async Task SendLargeBinaryAsync(byte[] data)
{
const int chunkSize = 32768; // 32KB 청크
var totalLength = data.Length;
var offset = 0;

while (offset < totalLength)
{
var remainingBytes = totalLength - offset;
var currentChunkSize = Math.Min(chunkSize, remainingBytes);
var isLastChunk = offset + currentChunkSize >= totalLength;

var segment = new ArraySegment<byte>(data, offset, currentChunkSize);
await WebSocket.SendAsync(segment, WebSocketMessageType.Binary, isLastChunk, CancellationToken.None);

offset += currentChunkSize;
}
}
}
}
Loading
Loading