윈도우 소켓? 그게 뭔데? 먹는거야?

· by 정연한

C++를 사용해 간단한 통신용 프로그램을 짠다고 가정해봅시다.

일단 TCP/IP를 통해 패킷을 만들고 네트워크를 사용해서…

음 여기서부터 시작하는건 아무래도 무리겠네요.

기존에 만들어진 라이브러리를 좀 살펴볼까요?

구글링을 좀 해보니 소켓…? 어디선가 들어는 본 것 같은데 코드를 슬쩍보니 C++보단 C에 가까운 문법이네요.

솔직히 불-편하긴 하지만 최소한 바닥부터 시작하는 것 보다는 충분히 나은 대안인 것 같습니다.

아, 우리 Boost는 예외로 두자고요. 소켓 얘기 하는데 Boost 나오면 할게 없어져요…

자, 그래요 뭐 최고의 대안은 아니긴 하지만 일단 간단한 코드부터 살펴보자고요.

#pragma comment(lib, "Ws2_32")

#include <WinSock2.h>

int main() {
	WSAData wsaData{ }; // 윈속 객체 생성
	WSAStartup(MAKEWORD(2, 2), &wsaData); // 윈속 객체 초기화

	WSACleanup();
	return 0;
}

(진행하기 전에 잠깐, 본 포스팅에선 모든 초기화 문법을 유니폼 초기화 문법으로 통일해서 사용하고 있습니다!)

코드를 좀 살펴봅시다.

우린 윈도우 프로그래밍을 할 것이기 때문에 windows socket, 즉 winsock을 쓸거에요. 보통 한국어로 윈속이라고 부릅니다.

가장 위에선 컴파일러 지시자 #pragma comment를 통해 ws2_32 library를 포함시켜주고 있습니다.

이걸 해줘야만 빌드 과정에서 WinSock2.h 헤더에 있는 선언들이 ws2_32.lib에 있는 구현에 Linking 될 수 있기 때문에 반드시 써주어야 합니다!

아니 이게 도대체 뭔소린가… 하고 이해 못해도 괜찮아요. 일단 저게 있어야 윈도우에서 소켓을 사용할 수 있다는 것만 일단 아시면 됩니다!

자 main함수로 가봅시다.

가장 처음으로 WSAData wsa{ };가 오네요. WSAData 구조체 변수, 즉 WSAData 객체를 선언해주고 있습니다.

그리고 WSAStartup(MAKEWORD(2, 2), &wsa);를 호출해주고 있는데, WSAStartupws2_32.lib을 사용 할 수 있도록 초기화 하는데 사용하는 함수 입니다.

첫번째 인자인 MAKEWORD는 윈속 버전을 지정하기 위해 사용되는 매크로에요. MAKEWORD(2, 2)는 2바이트 16진수 정수 0x0202를 반환하고, WSAStartup은 이 값이 인자값으로 들어오면 “윈속 2.2버전을 사용하겠다” 라는 뜻으로 판단하고 wsa에 윈속 2.2버전의 데이터를 사용해 초기값을 넣어주게 됩니다.

자, 그리고 그 아래의 WSACleanup();ws2_32.lib를 종료하는 함수입니다.

간단히 말해 다 쓴 소켓 리소스를 반환하는 함수입니다.

일단 여기까지가 윈속을 사용하기 위한 기본 준비 과정이었고, 지금부터 본격적으로 소켓을 사용해볼까요?

// 소켓 디스크립터 생성
auto sock{ socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) };

sock 변수를 socket 함수의 반환값으로 초기화를 해주고 있습니다. socket 함수는 소켓 디스크립터를 반환하는 함수로 유닉스 소켓과의 호환성을 위해 윈도우에서도 이런 방식으로 소켓 디스크립터를 생성합니다.

인자값들을 보면, PF_INET, SOCK_STREAM, IPPROTO_TCP 가 순서대로 들어가 있는데, 각각 네트워크 주소체계: IPv4를 사용, 소켓 타입: Stream을 사용(TCP를 사용하는 방식), 프로토콜: TCP를 사용 이라고 이해하시면 됩니다.

특별한 일이 없다면 위의 코드를 그대로 사용하게 될거에요.

constexpr int PORT{ 12345 };
sockaddr_in addr{ AF_INET, htons(PORT) };
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 서버
inet_pton(AF_INET, "127.0.0.1", &(addr.sin_addr.s_addr)); // 클라이언트

그 다음으로 소켓의 주소를 저장할 객체를 생성해줍시다.

여기서부터 서버 소켓과 클라이언트 소켓이 나뉘게 되는데, 일단 이 포스팅에서는 서버 소켓을 먼저 설명드리겠습니다.

sockaddr_in을 통해 인터넷 프로토콜을 사용하는 소켓 주소 객체를 만들고, IPv4, port번호를 초기화 값으로 넣어줍니다. 여기서는 12345를 포트번호로 넣어주었습니다.

값을 넣을 때 그냥 넣지 않고, htons 함수를 사용해 넣어주었는데, 이 함수는 host to network short의 약자로 2바이트 정수값을 호스트의 엔디언에서 네트워크에서 사용하는 빅 엔디언으로 바꿔서 반환해주는 함수입니다.

통상적으로 사용하는 PC(x86_64)의 엔디언은 리틀 엔디언이기 때문에 이런식으로 바꿔주는 것입니다.

그런 다음 서버의 경우 addr.sin_addr.s_addr = htonl(INADDR_ANY);를 클라이언트의 경우 inet_pton(AF_INET, "127.0.0.1", &(addr.sin_addr.s_addr));를 써주시면 되는데, inet_pton 함수는 추가적인 헤더파일을 포함시켜줘야 합니다. 때문에 여기서 다루진 않고 다음 포스팅에서 설명드리도록 하겠습니다.

서버는 INADDR_ANY를 사용해 주소를 지정하게 되면, 사용할 수 있는 랜카드의 IP주소 중 현재 사용 가능한 IP주소를 선택하게 됩니다.

bind(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
listen(sock, SOMAXCONN);

sockaddr_in client_addr{ };
int client_size{ sizeof(client_addr) };
auto client_sock{ accept(sock, reinterpret_cast<sockaddr*>(&client_addr), &client_size) };

그 다음, 서버와 클라이언트를 연결하기 위해 bind 함수를 이용하여 서버 소켓에 필요한 정보를 할당해줍니다. 그리고 listen 함수를 사용하면, 클라이언트 접속 요청이 들어오는 것을 대기하게됩니다.

  • 참고) reinterpret_cast는 C++의 형변환 연산자입니다. 모든 포인터 타입간의 형변환을 허용하는 연산자이며, 심지어 포인터 타입을 포인터가 아닌 타입으로도 캐스팅이 가능하며 그 반대도 가능한 무시무시한 힘을 가진 연산자입니다. 본 코드에선 sockaddr_in 타입을 sockaddr 타입으로 형변환 시키기 위해 사용이 되었습니다.

그 다음 클라이언트와 통신을 하기 위해 클라이언트 주소 정보를 담을 client_addr 객체를 생성해 그 크기와 같이 accept 함수로 넘기게 되면, 클라이언트와 연결을 진행하게 됩니다.

이제 이 아래서부터 closesocket 함수가 나오기 전까지 서버 소켓은 클라이언트 소켓과 계속 연결을 유지하게 됩니다.

closesocket(sock);

연결을 끝내고 소켓을 정리하기 위한 함수입니다.

#pragma comment(lib, "ws2_32")

#include <WinSock2.h>

int main() {
	WSAData wsaData{ }; // 윈속 객체 생성
	WSAStartup(MAKEWORD(2, 2), &wsaData); // 윈속 객체 초기화

	// 소켓 디스크립터 생성
	auto sock{ socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) };

	constexpr int PORT{ 12345 };
	sockaddr_in addr{ AF_INET, htons(PORT) };
	addr.sin_addr.s_addr = htonl(INADDR_ANY);

	bind(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
	listen(sock, SOMAXCONN);

	sockaddr_in client_addr{ };
	int client_size{ sizeof(client_addr) };
	auto client_sock{ accept(sock, reinterpret_cast<sockaddr*>(&client_addr), &client_size) };

	closesocket(client_sock);
	closesocket(sock);
	WSACleanup();
	return 0;
}

최종적으로 서버 코드는 위와 같이 나오게 됩니다.


여기까지 윈속 서버 소켓을 만드는 방법이었습니다. 다음 시간에는 본 편에 이어서 클라이언트 소켓을 만들고, 서버 소켓과 클라이언트 소켓이 서로 통신하는 것까지 진행해볼 계획입니다.