윈도우 소켓 2편 서버와 클라이언트

· by 정연한

지난편에서 우리는 소켓을 이용해 간단한 서버를 구성해보았습니다.

이번 편에서는 소켓을 활용해 클라이언트를 구성해보고, 서버 소켓과 클라이언트 소켓 간 통신을 진행해볼 계획입니다.

자, 그럼 저번 시간에 서버를 대충 구성했으니, 이번에는 클라이언트 코드를 구성해봅시다.

#pragma comment(lib, "Ws2_32")

#include <WS2tcpip.h>
#include <iostream>

using namespace std;

main 함수로 들어가기 전에 우선 전처리기 쪽 부터 보도록 합시다.

ws2_32 라이브러리를 가져오는 부분은 전과 동일한데, include 파트가 좀 달라졌죠?
iostream이야 통신 할거니까 화면에 입출력 하는데 쓰인다 치고, WinSock2.h는 없어지고 웬 WS2tcpip.h가 그 자리를 대신하고 있습니다.

WS2tcpip.h에는 IP 주소를 검색하는 데 사용되는 여러 함수들이 있는데, 이 헤더파일은 WinSock2.h의 내용을 포함하기 때문에 따로 WinSock2.hinclude 하지 않아도 정상적으로 윈속을 사용할 수 있습니다.

int main() {
	WSAData wsaData{ };
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	WSACleanup();
	return 0;
}

main함수를 보면 위의 부분까지는 지난번 서버와 동일하게 진행을 하게 됩니다.

그리고 윈도우 소켓을 구성하는 WSAStartup과 WSACleanup사이에 아래와 같이 코드를 짜줍시다.

auto sock{ socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) };

constexpr int PORT{ 12345 };

sockaddr_in addr{ AF_INET, htons(PORT) };
inet_pton(AF_INET, "127.0.0.1", &(addr.sin_addr.s_addr)); // WS2tcpip.h에 있음

이 부분 역시 그대로 socket 함수를 사용해 소켓 디스크립터를 구성하고, 소켓의 주소를 저장할 addr 객체를 생성해 초기화를 진행해줍니다.

이때, 저번과 달리 이번에는 inet_pton 함수를 사용해 addr.sin_addr.s_addr 위치에 값을 넣어주게 되는데, 이 함수를 사용하기 위해 위의 전처리기 부분에서 WS2tcpip.h를 포함 시켜주었습니다.

이 부분에선 inet_pton 함수를 통해 addr 객체에 서버의 IP주소를 입력하게 되며, 지금은 localhost 주소"127.0.0.1"를 사용하였지만 실제 사용시에는 실제 연결할 서버의 IP주소를 사용해야 합니다.

단, 실제 연결할 서버의 IP주소를 일일히 외우고 다니는 것은 무리고, 일반적으로는 해당 서버의 도메인 주소를 사용하게 됩니다.
이때, 해당 도메인 네임을 그대로 IP주소 위치에 대입하면 안되고, getaddrinfo 함수를 사용해 도메인 주소에서 IP주소의 정보를 추출해주어야 합니다.

	addrinfo* servinfo{ }; // IP 정보를 저장할 객체
	getaddrinfo("google.com", nullptr, nullptr, &servinfo);

	sockaddr_in addr{ AF_INET, htons(PORT) };
	addr.sin_addr = reinterpret_cast<sockaddr_in*>(servinfo->ai_addr)->sin_addr;

위의 코드에선 getaddrinfo 함수를 통해 주소 정보를 알아내기 위한 도메인 주소(여기선 임의로 google.com를 사용했습니다.)와 값을 받아오기 위한 servinfo 객체의 주소를 인자값으로 주어, 해당 정보를 servinfo 객체로 받아오게 됩니다.
그 후, servinfo->ai_addrsockaddr_in* 타입으로 형변환한 후 그 변수의 sin_addr 필드 값을 가져와 addr 객체의 sin_addr 필드에 대입해주어 도메인에서 IP주소를 가져와 넣어줄 수 있습니다.

다만, 본 포스트에선 로컬 서버를 사용할 것이기 때문에, 위의 코드는 참고만 하시고 나중에 외부 서버와 통신할 일이 생기신다면 그 때 사용하시는 것을 권장드립니다.

// 접속 될 때까지 무한 반복하면서 연결 시도
while (connect(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)));

위의 코드 다음으로, 서버에선 accept 함수를 통해 클라이언트와 연결을 진행했다면, 클라이언트에선 connect 함수를 통해 서버와의 연결을 진행합니다.

이때, 연결이 될 경우 connect 함수가 0을 반환하기 때문에 위와 같이 코드를 작성하게 되면, 연결이 되고 해당 반복문을 빠져나오게 됩니다.

자, 드디어 클라이언트 소켓도 서버와 연결하는 부분까지 코드를 작성했습니다.

이제 이 다음부터는 서버 코드도 같이 수정을 해주도록 하겠습니다.

서버 코드에선 accept 함수와 closesocket 함수 사이에 적어주시면 되며, 클라이언트 코드에선 connect함수와 closesocket 함수 사이에 적어주시면 됩니다.

	string sendMessage{ "hello" }; // 보낼 메세지 객체
	string recvMessage(1024, '\0'); // 받을 메세지 객체
	send(client_sock, &sendMessage.front(), sendMessage.length(), 0); // 메세지 송신
	recv(client_sock, &recvMessage.front(), 1024, 0); // 메세지 수신

	cout << recvMessage << endl;

코드를 살펴보면 sendMessage 객체와 recvMessage 객체를 선언해주고 있습니다.
이 둘은 수신할 메세지를 담는 문자열 객체와 받을 메세지를 담을 문자열 객체입니다.

위와 같이 코드를 짜고 서버와 클라이언트를 실행해 보면, 정상적으로 hello가 양측 콘솔에 뜨는 것을 볼 수 있습니다.

최종적으로 코드는 아래와 같이 나오게 됩니다.

서버 코드

#pragma comment(lib, "ws2_32")

#include <WS2tcpip.h>
#include <iostream>

using namespace std;

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) };

	string sendMessage{ "hello" }; // 보낼 메세지 객체
	string recvMessage(1024, '\0'); // 받을 메세지 객체
	send(client_sock, &sendMessage.front(), sendMessage.length(), 0); // 메세지 송신
	recv(client_sock, &recvMessage.front(), 1024, 0); // 메세지 수신

	cout << recvMessage << endl;

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

클라이언트 코드

#pragma comment(lib, "Ws2_32")

#include <WS2tcpip.h>
#include <iostream>

using namespace std;

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) };
	inet_pton(AF_INET, "127.0.0.1", &(addr.sin_addr.s_addr)); // IP주소

	while (connect(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)));

	string sendMessage{ "hello" }; // 보낼 메세지 객체
	string recvMessage(1024, '\0'); // 받을 메세지 객체
	send(sock, &sendMessage.front(), sendMessage.length(), 0); // 메세지 송신
	recv(sock, &recvMessage.front(), 1024, 0); // 메세지 수신

	cout << recvMessage << endl;

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


여기까지 소켓을 통한 서버-클라이언트 간 간단한 통신을 진행해보았습니다. 다음 글에서는 스레드를 사용해 실시간 채팅 프로그램을 만들어보도록 하겠습니다.