윈도우 소켓? 그게 뭔데? 먹는거야?
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);
를 호출해주고 있는데, WSAStartup
은 ws2_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;
}
최종적으로 서버 코드는 위와 같이 나오게 됩니다.
여기까지 윈속 서버 소켓을 만드는 방법이었습니다. 다음 시간에는 본 편에 이어서 클라이언트 소켓을 만들고, 서버 소켓과 클라이언트 소켓이 서로 통신하는 것까지 진행해볼 계획입니다.