Development note/C#

[C#] 소켓 통신을 이용해서 파일을 전송하는 방법

v명월v 2020. 6. 24. 16:29

안녕하세요. 명월입니다.


이 글은 C#에서 소켓 통신을 이용해서 파일을 전송하는 방법에 대한 글입니다.


소켓에서 파일을 전송하는 것은 생각보다 어렵지는 않습니다.

단순히 IO로 파일을 읽어와서(byte) 그대로 변화없이 소켓에 그대로 바이너리를 보내면 됩니다. 파일을 받는 쪽에는 반대가 되겠네요. 소켓으로 바이너리를 받아서 그대로 파일로 저장하면됩니다.

파일에는 단순히 바이너리만 필요한건 아닙니다. 메타 정보가 필요한데 그중 파일 이름 정도는 별도로 보내야 합니다.


그럼 만들어 보겠습니다.

서버는 IOCP를 이용해서 파일을 다운 받고 클라이언트는 그냥 Socket을 동기식으로 파일을 전송하는 것을 구현하겠습니다.

링크 - [C#] 비동기 소켓 통신(IOCP) - APM 패턴

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace FileServer
{
  // 상태 열거형
  enum State
  {
    STATE,
    FILENAMESIZE,
    FILENAME,
    FILESIZE,
    FILEDOWNLOAD
  }
  // 파일 클래스
  class File
  {
    // 상태
    protected State state = State.STATE;
    // 파일 이름
    public byte[] FileName { get; set; }
    // 파일
    public byte[] Binary { get; set; }
  }
  // 파일 클래스를 상속 받는다.
  class Client : File
  {
    // 클라이언트 소켓
    private Socket socket;
    // 버퍼
    private byte[] buffer;
    // 파일 다운로드 위치
    private int seek = 0;
    // 다운로드 디렉토리
    private string SaveDirectory = @"d:\work\";
    // 생성자
    public Client(Socket socket)
    {
      // 소켓 받기
      this.socket = socket;
      // 버퍼
      buffer = new byte[1];
      // buffer로 메시지를 받고 Receive함수로 메시지가 올 때까지 대기한다.
      this.socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, Receive, this);
      // 접속 환영 메시지
      var remoteAddr = (IPEndPoint)socket.RemoteEndPoint;
      // 콘솔 출력
      Console.WriteLine($"Client:(From:{remoteAddr.Address.ToString()}:{remoteAddr.Port},Connection time:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")})");
    }
    // 데이터 수신
    private void Receive(IAsyncResult result)
    {
      // 접속이 연결되어 있으면...
      if (socket.Connected)
      {
        // EndReceive를 호출하여 데이터 사이즈를 받는다.
        // EndReceive는 대기를 끝내는 것이다.
        int size = this.socket.EndReceive(result);
        // 상태
        if (state == State.STATE)
        {
          switch (buffer[0])
          {
            // 0이면 파일 이름 크기
            case 0:
              state = State.FILENAMESIZE;
              // 크기는 int형으로 받는다.
              buffer = new byte[4];
              break;
            // 1이면 파일 이름
            case 1:
              state = State.FILENAME;
              // 파일 이름 버퍼 설정
              buffer = new byte[FileName.Length];
              // 다운로드 위치 0
              seek = 0;
              break;
            // 2이면 파일 크기
            case 2:
              state = State.FILESIZE;
              // 크기는 int형으로 받는다.
              buffer = new byte[4];
              break;
            // 3이면 파일
            case 3:
              state = State.FILEDOWNLOAD;
              // 파일 버퍼 설정
              buffer = new byte[Binary.Length];
              seek = 0;
              break;
          }
        }
        // 파일 이름 사이즈
        else if (state == State.FILENAMESIZE)
        {
          // 데이터를 int형으로 변환(Bigendian)해서 FileName 변수를 할당한다.
          FileName = new byte[BitConverter.ToInt32(buffer, 0)];
          // 상태를 초기화
          buffer = new byte[1];
          state = State.STATE;
        }
        // 파일 이름
        else if (state == State.FILENAME)
        {
          // 다운 받은 데이터를 FileName 변수로 옮긴다.
          Array.Copy(buffer, 0, FileName, seek, size);
          // 받은 만큼 위치 옮긴다.
          seek += size;
          // 위치와 파일 이름 크기가 같으면 종료
          if (seek >= FileName.Length)
          {
            // 상태를 초기화
            buffer = new byte[1];
            state = State.STATE;
          }
        }
        // 파일 크기
        else if (state == State.FILESIZE)
        {
          // 데이터를 int형으로 변환(Bigendian)해서 파일 Binary를 할당한다.
          Binary = new byte[BitConverter.ToInt32(buffer, 0)];
          // 상태를 초기화
          buffer = new byte[1];
          state = State.STATE;
        }
        // 파일 다운로드
        else if (state == State.FILEDOWNLOAD)
        {
          // 다운 받은 데이터를 FileName 변수로 옮긴다.
          Array.Copy(buffer, 0, Binary, seek, size);
          // 받은 만큼 위치 옮긴다.
          seek += size;
          // 위치와 파일 크기가 같으면 종료
          if (seek >= Binary.Length)
          {
            // IO를 이용해서 binary를 파일로 저장한다.
            using (var stream = new FileStream(SaveDirectory + Encoding.UTF8.GetString(FileName), FileMode.Create, FileAccess.Write))
            {
              // 파일 쓰기
              stream.Write(Binary, 0, Binary.Length);
            }
            // 접속을 끊는다.
            this.socket.Disconnect(false);
            this.socket.Close();
            this.socket.Dispose();
            return;
          }
        }
        // buffer로 메시지를 받고 Receive함수로 메시지가 올 때까지 대기한다.
        this.socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, Receive, this);
      }
    }
  }
  // 메인 Program은 Socket을 상속받고 서버 Socket으로 사용한다.
  class Program : Socket
  {
    // 생성자
    public Program() : base(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
    {
      // 포트는 10000을 Listen한다.
      base.Bind(new IPEndPoint(IPAddress.Any, 10000));
      // 서버 대기
      base.Listen(10);
      // 비동기 소켓으로 Accept 클래스로 대기한다.
      BeginAccept(Accept, this);
    }
    // 클라이언트가 접속하면 함수가 호출된다.
    private void Accept(IAsyncResult result)
    {
      // EndAccept로 접속 Client Socket을 받는다. EndAccept는 대기를 끝나는 것이다.
      // Client 클래스를 생성한다.
      var client = new Client(EndAccept(result));
      // 비동기 소켓으로 Accept 클래스로 대기한다.
      BeginAccept(Accept, this);
    }
    // 프로그램 시작 함수
    static void Main(string[] args)
    {
      // Program 클래스를 실행한다.
      new Program();
      // q키를 누르면 서버는 종료한다.
      Console.WriteLine("Press the q key to exit.");
      while (true)
      {
        string k = Console.ReadLine();
        if ("q".Equals(k, StringComparison.OrdinalIgnoreCase))
        {
          break;
        }
      }
    }
  }
}

위는 클라이언트에서 접속해서 파일을 전송하면 서버에서 0을 받고 파일 이름 크기, 1를 받고 파일 이름, 2를 받고 파일 크기, 3을 받고 파일을 다운로드 받는 프로토콜로 움직인다.

이유는 파일 이름과 파일 크기는 string과 binary의 가변이기 때문에 그 크기를 알아야 합니다. 그래서 이전에 그 길이와 크기를 먼저 받아야 합니다.


그리고 중간에 파일 위치(seek)와 크기를 비교하는 부분이 있습니다. 이유는 클라이언트에서 100메가 보내면 한번에 받을 수 있으면 너무 좋은데 물리적으로 파일 전송 속도의 한계가 있습니다.

그래서 그걸 받는 부분마다 seek를 체크해서 바이너리를 다운로드합니다.


다음은 파일을 전송하는 클라이언트입니다.

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace FileClient
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 업로드 할 파일
      var filename = @"D:\nowonbuntistory.png";
      // 서버에 접속한다.
      var ipep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 10000);
      // FileInfo 생성
      var file = new FileInfo(filename);
      // 파일이 존재하는지
      if (file.Exists)
      {
        // 바이너리 버퍼
        var binary = new byte[file.Length];
        // 파일 IO 생성
        using (var stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read))
        {
          // 소켓 생성
          using (Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
          {
            // 접속
            client.Connect(ipep);
            // 파일을 IO로 읽어온다.
            stream.Read(binary, 0, binary.Length);
            // 상태 0 - 파일 이름 크기를 보낸다.
            client.Send(new byte[] { 0 });
            // 송신 - 파일 이름 크기 Bigendian
            client.Send(BitConverter.GetBytes(file.Name.Length));
            // 상태 1 - 파일 이름 보낸다.
            client.Send(new byte[] { 1 });
            // 송신 - 파일 이름 
            client.Send(Encoding.UTF8.GetBytes(file.Name));
            // 상태 2 - 파일 크기를 보낸다.
            client.Send(new byte[] { 2 });
            // 송신 - 파일 크기 Bigendian
            client.Send(BitConverter.GetBytes(binary.Length));
            // 상태 3 - 파일를 보낸다.
            client.Send(new byte[] { 3 });
            // 송신 - 파일
            client.Send(binary);
          }
        }
      }
      else
      {
        // 콘솔 출력
        Console.WriteLine("The file is not exists.");
      }
      // 아무 키나 누르면 종료
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

클라이언트에서 서버의 프로토콜을 맞추어서 파일 이름 크기, 이름, 파일 크기, 파일 전송합니다.


서버를 실행해서 전송하겠습니다.

그리고 클라이언트를 접속해서 파일을 전송합니다.

다시 서버를 확인하면 클라이언트가 접속한 걸 확인할 수 있습니다.

서버에서 클라이언트에서 파일을 전송받고 파일로 만든 것을 확인할 수 있습니다.

여기까지 C#에서 소켓 통신을 이용해서 파일을 전송하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.