# UploadBytesAsync 동작 플로우
```mermaid
flowchart TD
A[메서드 호출: UploadBytesAsync] --> B[입력값 검증: data가 null인지 확인]
B -->|null이면 예외| X[ArgumentNullException 발생]
B -->|정상| C[MemoryStream 생성 (data)]
C --> D[UploadStreamInternalAsync 호출]
D --> E[내부에서 FTP 업로드 진행]
E --> F[업로드 완료]
```
## UploadBytesAsync 네트워크 시퀀스 다이어그램
```mermaid
sequenceDiagram
participant App as 사용자 코드
participant FtpClient as FtpClient
participant NIC as 네트워크 인터페이스
participant FTPServer as FTP 서버
App->>FtpClient: UploadBytesAsync(remotePath, data)
FtpClient->>NIC: FTP 연결 요청 (TCP SYN)
NIC->>FTPServer: TCP SYN
FTPServer->>NIC: TCP SYN+ACK
NIC->>FtpClient: TCP SYN+ACK
FtpClient->>NIC: TCP ACK
NIC->>FTPServer: TCP ACK
FtpClient->>FTPServer: USER 명령 (아이디 전송)
FTPServer->>FtpClient: 331 User name okay, need password
FtpClient->>FTPServer: PASS 명령 (비밀번호 전송)
FTPServer->>FtpClient: 230 User logged in
FtpClient->>FTPServer: TYPE I (Binary 모드)
FTPServer->>FtpClient: 200 Command okay
FtpClient->>FTPServer: PASV (Passive 모드 요청)
FTPServer->>FtpClient: 227 Entering Passive Mode (IP,Port)
FtpClient->>FTPServer: STOR remotePath (업로드 시작)
FTPServer->>FtpClient: 150 File status okay; about to open data connection
FtpClient->>NIC: 데이터 전송 (MemoryStream의 byte[])
NIC->>FTPServer: 데이터 패킷 전송
FTPServer->>FtpClient: 226 Closing data connection (업로드 완료)
FtpClient->>FTPServer: QUIT
FTPServer->>FtpClient: 221 Service closing control connection
```
FtpClient.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace FtpClient
{
// FTP 클라이언트 기능을 제공하는 클래스
public class FtpClient
{
// FTP 접속 및 설정 정보를 담는 구성 객체
private readonly FtpClientConfig _config;
// 생성자: FTP 설정 객체를 받아서 초기화
public FtpClient(FtpClientConfig config)
{
// config가 null이면 예외 발생
_config = config ?? throw new ArgumentNullException(nameof(config));
}
/// <summary>
/// 로컬 파일에서 FTP로 업로드 (디스크에 있는 파일 사용)
/// </summary>
public Task UploadFileAsync(string remotePath, string localFilePath, CancellationToken cancellationToken = default(CancellationToken))
{
// 업로드할 로컬 파일 경로가 null 또는 빈 문자열이면 예외 발생
if (string.IsNullOrEmpty(localFilePath))
throw new ArgumentNullException(nameof(localFilePath));
// 파일이 실제로 존재하지 않으면 예외 발생
if (!File.Exists(localFilePath))
throw new FileNotFoundException("Local file not found.", localFilePath);
// 내부 공통 업로드 메서드 호출 (파일 스트림 생성)
return UploadStreamInternalAsync(remotePath, () => File.OpenRead(localFilePath), cancellationToken);
}
/// <summary>
/// 메모리에 있는 byte 배열을 바로 FTP로 업로드 (디스크 사용 X)
/// </summary>
public Task UploadBytesAsync(string remotePath, byte[] data, CancellationToken cancellationToken = default(CancellationToken))
{
// 업로드할 데이터가 null이면 예외 발생
if (data == null) throw new ArgumentNullException(nameof(data));
// MemoryStream을 바로 사용하여 업로드
return UploadStreamInternalAsync(
remotePath,
() => new MemoryStream(data, writable: false),
cancellationToken);
}
/// <summary>
/// 이미 가지고 있는 Stream을 FTP로 업로드 (디스크 사용 X).
/// Stream의 Position은 적절히 설정되어 있다고 가정.
/// </summary>
public Task UploadStreamAsync(string remotePath, Stream dataStream, CancellationToken cancellationToken = default(CancellationToken))
{
// 업로드할 스트림이 null이면 예외 발생
if (dataStream == null) throw new ArgumentNullException(nameof(dataStream));
// 스트림이 읽을 수 없으면 예외 발생
if (!dataStream.CanRead) throw new ArgumentException("Stream must be readable.", nameof(dataStream));
// 외부에서 전달한 Stream은 dispose하지 않음
return UploadStreamInternalAsync(remotePath, () => dataStream, cancellationToken, disposeStream: false);
}
/// <summary>
/// 내부 공통 구현부 (Stream을 FTP로 업로드)
/// </summary>
private async Task UploadStreamInternalAsync(
string remotePath,
Func<Stream> streamFactory,
CancellationToken cancellationToken,
bool disposeStream = true)
{
// 업로드할 FTP 경로가 null 또는 빈 문자열이면 예외 발생
if (string.IsNullOrEmpty(remotePath))
throw new ArgumentNullException(nameof(remotePath));
// FTP 업로드용 URI 생성
Uri uri = _config.BuildUri(remotePath);
// FTP 업로드 요청 객체 생성 및 각종 옵션 설정
var request = (FtpWebRequest)WebRequest.Create(uri);
request.Method = WebRequestMethods.Ftp.UploadFile; // 업로드 명령 지정
request.Credentials = _config.ToCredential(); // 인증 정보 설정
request.EnableSsl = _config.UseSsl; // SSL 사용 여부 지정
request.UsePassive = _config.UsePassive; // Passive 모드 사용 여부 지정
request.UseBinary = _config.UseBinary; // 바이너리 모드 사용 여부 지정
request.KeepAlive = false; // 업로드 후 연결 유지 안함
Stream requestStream = null; // FTP 서버로 데이터 전송할 스트림
Stream sourceStream = null; // 실제 업로드할 데이터 스트림
try
{
// 취소 요청이 들어오면 예외 발생
cancellationToken.ThrowIfCancellationRequested();
// FTP 서버로 데이터 전송 스트림 획득
requestStream = await request.GetRequestStreamAsync().ConfigureAwait(false);
// 실제 데이터를 제공할 스트림 생성 (파일/메모리)
sourceStream = streamFactory();
if (sourceStream == null)
throw new InvalidOperationException("Stream factory returned null.");
// 데이터를 복사할 때 사용할 버퍼(80KB) 생성
byte[] buffer = new byte[81920];
int bytesRead;
// sourceStream에서 데이터를 읽어서 FTP 서버로 전송
while ((bytesRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0)
{
await requestStream.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
}
// 서버의 응답을 받아 업로드 성공 여부 확인
using (var response = (FtpWebResponse)await request.GetResponseAsync().ConfigureAwait(false))
{
// 필요하다면 response.StatusDescription 로깅 가능
// Console.WriteLine($"Upload File Complete, status {response.StatusDescription}");
}
}
finally
{
// disposeStream 옵션에 따라 sourceStream 자원 해제
if (disposeStream && sourceStream != null)
sourceStream.Dispose();
// FTP 서버로 데이터 전송 스트림 자원 해제
if (requestStream != null)
requestStream.Dispose();
}
}
/// <summary>
/// 여러 chunk 파일을 순서대로 이어붙여 FTP 서버에 하나의 파일로 업로드한다.
/// chunkPaths 순서대로 데이터를 Write 하며, 서버에는 단일 파일로 저장된다.
/// </summary>
public async Task UploadLargeFileAsync(
string remotePath,
IList<string> chunkPaths,
CancellationToken cancellationToken = default(CancellationToken))
{
// 업로드할 FTP 경로가 null 또는 빈 문자열이면 예외 발생
if (string.IsNullOrEmpty(remotePath))
throw new ArgumentNullException(nameof(remotePath));
// chunkPaths가 null이거나 비어 있으면 예외 발생
if (chunkPaths == null || chunkPaths.Count == 0)
throw new ArgumentException("chunkPaths must contain at least one file path.");
// FTP 업로드용 URI 생성
Uri uri = _config.BuildUri(remotePath);
// FTP 업로드 요청 객체 생성 및 각종 옵션 설정
var request = (FtpWebRequest)WebRequest.Create(uri);
request.Method = WebRequestMethods.Ftp.UploadFile; // 업로드 명령 지정
request.Credentials = _config.ToCredential(); // 인증 정보 설정
request.EnableSsl = _config.UseSsl; // SSL 사용 여부 지정
request.UsePassive = _config.UsePassive; // Passive 모드 사용 여부 지정
request.UseBinary = _config.UseBinary; // 바이너리 모드 사용 여부 지정
request.KeepAlive = false; // 업로드 후 연결 유지 안함
Stream requestStream = null; // FTP 서버로 데이터 전송할 스트림
try
{
// 취소 요청이 들어오면 예외 발생
cancellationToken.ThrowIfCancellationRequested();
// FTP 서버로 데이터 전송 스트림 획득
requestStream = await request.GetRequestStreamAsync().ConfigureAwait(false);
// chunk 쓰기 버퍼
byte[] buffer = new byte[1024 * 1024]; // 1MB
int bytesRead;
// chunkPaths에 있는 각 파일을 순서대로 처리
foreach (var path in chunkPaths)
{
// 취소 요청이 들어오면 예외 발생
cancellationToken.ThrowIfCancellationRequested();
// 파일이 실제로 존재하는지 확인, 없으면 예외 발생
if (!File.Exists(path))
throw new FileNotFoundException("Chunk file not found: " + path);
// 파일을 읽기 전용으로 스트림을 열어서 사용
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
// 파일 스트림에서 데이터를 버퍼 단위로 읽어서 FTP 서버로 전송
while ((bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false)) > 0)
{
await requestStream.WriteAsync(buffer, 0, bytesRead, cancellationToken)
.ConfigureAwait(false);
}
}
}
// 서버의 응답을 받아 업로드 성공 여부 확인
using (var response = (FtpWebResponse)await request.GetResponseAsync().ConfigureAwait(false))
{
// 필요시 response.StatusDescription 사용
}
}
finally
{
// FTP 서버로 데이터 전송 스트림 자원 해제
if (requestStream != null)
requestStream.Dispose();
}
}
/// <summary>
/// 첫 번째 chunk는 메모리(byte[])에서 읽고,
/// 이후 chunk들은 파일에서 읽어 순서대로 이어붙여
/// FTP 서버에 하나의 파일로 업로드한다.
/// </summary>
public async Task UploadLargeFileWithMemoryAsync(
string remotePath,
byte[] memoryChunk,
IList<string> chunkPaths,
CancellationToken cancellationToken = default(CancellationToken))
{
// 업로드할 FTP 경로가 null 또는 빈 문자열이면 예외 발생 (필수 입력값 검증)
if (string.IsNullOrEmpty(remotePath))
throw new ArgumentNullException(nameof(remotePath));
// 첫 번째 chunk로 사용할 메모리 데이터가 null이면 예외 발생 (필수 입력값 검증)
if (memoryChunk == null)
throw new ArgumentNullException(nameof(memoryChunk));
// 이어붙일 파일 chunk 목록이 null이면 예외 발생 (필수 입력값 검증)
if (chunkPaths == null)
throw new ArgumentNullException(nameof(chunkPaths));
// FTP 서버에 업로드할 파일의 URI를 생성 (경로 조합)
Uri uri = _config.BuildUri(remotePath);
// FTP 업로드 요청 객체 생성 및 각종 옵션 설정
var request = (FtpWebRequest)WebRequest.Create(uri);
request.Method = WebRequestMethods.Ftp.UploadFile; // FTP 업로드 명령 지정
request.Credentials = _config.ToCredential(); // FTP 인증 정보 설정
request.EnableSsl = _config.UseSsl; // SSL 사용 여부 지정
request.UsePassive = _config.UsePassive; // Passive 모드 사용 여부 지정
request.UseBinary = _config.UseBinary; // 바이너리 모드 사용 여부 지정
request.KeepAlive = false; // 업로드 후 연결을 유지하지 않음
Stream requestStream = null; // FTP 서버로 데이터를 전송할 스트림 변수 선언
try
{
// 취소 요청이 들어오면 예외 발생 (비동기 작업 취소 지원)
cancellationToken.ThrowIfCancellationRequested();
// FTP 서버로 데이터를 전송할 스트림을 비동기로 획득
requestStream = await request.GetRequestStreamAsync().ConfigureAwait(false);
// 데이터를 복사할 때 사용할 버퍼(1MB 크기) 생성
byte[] buffer = new byte[1024 * 1024];
int bytesRead; // 실제로 읽은 바이트 수를 저장할 변수
///
/// 1) 먼저 메모리(byte[]) chunk를 FTP로 전송
///
// memoryChunk를 읽기 전용 메모리 스트림으로 감싸서 사용
using (var ms = new MemoryStream(memoryChunk, writable: false))
{
// 메모리 스트림에서 데이터를 버퍼 단위로 읽어서 FTP 서버로 전송
while ((bytesRead = await ms.ReadAsync(buffer, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false)) > 0)
{
// 읽은 만큼의 데이터를 FTP 서버로 전송
await requestStream.WriteAsync(buffer, 0, bytesRead, cancellationToken)
.ConfigureAwait(false);
}
}
///
/// 2) 그 뒤 파일 chunk들을 순차적으로 이어붙임
///
// chunkPaths에 있는 각 파일을 순서대로 처리
foreach (var filePath in chunkPaths)
{
// 취소 요청이 들어오면 예외 발생
cancellationToken.ThrowIfCancellationRequested();
// 파일이 실제로 존재하는지 확인, 없으면 예외 발생
if (!File.Exists(filePath))
throw new FileNotFoundException("Chunk file not found: " + filePath);
// 파일을 읽기 전용으로 스트림을 열어서 사용
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
// 파일 스트림에서 데이터를 버퍼 단위로 읽어서 FTP 서버로 전송
while ((bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false)) > 0)
{
// 읽은 만큼의 데이터를 FTP 서버로 전송
await requestStream.WriteAsync(buffer, 0, bytesRead, cancellationToken)
.ConfigureAwait(false);
}
}
}
// 서버의 응답을 받아 업로드 성공 여부 확인
using (var response = (FtpWebResponse)await request.GetResponseAsync().ConfigureAwait(false))
{
// 필요시 response.StatusDescription을 통해 서버 응답 메시지 확인 가능
}
}
finally
{
// FTP 서버로 데이터 전송 스트림 자원 해제 (메모리 누수 방지)
if (requestStream != null)
requestStream.Dispose();
}
}
}
}
FtpClientConfig.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace FtpClient
{
// FTP 접속 및 업로드 관련 설정 정보를 담는 클래스
[Serializable]
public class FtpClientConfig
{
// FTP 서버 주소 (예: "ftp.example.com")
public string Host { get; set; } // e.g. "ftp.example.com"
// FTP 서버 포트 (기본값: 21)
public int Port { get; set; } = 21;
// FTP 로그인 사용자명
public string Username { get; set; }
// FTP 로그인 비밀번호
public string Password { get; set; }
/// <summary>
/// FTP 서버의 루트 경로(옵션). 예: "/base/dir"
/// </summary>
public string BasePath { get; set; } = "/";
/// <summary>
/// FTPS(SSL) 사용 여부 (true면 SSL/TLS로 접속)
/// </summary>
public bool UseSsl { get; set; } = false;
/// <summary>
/// Passive 모드 사용 여부 (보통 true 권장, 방화벽 환경에서 유리)
/// </summary>
public bool UsePassive { get; set; } = true;
/// <summary>
/// Binary 모드 여부 (파일 업로드 시 보통 true)
/// </summary>
public bool UseBinary { get; set; } = true;
// FTP 인증 정보를 NetworkCredential 객체로 반환
public NetworkCredential ToCredential()
{
return new NetworkCredential(Username, Password);
}
// remotePath를 포함한 FTP 업로드용 URI를 생성
public Uri BuildUri(string remotePath)
{
// remotePath가 null 또는 빈 문자열이면 예외 발생
if (string.IsNullOrEmpty(remotePath))
throw new ArgumentNullException(nameof(remotePath));
// BasePath가 null/빈 문자열이면 기본값 "/" 사용
string basePath = string.IsNullOrEmpty(BasePath) ? "/" : BasePath;
// BasePath가 슬래시로 끝나지 않으면 슬래시 추가
if (!basePath.EndsWith("/")) basePath += "/";
// remotePath가 슬래시로 시작하면 제거 (중복 방지)
if (remotePath.StartsWith("/"))
remotePath = remotePath.Substring(1);
// 최종 FTP URI 문자열 조합
var uriString = $"ftp://{Host}:{Port}{basePath}{remotePath}";
// Uri 객체로 반환
return new Uri(uriString);
}
}
}
'C#' 카테고리의 다른 글
| 조건부 컴파일(Conditional Compilation)이란? (0) | 2023.08.28 |
|---|---|
| [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part2: 자료구조와 알고리즘 (0) | 2023.05.19 |
| [C#] IComparable (0) | 2023.05.18 |
| [VSCode: Error] Mac OS에서 FSharp Path 에러 생길 때 (0) | 2023.05.09 |
| 미로 생성 알고리즘 (0) | 2023.02.07 |