BSD Socket | 2020. 6. 7. 08:18

BSD Socket 사용법 정리


WinSock 버전 TCP 클라이언트측 코드


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

*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_ADDR "192.168.204.169"
#define SCK_BUFF 512
#define SCK_MESG "Hello, TCP Server!"

void OutputFormattedDebugString(LPCTSTR format, ...);

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
	DWORD dwThreadId;

	WSADATA wsaData;

	SOCKET fdserver;
	SOCKADDR_IN addrserver;
	int cbaddrserver;

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

	int sockresult;

	dwThreadId = GetCurrentThreadId();

	ZeroMemory(&wsaData, sizeof(WSADATA));
	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 = 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] WSAStartup() @ dwThreadId = %p\n"), dwThreadId);

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

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

	rwresult = recv(fdserver, buffserver, cbbuffserver, 0);
	cbbuffserver = strlen(buffserver) * sizeof(char);
	if (rwresult < 0) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] recv(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] recv(): %d byte(s) @ dwThreadId = %p\n"), rwresult, dwThreadId);
	OutputFormattedDebugString(TEXT(">> %hs\n"), buffserver);

	cbbuffclient = sizeof(buffclient);
	ZeroMemory(buffclient, cbbuffclient);

	sprintf(buffclient, "%s @ dwThreadId = %p", SCK_MESG, dwThreadId);
	cbbuffclient = strlen(buffclient) * sizeof(char);
	rwresult = send(fdserver, buffclient, cbbuffclient, 0);
	if (rwresult < 0) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] send(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] send(): %d byte(s) @ dwThreadId = %p\n"), rwresult, dwThreadId);
	OutputFormattedDebugString(TEXT("<< %hs\n"), buffclient);

EXITPROC_WINMAIN:
	if (fdserver != INVALID_SOCKET) {
		sockresult = closesocket(fdserver);
		if (sockresult != 0) {
			sockresult = WSAGetLastError();
			OutputFormattedDebugString(TEXT("[FAILURE] closesocket(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		}
		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);
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] WSACleanup() @ dwThreadId = %p\n"), dwThreadId);

	return 0;
}

WSAStartup, WSACleanup, socket, closesocket은 이전 포스트(WinSock 버전 TCP 서버측 코드)와 동일하므로 생략한다. 클라이언트 측에서 사용하는 함수는 connect이다.

 

connect


*nix버전 connect 함수의 선언은 다음과 같다.

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // sys/types.h, sys/socket.h 순으로 include

WinSock 버전 connect 함수의 선언은 다음과 같다.

int connect(SOCKET s, const struct sockaddr FAR *name, int namelen); // WinSock2.h

이 함수에서도 인터넷 주소를 나타내는 구조체인 struct sockaddr_in(SOCKADDR_IN)이 사용된다. 관련 내용은 이미 이전 포스트 (WinSock 버전 TCP 서버측 코드)에서 소개하였다.

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

 

recv, recvfrom, send, sendto


클라이언트와 서버 양측이 데이터를 송수신할 때 사용하는 함수이다. 이미 살펴 보았듯이 TCP에서는 recv, send를 사용하고 UDP에서는 recvfrom, sendto를 사용한다. 4가지 함수의 *nix 선언과 비교하여 WinSock에서 선언된 것과 비교한다.

먼저 *nix에서 4가지 함수 선언은 다음과 같았다.

ssize_t recv(int sockfd, void *buf, size_t len, int flags); // sys/types.h, sys/socket.h 순으로 include
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // sys/types.h, sys/socket.h 순으로 include
ssize_t send(int sockfd, const void *buf, size_t len, int flags); // sys/types.h, sys/socket.h 순으로 include
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // sys/types.h, sys/socket.h 순으로 include

또한 *nix에서 sockfd는 파일 디스크립터의 일종이므로 운영체제 입출력 함수인 read, write를 사용하면 flags = 0인 호출과 동일하다고 하였다.

ssize_t read(int fd, void *buf, size_t count); // unistd.h
ssize_t write(int fd, const void *buf, size_t count); // unistd.h

size_t는 unsigned형 정수이지만 ssize_t는 signed형 정수이다. 송수신에 성공하면 바이트 수를 반환하고 그렇지 않으면 -1을 반환하고 errno에 그 내용이 기록된다. 이에 비해 WinSock 버전에서는 read, write 같은 함수는 사용할 수 없고 기본 소켓 함수만 호출 가능하다.

int recv(SOCKET s, char FAR *buf, int len, int flags); // WinSock2.h
int recvfrom(SOCKET s, char FAR *buf, int len, int flags, struct sockaddr FAR *from, int FAR *fromlen);
int send(SOCKET s, const char FAR *buf, int len, int flags);
int sendto(SOCKET s, const char FAR *buf, int len, int flags, const struct sockaddr FAR *to, int tolen);

WinSock의 위 함수들은 송수신에 성공하면 바이트 수를 반환하고 그렇지 않으면 SOCKET_ERROR를 반환한다. 오류 내용은 WSAGetLastError로 확인할 수 있다.

 

실행 결과 보기


소스 코드를 실행한 결과는 다음과 같다.

서버와 문자열을 주고받은 TCP 클라이언트측 프로그램

여기서 0xFFF6242D는 클라이언트 측 Thread ID이다. 서버로부터는 0xFFF6A755라는 서버측 Thread ID를 수신하였고 이것으로 WinSock 서버 및 클라이언트 프로그램이 정상적으로 작동함을 확인할 수 있다.

 

댓글