멀티플레이어 게임 프로그래밍 - 3장 연습문제

jh Seo·2023년 3월 1일
0
  1. POSIX계열과 윈도 소켓 라이브러리의 차이점은

    1. 소켓 자체를 나타내는 자료형이 다르다.
    • 윈도 기반 플랫폼에선 socket()함수의 리턴형이 SOCKET형으로
      이 자료형은 UINT_PTR에 대한 typedef이고, 이 포인터는 소켓의 상태, 데이터를 저장하는
      메모리 영역을 가리킨다.
    • POSIX기반 플랫폼에선 그냥 int값 하나에 불과하다.
      SOCKET이란 자료형이 실존하지 않으며, int값을 리턴한다.
      이 값은 현재 열려있는 파일과 소켓의 목록상 인덱스를 나타낸다.
    1. 라이브러리를 사용하기 위한 헤더파일이 다르다.
    • 윈도용 소켓 라이브러리는 Winsock2이지만 windows.h에 구 소켓 라이브러리가
      포함되어 있어서 충돌을 신경써야한다.
      부가적인 주소 변환 기능등을 사용하려면 Ws2tcpip.h를 추가해야한다.
    • POSIX용 라이브러리는 sys/socket.h를 인클루드하면 된다.
      IPv4전용기능을 사용하려면 netinet/in.h를 사용하면 된다.
      주소변환 기능은 arpa/inet.h에 있고 네임 리졸루션 기능은 netdb.h에 있다,
  2. 네트워크 계층과 전송 계층 패킷을 접근할 수 있다.
    네트워크 계층에선 발신지 주소와 목적지 주소가 필요하고,
    전송 계층에선 발신지 포트와 목적지 포트가 필요하다.

  3. TCP는 신뢰성 보장으로 인해, 데이터를 주고받기 위해 두 호스트 사이의 연결을 맺어 두어야 한다.
    또한 누락된 패킷을 재전송하기 위해 상태 정보를 유지하고 저장해 놔야한다.
    버클리 소켓 API에서는 socket그자체에 그 연결 정보를 기록하므로 각 TCP클라이언트마다 소켓을
    만들어 둬야한다.

    • 소켓에 포트를 바인딩 하는 방법은 함수 bind()를 사용하면 된다.

      int bind(SOCKET sock, const sockaddr* address, int address_len)

      매개변수 sock은 바인딩할 소켓으로, socket()함수를 통해 만든다.

      address는 이 소켓으로 보낼 패킷의 발신지 주소(회신 주소)이다.
      멀티플레이어 게임용도로는 어느 네트워크 인터페이스에서 보냈는지 중요하지 않고,
      따라서 호스트에 장착된 모든 네트워크 인터페이스의 ip주소의
      해당 포트에 모두 바인딩하는게 바람직하다.
      바인딩할 주소 sockaddr_in의 sin_addr 필드에 INADDR_ANY매크로 값을 넣으면 된다.
      address_len에는 sockaddr의 길이를 넣어주어야 한다.

    • 소켓에 sockaddr을 바인딩하면 운영체제가 이 주소와 포트를 목적지로 발신된 패킷을
      수신하면 해당 소켓에 넣어준다.
      또한 bind()에서 지정한 주소 및 포트를 이 소켓을 통해 나가는 패킷의
      네트워크 계층과 전송 계층 헤더의 발신 주소와 포트로 사용된다.

      하지만 주소와 포트를 확실히 고정해둘 필요가 없다면 네트워크 라이브러리는 자동으로
      남아있는 포트에 소켓을 바인딩해주므로 사용을 안해도 된다.

  4. SocketAddress가 IPv6도 지원받으려면 생성자를 하나 더 작성해야한다.

    	SocketAddress(uint8_t* inAddress, uint16_t inPort) {
    		GetAsSockAddrIn6()->sin6_family = AF_INET6;
    		GetAsSockAddrIn6()->sin6_port = htonl(inPort);
    		memcpy(GetAsSockAddrIn6()->sin6_addr.u.Byte, inAddress,sizeof(inAddress));
    	}

    이런식으로 작성하였다.
    SocketAddressFactory가 IPv6를 지원 받으려면

    class SocketAddressFactory {
    public:
    	static SocketAddressPtr CreateIPv4FromString(const string& inString) {
    		auto pos = inString.find_last_of(':');
    		string host, service;
    		//호스트 서비스가 정해졌다면 잘라서 넣어줌
    		if (pos != string::npos) {
    			host = inString.substr(0, pos);
    			service = inString.substr(pos + 1);
    		}
    		//없다면 포트에 디폴트값 넣어줌
    		else {
    			host = inString;
    			service = "0";
    		}
    		addrinfo hint;
    		memset(&hint, 0, sizeof(hint));
    		hint.ai_family = AF_INET6;
    		addrinfo* result = nullptr;
    		int error = getaddrinfo(host.c_str(), service.c_str(), &hint, &result);
    		//메모리해제를 위해 처음값 저장
    		addrinfo* initResult = result;
    
    		//getaddrinfo는 성공하면 0을 반환 따라서 addrinfo가져오기를 실패했다면
    		if (error != 0 && result != nullptr) {
    			//메모리 헤제 시키고
    			freeaddrinfo(initResult);
    			//nullptr리턴
    			return nullptr;
    		}
    		//result의 어드레스가 0이면서, result의 다음 addrinfo값이 있다면 
    		while (!result->ai_addr && result->ai_next) {
    			//어드레스 가진 result나올때까지 계속 다음값으로
    			result = result->ai_next;
    		}
    		//만약 result가 끝값이라면 주소를 찾지 못한것이므로 
    		if (!result->ai_next) {
    			//메모리 해제후 nullptr리턴
    			freeaddrinfo(initResult);
    			return nullptr;
    		}
    		auto toRet = std::make_shared<SocketAddress>(*result->ai_addr);
    
    		//
    		freeaddrinfo(initResult);
    		return toRet;
    
    	}
    };

    이런식으로 hit.ai_family를 AF_INET6로 설정해주면 힌트를 통해 addrinfo구조체를
    IPv6형태로 바꿔준다.

  5. SocketUtils클래스가 Tcp소켓의 생성해주는 스태틱 멤버함수를 구현해보자가 무슨 소리인지 잘 모르겠다.

  6. 기본적으로 서버에선 listen함수로 클라이언트 소켓을 받고,
    accept함수로 클라이언트 소통용 소켓을 생성해 해당 소켓으로 통신하도록 구현하였다.

서버

#include<iostream>
#include<winsock2.h>
 #include<thread>
using namespace std;
#pragma comment(lib,"ws2_32.lib")


  # define Packet_size 1024
 //리스닝 소켓, accept로 만들 소켓
SOCKET skt, client_skt;

int main(){
   WSADATA wsa;
   WSAStartup(MAKEWORD(2,2),&wsa);
   //socket()으로 리스닝 소켓을 만든 후,
   skt= socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

   sockaddr_in addr={};
   addr.sin_family=AF_INET;
   addr.sin_port=htons(4444);
   addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);

   //bind()로 바인딩을 해준 후
   bind(skt,(SOCKADDR*)&addr,sizeof(addr));
   //listen함수를 이용해 리스닝을 시작 
   listen(skt,SOMAXCONN);

   SOCKADDR_IN client={};
   int clientSize=sizeof(client);
   ZeroMemory(&client,sizeof(client));
   //그 후 , TCP핸드셰이킹을 해나가기 위해 리스닝 소켓 skt를 이용해 accept함수를 호출하고 
   //성공적으로 accept함수가 실행된다면 accept에서 반환한 소켓은(client_sock) 클라이언트와 계속 통신하는 용도로 사용이 가능하다.
   client_skt=accept(skt, (SOCKADDR*)&client,&clientSize);
   
   char recvBuf[Packet_size]={};

   while(!WSAGetLastError()){
       ZeroMemory(recvBuf,sizeof(recvBuf));
       recv(client_skt,recvBuf,Packet_size,0);
       send(client_skt,recvBuf,sizeof(recvBuf),0);
   }
 
   closesocket(client_skt);
   closesocket(skt);
   WSACleanup();
  }

클라이언트

 //for using inet_addr
 #define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<iostream>
#include<winsock2.h>
#include<thread>
using namespace std;
#pragma comment(lib,"ws2_32.lib")


 #define PACKET_SIZE 1024

 SOCKET skt;

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

    skt=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

    SOCKADDR_IN addr={};
    addr.sin_family=AF_INET;
    addr.sin_port=htons(4444);
    addr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");

    while(1){
        //서버는 connect()만 호출하면 된다. 
        if(!connect(skt,(SOCKADDR*)&addr,sizeof(addr))) break;
    }

    char msg[PACKET_SIZE]={0};
    char recvBuf[PACKET_SIZE]={};
    string cmd;

    while(!WSAGetLastError()){
        cin>>msg;
        send(skt,msg,strlen(msg),0);
        ZeroMemory(recvBuf,sizeof(recvBuf));
        recv(skt,recvBuf,PACKET_SIZE,0);
        cmd=recvBuf;
        cout<<"My Message : "<<recvBuf<<endl;
    }

    closesocket(skt);
    WSACleanup();
}
  1. 서버에 select함수를 적용한 코드

    #include<iostream>
    #include<winsock2.h>
    #include<thread>
    #pragma comment(lib,"ws2_32.lib")
    using namespace std;
    
    # define Packet_size 1024
    SOCKET server_skt, client_skt;
    fd_set reads,copy_reads;
    SOCKADDR_IN client={};
    int clientSize=sizeof(client);
    char recvBuf[Packet_size]={};
    
    int main(){
       WSADATA wsa;
       WSAStartup(MAKEWORD(2,2),&wsa);
       //socket()으로 리스닝 소켓을 만든 후,
       server_skt= socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    
       sockaddr_in addr={};
       addr.sin_family=AF_INET;
       addr.sin_port=htons(4444);
       addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
    
       //bind()로 바인딩을 해준 후
       bind(server_skt,(SOCKADDR*)&addr,sizeof(addr));
       //listen함수를 이용해 리스닝을 시작 
       listen(server_skt,SOMAXCONN);
    
       //fd_set reads초기화
       FD_ZERO(&reads);
       //서버소켓 reads셋에 넣어줌
       FD_SET(server_skt, &reads);
       cout << "client Waiting" << '\n';
    
       while (1)
       {
           // 원본 FD_SET의 보존을 위해 복사본을 생성하여 진행
           copy_reads = reads;
           // select함수 실행
           int fd_num = select(0, &copy_reads, NULL, NULL, NULL);
    
           // select함수가 fd의 변화를 캐치한 후 , 복사본의 fd 갯수만큼 반복
           for (int i = 0; i <= copy_reads.fd_count; i++)
           {
               // 복사본 set의 첫번째 소켓 읽어옴
               SOCKET curSok = copy_reads.fd_array[i];
               //복사본 set에서 curSok 찾았다면
               if (FD_ISSET(curSok, &copy_reads))
               {
                   //server_skt라면 새로운 클라이언트가 접속했다는 뜻 (listen함수)
                   if (curSok == server_skt)
                   {
                       cout << "new Client" << '\n';
                       //SOCKADDR_IN구조체 초기화 해준 후,
                       ZeroMemory(&client, sizeof(client));
                       //accept함수로 새로운 클라이언트 소켓 할당
                       client_skt = accept(server_skt, (sockaddr *)&client, &clientSize);
                       //클라이언트 소켓 reads에 넣어줘서 관리
                       FD_SET(client_skt, &reads);
                   }
                   //이미 관리하고 있는 클라이언트에서 메세지가 온것이라면
                   else
                   {
                       //클라이언트에서 온 메세지 저장할 recvBuf 초기화
                       ZeroMemory(recvBuf, sizeof(recvBuf));
                       //recv함수 실행 후 반환 값 read_num에 저장
                       int read_num = recv(curSok, recvBuf, Packet_size, 0);
                       //만약 반환 값이 0 이하라면 오류가 생겼으므로 해당 클라이언트 제거 처리
                       if (read_num < 0)
                       {
                           cout << curSok << " Client Quit" << '\n';
                           //해당 소켓 close한 후
                           closesocket(curSok);
                           //reads fdset에서 해당 소켓 제거
                           FD_CLR(curSok, &reads);
                       }
                       //제대로된 메시지를 수신했을 때
                       else
                       {
                           cout << recvBuf << " Message from Client" << '\n';
                           //echo~
                           send(curSok, recvBuf, read_num, 0);
                       }
                   }
                   //변화 발생한 파일 디스크립터 수가 1개 이하면 break해서 반복문 탈출하게함
                   if (--fd_num <= 0)
                       break;
               }
           }
       }
    
       closesocket(server_skt);
       WSACleanup();
    }

    클라이언트에 논블로킹방식 ioctlsocket을 적용한 코드

    #include<iostream>
    #include<winsock2.h>
    #include<thread>
    using namespace std;
    
    #define PACKET_SIZE 1024
    
    SOCKET skt;
    
    int main(){
       WSADATA wsa;
       WSAStartup(MAKEWORD(2,2),&wsa);
    
       skt=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    
       SOCKADDR_IN addr={};
       addr.sin_family=AF_INET;
       addr.sin_port=htons(4444);
       addr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
       //0 for non-blocking, 1 for blocking
       ioctlsocket(skt,FIONBIO,0);
    
           //서버는 connect()만 호출하면 된다. 
       while(true){
           if (connect(skt, (SOCKADDR *)&addr, sizeof(addr)) == SOCKET_ERROR)
           {
               //만약 에러가 WSAEWOULDBLOCK이라면 논블로킹으로 설정해서 나오는것이므로 컨티뉴
               if (WSAGetLastError() == WSAEWOULDBLOCK)
                   continue;
    
               //아니라면 error
               break;
           }
       }
       cout<<"Server Connected"<<'\n';
    
       char msg[PACKET_SIZE]={0};
       char recvBuf[PACKET_SIZE]={};
       string cmd;
    
       while(1)
       {
           //보낼 메세지 
           cin >> msg;
           
           if(send(skt, msg, strlen(msg), 0)==SOCKET_ERROR){
               //에러가 WSAEWOULDBLOCK이라면 논블로킹이라 뜨는 메시지로 컨티뉴
               if(WSAGetLastError()==WSAEWOULDBLOCK)
               continue;
               break;
           }
           //수신 버퍼 초기화
           ZeroMemory(recvBuf, sizeof(recvBuf));
       
           while(true){
               //recv함수의 반환값 errCode에 저장
               int errCode=recv(skt, recvBuf, PACKET_SIZE, 0);
               //에러 났을 때
               if(errCode==SOCKET_ERROR){
                   //WSAEWOULDBLOCK이라면 논블로킹이라 에러 뜨는 것이므로 continue;
                   if(WSAGetLastError()==WSAEWOULDBLOCK)
                       continue;
    
                   break;
               }
               //WSAEWOULDBLOCK가 아니라면 에러
               else if(errCode==0){
                   break;
               }
               //메세지출력
               cout<<"My message from Server : "<<recvBuf<<'\n';
               break;
    
           }
       }
       closesocket(skt);
       WSACleanup();
    }

    레퍼런스

    https://1d1cblog.tistory.com/356
    https://dodo000.tistory.com/31
    https://yms2047.tistory.com/entry/select-%ED%95%A8%EC%88%98-%EC%82%AC%EC%9A%A9%EB%B2%95
    https://sanggoe.github.io/study/2020/12/23/network-Chap4_%EA%B3%A0%EA%B8%89_%EC%86%8C%EC%BC%93_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D.html
    https://dockdocklife.tistory.com/entry/%EB%85%BC%EB%B8%94%EB%A1%9D%ED%82%B9-%EC%86%8C%EC%BC%93

profile
코딩 창고!

0개의 댓글