본문 바로가기

윈도우 소켓 프로그래밍

[소켓프로그래밍] 1. 소켓 생성과 닫기, 바이트 정렬

이 게시글은 'TCP/IP 윈도우 소켓 프로그래밍 (김선우 저, 한빛아카데미)'를 공부한 내용을 기반으로 작성됨

 

 

#define _WINSOCK_DEPRECATED_NO_WARNINGS // winsock c4996 처리

#include <iostream>
#include <winsock2.h>
#include <thread>
using namespace std;

#pragma comment(lib,"ws2_32.lib") // ws2_32.lib 라이브러리를 링크



void err_quit(LPCTSTR msg)
{
	LPVOID lpMsgBuf;
	FormatMessage(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, WSAGetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPTSTR)&lpMsgBuf, 0, NULL);
	MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);
	LocalFree(lpMsgBuf);
	exit(1);
}


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

	MessageBox(NULL, TEXT("윈속 초기화 성공"), TEXT("알림"), MB_OK);

	// socket()
	SOCKET tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
	if (tcp_sock == INVALID_SOCKET) err_quit(TEXT("socket()"));
	MessageBox(NULL, TEXT("TCP 소켓 생성 성공"), TEXT("알림"), MB_OK);

	// closesocket()
	closesocket(tcp_sock);


	WSACleanup();
	return 0;

}

 

 

위 코드를 봐도봐도 무슨 말인지 모르겠다. 하나하나 해부해보도록 하자.

 

 

 

 

에러 메시지 출력

가장 무섭게 생긴 부분이다.

 

void err_quit(LPCTSTR msg)
{
	LPVOID lpMsgBuf;
	FormatMessage(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, WSAGetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPTSTR)&lpMsgBuf, 0, NULL);
	MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);
	LocalFree(lpMsgBuf);
	exit(1);
}

 

 

사실 이 부분은 '에러 메시지 출력' 소켓을 열고 닫는데 영향을 주지는 않는다.

그치만 나같은 초보자는 한 번쯤 알아보고 넘어가는 게 좋겠다. (나.. 씨언어를 배운지 너무 오래돼서 아무런 기억도 나지 않는 노베다.)

 

void err_quit(LPCTSTR msg)

 

'err_quit' 함수의 선언부

void : 이 함수는 값을 반환하지 않는다. 함수 내부에서 실행만 있다.

LPCTSTR msg : 여기서 LPCTSTR은 'const TCHAR*'의 약자로, 유니코드와 멀티바이트 문자 집합을 모두 지원하는 포인터 타입이라고 한다. (무슨 말인지 모르겠다. 나중에 이해하게 된다면 돌아와서 첨언해야지)

 

 

 

 

LPVOID lpMsgBuf;
	FormatMessage(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, WSAGetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPTSTR)&lpMsgBuf, 0, NULL);
	MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);
	LocalFree(lpMsgBuf);
	exit(1);

 

LPVOID : void*, void 포인터를 의미한다. 'LP'는 Windows API에서 사용되는 포인터 타입을 나타내는 접두사로, "Long Pointer"를 의미한다. 또한, void 포인터는 어떠한 타입의 데이터도 가리킬 수 있는 포인터이다.

LPVOID lpMsgBuf; : 'lpMsgBuf'라는 포인터 변수를 선언. 이 변수는 오류 메시지를 저장하기 위한 메모리 버퍼이다.

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM: 'FormatMessage'라는 함수가 호출되면 '시스템 메시지 가져오기', '메모리 버퍼 자동할당'을 수행하도록 지시하는 플래그

NULL : 메시지 소스는 지정되지 않음. 이 경우 시스템 메시지를 사용

WSAGetLastError() : 가장 최근의 소켓 오류 코드 반환. 이 오류 코드를 이용해서 오류 메시지를 생성

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT) : 기본 언어 ID 생성. 오류 메시지가 표시될 언어를 결정함.

(LPTSTR)&lpMsgBuf: FormatMessage : 생성된 오류 메시를 'lpMsgBuf'가 가리키는 메모리 버퍼에 저장

0 : 메시지의 최대 크기를 지정하지 않음. 이 경우에는 FormatMessage가 자동으로 크기를 조정

NULL : 함수에 전달되는 인수 목록이 없음

 

 

 

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

➜ 팝업 메시지 박스를 표시하는 함수

➜ NULL : 부모 창 핸들을 지정하지 않고, 팝업 창이 독립적으로 표시되도록 함

➜ (LPCTSTR)lpMsgBuf: FormatMessage로 생성된 오류 메시지를 팝업창에 표시

➜ msg : 함수에 전달된 메시지를 팝업 창의 제목으로 사용

➜ MB_ICONERROR : 팝업 창에 오류 아이콘 표시

 

 

LocalFree(lpMsgBuf);

➜ 'lpMsgBuf'가 가리키는 메모리 버퍼를 해제함. FormatMessage 함수가 할당한 메모리를 해제하여 메모리 낭비 방지.

 

 

exit(1);

➜ 1은 비정상 종료를 나타낸다. 에러가 발생한 상황이므로 1을 반환함으로써 OS에게 오류가 발생했음을 알림

 

 

 

 

 

소켓 열고 닫기

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

	MessageBox(NULL, TEXT("윈속 초기화 성공"), TEXT("알림"), MB_OK);

	// socket()
	SOCKET tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
	if (tcp_sock == INVALID_SOCKET) err_quit(TEXT("socket()"));
	MessageBox(NULL, TEXT("TCP 소켓 생성 성공"), TEXT("알림"), MB_OK);

	// closesocket()
	closesocket(tcp_sock);


	WSACleanup();
	return 0;

}

 

 

WSADATA wsa; : 윈속 API를 사용하기 위해 필요한 데이터를 담는 구조

 

WSAStartup(MAKEWORD(2, 2), &wsa) : 윈속 초기화. MAKEWORD(2, 2)는 Winsock ver 2.2를 사용하겠다는 의미이다. 윈속 초기화를 성공하면 0을, 실패하면 0이 아닌 값을 반환한다.

 

if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) : 윈속 초기화가 실패했다면? 밑 줄의 return 1을 이용하여 1을 반환하고 프로그램 종료 (일반적으로 에러가 발생했을 시 1을 반환한다.)

 

MessageBox(NULL, TEXT("윈속 초기화 성공"), TEXT("알림"), MB_OK); : 윈속 초기화가 성공했음을 사용자에게 알리기 위해 MessageBox를 띄운다. 맨 앞 NULL은 부모창을 지정하지 않고 독립적인 메시지 박스를 표시한다는 의미. 마지막 MB_OK는 확인 버튼만 있는 메시지 박스를 표시한다는 의미이다. 

 

SOCKET tcp_sock = socket(AF_INET, SOCK_STREAM, 0); : TCP 소켓을 생성. AF_INET는 IPv4 주소 체계를 사용한다는 의미이며, SOCK_STREAM은 TCP 프로토콜을 의미. 마지막 0은 기본 프로토콜을 사용 (TCP의 경우 기본 프로토콜은 IPPROTO_TCP)

 

if (tcp_sock == INVALID_SOCKET) err_quit(TEXT("socket()")); : 소켓 생성이 실패했는지 확인. 실패했다면 'INVALID_SOCKET'을 반환하며 'err_quit' 함수가 호출되며 프로그램 종료

 

MessageBox(NULL, TEXT("TCP 소켓 생성 성공"), TEXT("알림"), MB_OK); : TCP 소켓이 성공적으로 생성되었음을 MessageBox를 통해 알림

 

closesocket(tcp_sock); : 생성한 TCP 소켓을 닫는다.

 

WSACleanup(); : Winsock을 종료한다.

 

 

 

 

바이트 정렬

 

바이트 정렬(Byte ordering)이란 메모리에 데이터를 저장할 때 바이트 순서를 나타내는 용어이다.

  • 빅 엔디안(big-endian) : 최상위 바이트(MSB, Most Significant Byte)부터 차례로 저장
  • 리틀 엔디안(little-endian) : 최하위 바이트(LSB, Least Significant Byte)부터 차례로 저장

 

https://www.techtarget.com/whatis/definition/most-significant-bit-or-byte

 

위 그림은 MSB와 LSB를 이해하기 위한 예시이다. Decimal number 157을 binary로 표현하면 10011101이 되는데, bit가 왼쪽에 있을수록 significance가 올라가고, 오른쪽으로 갈수록 significance가 낮아진다고 생각하면 된다.

 

예를 들어 빅 엔디안 방식을 사용하여 0x12345678을 2bytes씩 정렬하여 저장한다면 0x12 / 0x34 / 0x56 / 0x78... 순으로 저장할 것이다.

반면 리틀 엔디안을 사용하여 저장한다면 빅 엔디안과 정확히 반대 순서로 데이터를 저장할 것이므로, 빅 엔디안으로 저장한 파일을 리틀 엔디안 방식으로 읽는 등의 오류가 발생하지 않도록 주의해야 한다.

 

 

 

int main(int argc, char* argv[])
{
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
		return 1;

	u_short x1 = 0x1234;
	u_long y1 = 0x12345678;
	u_short x2;
	u_long y2;


	// 호스트 바이트 -> 네트워크 바이트
	printf("[호스트 바이트 -> 네트워크 바이트]\n");
	printf("0x%x -> 0x%x\n", x1, x2 = htons(x1));
	printf("0x%x -> 0x%x\n", y1, y2 = htonl(y1));


	//네트워크 바이트 -> 호스트 바이트
	printf("\n[네트워크 바이트 -> 호스트 바이트]\n");
	printf("0x%x -> 0x%x\n", x2, ntohs(x2));
	printf("0x%x -> 0x%x\n", y2, ntohl(y2));


	// 잘못된 사용 예
	printf("\n[잘못된 사용 예]\n");
	printf("0x%x -> 0x%x\n", x1, htonl(x1));

	WSACleanup();
	return 0;
}

 

 

    u_short x1 = 0x1234; ➜ u_short는 unsigned_short를 의미. 따라서 x1은 2Bytes 부호없는 정수 '0x1234'로 초기화
    u_long y1 = 0x12345678; ➜ y1은 4Bytes 부호없는 정수 '0x12345678'
    u_short x2; u_long y2; ➜ x2와 y2는 변환된 값을 저장할 변수를 선언합니다.

 

printf("0x%x -> 0x%x\n", x1, x2 = htons(x1)); ➜ 2Bytes 값을 호스트 바이트 순서에서 네트워크 바이트 순서로 변환. x1의 값을 변환하여 x2에 저장.

printf("0x%x -> 0x%x\n", y1, y2 = htonl(y1)); ➜ 4Bytes 값을 호스트 바이트 순서에서 네트워크 바이트 순서로 변환. y1의 값을 변환하여 y2에 저장.

 

 

 

바이트 순서(Byte Order)

  • 호스트 바이트 순서: 컴퓨터의 CPU 아키텍처에 따라 다를 수 있는 바이트 순서. 예를 들어, x86 아키텍처는 리틀 엔디안(Little Endian) 방식을 사용.
  • 네트워크 바이트 순서: 인터넷 프로토콜(IP)에서 사용하는 표준 바이트 순서로, 빅 엔디안(Big Endian) 방식을 사용.

 

 

htons()란?

  • htons: host to network short
  • h: Host (호스트, 즉 로컬 머신의 바이트 순서)
  • n: Network (네트워크 표준 바이트 순서, 빅 엔디안)
  • s: Short (16비트 정수)
  • Host → Network이므로 Little Endian → Big Endian

 

ntohs()란?

  • ntohs : network to host short
  • Network → Host이므로 Big Endian → Little Endian

 

 

x2 = htons(x1) ➜ x1의 값을 htons()로 변환하여 x2에 저장.

x1 = 0x1234;로 저장되어 있으므로 이에 htons를 적용하면 Little Endian → Big Endian으로 데이터 저장 순서를 변환하여 x2 = 0x3412로 저장됨.