Kevin Dominic의 Studying Rock Drill~

네트워크 프로그래밍 기초 가이드 (1/2) 본문

Study/전공

네트워크 프로그래밍 기초 가이드 (1/2)

Kevin Dominic 2010. 6. 13. 08:36

원본 출처 : http://www.gamecode.org/bak/article.php3?no=2037&page=0&current=0&field=tip

서버 프로그래밍의 경험은 거의 없지만, 입문자들이 보기 쉽도록 보통 업계 클라이언트 프로그래머들이 아는 수준에서, 서버 프로그래밍과 플라이언트 네트워크 프로그래밍을 정리해 봅니다.


1. 가이드 소개

네트워크 프로그래밍이라 함은 보통 서로 다른 컴퓨터, 혹은 서로 다른 프로세스간에 통신을 얘기하며, 소켓 프로그래밍이 가장 일반적인 형태라고 생각됩니다. 기본적으로 정리하는 것도 이 소켓을 다루는 것에 대한 내용입니다.  기능적으로 다루는 것은 소켓 프로그래밍이 전부 일수도 있지만, 적절하게 활용하기 위해선 운영체제의 수행 파이프라인이라던가, 다중 프로세스/쓰레드에 대한 적절한 지식과 경험이 필요하다고 생각합니다. (그래서 게임 서버 프로그래밍은 전통적으로 전산과 출신의 프로그래머가 극 강세가 아닐까 생각됩니다. )

- 소켓프로그래밍개요
- TCP 소켓 프로그래밍
- UDP 소켓 프로그래밍
- Multiplex IO
- 동기화
- 네크워트 프로그래밍 이슈들
(정리에는 포함하지만 아주 부족하므로, 이 부분은 능동적으로 자료를 찾고 실험을 해보시면 시행착오를 많이 줄이실 수 있을 거 같습니다.)

단순히 네트워크의 전송 처리는 사실상 아주 베이스에 불과하며 실제적으로 게임에서 얘기하는 네트워크에는 게임 동기화, 패킷 디자인, DB연동 등의 게임의 많은 부분이 포함된다고 생각됩니다. 이 부분은 개발자 스스로 설계를 하는 것이 맞다고 생각되며, 아주 일반적인 형태의 서버는 다루는 글이나 책들을 먼저 참고한다면 시행착오를 줄이는데 도움은 될 거라 생각됩니다.
(기타 실제적으로 어플리케이션 레벨에서는 항상 알아야 하는 것은 아니지만 네트워크 토폴로지(network topology), OSI 네트워크 모델, IP 주소체계, 라우팅등 네트웍의 기본에 해당되는 주제들은 관심을 가지고 살펴볼 필요는 있습니다.)


2. 소켓 프로그래밍 개요

기본적으로 소켓은 통신을 위한 일종의 통로라고 생각할 수 있습니다. 기본적으로 소켓은 상대방에게 데이터를 보내거나 받는 역할을 하며, 연결을 수동적으로 기다리느냐, 능동적으로 연결을 하느냐로 서버 / 클라이언트냐를 구분할 수 있겠습니다. (기능상 그렇다는 것이고 서버 / 클라이언트의 정의라고는 볼 수 없겠습니다.)

실용적인 관점에서 소켓은 TCP(Tansmission Control Protocol)UDP(User Datagram Protocol)로 구분할 수 있습니다. 약간은 상반되는 장점과 단점을 가지고 있으며, 어떤 것을 적절하게 활용하느냐가 전체적인 성능에 큰 영향을 주게 됩니다. 간단하게 요약하면 TCP는 신뢰할 수 있는 통신을, UDP는 몇 가지 신뢰도는 포기하되 좀 더 직접적인 통신을 한다는 가지고 있습니다.

그리고 다른 관점에서 소켓 함수는 동기모드(블록킹)/비동기 모드(넌블록킹)로 동작합니다. 차이점은 만약 데이터가 도착하지 않는 상태에서 recv로 데이터를 수신하고자 했을 때 데이터가 올 때까지 대기(block) 하느냐, 그냥 수신된 데이터가 없다는 정보만 리턴하고 넘어가느냐 입니다. 실제로 대기한다는 의미는 시스템을 멈추고 기다린다는 것이 아니라 다른 쓰레드나 프로세스(process)로 실행 권을 넘기는 것이기 때문에 프로세서(processor)는 항상 적절한 동작을 하게 됩니다. 비동기 모드로 데이터가 올 때까지 폴링(polling)하면서 대기하는 것과는 기다린다는 의미에서는 동일하지만 프로세서를 활용한다는 면에서는 하늘과 땅 차이라고 할 수 있겠습니다. 이런 병렬적인 처리에 대한 고려가 필요하게 됩니다.

그리고 직접적인 소켓 통신을 처리하는 함수는 아니지만 소켓 처리에 대해서 Multiplex 처리(하나의 쓰레드, 혹은 적은 수의 쓰레드에서 여러 개의 소켓을 처리)를 해주는 select, epoll, IOCP 같은 기능적인 함수군도 염두해 두어야 겠습니다.


3. TCP 소켓 프로그래밍

실제적인 TCP의 특징을 먼저 살펴보겠습니다. 가장 큰 장점은 아래 두가지가 되겠습니다.

- 전송되는 데이터의 순서가 바뀌지 바뀌지 않는다.
- 내부적인 에러 정정 기능이 있기 때문에 신뢰할 수 있다. (중간에 데이타를 분실하지 않는다.)

안정적으로 데이터를 주고 받을 수 있다는 점이 최대 장점입니다만, 내부적인 확인을 위해 ack 신호를 주고 받으며, 순서(sequence number)정보나 체크섬(checksum)정보등의 추가적인 헤더가 포함되기 때문에 추가적인 네트웍 지연(latency)가 생길 수 있습니다.
스트림 형태의 데이터는 신뢰할 수 있지만, 주의할 것은 데이터를 ABCD로 보냈을 경우 받는 쪽에서는 ABC, D로 나눠서 받아질 수도 있다는 점입니다. 즉, 패킷의 크기를 받아진 크기로 처리하면 안되며, 논리적으로 구할 수 있어야 합니다.

TCP의 다른 특징은 연결 지향적이라는 것으로, 각 서버, 클라이언트 사이드에서는 연결된 소켓을 통해서 통신을 하게 됩니다. 
서버쪽에서의 소켓의 연결 흐름은 아래와 같습니다.

- socket 함수로 소켓 생성하기
- bind 함수로 특정 포트에 bind 하기
- listen 함수로 소켓을 외부 연결 대기 상태로 세팅하기
- accept 함수로 연결된 소켓 얻어오기
- 연결된 소켓으로 통신 (send, recv)

클라이언트의 연결흐름은 좀 더 간단합니다.

- socket 함수로 소켓 생성하기
- connect 함수로 특정 ip, port 의 서버에 연결하기
- 소켓으로 통신 (send, recv)

실제로 데이터를 보내고 받는 함수는 send, recv 함수로 파일의 read, write 인터페이스와 유사합니다.

가장 기본적인 형태로 echo 서버 / 클라이언트를 구성해보도록 하겠습니다.
(주의할 것은 아래 예제의 서버는 여러 개의 입력을 받을 수 없다는 것으로 단순한 기능을 확인하기 위한 예제입니다.)

TCP ECHO Server
1234 포트를 열고 기다렸다가 연결되면 그 소켓으로 받은 데이터를 그대로 보내주는 간단한 echo 서버 예제입니다.
(편의상 예제는 win32 콘솔 프로젝트로 작성하며, wsock32.lib 를 link 리스트에 추가해줘야 합니다.

#include <winsock.h>
#include <stdio.h>

#define PORT 1234

void main()
{
   WSADATA WSAData;
   SOCKADDR_IN addr;
   SOCKET server;
   SOCKET client;
   char buffer[1024];
   int readbytes;
   int addrlen;

   if (WSAStartup(MAKEWORD(1,1), &WSAData) != 0)   return;

   server = socket(AF_INET, SOCK_STREAM, 0);

   if (server == INVALID_SOCKET)   return ;

   addr.sin_family = AF_INET;
   addr.sin_port = htons(PORT);
   addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

   if (bind(server, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR)   return ;
   if (listen(server, SOMAXCONN) == SOCKET_ERROR)  return ;
   printf("wait for connectingn");

   addrlen = sizeof(addr);
   client = accept(server, (struct sockaddr*)&addr, &addrlen);
   printf("connected (%x) %d.%d.%d.%d:%dn", client, addr.sin_addr.S_un.S_un_b.s_b1, addr.sin_addr.S_un.S_un_b.s_b2,
     addr.sin_addr.S_un.S_un_b.s_b3, addr.sin_addr.S_un.S_un_b.s_b4, ntohs(addr.sin_port));

   while(1)
   {
        printf("wait for readingn");
        readbytes = recv(client, buffer, sizeof(buffer), 0);
        if (readbytes > 0)
        {
            printf("read bytes = %dn", readbytes);
            send(client, buffer, readbytes, 0);
        }
        else
        {
            printf("error detected (%d)n", WSAGetLastError());
            break;
         }
   };

   closesocket(client);
   closesocket(server);
}


TCP ECHO Client
편의상 local에서 테스트한다고 가정하고 IP는 127.0.0.1로 설정했으며, 입력한 한 라인의 내용을 서버로 보내고, 그 내용이 오기를 기다리는 매우 간단한 echo 클라이언트 예제입니다.

#include <winsock.h>
#include <stdio.h>

#define PORT 1234
#define IP  "127.0.0.1"

void main()
{
     WSADATA WSAData;
     SOCKADDR_IN addr;
     SOCKET s;
     char buffer[1024];
     int readbytes;
     int i, len;

     if (WSAStartup(MAKEWORD(1,1), &WSAData) != 0)  return;
     s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
     if (s == INVALID_SOCKET) return;

     addr.sin_family = AF_INET;
     addr.sin_port = htons(0);
     addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

     if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR)
     {
         closesocket(s);
          return;
     }

     addr.sin_family = AF_INET;
     addr.sin_port = htons(PORT);
     addr.sin_addr.S_un.S_addr = inet_addr(IP);

     if (connect(s, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR)
     {
         printf("fail to connectn");
         closesocket(s);
         return;
     }

     while(1)
     {
          printf("enter messagesn");
          for(i=0; 1; i++)
          {
                buffer[i] = getchar();
                if (buffer[i] == 'n') 
                {
                      buffer[i++] = '';
                      break;
                }
          }
         len = i;
         printf("send messages (%d bytes)n", len);
         send(s, buffer, len, 0);

         for(readbytes=0; readbytes<len;)
               readbytes += recv(s, buffer+readbytes, len-readbytes, 0);

         printf("recv messages = %sn", buffer);
    }
    closesocket(s);
}

참고로 소켓을 넌블럭모드로 설정하고자 할 경우 아래처럼 작성하면 됩니다.

unsigned long m = 1;
ioctlsocket(sock, FIONBIO, &m);


4. UDP 소켓 프로그래밍

UDP은 TCP의 아쉬운 부분을 보완해줄 수 있는데, 기본적으로 에러 정정이 없고, 붙은 헤더의 크기가 작아서 속도가 빠르다는 점이 가장 큰 장점입니다. (네트워크 하위 레벨에서 TCP나 UDP나 같이 때문에 속도가 빠르다라는 말은 약간 모순이 있지만, 반응하는 속도로 보자면 더 빠릅니다.)  다만 주의해야 될 점이 몇 가지 있습니다.

- 보낸 패킷이 도착하지 않을 수 있다
- 보낸 순서대로 도착하지 않을 수 있다
- 같은 내용이 반복해서 전송 될 수도 있다
- 보낸 내용과 받은 내용이 100% 일치하지 않을 수도 있다

 위의 요소들은 적절하게 구현하지 않는다면 아주 치명적인 문제를 일으킬 수 있는 부분입니다. 그리고 다른 특징은

- 패킷은 잘려서 전송되지 않는다. (보낸 패킷이 두개로 나뉘어서 받아지는 경우는 없다)
- 비 연결성이다

아마 느낌이 오시겠지만 편하게 사용하기엔 위험하고, 각종 에러 판단이나 에러 정정 코드를 직접 작성해야 한다는 부담이 생깁니다만, 이점을 충분히 고려하더라도 어플리케이션 레벨에서의 latency를 줄일 수 있다는 엄청난 장점은 포기할 수 없는 매력이라고 생각됩니다.

비연결(connectionless)적인 UDP의 통신 과정은 연결지향 (connection-oriented) 적인 TCP와는 조금 다릅니다.

UDP 소켓 서버 기본 흐름은

- socket 함수로 SOCK_DGRAM 형태로 생성하기
- bind 함수로 소켓을 대기할 port로 바인드하기
- 소켓으로 통신 (recvfrom, sendto)

UDP 클라이언트는 특정 포트에 바인드 하는 것을 제외하면 동일합니다

- socket 함수로 SOCK_DGRAM 형태로 생성하기
- bind 함수로 아무 로컬주소로나 바인드하기
- 소켓으로 통신 (recvfrom, sendto)

과정은 훨씬 간소화 되었지만 TCP는 연결 시에만 주소를 설정하면 되는데, UDP는 데이터를 보낼 때마다 도착점의 주소(ip, port)를 세팅해 주어야 합니다.
다만 서버 구성해서 편한 것은 해당 port의 모든 서비스를 하나의 소켓으로 처리하기 때문에 select, iocp 같은 별도의 API 없이도 하나의 쓰레드로 모든 서비스를 관리할 수 있습니다. 서버나 클라이언트에서 recvfrom으로 데이터가 도착 했을 때 어디서 보냈는지는 주소로 직접 판단해야 하기 때문에 여러 가지로 까다로운 면이 있습니다.
(보통 TCP를 메인 통신수단으로 사용하고, 필요한 부분에서만 UDP를 사용하는데, IP만으로 쉽게 매핑시킨다면 한 IP에서는 한명 밖에 게임을 못하는 등의 문제가 발생할 수 있습니다. – 이 경우 통신 내용으로 매핑을 해야 겠죠.)

UDP ECHO Server
UDP의 경우 소켓 하나로 통신하기 때문에 별도의 처리 없이도 여러 개의 클라이언트에 대해서도 기능을 수행합니다.

#include <winsock.h>
#include <stdio.h>

#define PORT 1234

void main()
{
    WSADATA WSAData;
    SOCKADDR_IN addr;
    SOCKET server;
    char buffer[1024];
    int readbytes, addrlen;

    if (WSAStartup(MAKEWORD(1,1), &WSAData) != 0)   return;
    server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (server == INVALID_SOCKET)   return ;

    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    if (bind(server, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR) return ;

    while(1)
    {
         addrlen = sizeof(addr);
         readbytes = recvfrom(server, buffer, sizeof(buffer), 0, (struct sockaddr*) &addr, &addrlen);
        printf("read bytes = %d (from %d.%d.%d.%d:%d)n", readbytes, addr.sin_addr.S_un.S_un_b.s_b1, addr.sin_addr.S_un.S_un_b.s_b2, addr.sin_addr.S_un.S_un_b.s_b3, addr.sin_addr.S_un.S_un_b.s_b4, ntohs(addr.sin_port));
       sendto(server, buffer, readbytes, 0, (struct sockaddr*) &addr, addrlen);
     }
     closesocket(server);
}



* UDP ECHO Client

#include <winsock.h>
#include <stdio.h>

#define PORT 1234
#define IP  "127.0.0.1"

void main()
{
     WSADATA WSAData;
     SOCKADDR_IN addr;
     SOCKET s;
     char buffer[1024];
     int readbytes, addrlen;
     int i;

     if (WSAStartup(MAKEWORD(1,1), &WSAData) != 0)  return;

     s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); 
     if (s == INVALID_SOCKET) return;

     addr.sin_family = AF_INET;
     addr.sin_port = htons(0);
     addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

     if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR)
     {
         closesocket(s);
         return;
     }

     while(1)
    {
         printf("enter messagesn");
         for(i=0; 1; i++)
         {
               buffer[i] = getchar();
               if (buffer[i] == 'n')
              {
                    buffer[i++] = '';
                    break;
              }
         }
         memset(&addr, 0, sizeof(addr));
         addr.sin_family = AF_INET;
         addr.sin_port = htons(PORT);
         addr.sin_addr.S_un.S_addr = inet_addr(IP);

         printf("send messages (%d bytes)n", i);
         sendto(s, buffer, i, 0, (struct sockaddr *)&addr, sizeof(addr));

         addrlen = sizeof(addr);
         readbytes = recvfrom(s, buffer, sizeof(buffer), 0, (struct sockaddr *)&addr, &addrlen);

        printf("recv messages = %s (%d bytes) : from %d.%d.%d.%d:%dn", buffer, readbytes,      addr.sin_addr.S_un.S_un_b.s_b1, addr.sin_addr.S_un.S_un_b.s_b2, addr.sin_addr.S_un.S_un_b.s_b3, addr.sin_addr.S_un.S_un_b.s_b4,  ntohs(addr.sin_port));
     }
     closesocket(s);
}