본문 바로가기
개발 서적 리뷰/게임서버 프로그래머 책

Socket과 select

by 거북이의 기술블로그 2024. 6. 2.
 client - server 구현 (1)
알아야할 것들
  1. ioctlsocket() : 비동기처리
  2. getsockopt() : 소켓 확인
  3. select() : 이벤트 처리 부분
  4. 동기적 처리가 일어나는 부분

 

ioctlsocket() 함수

  • 소켓핸들에 명령을 주어 설정을 하는 함수
  • 명령어 목록
    • FIONBIO : 소켓의 동기화/비동기화를 설정
    • FIONREAD : 소켓의 읽을 수 있는 데이터 양 검색
    • SIOCATMARK : 소켓의  urgent 데이터가 있는지 여부 확인
    • SIO_KEEPALIVE_VALS : TCP keep-alive 시간 설정을 변경
    • SIO_GET_EXTENSION_FUNCTION_POINTER : 확장 함수 포인터를 가져옴 (*사용자 명령 사용 가능)

 

getsockopt() 함수

  • 소켓의 상태 확인 (연결 중/ 연결 종료/ 연결 에러 ) 등등
  • 소켓의 옵션 값 확인
    • SO_REUSEADDR : 이미 사용중인 주소를 다른 소켓이 재사용할 수 있도록 허용
    • SO_KEEPALIVE : 소켓이 유휴 상태일때 네트워크 연결 상태 확인
    • SO_LINGER : 소켓을 닫을 때 해당 소켓에 대한 데이터 전송 대기
    • SO_SNDBUF : 송신 버퍼 크기 설정
    • SO_RCVBUF : 수신 버퍼 크기 설정
    • SO_ERROR : 소켓의 오류 상태 조회
    • 등등.. 

 

SELECT 간단 이론

  • 소켓이 비동기로 처리가 되고 있는 부분이 있다면, 이벤트로써 사용하여 가능한 상태인지 판단 후 처리를 해줘야할 때 사용
  • select는 I/O가 가능한지 판단하는 여부 탐색
  • 0/1로서 이벤트 처리가 일어난다.
  • timeout()값을 설정하여 지정한 시간동안 탐색 가능
  • 소켓의 연결 여부를 탐색하여 연결시도 

 

동기적 처리가 일어나는 부분

  • CLIENT
    • connect()
    • send()
    • recv()
  • SERVER
    • accept()
    • send()
    • recv()

 

CLIENT 구현

#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#include <stdexcept>
#include <string>

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

class Winsock {
public:
	Winsock() {
		WSADATA wsaData;
		int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
		if (result != 0)
		{
			throw std::runtime_error("WSAStartup failed with error : " + std::to_string(result));
		}
	}

	~Winsock()
	{
		WSACleanup();
	}
};

class ClientSocket {
public:
	// 소켓 생성자
	ClientSocket(const std::string& host, const std::string& port)
	{
		addrinfo hints = {};
		hints.ai_family = AF_INET;
		hints.ai_socktype = SOCK_STREAM;
		hints.ai_protocol = IPPROTO_TCP;

		addrinfo* result = nullptr;
		int addrResult = getaddrinfo(host.c_str(), port.c_str(), &hints, &result);

		if (addrResult != 0)
		{
			throw std::runtime_error("getaddrinfo failed with errro: " + std::to_string(addrResult));
		}

		for (addrinfo* ptr = result; ptr != nullptr; ptr = ptr->ai_next)
		{
			sock = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
			if (sock == INVALID_SOCKET)
			{
				continue;
			}

			u_long mode = 1;
			// ioctlsocket 함수 성공 -> 0 반환
            // FIONBIO 비동기처리 mode 설정
			// NO_ERROR == 0
			if (ioctlsocket(sock, FIONBIO, &mode) != NO_ERROR)
			{
				closesocket(sock);
				sock = INVALID_SOCKET;
				continue;
			}
			if (connect(sock, ptr->ai_addr, (int)ptr->ai_addrlen) == SOCKET_ERROR)
			{
				int error = WSAGetLastError();
				if (error != WSAEWOULDBLOCK)
				{
					closesocket(sock);
					sock = INVALID_SOCKET;
					continue;
				}
			}
			else
			{
				break;
			}
			
			fd_set writeSet; // write 사용가능여부 확인
			FD_ZERO(&writeSet); // 초기화
			FD_SET(sock, &writeSet); // writeSet 확인 대기

			timeval timeout;
			timeout.tv_sec = 10;
			timeout.tv_usec = 0;
			// select를 이용하여 writeSet부분 이벤트 확인
			int selectResult = select(0, nullptr, &writeSet, nullptr, &timeout);
			// wirteSet 이벤트 변경이 있을경우
            if (selectResult > 0 && FD_ISSET(sock, &writeSet))
			{
				int error;
				int len = sizeof(error);

				if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (char*)&error, &len) == 0)
				{
					if (error == 0)
					{
						break;
					}
					else
					{
						closesocket(sock);
						sock = INVALID_SOCKET;
					}
				}
			}
			else if (selectResult == 0)
			{
				throw std::runtime_error("connect timeout");
			}
			else
			{
				throw std::runtime_error("select failed with error : " + std::to_string(WSAGetLastError()));
			}
		}

		freeaddrinfo(result);

		if (sock == INVALID_SOCKET)
		{
			throw std::runtime_error("Unable to connect to server!");
		}
	}

	~ClientSocket()
	{
		if (sock != INVALID_SOCKET)
		{
			closesocket(sock);
		}
	}

	void send(const std::string& message)
	{
		fd_set writeSet;
		FD_ZERO(&writeSet);
		FD_SET(sock, &writeSet);

		timeval timeout;
		timeout.tv_sec = 1;
		timeout.tv_usec = 0;

		int selectResult = select(0, nullptr, &writeSet, nullptr, &timeout);
		if (selectResult > 0 && FD_ISSET(sock, &writeSet))
		{
			int sendResult = ::send(sock, message.c_str(), (int)message.size(), 0);
			if (sendResult == SOCKET_ERROR)
			{
				throw std::runtime_error("send failed with error : " + std::to_string(WSAGetLastError()));
			}
		}
		else if (selectResult == 0)
		{
			throw std::runtime_error("send timeout");
		}
		else
		{
			throw std::runtime_error("select failed with error : " + std::to_string(WSAGetLastError()));
		}
	}

	std::string receive(size_t size)
	{
		fd_set readSet;
		FD_ZERO(&readSet);
		FD_SET(sock, &readSet);

		timeval timeout;
		timeout.tv_sec = 1;
		timeout.tv_usec = 0;

		int selectResult = select(0, &readSet, nullptr, nullptr, &timeout);
		if (selectResult > 0 && FD_ISSET(sock, &readSet))
		{
			std::string buffer(size, '\0');
			int recvResult = recv(sock, &buffer[0], (int)size, 0);
			if (recvResult > 0)
			{
				buffer.resize(recvResult);
				return buffer;
			}
			else if (recvResult == 0)
			{
				return {};
			}
			else
			{
				throw std::runtime_error("recv failed with error : " + std::to_string(WSAGetLastError()));
			}
		}
		else if (selectResult == 0)
		{
			return {};
		}
		else
		{
			throw std::runtime_error("select failed with error : " + std::to_string(WSAGetLastError()));
		}
		
	}
private:
	SOCKET sock = INVALID_SOCKET;
};


int main()
{
	try
	{
		Winsock winsock;
		ClientSocket socket("localhost", "27015");

		std::string message = "Hello from client";

		socket.send(message);
		std::string response = socket.receive(512);


		std::cout << "Response : " << response << std::endl;
	}
	catch (const std::exception& e) {
		std::cerr << "Exception : " << e.what() << std::endl;
	}

	return 0;
}

 

 

SERVER 구현

#include <iostream>
#include <winsock2.h>
#include <string>

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

#define PORT 27015
#define BUFFER_SIZE 1024



std::string GetLastErrorAsString() {
    DWORD errorMessageID = ::WSAGetLastError();
    if (errorMessageID == 0) {
        return std::string();
    }

    LPSTR messageBuffer = nullptr;
    size_t size = FormatMessageA(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL
    );

    std::string message(messageBuffer, size);

    LocalFree(messageBuffer);

    return message;
}



class Winsock
{
public:
    Winsock()
    {
        WSADATA wsaData;
        int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
        if (result != 0)
        {
            throw std::runtime_error("WSAStartup failed with error : " + std::to_string(result));
        }
    }

    ~Winsock()
    {
        WSACleanup();
    }
};

class ServerSocket
{
public:
    ServerSocket()
    {
        serverSocket = socket(AF_INET, SOCK_STREAM, 0);
        if (serverSocket == INVALID_SOCKET)
        {
            WSACleanup();
            throw std::runtime_error("Socket creation failed : " + GetLastErrorAsString());
        }

        serverAddr.sin_family = AF_INET;
        serverAddr.sin_addr.s_addr = INADDR_ANY;
        serverAddr.sin_port = htons(PORT);

        if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        {
            closesocket(serverSocket);
            throw std::runtime_error("Socket bind failed : " + GetLastErrorAsString());
        }

        if (listen(serverSocket, 100) == SOCKET_ERROR)
        {
            closesocket(serverSocket);
            throw std::runtime_error("Socket listen failed : " + GetLastErrorAsString());
        }

        u_long mode = 1;
        // server accept() 비동기처리를 위한 serverSocket 설정
        if (ioctlsocket(serverSocket, FIONBIO, &mode) != NO_ERROR)
        {
            closesocket(serverSocket);
            throw std::runtime_error("ioctlsocket failed : " + GetLastErrorAsString());
        }

        std::cout << "Waiting for connection..." << std::endl;

    }

    void accept_connection()
    {
        while (true)
        {
        	// select를 사용하여 accept처리 이벤트 확인
            fd_set readSet; // read 이벤트 선언
            FD_ZERO(&readSet); // 초기화
            FD_SET(serverSocket, &readSet); // readSet 설정

            timeval timeout;
            timeout.tv_sec = 0;
            timeout.tv_usec = 0;

            int selectResult = select(0, &readSet, nullptr, nullptr, &timeout);
            if (selectResult > 0 && FD_ISSET(serverSocket, &readSet))
            {
            	//accept이벤트 확인시 accept 진행
                acceptSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrSize);
                if (acceptSocket != INVALID_SOCKET)
                {
                    std::cout << "Connection accepted " << std::endl;

                    u_long mode = 1;
                    if (ioctlsocket(acceptSocket, FIONBIO, &mode) != NO_ERROR)
                    {
                        closesocket(acceptSocket);
                        throw std::runtime_error("ioctlsocket failed : " + GetLastErrorAsString());
                    }
                    break;
                }
                else
                {
                    int error = WSAGetLastError();
                    if (error != WSAEWOULDBLOCK)
                    {
                        throw std::runtime_error("Socket accept failed : " + GetLastErrorAsString());
                    }
                }
            }
            else if (selectResult == 0)
            {
                continue;
            }
            else
            {
                throw std::runtime_error("select failed with error : " + std::to_string(WSAGetLastError()));
            }
        }
    }
    void socket_recv(const char* sendMessage)
    {
        char buffer[BUFFER_SIZE] = { 0, };
        fd_set readSet;
        FD_ZERO(&readSet);
        FD_SET(acceptSocket, &readSet);

        timeval timeout;
        timeout.tv_sec = 10;
        timeout.tv_usec = 0;

        int selectResult = select(0, &readSet, nullptr, nullptr, &timeout);
        if (selectResult > 0 && FD_ISSET(acceptSocket, &readSet))
        {
            int readValue = recv(acceptSocket, buffer, BUFFER_SIZE, 0);
            if(readValue > 0)
            {
                std::cout << "Message from client : " << buffer << std::endl;
                send(acceptSocket, sendMessage, strlen(sendMessage), 0);
                std::cout << "(server-> client) send message" << std::endl;
            }
            else if (readValue == 0)
            {
                std::cout << "Connection closed" << std::endl;
            }
            else
            {
                std::cerr << "recv failed with error : " << GetLastErrorAsString() << std::endl;
            }
        }
        else if (selectResult == 0)
        {
            throw std::runtime_error("recv timeout");
        }
        else
        {
            throw std::runtime_error("select failed with error : " + GetLastErrorAsString());
        }
        
    }
    ~ServerSocket()
    {
        if (acceptSocket != INVALID_SOCKET)
        {
            closesocket(serverSocket);
            serverSocket = INVALID_SOCKET;
        }

        if (acceptSocket != INVALID_SOCKET)
        {
            closesocket(acceptSocket);
            acceptSocket = INVALID_SOCKET;
        }

    }


private:
    SOCKET serverSocket = INVALID_SOCKET;
    SOCKET acceptSocket = INVALID_SOCKET;
    struct sockaddr_in serverAddr;
    struct sockaddr_in clientAddr;
    int clientAddrSize = sizeof(clientAddr);
};
int main()
{
    try
    {

        Winsock winsock;
        ServerSocket serverSocket;

        const char* message = "Hello from server";

        serverSocket.accept_connection();
        serverSocket.socket_recv(message);
    }
    catch (const std::exception& e)
    {
        std::cerr << "Exception : " << e.what() << std::endl;
    }
    return 0;
}