TCP와 UDP는 인터넷 프로콜의 전송 계층에서 가장 널리 사용되는 두 가지 프로토콜로, 서로 목적과 특징이 다르다.

 

🔷 TCP vs UDP 비교

항목 TCP UDP
연결 방식 연결 지향 비연결성
데이터 전송 방식 스트림 전송 데이터그램 전송
신뢰성 보장 보장함( 패킷 재전송, 순서 보정 ) 보장하지 않음
패킷 순서 보장 O X
에러 검출 및 복구 O( 체크섬, 재전송 등 ) O ( 체크섬, 복구는 없음 )
속도 상대적으로 느림( 오버헤드 존재 ) 매우 빠름( 오버헤드 적음 )
오버헤드 큼( 헤더 최소 20바이트 ) 작음 ( 헤더 8바이트 고정 )
혼잡 제어 O( 윈도우크기, 혼잡 제어 알고리즘 ) X
전송 단위 바이트 기반 스트림 메시지 기반 데이터 그램
브로드캐스트 / 멀티캐스트 지원하지 않음 지원

 

'네트워크' 카테고리의 다른 글

[네트워크] TCP  (0) 2025.04.22
[네트워크] UDP  (0) 2025.04.22
[네트워크] 소켓  (0) 2025.04.22
[네트워크] 프로토콜 적용 파일 클라  (0) 2025.04.22
[네트워크] 프로토콜 적용 파일 서버  (0) 2025.04.22
TCP는 신뢰성 있는 데이터 통신을 위해 설계된 전송 계층 프로토콜이다. 

 

TCP는 연결지향 방식의 신뢰성 있는 프로토콜로, 데이터를 정확하게, 순서대로, 중복 없이 전달하는 데 초점을 맞춘다.

  • 통신 전 반드시 연결 설정이 필요하다. ( 연결 지향 ) 3-way handshake
  • 손실/중복/순서 오류 방지 및 재전송 기능을 포함한다. ( 신뢰성 보장 )
  • 송신 측이 수신 측의 처리 속도를 초과하지 않도록 조절한다. ( 흐름 제어 )
  • 네트워크 과부하를 방지하기 위한 제어 알고리즘이 존재한다. ( 혼잡 제어 )
  • 양방향 데이터 전송이 가능하다. ( 전이중 통신 )
  • 패킷이 순서대로 도착하게 정렬되고 재조립한다. ( 패킷 순서 보장 )

🔷 TCP 헤더 구조

필드 설명
Source Port 송신 포트 번호
Dest Port 수신 포트 번호
Sequence Number 송신 데이터의 순서 지정
Acknowledgment Number ( ACK ) 수신 측에서 다음에 기대하는 순번
Data Offset 헤더 길이 ( 데이터 부분이 어디부터 시작하는지 나타냄 )
Flags 제어 플래그( SYN, ACK, FIN, RST 등 )
Window Size 수신측에서 송신측에 윈도우 사이즈( 수신 확인을 기다리지 않고 묶어서 송신할 수 있는 데이터 양 ) 
Checksum 오류 검출용
Urgent Pointer 긴급하게 처리해야 할 데이터의 위치를 나타냄

 

 

'네트워크' 카테고리의 다른 글

[네트워크] TCP와 UDP 비교  (0) 2025.04.22
[네트워크] UDP  (0) 2025.04.22
[네트워크] 소켓  (0) 2025.04.22
[네트워크] 프로토콜 적용 파일 클라  (0) 2025.04.22
[네트워크] 프로토콜 적용 파일 서버  (0) 2025.04.22
UDP( User Datagram Protocol )는 전송 계층(L4)에서 사용하는 프로토콜 중 하나로,
TCP와 함께 전송 계층의 핵심 요소다. 

 

UDP는 연결 지향이 아닌 비연결성 통신 프로토콜이다.

즉, 송신 측과 수신 측 간에 사전 연결을 설정하지 않고 데이터를 전송한다.

  • TCP와 달리 3-way handshake 과정이 없다. ( 비연결성 )
  • 오베허드가 적고 속도가 빠르다. ( 빠른 전송 )
  • 패킷이 유실되거나 순서가 바뀌어도 보장하지 않는다. ( 신뢰성 없음 )
  • 8바이트 고정 크기 ( 헤더 크기 작음 )
  • 브로드캐스트 / 멀티캐스트 가능 

🔷 UDP 헤더 구조

필드 크기 설명
Source Port 2 송신 측 포트
Destination Port 2 수신 측 포트
Length 2 전체 UDP 패킷 길이( 헤더 + 데이터 )
Checksum 2 오류 검사

'네트워크' 카테고리의 다른 글

[네트워크] TCP와 UDP 비교  (0) 2025.04.22
[네트워크] TCP  (0) 2025.04.22
[네트워크] 소켓  (0) 2025.04.22
[네트워크] 프로토콜 적용 파일 클라  (0) 2025.04.22
[네트워크] 프로토콜 적용 파일 서버  (0) 2025.04.22
소켓( Windows Sockets, Winsock )은 윈도우 운영체제에서 네트워크 통신을 할 수 있도록 해주는 API다.
  • 소켓( Socket )은 OS 커널에 구현되어 있는 프로토콜 요소에 대한 추상화된 인터페이스를 말한다.
  • 장치 파일의 일종으로 이해할 수 있다.
  • 일반 파일에 대한 개념이 대부분 적용된다.

추상화된 인터페이스라는 점은 fopen을 통해 좀 더 깊게 이해 할 수 있다.

예를 들어 fopen_s(&fp, "CON", "w");를 호출하면 장치 파일인 콘솔 즉, 화면으로 모든 출력이 나가는 것을 확인할 수 있다.

만약 CON이 TCP일 경우에는 네트워크로 데이터가 나가는 것이라고 생각할 수 있다.

 

이처럼 소켓은 본질이 파일이다.

 

파일에 대한 입출력이 발생할 때, 그 액세스의 주체는 프로세스다. 즉, 프로세스는 파일을 열고, 생성하며, 닫거나 삭제 할 수 있다.

 

그런데 만약 이 파일이 TCP 스택에 대한 추상화된 인터페이스를 제공하는 경우, 이 파일은 일반적인 파일이라 하지 않고 TCP 소켓이라고 부른다.

 

다시 말해 소켓프로그래밍의 또 다른 이름은 TCP라는 대상을 추상화 시킨 파일에 대한 입출력 방법론을 배우는 것이라고 할 수 있다. 파일에 쓰는 작업인 write에 대응하는 소켓 함수는 send, 파일을 읽는 작업인 read에 대응하는 소켓 함수는 receive, 파일을 닫는 함수 fclose에 대응하는 소켓 함수는 closesocket이다.

 

파일이 나오면 항상 엮어서 생각해야 될 단위는 스트림이다. 우리가 파일에 데이터를 쓸 때 연속해서 데이터가 붙듯이 소켓도 마찬가지다. 그래서 유저 모드 어플리케이션 수준(=L7)에서 소켓이 나오면 스트림이라는 말이 나와야하고, 이 스트림이 커널 모드로 내려가서 L4로 가면 데이터의 단위가 segment, L3로 가면 데이터의 단위가 packet으로 바뀌어 불린다.

 

직소 퍼즐처럼 세그먼트랑 패킷은 분리되어 조각 하나하나가 연속적으로 쭉 이어져 있다.

다시 말해 스트림은 기본적으로 패킷이 조립 되어 연속된 데이터들로 생각하면 되고 세그먼트와 패킷은 조각된 단편 하나다.

프로토콜이 적용된 파일 클라를 작성해보자.

 

if (::connect(hSocket,
	(SOCKADDR*)&svraddr, sizeof(svraddr)) == SOCKET_ERROR)
{
	ErrorHandler("서버에 연결할 수 없습니다.");
}		

//서버로부터 파일 리스트를 수신한다.
GetFileList(hSocket);

//전송받을 파일을 선택하고 수신한다.
GetFile(hSocket);

 

클라에서는 서버와 연결이 성공하면 서버로부터 파일 목록을 수신하고,

파일을 선택하여 서버로부터 파일을 수신한다.

 

GetFileList

void GetFileList(SOCKET hSocket)
{
    //서버에 파일 리스트를 요청한다.
    MYCMD cmd = { CMD_GET_LIST, 0 };
    ::send(hSocket, (const char*)&cmd, sizeof(cmd), 0);

    //서버로부터 파일 리스트를 수신한다.
    ::recv(hSocket, (char*)&cmd, sizeof(cmd), 0);
    if (cmd.nCode != CMD_SND_FILELIST)
    {
        ErrorHandler("서버에서 파일 리스트를 수신하지 못했습니다.");
    }		

    SEND_FILELIST filelist;
    ::recv(hSocket, (char*)&filelist, sizeof(filelist), 0);

    //수신한 리스트 정보를 화면에 출력한다.
    FILEINFO fInfo;
    for (unsigned int i = 0; i < filelist.nCount; ++i)
    {
        ::recv(hSocket, (char*)&fInfo, sizeof(fInfo), 0);
        printf("%d\t%s\t%d\n",
            fInfo.nIndex, fInfo.szFileName, fInfo.dwFileSize);
    }
}

 

서버에 파일 목록을 수신받는다.

 

GetFile

void GetFile(SOCKET hSocket)
{
    int nIndex;
    printf("수신할 파일의 인덱스(0~2)를 입력하세요.: ");
    fflush(stdin);
    scanf_s("%d", &nIndex);

    //1. 서버에 파일 전송을 요청
    BYTE *pCommand = new BYTE[sizeof(MYCMD) + sizeof(GETFILE)];
    memset(pCommand, 0, sizeof(MYCMD)+sizeof(GETFILE));

    MYCMD *pCmd = (MYCMD*)pCommand;
    pCmd->nCode = CMD_GET_FILE;
    pCmd->nSize = sizeof(GETFILE);

    GETFILE *pFile = (GETFILE*)(pCommand + sizeof(MYCMD));
    pFile->nIndex = nIndex;
    //두 헤더를 한 메모리에 묶어서 전송한다!
    ::send(hSocket,
        (const char*)pCommand, sizeof(MYCMD) + sizeof(GETFILE), 0);
    delete [] pCommand;

    //2. 전송받을 파일에 대한 상세 정보 수신.
    MYCMD cmd = { 0 };
    FILEINFO fInfo = { 0 };
    ::recv(hSocket, (char*)&cmd, sizeof(cmd), 0);
    if (cmd.nCode == CMD_ERROR)
    {
        ERRORDATA err = { 0 };
        ::recv(hSocket, (char*)&err, sizeof(err), 0);
        ErrorHandler(err.szDesc);
    }
    else
    {
        ::recv(hSocket, (char*)&fInfo, sizeof(fInfo), 0);    
    }
		

    //3. 파일을 수신한다.
    printf("%s 파일 수신을 시작합니다!\n", fInfo.szFileName);
    FILE *fp = NULL;
    errno_t nResult = fopen_s(&fp, fInfo.szFileName, "wb");
    if (nResult != 0)
    {
        ErrorHandler("파일을 생성 할 수 없습니다.");
    }		

    char byBuffer[65536]; //64KB
    int nRecv;
    DWORD dwTotalRecv = 0;
    while ((nRecv = ::recv(hSocket, byBuffer, 65536, 0)) > 0)
    {
        fwrite(byBuffer, nRecv, 1, fp);
        dwTotalRecv += nRecv;
        putchar('#');

        if (dwTotalRecv >= fInfo.dwFileSize)
        {
            putchar('\n');
            puts("파일 수신완료!");
            break;
        }
    }

    fclose(fp);
}

 

서버로부터 받은 파일 목록 중에서 원하는 파일을 선택하고,

기본 헤더와 CMD_GET_FILE을 동적할당 받은 메모리에 쓰고 서버로 전송한다.

 

이후 서버로부터 파일 정보를 헤더로 받고, 파일을 서버로부터 수신한다.

프로토콜이 적용된 파일 서버를 작성해보자.

 

MYCMD cmd;
while (::recv(hClient, (char*)&cmd, sizeof(MYCMD), 0) > 0)
{
	switch (cmd.nCode)
	{
	case CMD_GET_LIST:
		puts("클라이언트가 파일목록을 요구함.");
		SendFileList(hClient);
		break;

	case CMD_GET_FILE:
		puts("클라이언트가 파일전송을 요구함.");
		{
			GETFILE file;
			::recv(hClient, (char*)&file, sizeof(file), 0);
			SendFile(hClient, file.nIndex);
			break;
		}

	default:
		ErrorHandler("알 수 없는 명령을 수신했습니다.");
		break;
	}
}

 

recv 함수를 이용해 MYCMD 크기만큼 끊어서 가져온다.

nCode 즉, 명령 코드에 따라 switch-case로 분류해서 함수를 실행한다.

 

SendFile 함수 ( 파일을 클라에게 송신하는 함수 )

void SendFile(SOCKET hClient, int nIndex)
{
    MYCMD cmd;
    ERRORDATA err;
    //파일 리스트에서 인덱스에 맞는 파일을 검사한다.
    if (nIndex < 0 || nIndex > 2)
    {
        cmd.nCode = CMD_ERROR;
        cmd.nSize = sizeof(err);
        err.nErrorCode = 0;
        strcpy_s(err.szDesc, "잘못된 파일 인덱스 입니다.");

        //오류 정보를 클라이언트에게 전송.
        send(hClient, (const char*)&cmd, sizeof(cmd), 0);
        send(hClient, (const char*)&err, sizeof(err), 0);
        return;
    }

    //파일 송신 시작을 알리는 정보를 전송한다.
    cmd.nCode = CMD_BEGIN_FILE;
    cmd.nSize = sizeof(FILEINFO);
    send(hClient, (const char*)&cmd, sizeof(cmd), 0);
    //송신하는 파일에 대한 정보를 전송한다.
    send(hClient, (const char*)&g_aFInfo[nIndex], sizeof(FILEINFO), 0);

    FILE *fp = NULL;
    errno_t nResult = fopen_s(&fp, g_aFInfo[nIndex].szFileName, "rb");
    if (nResult != 0)
        ErrorHandler("전송할 파일을 개방할 수 없습니다.");

    //파일을 송신한다.
    char byBuffer[65536]; //64KB
    int nRead;
    while ((nRead = fread(byBuffer, sizeof(char), 65536, fp)) > 0)
    {
        send(hClient, byBuffer, nRead, 0);
    }		

    fclose(fp);
}

 

파일을 클라에게 전송하는 함수.

클라가 원하는 파일의 인덱스를 선택하면 서버가 해당 파일을 클라한테 전송한다.

 

MYCMD 헤더 정보로 전송할 파일에 대한 정보를 담아 먼저 보내주고 ( 2번으로 나눠서 보냄 ), 

파일 데이터를 읽어 클라에게 전송한다. 

 

Wireshark를 이용해서 실제로 데이터가 어떻게 갔는지 살펴보자.

 

 

cmd.nCode = CMD_BEGIN_FILE;
cmd.nSize = sizeof(FILEINFO);
send(hClient, (const char*)&cmd, sizeof(cmd), 0);

 

위 그림은 위 코드를 Wireshark로 살펴본 결과다. 

00 00 00 c9 : 201 은 나타내고 이는 CMD_BEGIN_FILE의 정수값과 같다.

00 00 01 08 : 264 은 FILEINFO의 크기를 나타낸다.

 

 

 

위 그림으로 클라가 선택한 파일의 이름이 전송되는 것을 확인할 수 있다.

프로토콜을 적용한 파일 송/수신을 작성해보자.

 

  • 기본 헤더와 확장 헤더로 나눠서 정의한다.
  • 기본 헤더는 이어지는 확장 헤더를 결정하기 위한 코드 값을 포함한다.
  • 코드 값에 따라 switch-case를 이용해 해당 코드에 따른 작업을 수행한다.

클라가 서버로 패킷을 스트림 형태로 전송했을 때, 서버에서는 recv를 이용해서 끊어내는 작업을 한다.

여기서 끊어낸다는 것은 패킷에 대해 헤더 크기만큼 끊어내서 헤더 정보를 알아내고, 헤더안에 있는 정보를 통해 나머지 페이로드를끊어내는 작업을 말한다.

 

기본 헤더 구조체

//기본헤더
typedef struct MYCMD
{
	int nCode;    //명령코드
	int nSize;    //데이터의 바이트 단위 크기
} MYCMD;

 

헤더는 명령 코드 값과 헤더를 제외한 페이로드의 크기를 담을 변수로 구성된다.

명령 코드는 확장 헤더의 해석 방식을 결정하는 역할을 한다.

사이즈 값은 데이터 수신의 완료 여부를 판단하는 기준으로 활용한다.

 

확장 헤더 구조체

//확장헤더: 에러 메시지 전송헤더
typedef struct ERRORDATA
{
    int nErrorCode;    //에러코드: ※향후 확장을 위한 멤버다.
    char szDesc[256];  //에러내용
} ERRORDATA;

 

//확장헤더: S->C: 파일 리스트 전송
typedef struct SEND_FILELIST
{
    unsigned int nCount;    //전송할 파일정보(GETFILE 구조체) 개수
} SEND_FILELIST;

 

기본 헤더 구조체 명령 코드에 따라 해석할 구조를 나타낸다.

 

명령 코드 열거형

//MYCMD 구조체의 nCode 멤버에 적용될 수 있는 값
typedef enum CMDCODE {
    CMD_ERROR = 50,          //에러
    CMD_GET_LIST = 100,      //C->S: 파일 리스트 요구
    CMD_GET_FILE,            //C->S: 파일 전송 요구
    CMD_SND_FILELIST = 200,  //S->C: 파일 리스트 전송
    CMD_BEGIN_FILE           //S->C: 파일 전송 시작을 알림.
} CMDCODE;

 

열거형을 이용해 명령코드를 정수형으로 정의한다.

일정 수 만큼 간격을 두어서 추후에 같은 카테고리안에 있는 에러 코드를 추가할 수 있도록 한다.

인프런으로 강의를 듣던중에 K-Shield 수업을 알게 되어, 기초 단계인 K-Shield Jr 수업을 신청하고 듣게 되었다.

 

https://academy.kisa.or.kr/cont/programInfo/kShieldJunior.do

 

KISA 아카데미-교육포털 : K-Shield 주니어

사이버보안 분야 입문 및 진출을 희망하는 교육생을 대상으로 정보보호 직무별 역량 강화를 위한 교육과정 입니다. 실무 위주의 교육 및 현장 문제해결형 프로젝트로 사이버보안 실무 인력을

academy.kisa.or.kr

 

위 사이트에서 신청할 수 있다. 나이 제한이 있는데 만19세 ~ 만34세까지 신청할 수 있고 온라인으로 진행된다.

 

서버에 있어서 보안은 정말 중요한 부분이라고 생각하고 있었는데, 질 좋은 강의를 찾고 운좋게 강의를 들을 수 있게 되어 매우 기쁘다.

 

2주간 모든 수업을 들어야하기 때문에 수업을 들으며 내용을 블로그에 정리할 예정이다.

 

TransmitFile을 이용하면 파일을 송신하기 전에 파일에 대한 정보를 보낼 수 있었다.

따라서 헤더파일이라고도 할 수 있는 구조체의 정보를 토대로 파일을 수신하면 된다.

 

//수신할 파일명, 크기 정보를 먼저 받는다.
MY_FILE_DATA fData = { 0 };
if (::recv(hSocket, (char*)&fData, sizeof(fData), 0) < sizeof(fData))
	ErrorHandler("파일 정보를 수신하지 못했습니다.");

 

파일을 수신하기 전에 헤더를 받아서 파일의 대한 정보를 받는다.

 

//수신할 파일을 생성한다.
puts("*** 파일 수신을 시작합니다. ***");
HANDLE hFile = ::CreateFileA(
	fData.szName,
	GENERIC_WRITE,
	0,
	NULL,
	CREATE_ALWAYS,	//파일을 생성한다.
	0,
	NULL);
if (hFile == INVALID_HANDLE_VALUE)
	ErrorHandler("전송할 파일을 개방할 수 없습니다.");

//서버가 전송하는 데이터를 반복해 파일에 붙여 넣는다.
char byBuffer[65536];		//64KB
int nRecv;
DWORD dwTotalRecv = 0, dwRead = 0;
while (dwTotalRecv < fData.dwSize)
{
	if ((nRecv = ::recv(hSocket, byBuffer, 65536, 0)) > 0)
	{
		dwTotalRecv += nRecv;
		//서버에서 받은 크기만큼 데이터를 파일에 쓴다.
		::WriteFile(hFile, byBuffer, nRecv, &dwRead, NULL);
		printf("Receive: %d/%d\n", dwTotalRecv, fData.dwSize);
		fflush(stdout);
	}
	else
	{
		puts("ERROR: 파일 수신 중에 오류가 발생했습니다.");
		break;
	}
}

 

앞서 받은 헤더 정보를 바탕으로 파일을 서버로부터 수신 받는다.

TransmitFile 함수는 Windows에서 소켓을 통해 파일을 전송할 때 사용하는 함수다.
주로 TCP 소켓과 함께 사용된다.

 

BOOL TransmitFile(
  SOCKET hSocket,
  HANDLE hFile,
  DWORD nNumberOfBytesToWrite,
  DWORD nNumberOfBytesPerSend,
  LPOVERLAPPED lpOverlapped,
  LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,
  DWORD dwFlags
);

 

  • hSocket : 데이터를 전송할 대상 소켓의 핸들
  • hFile : 전송할 파일의 핸들. CreateFile을 통해 얻은 읽기 가능한 파일 핸들이어야 한다.
  • nNumberOfBytesToWrite : 전송할 바이트 수를 말한다. 0을 지정하면 파일의 끝까지 전송한다.
  • nNumberOfBytesPerSend : 한 번에 보낼 데이터 바이트 수다. 0이면 시스템이 적절한 크기로 자동 설정한다.
  • lpOverlapped : 비동기(Overlapped) 전송을 위한 OVERLAPPED 구조체 포인터다.
  • lpTransmitBuffers : 전송 전에 붙일 헤더 데이터를 지정하는 구조체 포인터다. 사용하지 않으면 NULL
  • dwFlags : 전송 동작을 제어하는 플래그

위 함수를 이용해서 파일 송신하는 서버를 만들어보자.

//전송할 파일에 대한 정보를 담기위한 구조체
typedef struct MY_FILE_DATA
{
    char szName[_MAX_FNAME];
    DWORD dwSize;
} MY_FILE_DATA;

 

구조체를 선언한다. 

전송할 파일에 대한 간단한 정보를 담고 있다.

  • szName : 전송할 파일의 이름
  • dwSize : 전송할 파일의 총 크기
//전송할 파일 개방
HANDLE hFile = ::CreateFile(_T("Sleep Away.zip"),
    GENERIC_READ,
    FILE_SHARE_READ,
    NULL,
    OPEN_EXISTING,
    FILE_FLAG_SEQUENTIAL_SCAN,
    NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
    ErrorHandler("전송할 파일을 개방할 수 없습니다.");
}

 

파일을 CreateFile을 이용해 생성한다. fopens으로 하면 HANDLE 값을 반환받을 수 없기 때문.

 

//전송할 파일에 대한 정보를 작성한다.
MY_FILE_DATA fData = { "Sleep Away.zip", 0 };
fData.dwSize = ::GetFileSize(hFile, NULL);
TRANSMIT_FILE_BUFFERS tfb = { 0 };
tfb.Head = &fData;
tfb.HeadLength = sizeof(fData);

 

앞서 선언한 구조체에 전송할 파일의 정보를 작성한다. 

 

//파일송신
if ( ::TransmitFile(
        hClient,	//파일을 전송할 소켓 핸들.
        hFile,		//전송할 파일 핸들.
        0,			//전송할 크기. 0이면 전체.
        65536,		//한 번에 전송할 버퍼 크기.
        NULL,		//비동기 입/출력에 대한 OVERLAPPED 구조체.
        &tfb,		//파일 전송에 앞서 먼저 전송할 데이터.
        0			//기타 옵션.
        ) == FALSE )
    ErrorHandler("파일을 전송할 수 없습니다.");

 

TransmitFile을 사용해 파일을 송신한다.

 

https://learn.microsoft.com/ko-kr/windows/win32/api/mswsock/nf-mswsock-transmitfile

 

TransmitFile 함수(mswsock.h) - Win32 apps

TransmitFile 함수(mswsock.h)는 연결된 소켓 핸들을 통해 파일 데이터를 전송합니다.

learn.microsoft.com

 

+ Recent posts