Computer Science 기본 지식/소켓 프로그래밍

[TCP/IP 소켓 프로그래밍] (5) TCP 기반 파일 전송

로파이 2021. 3. 28. 21:49

출처 : 열혈 TCP/IP 소켓 프로그래밍 윤성우 저

 

파일 전송과 TCP 연결 종료

서버에서 클라이언트로 혹은 그 반대의 방향으로 파일을 전송한다고 하자.

송신자는 파일 데이터를 송신할 것이고 자신의 소켓을 종료한다. 수신자는 파일 데이터를 수신하고 소켓을 종료할 것이다.

 

만약 수신자가 파일 데이터를 모두 수신했을 때, "Thank You"라는 종료의 의미를 송신자에게 전송한다 하자.

 

그렇다면, 수신자는 파일 데이터를 모두 수신했다는 것을 언제 알 수 있을까.

1. 단순 현 시점에서 읽은 데이터를 기준으로 데이터가 없다면 종료시키는 것도 지연에 따른 수신 문제로 일찍이 종료해버릴 수도 있다.

2. 송신 종료에 대한 규약을 문자로 정한다면? 이러한 문자가 파일 내용에 포함될 수 도 있다.

 

TCP에는 송신자가 수신자에게 더 이상 데이터를 보내지 않는다는 "EOF"를 명시적으로 보낼 수 있는 기능이 있다. 이러한 출력 스트림의 종료 뿐만아니라 자신의 입력 스트림, 즉 수신 기능을 비활성화할 수 도 있다.

 

- TCP의 Half Close

소켓의 입력 스트림, 출력 스트림 혹은 입출력 스트림을 종료하는 함수가 있다. 

기존 closesocket() 함수는 소켓을 완전히 종료시키 때문에 소켓의 기능 종료로 중간에 전송 중인 데이터는 수신 되지 않고 출력 버퍼에 남아있는 데이터도 송신되지 않는다.

 

- 윈도우 기준

int shutdown(SOCKET sock, int howto);
  • 성공 시 0, 실패 시 SOCKET_ERROR 반환
  • sock: 종료할 소켓의 핸들
  • howto: 종료 방법

- 종료 방법

  • SD_RECEIVE: 입력 스트림 종료, 더 이상 수신관련 함수 호출이 불가하며 입력 버퍼에 데이터가 들어와도 지워진다.
  • SD_SEND: 출력 스트림 종료, 더 이상 송신이 불가하다. 대신 출력 버퍼에 전송할 데이터가 남아 있다면, 남은 데이터가 전송된다.
  • SD_BOTH 입출력 스트림 종료, SD_RECEIVE, SD_SEND가 각각 호출된다.

TCP 기반 파일 전송 예제

 

출력 스트림 종료시 EOF를 전달하여 상대방의 수신 함수에서 EOF 반환을 통해 파일 끝을 알린다. 송신자는 출력 스트림만 종료 시키고 수신자가 "Thank You"를 보낼 때 수신할 수 있도록 입력 스트림 기능은 활성화 상태로 유지한다.

 

file_server_win.cpp

더보기
#include <WinSock2.h>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include "../ErrorHandling.h"
#define SEND_SIZE 64
#define PORT 9190
using namespace std;
/*
   파일 전송 서버 예제 입니다.
*/
int main(int argc, char* argv[])
{
	// 윈속 라이브러리 초기화
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");

	// 1. 서버 소켓 생성
	SOCKET hServSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServSock == INVALID_SOCKET)
		ErrorHandling("socket() error");

	// 2. IP 주소, Port 번호 할당
	SOCKADDR_IN servAddr = {};
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(PORT);
	if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("bind() error");

	// 3. 연결 대기 상태 진입
	if (listen(hServSock, 5) == SOCKET_ERROR)
		ErrorHandling("listen() error");

	// 4. 클라이언트 연결
	SOCKADDR_IN clntAddr = {};
	int szClntAddr = sizeof(clntAddr);
	SOCKET hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
	if (hClntSock == -1)
	{
		ErrorHandling("Accept() error");
	}

	// 5. 파일 전송
	FILE* pFile;
	fopen_s(&pFile, "../FileServer/file_server_win.cpp", "rb");
    
	// 파일 열기 실패
	if (!pFile)
	{
		printf("File Read Error\n");

		closesocket(hServSock);
		// 윈속 라이브러리 해제
		WSACleanup();
		return 0;
	}

	char buffer[SEND_SIZE] = {};
	while (1)
	{
		int readCnt = fread((void*)buffer, 1, SEND_SIZE, pFile);

		send(hClntSock, (char*)&buffer, readCnt, 0);

		if (readCnt < SEND_SIZE) break;
	}

	shutdown(hClntSock, SD_SEND);
	recv(hClntSock, (char*)buffer, SEND_SIZE, 0);
	printf("Message from client: %s \n", buffer);
	fclose(pFile);

	system("pause");
	closesocket(hServSock); closesocket(hClntSock);
	// 윈속 라이브러리 해제
	WSACleanup();

	return 0;
}

file_client_win.cpp

더보기
#include <WinSock2.h>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include "../ErrorHandling.h"
#define BUF_SIZE 64
#define IP "127.0.0.1"
#define PORT 9190
using namespace std;
/*
   파일 전송 클라이언트 예제 입니다.
*/
int main(int argc, char* argv[])
{

	// 윈속 라이브러리 초기화
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");

	// 1. 클라이언트 소켓 생성
	SOCKET hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
		ErrorHandling("socket() error");

	// 2. 서버 IP 주소, Port 번호 할당
	SOCKADDR_IN servAddr = {};
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(IP);
	servAddr.sin_port = htons(PORT);
	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("connect() error");
	else
		puts("Connected......");

	// 파일 수신
	FILE* pFile;
	fopen_s(&pFile, "file.txt", "wb");
    
    // 파일 열기 실패
	if (!pFile)
	{
		closesocket(hSocket);
		WSACleanup();
		return 0;
	}

	char message[BUF_SIZE] = {};
	int strLen = 0;
	while ((strLen = recv(hSocket, message, BUF_SIZE - 1, 0)) != 0)
	{
		fwrite(message, strLen, 1, pFile);
		message[strLen] = 0;
		printf("%s", message);
	}
	printf("\nFile Received\n");
	send(hSocket, "Thank you", 10, 0);
	fclose(pFile);

	system("pause");

	// 소켓 종료
	closesocket(hSocket);
	// 윈속 라이브러리 해제
	WSACleanup();

	return 0;
}

참고: 윤성우 저 TCP/IP 소켓 프로그래밍