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

 

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

 

앞서 살펴본 파일 송신에서 데이터를 보낼 때, 데이터의 크기만큼 실제로 데이터가 갔는지 확인하는 코드가 존재하지 않는다.

인터넷 환경은 기본적으로 불안정하기 때문에 데이터가 실제로 갔는지 확인하는 코드가 반드시 필요하다.

 

파일 송신 서버

 // 파일 전송 (부분 전송 처리)
    char byBuffer[65536];  // 64KB 버퍼
    int nRead, nSent, i = 0;
    while ((nRead = fread(byBuffer, sizeof(char), sizeof(byBuffer), fp)) > 0)
    {
        int nTotalSent = 0;
        while (nTotalSent < nRead)
        {
            nSent = send(hClient, byBuffer + nTotalSent, nRead - nTotalSent, 0);
            if (nSent == SOCKET_ERROR)
            {
                ErrorHandler("데이터 전송 중 오류가 발생했습니다.");
            }
            nTotalSent += nSent;
            printf("[%04d] 전송된 데이터 크기: %d / 누적: %d\n", ++i, nSent, nTotalSent);
            fflush(stdout);
        }
    }

 

파일을 송신한다. 

송신을 했는데 실제 송신한 데이터가 읽어온 데이터보다 작을 경우 그 차이만큼 다시 전송한다.

앞선 파일 송신에 이어 파일 수신 예제를 살펴보자.

이 코드도 프로토콜이 없는 파일 수신이다.

 

//수신할 파일을 생성한다.
puts("*** 파일 수신을 시작합니다. ***");
FILE *fp = NULL;
errno_t nResult = fopen_s(&fp, "Sleep away.zip", "wb");
if (nResult != 0)
	ErrorHandler("파일을 생성 할 수 없습니다.");

 

수신할 파일을 생성한다. .zip로 생성했는데, zip 파일로 생성하고 압축을 풀면 crc체크로 오류를 체크해주기 때문에 테스트 하기에 딱이다. 정석대로 하려면 MD5라는 해시 알고리즘을 사용해서 무결성을 체크해줘야한다.

 

//서버가 전송하는 데이터를 반복해 파일에 붙여 넣는다.
char byBuffer[65536];		//64KB
int nRecv;
while ((nRecv = ::recv(hSocket, byBuffer, 65536, 0)) > 0)
{
	//서버에서 받은 크기만큼 데이터를 파일에 쓴다.
	fwrite(byBuffer, nRecv, 1, fp);
	putchar('#');
}

 

서버로부터 데이터를 수신받아 파일에 쓴다

간단한 파일 송신 예제를 살펴보자. 

프로토콜이 없는 파일 송신이다.

 

//전송할 파일 개방
FILE *fp = NULL;
errno_t nResult = fopen_s(&fp, "Sleep Away.zip", "rb");
if (nResult != 0)
	ErrorHandler("전송할 파일을 개방할 수 없습니다.");

 

전송할 파일을 연다.

 

//파일송신
char byBuffer[65536];		//64KB
int nRead, nSent, i = 0;
while ((nRead = fread(byBuffer, sizeof(char), 65536, fp)) > 0)
{
	//파일에서 읽고 소켓으로 전송한다.
	//전송에 성공하더라도 nRead와 nSent 값은 다를 수 있다!!!
	nSent = send(hClient, byBuffer, nRead, 0);
	printf("[%04d] 전송된 데이터 크기: %d\n", ++i, nSent);
	fflush(stdout);
}

 

 

함수 호출을 덜하고 송신 자체를 적게 하므로 파일을 송신할때 송신하는 크기는 클수록 보통 좋다. 

위 코드에서는 64KB 만큼 송신하지만, 어느정도가 적당한지는 실제로 용량을 바꾸면서 직접 알아내야한다.

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

[네트워크] 파일 송신 개선  (0) 2025.04.20
[네트워크] 파일 수신  (0) 2025.04.20
[네트워크] TCP 장애 유형  (0) 2025.04.20
[네트워크] TCP/IP 연결의 의미  (0) 2025.04.20
[네트워크] select  (0) 2025.04.17
  • Packet Loss ( 패킷이 유실된 경우 )
  • TCP Out of order ( 패킷의 순서가 뒤바뀐 경우 )
  • Retransmission과 Dup ACK
  • Zero - window ( 수신측 버퍼에 여유 공간이 하나도 없는 경우 )

TCP 계층 즉, 4계층 부터 아래 1계층 까지 따져봤을 때 발생할 수 있는 장애 유형이 있다.

 

📘 Packet Loss ( 패킷이 유실된 경우 )

인터넷이라는 네트워크 환경은 기본적으로 불안정하다.

그래서 인터넷 수준, L3 혹은 그 이하에서 발생하는 것 중에 가장 흔한 게 패킷이 아예 유실되어 버릴 수 있다.

자주 일어나는 일은 아니지만 간간히 일어난다.

 

📘 TCP Out of order ( 패킷의 순서가 뒤바뀐 경우 ).

수신 측에서 예상한 순서와 다른 TCP 세그먼트가 도착하는 현상을 말한다.

 

TCP는 연결이라는 개념을 포함하고 있는데, 데이터들이 날아올 때 순서를 따진다.

연결이 있고 순서 개념이 있다 보니 데이터가 왔을 때, 1번이 오면 그다음은 2번 그다음은 3번이 와야하는데

가끔 순서가 뒤바뀔 수 있다.

 

발생하는 주요 원인

  • 멀티패스 라우팅
    • 하나의 TCP 연결이 여러 네트워크 경로를 통해 전송될 경우
    • 각 경로마다 지연이 다르기 때문에 순서가 바뀌기 쉽다.
  • 네트워크 혼잡
    • 일부 패킷이 네트워크 장비에서 큐잉되거나 드롭되면서 재전송이 발생
    • 나중에 보낸 패킷이 먼저 도착하는 상황이 생길 수 있다.
  • 리트랜스미션
    • ACK 손실 또는 지연으로 인해 송신 측이 재전송하게 되어, 그 패킷이 원래 패킷보다 먼저 도착할 수 있다.

 

📘 Dup ACK (  Duplicate ACK )

Dup ACK는 말 그대로 중복된 ACK 패킷을 의미한다.

 

TCP 수신 측이 같은 ACK 번호를 반복해서 보낼 때, 이를 송신 측에서 중복 ACK로 인식한다.

이는 일반적으로 데이터 손실 또는 Out of order 상황을 의미한다.

 

특정 패킷이 안와서 재전송을 요청했는데 해당 패킷이 도착하고, 재전송한 패킷이 도착하면 

Dup ACK가 발생할 수 있는 것이다.

 

발생하는 주요 원인

  • 패킷 손실 
    • 가장 흔한 원인. 특정 시퀀스의 세그먼트가 손실되었을 때 발생
  • 패킷 재정렬
    • 순서가 어긋난 패킷이 먼저 도착하여, 이전 시퀀스 번호에 대한 ACK가 반복되는 경우

 

📘 Zero-window ( 수신측 버퍼에 여유 공간이 하나도 없는 경우 )

윈도우 사이즈가 0이 된다는 것은 수신 측에서 더 이상 데이터를 받을 수 없는 상태를 의미한다.
다시 말해 수신 측 버퍼가 꽉 찼다는 의미

 

소켓 수신 버퍼에 데이터가 차곡차곡 쌓일 때, 

recv를 이용해 소켓 수신 버퍼에 있는 데이터를 L7에 있는 버퍼에 데이터를 담는 속도보다

소켓 수신 버퍼에 데이터가 담기는 속도보다 빠르지 않으면 윈도우 사이즈가 0이 된다.

 

이는 위 3가지의 오류 사항과는 성격이 다르다.

위 3가지의 오류 사항은 네트워크 인프라 수준에서의 오류라고 생각 할 수 있는 반면

Zero-window는 개발자가 구현하는 코드에 따라 해결이 가능하기 때문이다.

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

[네트워크] 파일 수신  (0) 2025.04.20
[네트워크] 파일 송신  (0) 2025.04.20
[네트워크] TCP/IP 연결의 의미  (0) 2025.04.20
[네트워크] select  (0) 2025.04.17
[네트워크] I / O 멀티플렉싱  (0) 2025.04.17

+ Recent posts