BSD Socket | 2020. 6. 5. 23:54

BSD Socket 사용법 정리


WinSock 버전 TCP 서버측 코드


BSD Socket 프로그램은 WinSock 기반의 Windows 프로그램으로 포팅이 가능하다. 본 포스팅에서는 이전 내용(FreeBSD(+*nix) 버전 TCP 서버측 코드)에서 작성한 *nix용 소스 코드를 Windows용 프로그램으로 옮긴 예를 통해 WinSock에서 TCP 서버 코드를 작성하는 방법에 대해 정리한다.

*nix용 소스코드는 프롬프트 상에서 작동되기 때문에 printf 함수에 의한 표준 출력(stdout)으로 프로그램의 작동 과정을 출력하였지만, Windows에서는 프롬프트 상에서 구동되는 프로그램을 작성하는 경우보다는 Windows API 또는 MFC 기반의 창(Window) 형태의 프로그램을 작성하는 경우가 더 많을 것이므로 프로그램의 작동 과정도 표준 출력이 아니라 디버그 출력을 통해 나타낼 것이다. 변수 또는 버퍼의 값을 간편하게 출력하기 위해 OutputFormattedDebugString이라는 사용자 정의 함수를 사용하였다. 이 함수에 대한 선언과 정의는 다른 포스트([Windows API] OutputDebugString을 printf처럼 서식(포맷) 적용하여 사용하기)에 자세히 적어 두었다.

 

포팅된 코드

#pragma comment(lib, "ws2_32.lib")

#include <WinSock2.h>
#include <Windows.h>

#define SCK_PORT 9999
#define SCK_BUFF 512
#define SCK_MESG "Hello, TCP Client!"

VOID OutputFormattedDebugString(LPCTSTR format, ...);
DWORD WINAPI ClientThreadProc(LPVOID lpParameter);

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
	WSADATA wsaData;
	DWORD dwThreadId, dwClientThreadId;
	int cbaddrserver, cbaddrclient;
	SOCKADDR_IN addrserver, addrclient;
	SOCKET fdserver, fdclient;

	int sockresult;

	dwThreadId = GetCurrentThreadId();

	sockresult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (sockresult != 0) {
		OutputFormattedDebugString(TEXT("[FAILURE] WSAStartup(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] WSAStartup() @ dwThreadId = %p\n"), dwThreadId);

	fdserver = fdclient = INVALID_SOCKET;
	fdserver = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (fdserver == INVALID_SOCKET) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] socket(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] socket() @ dwThreadId = %p\n"), dwThreadId);

	cbaddrserver = sizeof(SOCKADDR_IN);
	ZeroMemory(&addrserver, cbaddrserver);
	addrserver.sin_family = AF_INET;
	addrserver.sin_port = htons(SCK_PORT);
	addrserver.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

	sockresult = bind(fdserver, (SOCKADDR *)&addrserver, cbaddrserver);
	if (sockresult != 0) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] bind(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] bind() @ dwThreadId = %p\n"), dwThreadId);

	sockresult = listen(fdserver, SOMAXCONN);
	if (sockresult != 0) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] listen(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] listen() @ dwThreadId = %p\n"), dwThreadId);

	while (TRUE) {
		cbaddrclient = sizeof(SOCKADDR_IN);
		ZeroMemory(&addrclient, cbaddrclient);

		fdclient = accept(fdserver, (SOCKADDR *)&addrclient, &cbaddrclient);
		if (fdclient == INVALID_SOCKET) {
			sockresult = WSAGetLastError();
			OutputFormattedDebugString(TEXT("[FAILURE] accept(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
			goto EXITPROC_WINMAIN;
		}
		OutputFormattedDebugString(TEXT("[SUCCESS] accept() @ dwThreadId = %p\n"), dwThreadId);

		dwClientThreadId = 0;
		if (CreateThread(NULL, 0, ClientThreadProc, (LPVOID)((INT_PTR)fdclient), 0, &dwClientThreadId) == NULL) {
			DWORD dwLastError = GetLastError();
			OutputFormattedDebugString(TEXT("[FAILURE] CreateThread(): 0x%08x @ dwThreadId = %p\n"), dwLastError, dwThreadId);
		}
		OutputFormattedDebugString(TEXT("[SUCCESS] CreateThread(): dwClientThreadId = %p @ dwThreadId = %p\n"), dwClientThreadId, dwThreadId);
	}

EXITPROC_WINMAIN:
	if (fdserver != INVALID_SOCKET) {
		sockresult = closesocket(fdserver);
		if (sockresult != 0) {
			sockresult = WSAGetLastError();
			OutputFormattedDebugString(TEXT("[FAILURE] closesocket(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
			ExitThread(sockresult);
		}
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] closesocket() @ dwThreadId = %p\n"), dwThreadId);

	sockresult = WSACleanup();
	if (sockresult != 0) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] WSACleanup(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		ExitThread(sockresult);
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] WSACleanup() @ dwThreadId = %p\n"), dwThreadId);

	return 0;
}

DWORD WINAPI ClientThreadProc(LPVOID lpParameter) {
	DWORD dwThreadId;

	int fdclient;
	int sockresult;

	int cbbuffserver, cbbuffclient;
	char buffserver[SCK_BUFF], buffclient[SCK_BUFF];
	int rwresult;

	dwThreadId = GetCurrentThreadId();
	fdclient = (int)((INT_PTR)lpParameter);

	cbbuffserver = sizeof(buffserver);
	ZeroMemory(buffserver, cbbuffserver);
	sprintf(buffserver, "%s @ dwThreadId = %p", SCK_MESG, dwThreadId);

	cbbuffserver = (strlen(buffserver) + 1) * sizeof(char);
	rwresult = send(fdclient, buffserver, cbbuffserver, 0);
	if (rwresult < 0) {
		// 오류가 발생하면 오류 내용을 출력 후 Thread를 종료시킨다.
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] send(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_THREAD;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] send(): %d byte(s) @ dwThreadId = %p\n"), rwresult, dwThreadId);
	OutputFormattedDebugString(TEXT("<< %hs\n"), buffserver);

	cbbuffclient = sizeof(buffclient);
	ZeroMemory(buffclient, cbbuffclient);
	rwresult = recv(fdclient, buffclient, cbbuffclient, 0);
	if (rwresult < 0) {
		// 오류가 발생하면 오류 내용을 출력 후 Thread를 종료시킨다.
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] recv(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_THREAD;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] recv(): %d byte(s) @ dwThreadId = %p\n"), rwresult, dwThreadId);
	OutputFormattedDebugString(TEXT(">> %hs\n"), buffclient);

EXITPROC_THREAD: // Thread가 종료되기 전 실행되어야 할 내용
	if (fdclient != INVALID_SOCKET) {
		sockresult = closesocket(fdclient);
		if (sockresult < 0) {
			// 오류가 발생하면 오류 내용을 출력 후 Thread를 종료시킨다.
			sockresult = WSAGetLastError();
			OutputFormattedDebugString(TEXT("[FAILURE] closesocket(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
			ExitThread(sockresult);
		}
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] closesocket() @ dwThreadId = %p\n"), dwThreadId);
	
	return 0;
}

전체적인 구조는 *nix버전과 크게 차이가 없다. socket, bind, listen 순서로 서버 소켓을 구성하고, 무한 루프 내에서 accept가 클라이언트를 기다리다가, 클라이언트 접속이 확인되면 새로운 thread를 생성하고, 새로운 thread에서 클라이언트와의 통신을 수행한다. 그러나 몇 가지 다른 부분을 살펴보면,

 

WSAStartup, WSACleanup


WSAStartupWSACleanup은 WinSock을 사용하기 전에 리셋하고, 사용 후 정리하는 함수이다. 이는 Unix/Linux의 Socket에는 없는 Windows만의 과정이다.

int WSAStartup(
	WORD wVersionRequested,
	LPWSADATA lpWSAData
);
int WSACleanup(void);

 

함수의 실행 결과는 정수로 리턴한다. 성공하면 0을 반환하고 그렇지 않으면 다른 값을 반환한다. 오류의 상세한 종류는 WSAGetLastError의 반환값으로 확인 가능하다. 단, WSAStartup이 실패할 경우 WinSock이 아예 적용되지 않은 상태(WinSock의 오류 코드 변수를 사용할 수 없는 상태)이기 때문에 WSAStartup이 반환하는 값이 곧 상세한 오류 내용이다.

int WSAGetLastError(void);

 

다음은 Windows 프로그램에서 WinSock을 시작하고 종료하는 예이다. 윈도우 운영체제는 Windows 95/NT를 전후로 하여 매우 크게 변하였고, WinSock역시 이 때를 기준으로 WinSock 1과 WinSock 2로 나뉜다. Windows 10을 쓰는 지금까지도 WinSock 2가 이어져 내려온다.

먼저 WinSock을 사용하려면 별도로 라이브러리(ws2_32.lib)와 헤더(ws2_32.h)를 끌어와야 한다.

#pragma comment(lib, "ws2_32.lib")
#include <WinSock2.h>

 

그리고 다음과 같은 뼈대를 바탕으로 변용해서 사용하면 된다. 이후의 함수 호출은 Unix/Linux와 같다.

WORD wVersionRequested;
WSADATA stWSAData;

/* using WinSock 2.2 */
wVersionRequested = MAKEWORD(2, 2);
if (WSAStartup(wVersionRequested, &stWSAData) == 0) {
	/* TODO: WinSock */
} else {
	/* TODO: 오류 처리 */
}

WSACleanup();

그리고 DWORD와 같은 자료형 및 각종 운영체제 함수들을 사용하기 위하여 윈도우 헤더를 가져온다.

#include <Windows.h>

 

SOCKET, SOCKADDR_IN


*nix에서 파일 디스크립터의 일종으로서 int형으로 다뤄지던 소켓 객체가 WinSock에서는 의도를 약간 변형시켜 SOCKET이라는 별도의 자료형으로 다뤄진다(어쨌거나 정수인 것은 마찬가지임). 또한 인터넷 주소를 지정하는 struct sockaddr_in 구조체는 SOCKADDR_IN으로 typedef되었다. 몰론 전자를 사용해도 된다.

 

그 외 선언의 차이


그 외 소켓 함수의 선언에서 세세한 차이가 있다.

*nix의 socket 선언

int socket(int domain, int type, int protocol);

WinSock의 socket 선언

SOCKET socket(int af, int type, int protocol);

 

*nix의 bind 선언

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

WinSock의 bind 선언

int bind(SOCKET s, const struct sockaddr FAR *name, int namelen);

이 부분에서 특히 struct sockaddr_in의 멤버 이름에 차이가 있음을 확인할 수 있다. AF_INET 모드로 인터넷 주소를 지정할 때 *nix에서는

struct sockaddr_in {
	sa_family_t    sin_family; /* address family: AF_INET */
	in_port_t      sin_port;   /* port in network byte order */
	struct in_addr sin_addr;   /* internet address */
};

와 같이 선언되었다면 WinSock에서는,

struct sockaddr_in {
	short          sin_family;
	unsigned short sin_port;
	struct in_addr sin_addr;
	char           sin_zero[8];
};

와 같이 sin_zero 멤버가 추가됨을 확인할 수 있다. 그러나 이 멤버는 어차피 사용되지 않고 0으로 리셋할 멤버이므로 무시한다 하더라도, sin_addr으로 명명된 struct in_addr 멤버는 *nix와 WinSock에서 중대한 차이가 있다. *nix와 WinSock 사이에 포팅을 할 때는 이 부분에서 "정의되지 않은 식별자" 오류가 높은 확률로 발생할 것이다.

*nix의 struct in_addr 선언은 다음과 같다.

struct in_addr {
	uint32_t       s_addr;     /* address in network byte order */
};

그러나 WinSock의 struct in_addr 선언은 다음과 같다.

struct in_addr {
	union {
		struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
		struct { u_short s_w1,s_w2; } S_un_w;
		u_long S_addr;
	} S_un;
}

struct in_addr 구조체 안에 S_un 공용체가 있다.

이 공용체 안에 xxx.xxx.xxx.xxx 형태의 4바이트 아이피 주소를 직접 적을 수 있는 S_un_b이 있고, 딱히 비중있는 용도는 없어 보이는 S_un_w가 있고, 또한 *nix의 s_addr과 같은 방식으로 접근하는 S_addr이 있다.

따라서 *nix에서 주소 지정을 위해 코딩했던 (struct sockaddr_in).sin_addr.s_addr의 접근은 WinSock에서 (struct sockaddr_in).sin_addr.S_un.S_addr이 된다.

 

*nix의 listenaccept 선언

int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

WinSock의 listenaccept 선언

int listen(SOCKET s, int backlog);
SOCKET accept(SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen);

 

closesocket


*nix에서 socket이 반환하는 정수는 파일 디스크립터의 일종이므로, 운영체제 파일 입출력 함수인 close로 소켓 통신을 통료할 수 있다. 그러나 Windows 운영체제는 파일 디스크립터에 해당하는 개념이 없고, 입출력 대상에 따라 사용하는 함수가 파편화되어 있다. Windows에서는 socket으로 생성한 소켓은 closesocket으로 종료시킨다.

int close(int fd); // unistd.h
int closesocket(SOCKET s); // WinSock2.h

소켓 종료에 성공하면 두 함수 모두 0을 반환한다. 그렇지 않은 경우 *nix 버전은 -1을 반환하고 errno에 오류 내용을 설정한다. WinSock 버전은 SOCKET_ERROR를 반환한다. 오류 내용은 WSAGetLastError로 확인할 수 있다.

 

Thread 작업에서 나타나는 차이


*nix와 Windows 둘 다 다중 스레드를 지원하기는 하지만, 세밀한 부분에서 차이가 있다.

먼저 *nix에서는 thread를 종료할 때 자동으로 실행되는 내용을 기술할 수 있는 pthread_cleanup_push 함수가 있었다. 그러나 Windows API에는 Thread가 종료될 때 특정 작동을 기술할 수 있는 기능이 없으므로, 부득이하게 goto 문을 사용하였다. 이는 온전한 Windows 응용 프로그램에서 event에 의한 실행으로 대체될 수 있다.

 

실행 결과 보기


다음은 소스 코드를 실행한 화면이다.

클라이언트와 문자열 송수신을 끝내고 다른 클라이언트의 연결을 기다리고 있는 TCP 서버 측 프로그램

현재 WinMain을 구동하고 있는 thread의 번호는 0xFFF688ED이다. 무한루프를 돌리면서 클라이언트를 기다리다가, 클라이언트 접속이 확인되면 새 스레드를 생성하여 클라이언트와 통신한다. 이 새로운 스레드의 번호는 0xFFF6A755이며 클라이언트가 접속할 때마다 전혀 다른 번호로 thread가 생성될 것이다. 또한 클라이언트로부터 문자열을 수신하여 클라이언트 측에서 구동하고 있는 thread의 번호가 0xFFF6242D임을 확인할 수 있었다. 이 번호는 실행 할 때마다 달라질 수 있다.

댓글