책정리/윈도우 네트워크 프로그래밍

1장 네트워크와 소켓 프로그래밍

GONII 2015. 4. 7. 17:16
  • TCP/IP 개요

    TCP/IP 프로토콜 구조

    인터넷을 통해 통신을 수행하는 개체는 크게 종단 시스템과 라우터로 나눌 수 있다. 종단 시스템(end-system)은 최종 사용자(end-user)를 위한 애플리케이션을 수행하는 주체가 되며, 인터넷에 연결된 PC, 워크스테이션, PDA, 휴대폰 등이 여기에 속한다. 라우터(router)는 종단 시스템이 속한 네트워크와 다른 네트워크를 연결함으로써 서로 다른 네트워크에 속한 종단 시스템끼리 상호 데이터를 교환할 수 있도록 하는 장비다. 종단 시스템과 라우터간, 라우터와 라우터간, 그리고 종단 시스템과 종단 시스템간 통신을 수행하기 위해서는 정해진 절차와 방법을 따라야 하는데 이를 프로토콜(protocol)이라 부른다. 1~4는 모두 상호 약속한 프로토콜을 이용하여 통신을 수행한다.

    인터넷에서 사용하는 핵심적인 프로토콜은 TCP와 IP로, 이를 포함한 각종 프로토콜을 총칭하여 TCP/IP 프로토콜이라 부른다. TCP/IP 프로토콜은 일반적으로 운영체제에서 그 구현을 제공하여, 일반 애플리케이션은 운영체제가 제공하는 TCP/IP 프로토콜의 서비스를 사용하여 통신을 수행하게 된다.

    애플리케이션간 통신을 수행하기 위해서는 다양한 요구 조건을 만족시켜야 한다. 이러한 요구 조건의 예로는 통신할 대상을 지정하는 상호 약속된 방법, 전송 오류 확인 기능, 오류 발생 시 재전송 기능, 데이터의 순서 관계 유지 등을 들 수 있다. 일반적으로 이와 같은 기능은 프로토콜 수준에서 제공한다.

    일반적으로 프로토콜은 기능별로 나누어 계층적으로 구현하며, TCP/IP 프로토콜도 이와 같은 구조를 따른다.

    네트워크 액세스 계층

    네트워크 액세스 계층(network access layer)은 물리적 네트워크를 통한 실제적인 데이터 전송을 담당한다. 구성 요소로는 물리적인 신호를 처리하여 데이터를 송수신하는 네트워크 하드웨어 그리고 운영체제가 제공하는 디바이스 드라이버가 있다.

    네트워크 액세스 계층에서는 하드웨어적으로 정의된 물리 주소(physical address)를 사용하여 통신을 수행하는데, 이러한 물리 주소는 네트워크 방식에 따라 서로 다르며, 일반적으로 상호 호환되지 않는다.

    인터넷 계층

    인터넷 계층(Internet layer)은 네트워크 액세스 계층의 도움을 받아, 전송 계층이 내려 보낸 데이터를 종단 시스템까지 전달하는 역할을 한다. 네트워크 액세스 계층과 마찬가지로 여기서도 주소를 지정하는 방법이 필요하다. 여기서는 소프트웨어적으로 정의된 논리 주소를 사용하느데 이를 IP주소(Internet Protocol Address)라고 부른다.

    IP주소는 네트워크 액세스 계층에서 사용하는 물리 주소와는 무관하게 정의하며, 전세계적으로 유일성이 보장된다. 따라서 서로 다른 기술에 기반한 네트워크를 연결하여 인터-네트워크를 구성하는 데 핵심적인 역할을 한다.

    IP주소는 통신에 참여하는 종단 시스템을 유일하게 지정할 수 있는 방법을 제공하지만, 실제 데이터를 전송하려면 전송 경로를 알아야 한다. 따라서 전송 경로를 알아내기 위해 라우팅 작업이 필요하게 된다. 라우팅(routing)이란 목적지까지 데이터를 전달하기 위한 일련의 작업을 가리키는 용어로, 라우팅을 위한 정보를 획득하는 작업과 라우팅 정보를 기초로 실제 데이터를 전달(forward)하는 작업을 포함한다. 라우팅을 담당하는 전용 컴퓨터를 라우터(router)라 부르며, 라우터를 통해 다양한 종류의 네트워크가 연결되어 통신을 수행할 수 있다.

    전송 계층

    전송 계층(transport layer)은 최종적인 통신 목적지를 지정하고, 오류 없이 데이터를 전송하는 역할을 한다. 인터넷 계층이 담당하는 역할은 IP주소와 라우팅을 이요하여 목적지 종단 시스템에 데이터가 도달할 수 있도록 하는 것이다. 그러나 통신의 최종 목적지는 컴퓨터가 아닌 해당 컴퓨터에서 실행되고 있는 프로그렘(프로세스)이다. 따라서 전송 계층에서는 해당 프로세스를 지정하는 일종의 주소를 사용하며, 이를 포트 번호(port number)라 부른다.

    인터넷 계층의 IP가 제공하는 전송 서비스는 최선을 다하지만(best-effort), 신뢰성은 없다는(unreliable) 특징이 있다. 즉, 데이터에 문제가 생기면 목적지에 도달하지 못하는 상황이 발생할 수 있으며, 목적지에 도달하였더라도 실제 데이터의 내용이 손상됐을 가능성도 있게 된다. 전송 계층에서는 이러한 데이터 손실 또는 손상을 검출하여 잘못된 데이터가 목적지 프로세스에 전달되는 것을 방지한다.

    TCP/IP 프로토콜에서 전송 계층에 해당하는 프로토콜로는 TCP(Transmission Control Protocol)와 UDP(User Datagram Protocol)이 있다.

    애플리케이션 계층

    애플리케이션 계층(application layer)은 전송 계층을 기반으로 한 다수의 프로토콜(Telnet, FTP, HTTP, SMTP, ...)과 이 프로토콜을 이용하는 애플리케이션을 포괄한다.

    패킷 전송 원리

    애플리케이션에서 보내는 데이터를 목적지까지 전송하기 위해서는 각각의 프로토콜에서 정의한 제어 정보(IP주소, 포트번호, 오류 체크 코드, ...)가 필요하다. 제어 정보는 위치에 따라, 앞쪽에 붙는 헤더(header)와 뒤쪽에 붙는 트레일러(trailer)로 나눌 수 있다. 이러한 제어 정보가 결합된 형태로 실제 데이터가 전송되는데 이를 패킷(packet)이라 부른다.

    송신측에서 보낸 데이터는 TCP/IP/이더넷 계층을 지나면서 헤더 또를 트레일러 형태로 제어 정보가 덧붙여진 패킷이 생성된다.

    이 패킷이 수신측에 도달하면 이더넷/IP/TCP 계층을 지나면서 차례대로 헤더 또는 트레일러가 제거되고 최종적으로 송신측이 보낸 데이터를 애플리케이션이 받게 된다.

    패킷 전송 형태 - 송신측

    패킷 전송 형태 - 수신측

       

    각 계층은 마치 동일한 위치의 상대편 계층과 통신을 하는 것으로 간주할 수 있다. 이로 인해 애플리케이션 프로그래머는 주고 받을 데이터에만 집중하여 구현하면 되고, 나머지 부분은 운영체제가 제공하는 프로토콜이 처리하는 것이다.

    패킷 전송 형태 - 계층별

       

    애플리케이션 계층과 전송 계층의 경우 실제 물리적인 연결은 존재하지 않으나 인터넷 계층과 네트워크 액세스 계층이 제공하는 기능으로 인해 가상적인 연결이 존재하는 것으로 간주한다. 이 두 계층은 모두 종단에서만(end-to-end) 작동한다. 한편, 인터넷 계층은 종단 시스템과 라우터에 모두 존재하며, IP주소와 라우팅 기능을 이용하여 패킷 전송 경로를 결정한다. 실제 패킷 전송을 위해서는 네트워크 액세스 계층에서 제공하는 물리 주소를 사용하는데, 라우터를 통과할 때마다 이러한 물리 주소는 다음 목적지로 패킷을 보내기 위해 계속 변경된다.

    TCP/IP 계층간 통신

    IP 주소, 포트 번호

    TCP/IP 프로토콜을 이용하여 통신할 때는 IP주소와 포트 번호를 사용한다. IP주소는 32비트 값으로 일반적으로 8비트 단위로 구분하여 각각 10진수로 표기한다.(127.0.0.1)

    폐쇄된 네트워크나 IP르르 공유하는 경우가 아니라면 IP주소는 전세계적으로 유일한 값을 가진다. 라우터가 라우팅을 할 때는 IP헤더에 포함된 수신자 IP주소를 이용하며, 데이터를 받는 쪽에서 답장을 보내고자 할 때도 역시 IP헤더에 포함된 송신자 IP주소를 이용한다.

    IP주소는 인터넷에 존재하는 호스트(종단 시스템, 라우터)를 유일하게 구별할 수 있지만 통신의 최종 주체인 프로세스를 식별하지는 못한다. 포트 번호는 각 프로세스를 구별하는 식별자다. 한 프로세스가 두 개 이상의 포트 번호를 사용하는 경우가 종종 있으며, 한 포트 번호를 두 개 이상의 프로세스가 사용하는 것도 가능하다. 따라서 포트 번호는 프로세스를 구별하는 식별자라기보다는 통신의 종착지를 나타내는 식별자로 보는 것이 바람직하다.

포트 번호

분류

0 ~ 1023

Well-known ports

1024 ~ 49151

Registered ports

49152 ~ 65535

Dynamic and/or private ports

한 프로세스가 두 개 이상의 TCP 혹은 UDP 포트를 사용하거나, 한 포트 번호를 두 개 이상의 프로세스가 이용할 수 있다.

IP주소는 사람이 기억하고 사용하기에는 불편한 점이 많으므로 "www.microsoft.com"같은 도메인 이름(domain name)을 사용하는 경우도 많다. 도메인 네임은 IP주소에 대한 별명에 불과하므로 실제 통신을 하기 위해서는 IP주소로 변환해야 한다.

클라이언트/서버 모델

네트워크 프로그램은 일반적으로 클라이언트/서버(client/server model)로 작성한다. 여기서 클라이언트/서버는 두 개의 애플리케이션이 상호 작용하는 방식을 나타내는 용어로, 클라이언트가 서버에 요청을 하면 서버는 이 요청을 받아 처리하게 된다. 하나의 컴퓨터에서 실행되는 두 프로그램 간에 클라이언트/서버 모델이 적용될 경우, 두 프로그램은 프로세스 간 통신(IPC, Inter-Process Communication) 기법을 사용하여 상호 정보를 교환한다. 반면, 네트워크로 연결된 두 컴퓨터에서 실행되는 두 프로그램 간에 클라이언트/서버 모델이 적용될 경우, 두 프로그램은 통신 프로토콜을 사용하여 상호 정보를 교환한다.

클라이언트 서버 모델에서는 한 프로세스가 먼저 실행하여 대기하고, 다른 프로세스가 나중에 실행하여 접속하도록 해야 한다. 이때 먼저 실행하는 쪽이 서버(server)고, 나중에 실행하는 쪽이 클라이언트(client)다. 클라이언트가 서버에 접속하기 위해서는 서버의 IP주소(또는 도메인 이름)와 포트 번호를 미리 알고 있어야 한다. 반면, 서버는 클라이언트의 주소를 미리 알 필요가 없다. 클라이언트가 보낸 패킷에는 클라이언트 주소 정보가 모두 들어있기 때문에, 서버는 이 정보를 이용하여 언제든지 해당 클라이언트에 데이터를 보낼 수 있다.

클라이언트/서버 모델은 네트워크에 분산된 프로그램 간의 상호 작용을 위해 자주 사용된다.

  • 소켓의 개념

    데이터 타입

    소켓은 파일 디스크립터(file descriptor)혹은 핸들(handle)과 유사한 개념으로 운영체제가 통신을 위해 관리하는 데이터를 간접적으로 참조할 수 있도록 만든 것이다.

    int fd = open("myFile", ...); // 파일 생성

    ...

    read(fd, ...); // 읽기

    write(rd, ...); // 쓰기

    ///////////////////////////////////////////

    SOCKET sock = socket(...); // 소켓 생성

    ...

    recv(sock, ...); // 받기

    send(sock, ...); // 보내기

    파일 입출력 코드와 소켓 입출력 코드가 비슷한 형태를 지니고 있다. 프로그래머 관점에서 본다면 소켓은, 생성과 설정 과정이 끝나면 이를 이용하여 통신과 관련된 다양한 작업을 할 수 있는 간편한 데이터 타입인 것이다.

    통신 종단점

    TCP/IP 프로토콜을 이용하여 애플리케이션이 통신을 하기 위해서는 다음과 같은 다섯 가지 요소가 결정되어야 한다.

    • 사용할 프로토콜(TCP/IP, UDP/IP, ...)
    • 송신측 IP주소
    • 송식측 포트 번호
    • 수신측 IP주소
    • 수신측 포트 번호

    소켓은 이러한 정보의 집합체로써, 애플리케이션 관점에서 본다면 통신 종단점이라 할 수 있다. 클라이언트는 자신의 소켓이 서버측 소켓과 연결된 것으로 생각하고 send() 함수를 호출하여 데이터를 보낸다. 서버 역시 자신의 소켓이 클라이언트측 소켓과 연결된 것으로 생각하고 recv() 함수를 호출하여 클라이언트가 보낸 데이터를 받을 수 있다.

    네트워크 프로그래밍 인터페이스

    TCP/IP 프로토콜의 관점에서 소켓은 하나의 네트워크 프로그래밍 인터페이스에 불과하다. 따라서 애플리케이션이 통신을 하기 위해 양쪽 모두 소켓을 사용해야 하는 것은 아니다. 단지 양쪽 모두 동일한 프로토콜을 사용하고, 정해진 형태와 절차에 따라 데이터를 주고 받으면 되는 것이다.

    TCP/IP 프로토콜 구조에서 소켓은 애플리케이션 계층과 전송 계층 사이에 위치하는 것으로 간주한다. 그래서 전송 계층을 건너 뛰고 곧바로 인터넷 계층과 연결하는 것도 가능하다.

  • 윈도우 소켓

    특징

    윈도우 소켓은 유닉스 소켓을 기반으로 한 네트워크 프로그래밍 인터페이스므로 소스 코드 수준에서 비교적 호환성이 높다. 그러나 다음과 같은 이유로 유닉스 소켓 프로그래밍과 다른 부분도 존재한다.

    • 윈도우 소켓은 DLL을 통해 대부분의 기능이 제공되므로 DLL 초기화와 종료 작업을 위한 함수가 필요하다.
    • 윈도우 애플리케이션은 대개 그래픽 사용자 인터페이스(GUI, Graphic User Interface)를 기반으로 하며, 메시지 구동 방식으로 동작하므로 이를 위한 확장 함수가 존재한다.
    • 윈도우는 운영체제 차원에서 멀티스레드(multithread)를 지원하므로 멀티스레드 환경에서 안정적으로 동작하기 위한 구조와 이를 위한 함수가 필요하다.

    윈속의 장점

    • 유닉스 소켓과 소스 코드 수준에서 호환성이 높으므로 기존 프로그램을 포팅하기 쉽다.
    • 가장 널리 쓰이는 네트워크 프로그래밍 인터페이스이므로 한번 배워두면 여러 환경(윈도우, 유닉스,...)에서 사용할 수 있다.
    • TCP/IP 외에도 다양한 종류의 프로토콜을 지원하므로 최소한의 코드 수정으로 애플리케이션에서 사용할 프로토콜을 변경할 수 있다.
    • 비교적 저수준(low-level 혹은 mid-level)의 프로그래밍 인터페이스로, 세부적인 제어가 가능하므로 고성능의 네트워크 애플리케이션을 개발할 수 있다.

    윈속의 단점

    • 애플리케이션 수준의 프로토콜을 프로그래머가 직접 설계해야 한다. 즉 데이터 포맷이나 전송 절차 등을 고려하여 프로그래밍 해야 한다. 따라서 프로토콜을 변경할 경우 코드 수정이 불가피하다.
    • 서로 다른 바이트 정렬(byte ordering)방식을 사용하거나 데이터 처리 단위(32비트, 64비트, ...)가 서로 다른 종단 시스템간 통신을 할 경우, 애플리케이션 수준에서 데이터 변환을 처리해야 한다.

    구조

    윈속이 지원하는 대부분의 기능은 WS2_32.DLL로 제공되며, 윈속 1.x 애플리케이션은 WINSOCK.DLL이나 WSOCK32.DLL을 통해 WS2_32.DLL의 기능을 궁극적으로 사용하게 된다. 표준 윈속 API 외에도 마이크로 소프트에서 확장한 API가 존재하며 MSWSOCK.DLL로 제공된다. 어느 경우든 애플리케이션은 소수의 DLL과 링크되어 실행되며, 애플리케이션에서 실제로 사용할 하부 프로토콜은 WS2_32.DLL이 적절히 선택하여 연결해준다.

    윈속 어플리케이션 맛보기

    • 예제 Server.cpp

      #pragma comment(lib, "ws2_32.lib") 이나

      'Object/library modules'에 ws2_32.lib를 입력해준다.

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

   

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

   

   

#define BUFSIZE 512

   

// 소켓 함수 오류 출력 후 종료

void err_quit( char* msg )

{

LPVOID lpMsgBuf;

FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER |

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ),

(LPTSTR)&lpMsgBuf, 0, NULL );

MessageBox( NULL, (LPCTSTR)lpMsgBuf, (LPCWSTR)msg, MB_ICONERROR ) ;

LocalFree( lpMsgBuf ) ;

exit(-1);

}

   

void err_display( char* msg )

{

LPVOID lpMsgBuf ;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER |

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL );

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

int main( int argc, char* argv[] )

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

return -1 ;

   

// socket()

SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0 ) ;

if( listen_sock == INVALID_SOCKET ) err_quit("*sock()");

   

// bind()

SOCKADDR_IN serveraddr;

ZeroMemory( &serveraddr, sizeof( serveraddr) );

serveraddr.sin_family = AF_INET;

serveraddr.sin_port = htons(9000);

serveraddr.sin_addr.s_addr = htonl( INADDR_ANY ) ;

retval = bind( listen_sock, (SOCKADDR*)&serveraddr, sizeof(serveraddr) );

if( retval == SOCKET_ERROR) err_quit("bind()");

   

// listen()

retval = listen(listen_sock, SOMAXCONN ) ;

if( retval == SOCKET_ERROR ) err_quit( "listen()" );

   

// 데이터 통신에 사용할 변수

SOCKET client_sock;

SOCKADDR_IN clientaddr;

int addrlen;

char buf[BUFSIZE+1];

   

while(1)

{

// accept()

addrlen = sizeof(clientaddr);

client_sock = accept( listen_sock,

(SOCKADDR*)&clientaddr, &addrlen ) ;

if( client_sock == INVALID_SOCKET )

{

err_display("accept()") ;

continue;

}

printf("\n[TCP 서버] 클라이언트 접속 : IP 주소 : %s, 포트번호=%d\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port));

// 클라이언트와 데이터 통신

while(1)

{

// 데이터 받기

retval = recv(client_sock, buf, BUFSIZE, 0 ) ;

if ( retval == SOCKET_ERROR )

{

err_display("recv()");

break;

}

else if ( retval == 0 )

{

break ;

}

else

{

// 받은 데이터 출력

buf[retval] = '\0';

printf("%s", buf ) ;

}

}

   

// closesocket()

closesocket( client_sock ) ;

printf("\n[TCP 서버]클라이언트 종료: IP 주소=%s, 포트번호=%d\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port));

}

   

// closesocket()

closesocket(listen_sock);

   

// 윈속 종료

WSACleanup();

return 0 ;        

}

F7키를 눌러 컴파일 한 후 실행파일을 실행

cmd 실행 > telnet 127.0.0.1 9000을 입력한 후 Enter

글지를 입력하면 server측에 출력됨

"Ctrl + ]" 입력 후 'quit' 명령을 입력하면 종료되고 서버에 클라이언트가 종료한 것을 감지하여 표시됨

   

반응형

'책정리 > 윈도우 네트워크 프로그래밍' 카테고리의 다른 글

5장 멀티스레드  (0) 2015.04.20
4장 TCP 서버/클라이언트 구조  (0) 2015.04.13
3장 소켓 주소 구조체 다루기  (0) 2015.04.11
2장 윈도우 소켓 시작하기  (0) 2015.04.10
목차  (0) 2015.04.07