Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

nomad-programmer

[Programming/C#] 프로토콜 설계와 네트워크 애플리케이션 프로그래밍 예제 본문

Programming/C#

[Programming/C#] 프로토콜 설계와 네트워크 애플리케이션 프로그래밍 예제

scii 2020. 10. 4. 14:21

파일 업로드 프로토콜

파일 전송을 위한 프로토콜 FTP(File Transfer Protocol)이 있긴 하지만 직접 프로토콜 설계를 해보자. 그러면 다른 프로토콜을 이해하는 데에도 도움이된다.

설계할 파일 업로드 프로토콜의 이름은 FUP(File Upload Protocol)이라고 해보자. 이 프로토콜은 헤더와 바디의 두 부분으로 나뉜다. 바디에는 실제로 전달하고자 하는 데이터를 담고, 헤더에는 본문 길이를 비롯하여 메시지의 속성 몇 가지를 담을 것이다. 바디의 길이는 담는 데이터에 따라 달라지지만 헤더의 길이는 16바이트로 항상 일정하다. 따라서 수신한 패킷을 분석할 때는 가장 먼저 16바이트를 먼저 확인하여 바디의 길이를 포함한 메시지의 속성을 확인하고, 그 다음 바디의 길이만큼을 또 읽어 하나의 메시지 끝을 끊어내야 한다.

FUP의 구조

고정 길이 vs 가변 길이
스트림에서 패킷의 경계를 구분해 내는 일은 TCP 네트워크 프로그래밍에서 필수적이다. 패킷의 경계를 구분하는 방법은 메시지 포맷을 설계할 때 고려해야 하는데 대표적인 방법이 고정 길이 형식과 가변 길이 형식이다.

고정 길이 형식에서는 모든 메시지가 같은 길이를 갖는다. 16바이트면 16바이트씩만, 32바이트면 32바이트씩만 항상 잘라내는 것이다. 구현하기는 간편하지만, 이 방식은 패역폭이 낭비될 가능성이 높다는 단점이 있다.

가변 길이 형식에는 흔히 두 가지 방식을 사용하는데, 메시지를 두 부분으로 나눠서 길이가 고정된 앞부분에 뒷부분의 길이를 기입하는 방식과 메시지를 구분하는 특정 값(' '라던가 캐리지 리턴 등)을 이용하는 방식이있다. 
후자의 경우에는 텍스트 방식의 통신에 주로 이용되고 전자는 바이너리 통신에 이용된다.

FUP는 가변 길이 형식에서도 바디부분의 길이를 기입하는 방식으로 설계할 것이다.

FUP의 헤더가 갖고 있는 각 속성 필드에 대한 설명

필드 이름 크기(바이트) 설명
MSGID 4 메시지 식별 번호
MSGTYPE 4 메시지의 종류
* 0x01: 파일 전송 요청
* 0x02: 파일 전송 요청에 대한 응답
* 0x03: 파일 전송 데이터
* 0x04: 파일 수신 결과
BODYLEN 4 메시지 본문의 길이(단위: 바이트)
FRAGMENTED 1 메시지의 분할 여부
* 미분할: 0x0
* 분할: 0x1
LASTMSG 1 분할된 메시지가 마지막인지 여부
* 마지막 아님: 0x0
* 마지막: 0x1
SEQ 2 메시지의 파편 번호

FUP의 바디

FUP의 바디는 모두 네 가지다. 헤더의 MSGTYPE이 가질 수 있는 값이 모두 네 개(0x01~0x04)이므로 바디의 종류도 네 가지로 나뉜다.

먼저 MSGTYPE이 파일 전송 요청(0x01)인 경우의 바디 구조를 보자. 이 메시지는 클라이언트에서 사용한다. MSGTYPE 0x01의 바디는 다음 표와 같이 파일의 크기와 파일의 이름으로 이루어져 있다.

필드 이름 크기(바이트) 설명
FILESIZE 8 전송할 파일 크기(단위: 바이트)
FILENAME BODYLEN - FILESIZE(8 byte) 전송할 파일의 이름

다음 표는 파일 전송 요청에 대한 응답(0x02) 메시지의 바디 구조를 나타낸다. 이 메시지는 서버에서 사용하며, 클라이언트에서 보낸 파일 전송 요청(0x01) 메시지의 메시지 식별 번호와 같이 결과를 클라이언트에게 전송한다.

필드 이름 크기(바이트) 설명
MSGID 4 파일 전송 요청 메시지(0x01)의 메시지 식별 번호
RESPONSE 1 파일 전송 승인 여부
* 거절: 0x0
* 승인: 0x1

파일 전송 요청에 대한 응답(0x02) 메시지의 RESPONSE 필드가 0x1을 담고 클라이언트에 돌아오면, 클라이언트는 파일 전송을 개시한다. 클라이언트의 파일은 네트워크 전송에 알맞도록 잘 쪼개져서 파일 전송 데이터(0x03) 메시지에 담겨 서버로 날아간다. 이 경우 FUP의 바디는 DATA만 담는다.

필드 이름 크기(바이트) 설명
DATA 헤더의 BODYLEN 파일 내용

클라이언트가 마지막 파일 데이터를 전송할 때에는 파일 전송 데이터 메시지 헤더의 LASTMSG 필드에 0x01을 담아 보낸다. 마지막 파일 전송 데이터 메시지를 수신한 서버는 파일이 제대로 수신됐는지를 확인해서 파일 수신 결과(0x04) 메시지를 클라이언트에 보낸다. 이 때 메시지 바디에는 파일 전송 데이터(0x03) 메시지의 MSGID와 파일 수신 결과가 함께 담긴다.

필드 이름 크기(바이트) 설명
MSGID 4 파일 전송 데이터(0x03)의 식별 번호
RESULT 1 파일 전송 성공 여부
* 실패: 0x0
* 성공: 0x1

프로토콜 설계가 끝났다. 이 프로토콜을 코드로 옮겨보자.

C#으로만 이루어진 애플리케이션과 통신할 것이라면 이런 복잡한 프로토콜보다는 직렬화가 더 적절한 답일 수 있다. 하지만 대개의 경우, 통신 애플리케이션은 자신과 상이한 환경에서 이미 터를 잡고 동작하고 있는 애플리케이션과 통신을 해야 한다.
인터넷은 이미 그 역사가 수십년에 이르고 있다. 이 역사 속에서 개발되어 동작하고 있는 네트워크 애플리케이션의 상당수가 C나 C++로 작성되었고, 이들과 어울리려면 이들이 지원하는 프로토콜을 지원해야 한다.
이 문제는 C#의 직렬화 매커니즘으로 해결할 수 없고, 직접 프로토콜을 구현하는 수밖에는 없다.

파일 업로드 서버와 클라이언트 구현하기

  • 서버/클라이언트 공용 클래스 라이브러리 구현
  • 서버 구현
  • 클라이언트 구현

서버/클라이언트가 같이 사용할 클래스 라이브러리 만들기

파일 업로드 서버와 클라이언트는 모두 FUP 프로토콜을 사용한다. 이 말은 즉, FUP 프로토콜을 처리하는 코드를 서버와 클라이언트 양쪽에서 공유할 수 있다는 뜻이다. 

Step 1. 비주얼 스튜디오 [새 프로젝트] - [클래스 라이브러리 (.NET 프레임워크)]를 선택한 뒤 프로젝트를 생성한다.

Step 2. 코드를 작성한다.

// Message.cs

namespace FUP
{
    public class CONSTANTS
    {
        // 메시지 타입(MSGTYPE) 상수 정의
        public const uint REQ_FILE_SEND = 0x01;
        public const uint REP_FILE_SEND = 0x02;
        public const uint FILE_SEND_DATA = 0x03;
        public const uint FILE_SEND_RES = 0x04;

        public const byte NOT_FRAGMENTED = 0x00;
        public const byte FRAGMENTED = 0x01;

        public const byte NOT_LASTMSG = 0x00;
        public const byte LASTMSG = 0x01;

        public const byte ACCEPTED = 0x01;
        public const byte DENIED = 0x00;

        public const byte FAIL = 0x00;
        public const byte SUCCESS = 0x01;
    }

    // 메시지, 헤더, 바디는 모두 이 인터페이스를 상속한다.
    // 즉, 이들은 자신의 데이터를 바이트 배열로 변환하고 그 바이트 배열의 크기를 반환해야 한다.
    public interface ISerializable
    {
        byte[] GetBytes();
        int GetSize();
    }

    // FUP의 메시지를 나타내는 클래스. Header와 Body로 구성된다.
    public class Message : ISerializable
    {
        public Header Header { get; set; }
        public ISerializable Body { get; set; }

        public byte[] GetBytes()
        {
            byte[] bytes = new byte[GetSize()];

            Header.GetBytes().CopyTo(bytes, 0);
            Body.GetBytes().CopyTo(bytes, Header.GetSize());

            return bytes;
        }

        public int GetSize()
        {
            return Header.GetSize() + Body.GetSize();
        }
    }
}
// Header.cs

using System;

namespace FUP
{
    public class Header : ISerializable
    {
        public uint MSGID { get; set; }
        public uint MSGTYPE { get; set; }
        public uint BODYLEN { get; set; }
        public byte FRAGMENTED { get; set; }
        public byte LASTMSG { get; set; }
        public ushort SEQ { get; set; }

        public Header() { }
        public Header(byte[] bytes)
        {
            MSGID = BitConverter.ToUInt32(bytes, 0);
            MSGTYPE = BitConverter.ToUInt32(bytes, 4);
            BODYLEN = BitConverter.ToUInt32(bytes, 8);
            FRAGMENTED = bytes[12];
            LASTMSG = bytes[13];
            SEQ = BitConverter.ToUInt16(bytes, 14);
        }

        public byte[] GetBytes()
        {
            byte[] bytes = new byte[16];

            byte[] temp = BitConverter.GetBytes(MSGID);
            Array.Copy(temp, 0, bytes, 0, temp.Length);

            temp = BitConverter.GetBytes(MSGTYPE);
            Array.Copy(temp, 0, bytes, 4, temp.Length);

            temp = BitConverter.GetBytes(BODYLEN);
            Array.Copy(temp, 0, bytes, 8, temp.Length);

            bytes[12] = FRAGMENTED;
            bytes[13] = LASTMSG;

            temp = BitConverter.GetBytes(SEQ);
            Array.Copy(temp, 0, bytes, 14, temp.Length);

            return bytes;
        }

        public int GetSize()
        {
            return 16;
        }
    }
}
// Body.cs

using System;

namespace FUP
{
    // 파일 전송 요청 메시지(0x01)에 사용할 본문 클래스이다.
    // FILESIZE와 FILENAME 필드를 가진다.
    public class BodyRequest : ISerializable
    {
        public long FILESIZE;
        public byte[] FILENAME;

        public BodyRequest() { }
        public BodyRequest(byte[] bytes)
        {
            FILESIZE = BitConverter.ToInt64(bytes, 0);
            FILENAME = new byte[bytes.Length - sizeof(long)];
            Array.Copy(bytes, sizeof(long), FILENAME, 0, FILENAME.Length);
        }

        public byte[] GetBytes()
        {
            byte[] bytes = new byte[GetSize()];
            byte[] temp = BitConverter.GetBytes(FILESIZE);
            Array.Copy(temp, 0, bytes, 0, temp.Length);
            Array.Copy(FILENAME, 0, bytes, temp.Length, FILENAME.Length);
            return bytes;
        }

        public int GetSize()
        {
            return sizeof(long) + FILENAME.Length;
        }
    }

    // 파일 전송 요청에 대한 응답(0x02)에 사용할 본문 클래스이다.
    // MSGID와 RESPONSE 필드를 가진다.
    public class BodyResponse : ISerializable
    {
        public uint MSGID;
        public byte RESPONSE;

        public BodyResponse() { }
        public BodyResponse(byte[] bytes)
        {
            MSGID = BitConverter.ToUInt32(bytes, 0);
            RESPONSE = bytes[4];
        }

        public byte[] GetBytes()
        {
            byte[] bytes = new byte[GetSize()];
            byte[] temp = BitConverter.GetBytes(MSGID);
            Array.Copy(temp, 0, bytes, 0, temp.Length);
            bytes[temp.Length] = RESPONSE;
            return bytes;
        }

        public int GetSize()
        {
            return sizeof(uint) + sizeof(byte);
        }
    }

    // 실제 파일을 전송하는 메시지(0x03)에 사용할 본문 클래스이다. DATA 필드만 갖고 있다.
    public class BodyData : ISerializable
    {
        public byte[] DATA;

        public BodyData(byte[] bytes)
        {
            DATA = new byte[bytes.Length];
            bytes.CopyTo(DATA, 0);
        }

        public byte[] GetBytes()
        {
            return DATA;
        }

        public int GetSize()
        {
            return DATA.Length;
        }
    }

    // 파일 전송 결과 메시지(0x04)에 사용할 본문 클래스이다. 
    // 요청 메시지의 MSGID와 성공 여부를 나타내는 RESULT 프로퍼티를 가진다.
    public class BodyResult : ISerializable
    {
        public uint MSGID;
        public byte RESULT;

        public BodyResult() { }
        public BodyResult(byte[] bytes)
        {
            MSGID = BitConverter.ToUInt32(bytes, 0);
            RESULT = bytes[4];
        }

        public byte[] GetBytes()
        {
            byte[] bytes = new byte[GetSize()];
            byte[] temp = BitConverter.GetBytes(MSGID);
            Array.Copy(temp, 0, bytes, 0, temp.Length);
            bytes[temp.Length] = RESULT;
            return bytes;
        }

        public int GetSize()
        {
            return sizeof(uint) + sizeof(byte);
        }
    }
}
// MessageUtil.cs

using System.IO;

/// <summary>
/// 이 소스 코드 파일은 스트림으로부터 메시지를 보내고 받기 위한 메소드를 가지는
/// MessageUtil 클래스이다.
/// </summary>
namespace FUP
{
    public class MessageUtil
    {
        // Send() 메소드는 스트림을 통해 메시지를 보낸다.
        public static void Send(Stream writer, Message msg)
        {
            writer.Write(msg.GetBytes(), 0, msg.GetSize());
        }

        public static Message Receive(Stream reader)
        {
            int totalRecv = 0;
            int sizeToRead = 16;
            byte[] hBuffer = new byte[sizeToRead];

            while (sizeToRead > 0)
            {
                byte[] buffer = new byte[sizeToRead];
                int recv = reader.Read(buffer, 0, sizeToRead);
                if (recv == 0)
                {
                    return null;
                }
                buffer.CopyTo(hBuffer, totalRecv);
                totalRecv += recv;
                sizeToRead -= recv;
            }
            Header header = new Header(hBuffer);

            totalRecv = 0;
            byte[] bBuffer = new byte[header.BODYLEN];
            sizeToRead = (int)header.BODYLEN;

            while (sizeToRead > 0)
            {
                byte[] buffer = new byte[sizeToRead];
                int recv = reader.Read(buffer, 0, sizeToRead);
                if (recv == 0)
                {
                    return null;
                }
                buffer.CopyTo(bBuffer, totalRecv);
                totalRecv += recv;
                sizeToRead -= recv;
            }
            ISerializable body = null;
            // 헤더의 MSGTYPE 프로퍼티를 통해 어떤 Body 클래스의 생성자를 호출할지 결정한다.
            switch (header.MSGTYPE)
            {
                case CONSTANTS.REQ_FILE_SEND:
                    body = new BodyRequest(bBuffer);
                    break;
                case CONSTANTS.REP_FILE_SEND:
                    body = new BodyResponse(bBuffer);
                    break;
                case CONSTANTS.FILE_SEND_DATA:
                    body = new BodyData(bBuffer);
                    break;
                case CONSTANTS.FILE_SEND_RES:
                    body = new BodyResult(bBuffer);
                    break;
                default:
                    throw new System.Exception(
                        string.Format("Unknown MSGTYPE : {0}", header.MSGTYPE));
            }
            return new Message() { Header = header, Body = body };
        }
    }
}

Step 3. 코드를 모두 작성했다면 솔루션 탐색기에서 "FUP 프로젝트" 항목 위에 마우스 오른쪽 클릭으로 '빌드' 항목을 클릭해서 클래스 라이브러리를 만든다.


파일 업로드 서버 구현하기

Step 1. 비주얼 스튜디오를 실행하고 [새 프로젝트] - [콘솔 앱(.NET Framework)] 선택한다.

Step 2. 파일 업로드 프로토콜을 구현한 FUP.dll을 프로젝트 참조에 추가시킨다. '참조' 항목을 선택하고 [찾아보기] 버튼을 이용해 빌드한 FUP.dll 파일을 선택 후 추가한다.

FUP.dll 동적 라이브러리를 추가한 모습

Step 3. 코드를 작성한다.

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using FUP;

namespace FileReceiver
{
    class MainApp
    {
        static void Main(string[] args)
        {
            if (args.Length < 1)
            {
                Console.WriteLine("usage: {0} <Directory>",
                    Process.GetCurrentProcess().ProcessName);
                return;
            }
            uint msgId = 0;

            string dir = args[0];
            if (!Directory.Exists(dir))
            {
                Directory.CreateDirectory(dir);
            }

            // 서버 포트는 5454
            const int bindPort = 5454;
            TcpListener server = null;

            try
            {
                // IP주소를 0으로 입력하면 127.0.0.1뿐 아니라 OS에 할당되어 있는
                // 어떤 주소로도 서버에 접속이 가능하다.
                IPEndPoint localAddress = new IPEndPoint(0, bindPort);
                server = new TcpListener(localAddress);
                server.Start();

                Console.WriteLine("파일 업로드 서버 시작...");

                while (true)
                {
                    TcpClient client = server.AcceptTcpClient();
                    Console.WriteLine("클라이언트 접속: {0}",
                        ((IPEndPoint)client.Client.RemoteEndPoint).ToString());

                    NetworkStream stream = client.GetStream();

                    // 클라이언트가 보내온 파일 전송 요청 메시지를 수신한다.
                    Message reqMsg = MessageUtil.Receive(stream);

                    if (reqMsg.Header.MSGTYPE != CONSTANTS.REQ_FILE_SEND)
                    {
                        stream.Close();
                        client.Close();
                        continue;
                    }

                    BodyRequest reqBody = (BodyRequest)reqMsg.Body;

                    Console.WriteLine(
                        "파일 업로드 요청이 왔습니다. 수락하시겠습니까? (yes/no)");
                    string answer = Console.ReadLine();

                    Message rspMsg = new Message();
                    rspMsg.Body = new BodyResponse()
                    {
                        MSGID = reqMsg.Header.MSGID,
                        RESPONSE = CONSTANTS.ACCEPTED
                    };
                    rspMsg.Header = new Header()
                    {
                        MSGID = msgId++,
                        MSGTYPE = CONSTANTS.REP_FILE_SEND,
                        BODYLEN = (uint)rspMsg.Body.GetSize(),
                        FRAGMENTED = CONSTANTS.NOT_FRAGMENTED,
                        LASTMSG = CONSTANTS.LASTMSG,
                        SEQ = 0
                    };
                    if (answer != "yes")
                    {
                        // 사용자가 "yes"가 아닌 답을 입력하면 클라이언트에게 "거부" 응답을 보낸다.
                        rspMsg.Body = new BodyResponse()
                        {
                            MSGID = reqMsg.Header.MSGID,
                            RESPONSE = CONSTANTS.DENIED
                        };
                        MessageUtil.Send(stream, rspMsg);
                        stream.Close();
                        client.Close();

                        continue;
                    }
                    else
                    {
                        // 클라이언트에게 "승인" 응답을 보낸다.
                        MessageUtil.Send(stream, rspMsg);
                    }
                    Console.WriteLine("파일 전송을 시작합니다...");

                    long fileSize = reqBody.FILESIZE;
                    string fileName = Encoding.Default.GetString(reqBody.FILENAME);

                    // 업로드 파일 스트림을 생성한다.
                    FileStream file = new FileStream(dir + "\\" + fileName, FileMode.Create);

                    uint? dataMsgId = null;
                    ushort prevSeq = 0;
                    while ((reqMsg = MessageUtil.Receive(stream)) != null)
                    {
                        Console.Write('#');
                        if (reqMsg.Header.MSGTYPE != CONSTANTS.FILE_SEND_DATA)
                        {
                            break;
                        }
                        if (dataMsgId == null)
                        {
                            dataMsgId = reqMsg.Header.MSGID;
                        }
                        else
                        {
                            if (dataMsgId != reqMsg.Header.MSGID)
                            {
                                break;
                            }
                        }
                        // 메시지 순서가 어긋나면 전송을 중단한다.
                        if (prevSeq++ != reqMsg.Header.SEQ)
                        {
                            Console.WriteLine("{0}, {1}", prevSeq, reqMsg.Header.SEQ);
                            break;
                        }

                        // 전송받은 스트림을 서버에서 생성한 파일에 기록한다.
                        file.Write(reqMsg.Body.GetBytes(), 0, reqMsg.Body.GetSize());

                        // 분할 메시지가 아니라면 반복을 한 번만 하고 빠져나온다.
                        if (reqMsg.Header.FRAGMENTED == CONSTANTS.NOT_FRAGMENTED)
                        {
                            break;
                        }
                        // 마지막 메시지면 반복문을 빠져나온다.
                        if (reqMsg.Header.LASTMSG == CONSTANTS.LASTMSG)
                        {
                            break;
                        }
                    }

                    long recvFileSize = file.Length;
                    file.Close();

                    Console.WriteLine();
                    Console.WriteLine("수신 파일 크기: {0} bytes", recvFileSize);

                    Message rstMsg = new Message();
                    rstMsg.Body = new BodyResult()
                    {
                        MSGID = reqMsg.Header.MSGID,
                        RESULT = CONSTANTS.SUCCESS
                    };
                    rstMsg.Header = new Header()
                    {
                        MSGID = msgId++,
                        MSGTYPE = CONSTANTS.FILE_SEND_RES,
                        BODYLEN = (uint)rstMsg.Body.GetSize(),
                        FRAGMENTED = CONSTANTS.NOT_FRAGMENTED,
                        LASTMSG = CONSTANTS.LASTMSG,
                        SEQ = 0
                    };

                    if (fileSize == recvFileSize)
                    {
                        // 파일 전송 요청에 담겨온 파일 크기와 실제로 받은 파일의 크기를
                        // 비교하여 같으면 성공 메시지를 보낸다.
                        MessageUtil.Send(stream, rstMsg);
                    }
                    else
                    {
                        rstMsg.Body = new BodyResult()
                        {
                            MSGID = reqMsg.Header.MSGID,
                            RESULT = CONSTANTS.FAIL
                        };

                        // 파일 크기에 이상이 있다면 실패 메시지를 보낸다.
                        MessageUtil.Send(stream, rstMsg);
                    }
                    Console.WriteLine("파일 전송을 마쳤습니다.");

                    stream.Close();
                    client.Close();
                }
            }
            catch (SocketException err)
            {
                Console.WriteLine(err);
            }
            finally
            {
                server.Stop();
            }

            Console.WriteLine("서버를 종료합니다.");
        }
    }
}

Step 4. 빌드를 진행한다.


클라이언트 구현하기

Step 1. 비주얼 스튜디오를 실행하고 [새 프로젝트] - [콘솔 앱(.NET Framework)] 선택하여 프로젝트를 생성한다.

Step 2. 클라이언트도 파일 업로드 프로토콜을 구현하고 있는 FUP.dll을 필요로 한다. FUP.dll을 프로젝트에 추가한다.

Step 3. 코드 작성

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using FUP;

namespace FileSender
{
    class MainApp
    {
        const int CHUNK_SIZE = 4096;

        static void Main(string[] args)
        {
            if (args.Length < 2)
            {
                Console.WriteLine("usage: {0} <server IP> <File Path>",
                    Process.GetCurrentProcess().ProcessName);
                return;
            }

            string serverIp = args[0];
            const int serverPort = 5454;
            string filePath = args[1];

            try
            {
                // 클라이언트는 OS에서 할당한 IP주소와 포트에 바인딩한다.
                IPEndPoint clientAddress = new IPEndPoint(0, 0);
                IPEndPoint serverAddress = new IPEndPoint(IPAddress.Parse(serverIp), serverPort);

                Console.WriteLine("클라이언트: {0}, 서버: {1}",
                    clientAddress.ToString(), serverAddress.ToString());

                uint msgId = 0;

                Message reqMsg = new Message();
                reqMsg.Body = new BodyRequest()
                {
                    FILESIZE = new FileInfo(filePath).Length,
                    FILENAME = Encoding.Default.GetBytes(filePath)
                };
                reqMsg.Header = new Header()
                {
                    MSGID = msgId++,
                    MSGTYPE = CONSTANTS.REQ_FILE_SEND,
                    BODYLEN = (uint)reqMsg.Body.GetSize(),
                    FRAGMENTED = CONSTANTS.NOT_FRAGMENTED,
                    LASTMSG = CONSTANTS.LASTMSG,
                    SEQ = 0
                };

                TcpClient client = new TcpClient(clientAddress);
                client.Connect(serverAddress);

                NetworkStream stream = client.GetStream();

                // 클라이언트는 서버에 접속하자마자 파일 전송 요청 메시지를 보낸다.
                MessageUtil.Send(stream, reqMsg);

                // 서버의 응답을 받는다.
                Message rspMsg = MessageUtil.Receive(stream);

                if (rspMsg.Header.MSGTYPE != CONSTANTS.REP_FILE_SEND)
                {
                    Console.WriteLine("정상적인 서버 응답이 아닙니다. {0}",
                        rspMsg.Header.MSGTYPE);
                    return;
                }

                if (((BodyResponse)rspMsg.Body).RESPONSE == CONSTANTS.DENIED)
                {
                    Console.WriteLine("서버에서 파일 전송을 거부했습니다.");
                    return;
                }

                // 서버에서 전송 요청을 수락했다면, 파일 스트림을 열어 서버로 보낼 준비를 한다.
                using (Stream fileStream = new FileStream(filePath, FileMode.Open))
                {
                    byte[] rbytes = new byte[CHUNK_SIZE];

                    long readValue = BitConverter.ToInt64(rbytes, 0);

                    int totalRead = 0;
                    ushort msgSeq = 0;
                    byte fragmented =
                        (fileStream.Length < CHUNK_SIZE) ?
                        CONSTANTS.NOT_FRAGMENTED : CONSTANTS.FRAGMENTED;

                    while (totalRead < fileStream.Length)
                    {
                        int read = fileStream.Read(rbytes, 0, CHUNK_SIZE);
                        totalRead += read;
                        Message fileMsg = new Message();

                        byte[] sendBytes = new byte[read];
                        Array.Copy(rbytes, 0, sendBytes, 0, read);

                        fileMsg.Body = new BodyData(sendBytes);
                        fileMsg.Header = new Header()
                        {
                            MSGID = msgId,
                            MSGTYPE = CONSTANTS.FILE_SEND_DATA,
                            BODYLEN = (uint)fileMsg.Body.GetSize(),
                            FRAGMENTED = fragmented,
                            LASTMSG = (totalRead < fileStream.Length) ?
                            CONSTANTS.NOT_FRAGMENTED : CONSTANTS.FRAGMENTED,
                            SEQ = msgSeq++
                        };
                        Console.Write('#');

                        // 모든 파일의 내용이 전송될 때까지 파일 스트림을 0x03 메시지에 담아 서버에 보낸다.
                        MessageUtil.Send(stream, fileMsg);
                    }
                    Console.WriteLine();

                    // 서버에서 파일을 제대로 받았는지에 대한 응답을 받는다.
                    Message rstMsg = MessageUtil.Receive(stream);

                    BodyResult result = ((BodyResult)rstMsg.Body);
                    Console.WriteLine("파일 전송 성공: {0}", result.RESULT == CONSTANTS.SUCCESS);
                }
                stream.Close();
                client.Close();
            }
            catch (SocketException err)
            {
                Console.WriteLine(err);
            }
            finally
            {
                Console.WriteLine("클라이언트를 종료합니다.");
            }
        }
    }
}

실행 결과

Comments