728x90

오늘은 지난 포스트에 이어서 Winsock을 사용하여 간단한 server를 만들어보려고 한다.

 

지난 포스트에서 서버는 아래와 같은 절차가 있다고 언급했다.

 

  1. Winsock을 초기화합니다.
  2. 소켓을 만듭니다.
  3. 소켓을 바인딩합니다.
  4. 클라이언트에 대한 소켓에서 수신 대기합니다.
  5. 클라이언트에서 연결을 수락합니다.
  6. 데이터를 수신하고 보냅니다.
  7. 연결 끊기를 선택합니다.

위와 같은 순서로 한번 만들어 보려고 한다.

 

Winsock을 초기화합니다.

우선 헤더 파일 및 lib 파일과 연결한다.

Winsock 초기화에 대한 부분은 저번 포스트에서 다뤘으니 넘어가도록 하겠다.

#include <stdio.h>
#include <WinSock2.h>
#include <WS2tcpip.h>

#pragma comment(lib, "ws2_32.lib")

int main()
{
	WSADATA wsadata;
	WSAStartup(MAKEWORD(2, 2), &wsadata);
	/*...*/
	WSACleanup();
}

소켓을 만듭니다.

서버가 클라이언트 연결을 수신 대기할 수 있도록 listenSocket이라는 SOCKET 개체를 만든다.

socket 함수의 원형은 아래와 같다.

SOCKET WSAAPI socket( [in] int af, [in] int type, [in] int protocol );

af(address family specification)에 들어가는 값은 주로 AF_INET,(IPv4) AF_INET6(IPv6) 이다.

type에 들어가는 값은 소켓의 Type을 결정하게 되는데, 각각의 설명은 아래와 같다.

 

SOCK_STREAM 데이터 중복이나 경계 유지 없이 신뢰성 있는 양방향 연결 기반의 바이트 스트림을 지원합니다. 이 종류의 Socket은 단일 피어와 통신하며 이 소켓을 사용할 경우 통신을 시작하기 전에 원격 호스트에 연결해야 합니다. Stream은 Transmission Control Protocol(ProtocolType.Tcp) 및 AddressFamily.InterNetwork 주소 패밀리를 사용합니다.

 

SOCK_DGRAM의 경우 고정된 최대 길이(대개 작음)의 신뢰할 수 없고 연결 없는 메시지인 데이터그램을 지원합니다. 메시지가 손실되거나 중복될 수 있으며 메시지 순서가 잘못될 수도 있습니다. Socket 종류의 Dgram은 데이터를 보내고 받기 전에 연결하지 않고도 여러 피어와 통신할 수 있습니다. Dgram은 Datagram Protocol(ProtocolType.Udp)과 AddressFamily.InterNetwork 주소 패밀리를 사용합니다.

 

즉, 간단하게 설명하자면 TCP - SOCK_STREAM, UDP - SOCK_DGRAM이라고 설명 할 수있다.

 

protocol에는 어떤 프로토콜을 사용할 것인지에 대한 인자이다.

주로, TCP - IPPROTO_TCP, UPD - IPPROTO_UDP 이런식으로 들어가게 되지만,

사실상 type으로 거의 결정이 되므로, 0을 넣으면 type에 맞는 값으로 자동으로 설정된다.

 

이후 Listen 소켓의 정보를 저장해주면 된다.

SOCKADDR_IN 구조체를 사용한다.

inet_pton 함수를 이용하여 서버의 주소를 저장한다.

sin_family에는 IPv4인지 IPv6인지에 대한 값을 넣어준다. socket함수의 첫번째 인자로 들어가는 값과 같다.

sin_port는 몇번 포트를 할당 할 것인지에 대한 내용이다.

사용 가능한 Port 범위는 0 ~ 65535번까지 사용 가능하다(unsigned short)

0 ~ 1023까지는 well-known port 영역으로 예약 영역이라고 볼 수 있다.

1024~49151번까지는 등록된 포트 (registered port)이고, 주로 서버 소켓으로 사용하는 영역이다.

49152 ~ 65535번까지는 동적 포트로 client에서 자동으로 할당되는 영역이다. 

따라서 우리는 1024~49151중에서 하나 선택해서 사용하면 되겠다.

	/*...*/    
	SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN serverSockAddr;
	inet_pton(AF_INET, "127.0.0.1" , &serverSockAddr.sin_addr);
	serverSockAddr.sin_family = AF_INET;
	serverSockAddr.sin_port = htons(7777);
	/*...*/

소켓을 바인딩합니다.

bind 함수를 통해 소켓을 바인딩 한다.

들어가는 인자는 listen소켓, 위에서 저장한 소켓 정보 구조체, 해당 구조체 크기를 입력해야 한다.

SOCKADDR*로 형변환 하는 이유는 확장성때문이라고 한다.

우리가 사용하고 있는 SOCKADDR_IN은 IPv4인 환경에서만 사용되기 때문에 혹여나 다른 IPv6나 다른 주소체계일 경우에도 사용 가능하도록 SOCKADDR*로 형변환 하는것이라고 한다.

	/*...*/
	if (::bind(listenSocket, reinterpret_cast<SOCKADDR*>(&serverSockAddr), sizeof(serverSockAddr)) == SOCKET_ERROR) {
		wprintf(L"bind failed with error: %ld\n", WSAGetLastError());
	}
	/*...*/

클라이언트에 대한 소켓에서 수신 대기합니다.

이제 Listen 함수를 호출하므로써 실제로 클라이언트 접속을 처리할 준비가 됐다.

listen 함수에 listen소켓을 넣어주고, backlog를 넣어주는데 backlog에 넣어주는 값은 입력 요청을 받았을때 대기할 수 있는 큐의 최대값이다.

	/*...*/
	if (listen(listenSocket, 10) == SOCKET_ERROR) {
		wprintf(L"listen failed with error: %ld\n", WSAGetLastError());
	}
	/*...*/

클라이언트에서 연결을 수락합니다.

accept 함수를 통해 클라이언트의 연결 요청을 수락한다.

연결된 클라이언트의 정보를 얻기 위해 clientSockAddr이라는 SOCKADDR_IN 구조체를 하나 만든다.

clientSockSize는 해당 구조체의 크기를 저장한다.

accept함수에 listen소켓, clientSockAddr, clientSockSize를 넣고 호출하고 문제가 없다면 연결 요청한 client 소켓을 반환한다.

반환된 socket을 통해 데이터를 송수신 할수있다.

	/*...*/
    
	SOCKADDR_IN clientSockAddr;
	int clientSockSize = sizeof(clientSockAddr);
	memset(&clientSockAddr, 0, clientSockSize);
	SOCKET clientSock = accept(socket, reinterpret_cast<SOCKADDR*>(&clientSockAddr), &clientSockSize);
	if (clientSock == INVALID_SOCKET) {
		wprintf(L"accept failed with error: %ld\n", WSAGetLastError());
	}
	else
		wprintf(L"Client connected.\n");
	/*...*/

데이터를 수신하고 보냅니다.

recv 함수를 통해 위에서 연결된 소켓에서 데이터를 recv한다.

send 함수를 통해 받은 데이터를 그대로 다시 client에 전달한다. 

위 두 함수에 들어가는 인자들은 동일하다. 마지막에 0으로 설정한 부분은 flag로 OOB관련 설정을 할때 필요하다.

	/*...*/
	while (1)
	{
		char recvBuff[100];
		memset(recvBuff, 0, sizeof(recvBuff));
		int len = recv(clientSock, recvBuff, 100, 0);
		printf("recv data = %s\n", recvBuff);
		send(clientSock, recvBuff, len, 0);
	}
	/*...*/

 

이렇게 서버 코드를 한번 훑어봤다.

아래는 전체 소스 코드이다.

 

#include <stdio.h>
#include <WinSock2.h>
#include <WS2tcpip.h>

#pragma comment(lib, "ws2_32.lib")

int main()
{
	WSADATA wsadata;
	WSAStartup(MAKEWORD(2, 2), &wsadata);

	SOCKET listenSock = ::socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN serverSockAddr;
	inet_pton(AF_INET, "127.0.0.1", &serverSockAddr.sin_addr);
	serverSockAddr.sin_family = AF_INET;
	serverSockAddr.sin_port = htons(7777);
	
	if (::bind(listenSock, reinterpret_cast<SOCKADDR*>(&serverSockAddr), sizeof(serverSockAddr)) == SOCKET_ERROR) {
		wprintf(L"bind failed with error: %ld\n", WSAGetLastError());
	}
	
	if (listen(listenSock, 10) == SOCKET_ERROR) {
		wprintf(L"listen failed with error: %ld\n", WSAGetLastError());
	}

	SOCKADDR_IN clientSockAddr;
	int clientSockSize = sizeof(clientSockAddr);
	memset(&clientSockAddr, 0, clientSockSize);
	SOCKET clientSock = accept(listenSock, reinterpret_cast<SOCKADDR*>(&clientSockAddr), &clientSockSize);
	if (clientSock == INVALID_SOCKET) {
		wprintf(L"accept failed with error: %ld\n", WSAGetLastError());
	}
	else
		wprintf(L"Client connected.\n");

	while (1)
	{
		char recvBuff[100];
		memset(recvBuff, 0, sizeof(recvBuff));
		int len = recv(clientSock, recvBuff, 100, 0);
		printf("recv data = %s\n", recvBuff);
		send(clientSock, recvBuff, len, 0);
	}

	WSACleanup();
}

 

'Development > Network' 카테고리의 다른 글

멀티프로세싱, 멀티쓰레딩, 멀티플렉싱  (1) 2022.10.03
[Windows Socket] - Client  (0) 2022.09.08
[Windows Socket] - 시작하기  (0) 2022.09.08
복사했습니다!