소켓( 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으로 바뀌어 불린다.
직소 퍼즐처럼 세그먼트랑 패킷은 분리되어 조각 하나하나가 연속적으로 쭉 이어져 있다.
다시 말해 스트림은 기본적으로 패킷이 조립 되어 연속된 데이터들로 생각하면 되고 세그먼트와 패킷은 조각된 단편 하나다.
클라가 서버로 패킷을 스트림 형태로 전송했을 때, 서버에서는 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;
열거형을 이용해 명령코드를 정수형으로 정의한다.
일정 수 만큼 간격을 두어서 추후에 같은 카테고리안에 있는 에러 코드를 추가할 수 있도록 한다.
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;
}
}