이번 포스팅에서는 서로 통신을 하는 서버와 클라이언트를 다루어볼 것이다.
여기서 가장 간단한 형태인 iterative 방식의 echoServer를 구현하면서, 서버와 클라이언트 소켓생성과 통신까지 전체적인 과정을 살펴본다.

1) echoServer.cpp

#pragma comment(lib, "ws2_32")
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <stdio.h>
#include <winsock2.h>
#include <memory>
#define BUF_SIZE 1024

int main()
{
	printf("Server Start!\n");
    
 // 1) WinSock 라이브러리를 초기화한다   
	WSAData wsaData; //WinSock 초기화 구조체
	int initWinSockRes = WSAStartup(MAKEWORD(2, 0), &wsaData);

	if (initWinSockRes != 0) {
		printf("윈도우소켓 초기화 실패");
		return 0;
	}

 // 2) 서버와 클라이언트 소켓을 생성하고 서버소켓을 bind, listen한다.
	SOCKET servSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	SOCKET clntSocket;
	sockaddr_in servAddr, clntAddr;

	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	servAddr.sin_port = htons(11000);

	if (bind(servSocket, (sockaddr*)& servAddr, sizeof(servAddr)) != 0) {
		printf("bind() 오류 [오류코드: %d]\n", WSAGetLastError());
		return 0;
	}
	
    // 크기 3의 대기열을 만든다.
    // 클라이언트의 연결요청이 왔을때 서버에서 준비된 소켓이 없다면 대기열에서 연결을 기다리게된다. 
	if (listen(servSocket, 3) != 0) {
		printf("listen() 오류 [오류코드: %d]\n", WSAGetLastError());
		return 0;
	}
	
	char buf[BUF_SIZE] = {};
	int  clntAddrLen = sizeof(clntAddr);


	// 순차적으로 3개까지의 클라이언트를 연결해서 송수신작업을 한다.
	for (int i = 0; i < 3; i++) {
    
 // 3) 클라이언트가 연결을 요청하면 수락한다.
		clntSocket = accept(servSocket, (sockaddr*)& clntAddr, &clntAddrLen);
		if (clntSocket == -1) {
			printf("accept() 오류 [오류 코드: %d]\n", WSAGetLastError());
			return 0;
		}
		printf("accept client\n");

		while (1) {
			memset(buf, 0, BUF_SIZE);
            
		//4) 클라이언트로부터 온 메세지를 받는다.
			int recvBytes = recv(clntSocket, buf, BUF_SIZE, 0);
			if (recvBytes == SOCKET_ERROR) {
				printf("recv() 오류 [오류 코드: %d]\n", WSAGetLastError());
				break;
			}

			printf("[message from client]: %s (%d bytes)\n", buf, recvBytes);

			if (recvBytes == 0) {
				printf("클라이언트 연결 종료됨.");
				break;
			}
            
        //5) 클라이언트로부터 온 메세지를 그대로 클라이언트에게 전송한다.
			send(clntSocket, buf, recvBytes - 1, 0);
		}
		
	}

	WSACleanup();

	return 0;
}

 먼저 서버의 코드이다.

 한꺼번에 1명의 클라이언트를 받을 수 있으며, 한 개의 클라이언트와 통신중에 다른 클라이언트의 요청이 온다면 해당 클라이언트는 이전에 먼저 연결된 클라이언트가 종료될때까지 연결을 기다려야 한다.
 서버는 연결된 클라이언트가 종료될때까지 클라이언트로부터 오는 메세지를 받고, 그대로 전송하는 작업을 한다.
 3번까지 클라이언트에게 echoServer로써 기능하는 역할을 마무리하면 서버는 종료된다.


2) echoClient.cpp

#pragma comment(lib, "ws2_32")
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <string>
#include <iostream>
#include <winsock2.h>
#include <memory>
#define BUF_SIZE 1024

using namespace std;

int main() {
	WSAData wsaData;
	SOCKET clnt_socket;
	
	printf("Client Start!\n");

	// WINSOCK 초기화
	int initWinSockRes = WSAStartup(MAKEWORD(2, 0), &wsaData);

	if (initWinSockRes == SOCKET_ERROR) {
		return 0;
	}

	clnt_socket = socket(AF_INET, SOCK_STREAM, 0);
	if (clnt_socket == INVALID_SOCKET) {
		printf("윈도우소켓 초기화 실패");
		return 0;
	}

	sockaddr_in servAddr;

	// 로컬 주소로 연결한다. 포트는 서버에서 bind한 포트와 동일해야, 서버로 연결이 가능하다
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	servAddr.sin_port = htons(11000);


	if (connect(clnt_socket, (sockaddr*)& servAddr, sizeof(servAddr)) != 0) {
		printf("connect() 오류 [오류 코드: %d\]n", WSAGetLastError());
		return 0;
	}

	char input[BUF_SIZE];
	char received[BUF_SIZE];

	while (1) {
		memset(input, 0, BUF_SIZE);
		memset(received, 0, BUF_SIZE);

		cin.getline(input, BUF_SIZE);
		
		if (string{ input } == "quit") {
			closesocket(clnt_socket);
			break;
		}

		if ( send(clnt_socket, input, sizeof(input), 0) == SOCKET_ERROR ) {
			printf("send() 오류 [오류 코드: %d]\n", WSAGetLastError());
			break;
		}
		
		if (recv(clnt_socket, received, BUF_SIZE, 0) == SOCKET_ERROR ) {
			printf("recv() 오류 [오류 코드: %d]\n", WSAGetLastError());
			break;
		}

		printf("[message from server] %s \n", received);
	}

	return 0;
}

 다음은 클라이언트의 코드이다. 서버와 마찬가지로 WinSock라이브러리를 초기화한 뒤 서버에 연결한다.
서버와 연결되면 클라이언트에 보내고싶은 문자열을 입력해 전송이 가능하다. 전송된 문자열은 곧 서버로부터 똑같이 되돌려받는다.
 만일 quit라고 입력한다면, 클라이언트는 종료되며 서버는 다음 클라이언트를 받을 준비를 한다(3번의 서비스를 했다면 서버도 종료된다)

 


3) 실행 결과

먼저 서버에 첫번째 클라이언트를 연결한 뒤 메시지를 보낸 결과다

서버 1번 클라이언트

 1번 클라이언트에게 메세지를 보낸대로 서버는 대답을 보내준다.
여기에서 1번 클라이언트가 종료되지 않은 시점에서 2번과 3번 클라이언트를 연결시키고, 메세지를 보낸다.

2번 클라이언트

3번 클라이언트 

 2번과 3번 클라이언트는 메세지를 송신해도 이미 1번 클라이언트가 선점되어 처리되고 있으므로,
메세지에 대한 답변을 받지못한채 블록킹(다음 처리를 하지못하고, 현재 작업이 끝날때까지 기다림) 되고있다.

여기에서 1번클라이언트를 종료시킨다.

서버

 

1번 클라이언트를 종료시키면 대기하고 있던 2번 클라이언트의 메세지가 보내진다

2번 클라이언트
3번 클라이언트

2번 클라이언트는 메세지를 받고 다음 메세지를 받을 수 있지만, 3번 클라이언트는 여전히 recv함수에서 블록킹 된 상태다.

서버 3번 클라이언트

2번 클라이언트가 종료되고 나서야 마지막으로 3번 클라이언트가 메세지를 보내고 받을 수 있다.

※ 클라이언트의 코드에서 send함수 뒤에 send함수가 완료되었다는 로그를 남기면, 2번 3번 클라이언트가 블록킹 되는 시점은 send가 아닌 recv라는 것을 알 수 있다.


윈도우 소켓으로 서버와 클라이언트의 통신을 구현해보고 알아보았다.
하지만 지금 간단하게 구현된 서버는 서비스에서 거의 사용하지 못한다. 클라이언트를 한명씩 순차적으로만 처리할 수 있다면 기다리는 다른 클라이언트들은 긴 대기시간을 가지게 된다.

서버는 빠른 응답도 중요하기 때문에 이후 포스팅에서는 지금의 구조를 개선해서, 다수의 클라이언트를 동시에 처리할 수 있는 서버(병렬 처리서버)를 다뤄볼 것이다.

+ Recent posts