코승호딩의 메모장

[소켓 시작하기] 본문

네트워크 프로그래밍

[소켓 시작하기]

코승호딩 2023. 9. 10. 01:19

01 오류 처리

네트워크 프로그램에서는 오류 발생이 잦고, 오류 발생 확률도 비교적 높기 때문에 함수 호출 시 오류를 조사하여 구체적인 오류 내용을 알려주는 것이 매우 중요하다. 다음은 오류 처리 방법에 따른 세 가지 유형이다.

  • 오류를 처리할 필요가 없는 경우 : 리턴값이 없거나 호출 시 항상 성공하는 일부 소켓 함수
  • 리턴값만으로 오류를 확인하고 처리하는 경우 : 윈도우의 WSAStartup 함수
  • 리턴값으로 오류 발생을 확인하고, 구체적인 내용은 오류 코드로 확인하는 경우 : 대부분의 소켓 함수

소켓 함수의 리턴값에 오류 발생이 확인되었다면,

  • WSAGetLastError 함수 :  구체적인 오류 코드를 얻을 수 있다.
  • FormatMessage 함수 : 오류 코드에 대응하는 오류 메시지를 얻을 수 있다.

  1. dwFlags : FORMAT_MESSAGE_ALLOCATE_BUFFER 또는 FORMAT_MESSAGE_FROM_SYSTEM 값을 사용하는데 우선 첫 번째는 오류 메시지를 저장할 공간을 FormatMessage 함수가 알아서 할당한다는 의미이며 두 번째는 운영체제가 오류 메시지를 가져온다는 의미이다. 후자의 경우 lpSource와 Arguments에 NULL, nSize에 0을 넣으면 된다.
  2. dwMessageId : 오류 코드를 나타내며 WSAGetLastError 함수의 리턴값을 여기에 넣는다.
  3. dwLanguageId : 오류 메시지를 표시할 언어를 나타낸다. 
  4. lpBuffer : 오류 메시지의 시작 주소가 저장된다. 오류 메시지를 저장할 공간은 FormatMessage 함수가 알아서 할당하기 때문에 사용자는 주솟값을 저장할 변수를 여기에 넣어주면 된다. 오류 메시지 사용 후 LocalFree 함수를 사용해 시스템이 할당한 메모리를 반환해야 한다.

 

void err_quit(const char *msg)
{
	LPVOID lpMsgBuf;
    
	FormatMessageA(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, 
        	WSAGetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(char*)&lpMsgBuf, 
        	0, NULL);
        
	MessageBoxA(NULL, (const char *)lpMsgBuf, msg, MB_ICONERROR);
    
	LocalFree(lpMsgBuf);
	exit(1);
}

 

위 함수는 FormatMessage 함수를 사용해서 작성한 오류 처리 함수이다. err_quit 함수는 msg 인수로 전달된 문자열과 현재 발생한 오류 메시지를 화면에 메시지 상자로 표시하고 응용 프로그램을 종료한다. 

A가 붙은 함수를 사용한다는 것을 볼 수 있는데 이는 유니코드가 아닌 ANSI 버전을 사용하기로 명시한것이다.

만약 매개 변수에 const char이 아닌 const wchar_t가 온다면 W가 붙는 함수 유니코드를 사용해야할 것이다.

 

if (socket(...) == INVALID_SOCKET) err_quit("socket()");
if (bind(...) == SOCKET_ERROR) err_quit("bind()");

만약 위 코드의 bind 함수에서 오류가 발생한다면 오른쪽 그림과 같이 메시지 상자가 화면에 표시되며 확인을 눌면 응용 프로그램이 종료된다. 

 

 

err_quit 함수의 MessageBox 함수 대신 printf 함수를 사용하여 오류 메시지만 출력하고 응용 프로그램을 종료하지 않을 수 있다. ex) printf("[%s] %s \n", msg, (char*)lpMsgBuf) 로 출력이 가능하며 err_display 함수를 따로 사용한다.

이처럼 종료하지 않고 출력만 하는 이유는 사소한 오류가 발생할 때마다 종료하는 것은 비효율적이기 때문이다.


02 소켓 초기화와 종료

윈도우에서는 소켓 생성 전에 윈속 초기화가 필요하며 소켓 닫기 후에 윈속 종료 단계가 필요하다. 따라서 모든 윈속 프로그램은 최초로 소켓 함수를 호출하기 전에 반드시 윈속을 초기화 하는 함수 WSAStartup을 호출해야 한다.

 

WSAStartup 함수 : 프로그램에서 사용할 윈속 버전을 요청하여 윈속 라이브러리(WS2_32.DLL)를 초기화한다. 만약 실패하면 윈속 라이브러리가 메모리에 로드되지 않는다.

int WSAStartup (
	WORD wVersionRequested,
	LPWSADATA lpWSAData
);

성공: 0, 실패: 오류 코드
  • wVersionRequested : 프로그램이 요구하는 최상위 윈속 버전. 하위 8비트에 주 버전, 상위 8비트에 부 버전을 넣는다. 만약 3.2버전을 사용하고 싶다면 0x0203 또는 MAKEWORD(3, 2)를 사용한다. 
  • lpWSAData : 윈도우 운영체제가 제공하는 윈속 구현에 관한 정보를 얻을 수 있다(거의 사용 안함)

 

WSACleanup 함수 : 윈속 종료 함수로 프로그램을 종료할 때 호출해야 한다. 위 함수는 윈속 사용 중지를 운영체제에 알려 관련 리소스를 반환한다. 

int WSACleanup(void);

성공: 0, 실패: SOCKET_ERROR

03 소켓 생성과 닫기

소켓 생성

소켓을 사용해 통신하기 위해선 양쪽이 모두 같은 프로토콜을 사용해야만 한다. 

  • socket 함수 : 사용자가 요청한 프로토콜을 사용해 통신할 수 있도록 내부적으로 리소스를 할당하고 접근 가능한 핸들 값을 리턴한다. 이 핸들 값을 소켓 디스크립터라고 부르며 각종 소켓 함수를 호출할 때 인수로 전달한다.
SOCKET socket(
    int af, 		// 주소 체계
    int type, 		// 소켓 타입
    int protocol 	// 프로토콜
);

성공: 새로운 소켓, 실패: INVALID_SOCKET

 

통신하기 위해선 상대를 유일하게 지정할 수 있는 주소가 필요하다. 주소 체계란 이런 주소 지정 방식을 나타내는 값이다. 주소 체계는 네트워크 프로토콜의 종류에 따라 달라지므로, 프로토콜을 선택하는 것이 가장 첫 번째이다. 

다음 코드와 같이 가독성을 위해 매크로로 정의되어 있는 주소 체계를 socket 함수의 첫 번째 인자로 넣어주면 된다. 

#define AF_INET 2 	// Internetwork: UDP, TCP, etc.
#define AF_INET6 23	// Internetwork Version 6
#define AF_BTH 32 	// Bluetooth RFCOMM/L2CAP protocols

 

소켓 타입이란 사용할 프로토콜의 특성을 나타내는 값이다. 소켓 타입 또한 네트워크 프로토콜에 따라 종류가 달라지므로, 이를 선택하는 것이 두 번째이다. 앞서 [네트워크와 소켓 프로그래밍]에서 나왔듯이 TCP는 신뢰성 있는 데이터 전송 기능을 제공하는 연결형 프로토콜이고 UDP는 신뢰성 없는 데이터 전송기능을 제공하는 비연결형 프로토콜이다. 

예를 들어 TCP는 전화기라고 볼 수 있는데 연결이 되기 전까지는 어떠한 데이터도 보내지 않고 연결이 되어야 보낼 수 있기 때문이다. 그리고 UDP는 카카오톡이나 메신저라고 볼 수 있는데 연결이 되지 않더라도 일방적으로 보낼 수 있다.

따라서 다음 오른쪽 그림과 같이 각 프로토콜에 맞는 주소 체계와 소켓 타입을 지정할 수 있다.

 

위 그림과 같이 추소 체계와 소켓 타입만으로도 프로토콜을 결정할 수 있지만 일반적으로 주소 체계와 소켓 타입이 같아도 해당하는 프로토콜이 두 개 이상 존재한다. 따라서 명시적으로 프로토콜을 지정해야 하는데, TCP와 UDP는 주소 체계와 소켓 타입만으로 프로토콜을 결정할 수 있기 때문에 보통 socket 함수의 마지막 부분에 0을 넣어도 된다.


소켓 닫기

소켓은 통신을 마치면 관련 리소스를 반환해야 한다. 이때 closesocket 함수를 사용한다.

int closesocket(SOCKET s);

성공: 0, 실패: SOCKET_ERROR

 

마지막으로 소켓 생성과 닫기 예제를 실행해보자. 마찬가지로 아래 사이트에 예제가 나와 있다.

 

GitHub - promche/TCP-IP-Socket-Prog-Book-2nd: TCP/IP 소켓 프로그래밍 2판(한빛아카데미, 2022) 예제 코드입니

TCP/IP 소켓 프로그래밍 2판(한빛아카데미, 2022) 예제 코드입니다. Contribute to promche/TCP-IP-Socket-Prog-Book-2nd development by creating an account on GitHub.

github.com

 

int main(int argc, char* argv[])
{
	// 윈속 초기화
	WSADATA wsa;
	/* if (MAKEWORD(3, 2) != 0)
		return 1; */
	if (WSAStartup(0x0202, &wsa) != 0)
		return 1;

	printf("[알림] 윈속 초기화 성공\n");

	printf("Version : %d.%d \n", HIBYTE(wsa.wVersion), LOBYTE(wsa.wVersion));
	printf("HighVersion : %d.%d \n", HIBYTE(wsa.wHighVersion), LOBYTE(wsa.wHighVersion));
	printf("Description : %s\n", wsa.szDescription);
	printf("SystemStatus : %s\n", wsa.szSystemStatus);

	// 소켓 생성
	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
	if (sock == INVALID_SOCKET) err_quit("socket()");
	printf("[알림] 소켓 생성 성공\n");

	// 소켓 닫기
	closesocket(sock);

	// 윈속 종료
	WSACleanup();

	return 0;
}

코드를 분석하기 전, MAKEWORD의 매크로에 대해서 알아보자. MAKEWORD는 두 개의 BYTE 매개 변수를 입력 받아 WORD를 반환한다. 첫 번째 인자 bLow는 하위 1바이트에 해당하며 주 버전에 해당한다. 그리고 bHigh는 상위 1바이트에 해당하며 부 버전에 해당한다. 앞서 설명했듯이 윈속을 초기화할 때, WSAStartup에 첫 번째 인자에 버전에 해당하는 WORD를 넣어줘야 하는데 WORD는 2바이트이기 때문에 상위 8비트 하위 8비트에 각각 부, 주 버전을 넣어줘야 한다고 했다. 따라서 MAKEWORD를 통해서 바이트 두 개를 넘겨주게 되면 비트 연산을 통해 결합된 버전 값을 반환받을 수 있다. 때문에 만약 2.2버전을 사용하고 싶다면 MAKEWORD(2, 2)를 사용하면 되지만, 만약 MAKEWORD를 사용하고 싶지 않다면 하드 코딩 0x0202 16진수를 통해 상위에 2, 하위에 2를 채워 넣어주면 같은 의미가 된다.

 

위 코드는 윈속 초기화 > 소켓 생성 > 소켓 닫기 > 윈속 종료의 과정을 보여준다. 윈속 초기화 부분에 주석 친 것은 2바이트에 해당하는 버전에 MAKEWORD뿐만 아니라 0x0202와 같이 16진수 또한 들어갈 수 있다는 것을 보여준다. wsa.wVersion과 wsa.wHighVersion을 그냥 출력하게 되면 10진수 514 또는 16진수 202로 출력이 된다. 따라서 2.2 버전에 해당하는 2.2를 출력하기 위해서 소숫점 앞 자리와 뒷 자리를 나눠서 출력한다. 비트 연산을 굳이 힘들게 하지 않더라도 MAKEWORD를 F12를 통해 타고 들어가면 minwindef.h 헤더에 LOBYTE 매크로와 HIBYTE 매크로를 찾을 수 있다. 바로 상위 바이트만 사용하거나 하위 바이트만 따로 사용할 수 있는 매크로 함수이다. 이를 통해 출력을 하면 정확히 Version : 2.2 를 확인할 수 있을 것이다.

'네트워크 프로그래밍' 카테고리의 다른 글

[멀티스레드]  (0) 2023.10.11
[데이터 전송하기]  (0) 2023.09.27
[TCP 서버-클라이언트]  (0) 2023.09.26
[소켓 주소 구조체]  (0) 2023.09.12
[네트워크와 소켓 프로그래밍]  (0) 2023.09.09