ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Socket과 select
    책/게임서버 프로그래머 책 2024. 6. 2. 02:13
     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;
    }
Designed by Tistory.