# 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);
        }
    }
}

 

블로그 이미지

RIsN

,

목표

CANoe에서 사용 가능한 UI에 대해서 시간을 들여서 좀 더 상세히 확인

제작

1. System Variable제작

2. Input/Output Box를 드래그해서 제작 후, Property설정 후 System Variable 연결

3. 코드 제작 후 실행 확인

/*@!Encoding:65001*/

on start {
  // Initialize the CAN address when the program starts
  AdjustCANAddress();  
}

on sysvar sysvar::Engine::_btnTransmit {
  // Buffer to store the CAN address (maximum 8 characters + null terminator)
  char canAddress[9];  
  
  // Check if the button is pressed
  if(@this == 1) {
    // Retrieve the current CAN address
    GetCANAddress(canAddress);
    // Print the CAN address when the button is clicked
    write(":: Current CAN Address: %s", canAddress);
  }
}

// This event triggers when the CAN address input field changes
on sysvar sysvar::Engine::_txtCANAddress {
  // Adjust the CAN address whenever it is modified
  AdjustCANAddress();  
}

// Function to adjust the CAN address
void AdjustCANAddress() {
  // Buffer to hold the input CAN address (temporary storage)
  char buffer[128];  
  // Buffer to store a valid CAN address (max 8 characters + null terminator)
  char limit[9];  
  
  // Retrieve the current CAN address value from the system variable
  sysGetVariableString(sysvar::Engine::_txtCANAddress, buffer, elcount(buffer));
  
  // If the input field is empty, set a default CAN address
  if(strlen(buffer) <= 0) {
    sysSetVariableString(sysvar::Engine::_txtCANAddress, "18DA01F1");
  } 
  // If the input exceeds 8 characters, truncate it to 8 characters
  else if(strlen(buffer) > 8) {
    // Copy only the first 8 characters
    mbstrncpy(limit, buffer, 8);
    // Update the system variable with the trimmed address
    sysSetVariableString(sysvar::Engine::_txtCANAddress, limit);  
  }
}

// Function to retrieve the adjusted CAN address
void GetCANAddress(char buffer[]) {
  AdjustCANAddress();  // Ensure the CAN address is properly formatted
  
  // Retrieve the adjusted CAN address from the system variable
  sysGetVariableString(sysvar::Engine::_txtCANAddress, buffer, elcount(buffer));
  
  // Ensure the string is properly null-terminated
  buffer[strlen(buffer)] = '\\0';
}

'Programming > CANoe' 카테고리의 다른 글

CANoe UI 만들기: 버튼  (0) 2025.03.10
CANoe 함수: mbstrncpy  (0) 2025.03.10
CANoe 함수: strlen  (0) 2025.03.10
CANoe 함수: sysSetVariableString  (0) 2025.03.10
CANoe 함수: sysGetVariableString  (0) 2025.03.02
블로그 이미지

RIsN

,

목표

CANoe에서 사용 가능한 UI에 대해서 시간을 들여서 좀 더 상세히 확인

제작

1. Environment → System Variables → System Variables Configuration으로 이동

2. 오른쪽 클릭으로 New 선택

3. 수치 설정 후 OK

  • 초기값 (Initial Value)또한, 글로벌 설정에서 특정 옵션을 활성화한 경우, 측정이 시작될 때마다 시스템 변수는 이 초기값으로 리셋됩니다.
    1. 데이터 (Data 타입)
      • 값을 두 자리 16진수(HEX) 숫자로 입력하며, 값 사이를 공백(space) 으로 구분합니다.
      • 예시: 01 2A FF → 3바이트 데이터 (01, 2A, FF)
    2. 정수 배열 (Integer Array 타입)
      • 배열 요소를 Int32(32비트 정수) 값으로 입력하며, 값 사이를 세미콜론(;) 으로 구분합니다.
      • 예시: 5;8;33 → 크기가 3인 배열 ([5, 8, 33])
    3. 기타 데이터 타입 (Other Data Types)
      • 해당 데이터 타입의 값 범위 내에서 적절한 값을 입력해야 합니다.
  • 초기값을 지정하는 방법은 시스템 변수의 데이터 타입에 따라 다릅니다:
  • 초기값을 지정하면, 시스템 변수가 처음 정의될 때 해당 값으로 설정됩니다.
  • 값 할당 시 이벤트 (Events on Value Assignment)설정 가능한 이벤트 생성 방식은 다음과 같습니다:
    1. 마지막 값으로 한 번만 이벤트 발생 (One Event with Last Value)
      • 여러 값이 한 번에 할당되더라도, 가장 마지막에 할당된 값으로 단 한 번의 이벤트만 생성됩니다.
    2. 모든 값에 대해 이벤트 발생 (All Events)
      • 할당된 각각의 값에 대해 이벤트가 개별적으로 생성됩니다.
      • 이 경우, 모든 이벤트는 같은 타임스탬프(time stamp) 를 갖게 됩니다.
  • 시스템 변수의 값이 변경될 때 이벤트가 트리거되는데, 특정 옵션을 통해 여러 값이 동시에 할당될 경우 이벤트 생성 방식을 설정할 수 있습니다.

4. Panel에서 Button을 Drag로 생성

5. Property를 수정

6. Symbol Values의 Value 선택

7. 제작한 System Variable을 선택 후 OK

8. 버튼용 함수를 제작

// :: Transmit Button
on sysvar sysvar::Engine::_btnTransmit {
  // :: Click
  if(@this == 1) {
    write(":: Button Clicked");
  }
}

9. 실행 확인

 

'Programming > CANoe' 카테고리의 다른 글

CANoe UI 만들기: 텍스트 박스  (0) 2025.03.10
CANoe 함수: mbstrncpy  (0) 2025.03.10
CANoe 함수: strlen  (0) 2025.03.10
CANoe 함수: sysSetVariableString  (0) 2025.03.10
CANoe 함수: sysGetVariableString  (0) 2025.03.02
블로그 이미지

RIsN

,