뇌를 자극하는 C# 4.0 프로그래밍/ 네트워크 프로그래밍

네트워크 프로그래밍에 앞서 알아 두어야 할 기초

인터넷의 유래

  • 천공 카드로 입, 출력을 하던 시기를 지나 프로그램을 하나만 실행할 수 있었던 컴퓨터는 동시에 여러 가지 프로그램을 실행할 수 있었다.
  • 이 시기의 컴퓨터 사용 방식은 아래 그림처럼 중앙 컴퓨터에 더미 터미널 여러개를 연결하는 것이었다.
  • 더미 터미널은 연산 능력은 전혀 없는 대신 입력과 출력을 할 수 있는 기능만 있었다. 이 더미 터미널은 아무 지능이 없었지만 큰 의미가 있는 기능을 갖추고 있었는데 그것이 바로 중앙 컴퓨터와 데이터를 주고 받는 기능이었다.

  • 1957년 소련이 미국보다 먼저 우주에 위성을 쏘는데 성공하자 이에 충격을 받은 미국은 과학 기술에 많은 투자를 하게 되었는데 그 중 하나가 1958년 설립된 DARPA(Defence Advanced Research Project Agency) 였다.
  • DARPA는 막대한 예산을 지원 받으며 미군을 위한 연구를 수행했는데 DARPA의 본부는 알링톤에 있었지만 DARPA의 프로젝트 상당수가 대학과 외부의 연구소에서 진행되어 DARPA가 외부 자료에 접근할 수 있는 방법은 ‘택배’ 밖에 없었다.
  • DARPA는 이 문제를 위해 새로운 구상을 했는데, 그것은 네트워크와 네트워크를 연결하는 시스템이었다. 이른바 DARPANET 이라고 불리던 이 네트워크의 네트워크는 더 많은 대학과 연구 기관으로 또한 세계의 연구 기관과 민간으로 연결되기 시작하더니 1980년 말에 이르러 인터넷이라고 하는 국제 통신망을 형성하게 되었다.

TCP/IP 스택

  • 컴퓨터끼리 데이터를 주고 받기 위해서는 그 네트워크에서 통용되는 ‘프로토콜(Protocol)’이라는 통신 규약을 따라야 한다.
  • 인터넷은 전세계에서 가장 거대한 네트워크긴 하지만 유일한 네트워크는 아니다. 또한 프로토콜도 굉장히 다양한 종류가 많다. 다행히 인터넷이 사실상 전세계 컴퓨터 네트워크의 표준이라고 할 수 있을 정도로 자리를 잡았고 인터넷의 통신 프로토콜로 사용되는 TCP/IP도 실질적인 인터넷 표준 프로토콜로 자리 잡았다.
  • TCP/IP는 표준 프로토콜로써 인터넷에서 데이터를 주고 받는데 필요한 일련의 프로토콜 모음(Suite)이다. TCP/IP는 아래 그림과 같이 4개의 계층으로 구성되어 있으며, 한 계층 위에 다른 계층이 포개져 있는 형태 때문에 TCP/IP 스택이라고 부르기도 한다.

링크 계층

  • 링크 계층은 물리 계층(Physical Layer), 네트워크 접속 계층(Network Interface Layer), 미디어 접근 계층(Media Access Layer) 등으로 불리기도 한다.
  • TCP/IP는 네트워크의 물리적인 구성으로부터 독립적인 프로토콜로 컴퓨터가 네트워크에 전화선에 모뎀으로 연결되어 있든, LAN에 이더넷 케이블로 연결되어 있든, Wi-Fi에 연결되어 있든 전혀 신경 쓰지 않는다. 이는 링크 계층에서 네트워크의 물리적인 연결 매체를 통해 패킷을 주고 받는 작업을 담당해 주기 때문에 가능한 일이다.
    • 네트워크를 통해 전송되는 데이터를 패킷이라고 부르는 까닭은 데이터가 소포처럼 포장지를 필요로 하기 때문이다. 이 포장지는 데이터를 싸서 보호하고 데이터가 어디에서 어디로 가는지가 기록 된다.
    • 네트워크 패킷은 여러 겹의 포장지로 포장되는데 어플레이케이션 계층, 전송 계층, 인터넷 계층, 링크 계층이 모두 패킷의 포장지가 된다. 데이터를 보낼 때는 어플레이케이션 계층부터 시작해서 링크 계층까지 포장을 하고, 데이터를 받을 때는 링크 계층부터 어플리케이션 계층까지 포장을 뜯어 내용물을 꺼낸다.
  • 어떤 패킷이 네트워크를 통해 컴퓨터에 들어오면 제일 먼저 링크 계층이 맞이한다. 이 링크 계층은 이 패킷에서 물리적 데이터 전송에 사용되던 부분을 제거하고 인터넷 계층에 넘긴다. 이렇게 함으로써 인터넷 계층에서는 패킷이 전파를 타고 넘어 왔든 광케이블을 타고 넘어왔든 간에 상관 없이 자신의 일을 처리할 수 있게 된다.

인터넷 계층

  • 인터넷 계층은 패킷을 수신해야 할 상대의 주소를 지정하고, 나가는 패킷에 대해서는 적절한 크기로 분할하며 들어오는 패킷에 대해서는 재조립을 수행한다. 이 계층에서 사용되는 규약이 바로 인터넷 프로토콜(Internet Protocol, IP)이다. TCP/IP에서의 IP가 바로 이것.
  • IP는 우체국 일반우편과 비슷한 특징이 있는데, 내보낸 패킷을 상대방이 잘 수령했는지에 대해 전혀 보장하지 않기 때문. 배달 중에 문제가 생겨서 패킷이 손상되거나 분실된다 해도 전혀 책임을 지지 않는다.
  • 일단 IP는 상대방이 패킷을 잘 수령했는지의 여부를 파악하는 기능 자체가 없다. 그저 전송 계층에서 내려온 패킷에 주소를 붙여 링크 계층으로 보내기만 할 뿐. 따라서 여러 개의 패킷을 전송했을 때 순서대로 도착하지 않아도 이상할 것이 전혀 없다.
    • 여기서 사용하는 주소 체계가 바로 IP 주소이다.

전송 계층

  • 전송 계층에는 이름 그대로 패킷의 운송을 담당하는 프로토콜들이 정의되어 있다. 이 중에서도 전송 제어 프로토콜(Transmission Control Protocol, TCP)은 송신측과 수신측 간의 연결성을 제공하며, 신뢰할 수 있는 패킷 전송 서비스를 제공한다.
  • 여러 개의 패킷을 송신하는 경우 패킷 사이의 순서를 보장하며, 패킷이 유실되기라도 하면 재전송 해주기도 한다. TCP/IP 프로토콜에서 TCP가 바로 이 프로토콜을 가리키는 것이며, TCP는 IP가 제공하지 않는 연결성, 신뢰성을 제공한다.
    • 웹 문서를 전달하는 기능을 하는 HTTP를 비롯한 수많은 응용 프로토콜들이 바로 이 TCP와 IP 프로토콜 위에서 동장한다.
  • 한편 TCP는 IP가 제공하지 않는 연결성과 신뢰성을 제공하느라 성능에서 손실을 본다. 데이터가 큰 경우에는 여러 개의 패킷에 나눠 담아 순서대로 보내야 하지만, 데이터가 충분히 작은 경우에는 하나의 패킷에 담아 보내도 된다. 이런 상황에서는 TCP가 제공하는 패킷의 순서 보장성이 필요가 없다. 또한 받아도 그만, 안 받아도 그만인 패킷의 경우에는 굳이 재전송을 할 필요가 없다. 다시 말해 충분히 작고 전송 신뢰성을 요구하지 않는 데이터는 TCP의 장점이 단점이 되는데, 이를 위한 대안으로 전송 계층에는 UDP(User Datagram Protocol)이라는 프로토콜이 정의되어 있다.
    • 이 프로토콜은 연결성과 신뢰성을 제공하지 않지만 성능이 TCP에 비해 우수하기 때문에 전송 제어를 직접 처리하는 응용 프로그램 수준에서 채용되는 경우가 많다.

어플리케이션 계층

  • 이 계층은 각 응용 프로그램 나름의 프로토콜들이 정의되는 곳이다. 웹 문서를 주고 받기 위한 HTTP(Hyper Text Transfer Protocol), 파일 교환을 위한 FTP(File Transfer Protocol), 네트워크 관리를 위한 SNMP(Simple Network Management Protocol) 등이 어플리케이션 계층에서 정의된 프로토콜의 대표적인 예이다.
  • 어플리케이션 계층의 프로토콜들은 전송 계층의 프로토콜 중 TCP에 기반할 수도 있고, UDP에 기반할 수도 있다. HTTP와 FTP는 상대적으로 큰 데이터를 처리해야 하기 때문에 TCP에 기반하고 SNMP는 단순한 정보만 다루는데다 패킷을 일부 유실해도 임무에 지장을 주지 않기 때문에 비용이 저렴한 UDP에 의존한다.
  • 한편 HTTP나 FTP처럼 표준화된 프로토콜이 아니더라도 이 계층에서는 우리 나름대로의 프로토콜을 정의해서 사용할 수 있습니다. 예컨대 인스턴트 메신저를 개발하는데 필요한 프로토콜을 정의하면 그 프로토콜은 이 어플리케이션 계층에 속한다.

TCP/IP의 주소 체계: IP 주소

  • 우편물을 전달하기 위해서는 ‘주소’가 필요하듯이 인터넷에서도 패킷을 전달하려면 이것을 어디에서 보냈으며 어디로 보낼지에 대한 정보가 필요하다. 그리고 인터넷에서 사용하는 이 주소를 IP 주소(Address)라고 부른다.

포트

  • 큰 빌딩은 대개 출입구가 여러개 있게 마련인데, 컴퓨터도 네트워크 패킷이 드나들려면 출입문이 있어야 한다. 이 출입문을 가리켜 ‘포트(Port)’라고 부른다.
  • 포트는 부호가 없는 16비트 정수로써 0~65535 사이의 값을 이용한다. 예컨대 HTTP는 80번 포트를 사용하고 FTP는 21번 Telnet은 23번을 사용한다.
    • HTTP나 FTP, Telnet과 같은 표준 프로토콜이 사용하고 있는 포트 번호는 전세계적으로 합의된 값이다. 이러한 포트 번호를 일컬어 ‘잘 알려진 포트 번호(Well Known Port Number)’라고 부른다. 잘 알려진 포트 번호는 1-1023 사이의 수를 사용하므로 새로운 응용 프로그램 프로토콜을 정의할 때는 이 범위를 피해서 정하는 것이 좋다.
      • HTTP: 80
      • HTTPS: 443
      • FTP: 21
      • Telnet: 23
      • SMTP: 25
      • IRC: 194
      • IIOP: 535
  • 건물의 보안이 출입구를 얼마나 잘 단속하느냐에 달려 있듯 네트워크의 보안도 이런 포트 단속이 큰 비중을 차지 한다. 꼭 필요한 포트만 열어 놓는 것이 최선이다.

TCP/IP의 동작 과정

  • TCP/IP는 서버/ 클라이언트 방식으로 동작한다. 통신을 수행하는 양단 중 한 쪽에서는 다른 한 쪽에게 서비스를 제공한다는 뜻.
  • TCP/IP 통신을 위해서는 먼저 서버가 이 서비스를 시작해야 한다. 클라이언트가 접속해 올 수 있도록 준비를 하는 것. 서버가 준비되고 나면 클라이언트는 서버에 접속을 시도하고 서버가 이 접속을 수락하면 서버와 클라이언트는 동등한 입장에서 데이터를 주고 받을 수 있게 된다.

TcpListener와 TcpClient

  • TcpListener 클래스는 서버 응용 프로그램에서 사용되며, 클라이언트의 연결 요청을 기다리는 역할을 하며, TcpClient는 서버 응용 프로그램과 클라이언트 응용 프로그램 양쪽에서 사용된다. 클라이언트에서는 TcpClient가 서버에 연결 요청을 하는 역할을 수행하며, 서버에서는 클라이언트의 요청을 수락하면 클라이언트와의 통신에 사용할 수 있는 TcpClient의 인스턴스가 반환된다.
  • 서버와 클라리언트 각각이 갖고 있는 TcpClient는 GetStream()이라는 메소드를 갖고 있어서 양쪽의 응용 프로그램은 이 메소드가 반환하는 NetworkStream 객체를 통해 데이터를 주고 받는다. 데이터를 보낼 떄는 NetworkStream.Write()를, 데이터를 읽을 때는 NetworkStream.Read()를 호출한다. 데이터를 주고 받는 일을 마치고 서버와 클라이언트의 연결을 종료할 때는 NetworkStream 객체와 TcpClient 객체 모두의 Close() 메소드를 호출한다.

클래스 메소드 설명
TcpListener Start() 연결 요청 수신 대기를 시작한다.
TcpListener AcceptTcpClient() 클라이언트의 연결 요청을 수락한다. 이 메소드는 TcpClient 객체를 반환한다.
TcpListener Stop() 연결 요청 수신 대기를 종료한다.
TcpClient Connect() 서버에 연결을 요청한다.
TcpClient GetStream() 데이터를 주고 받는데 사용하는 매개체인 NetworkStream을 가져온다.
TcpClient Close() 연결을 닫는다.
/* 서버의 TcpListener 코드 예시 */

// IPEndPoint는 IP 통신에 필요한 IP 주소와 출입구(포트)를 나타낸다.
IPEndPoint localAddress = new IPEndPoint(IPAddress.Parse("192.168.100.17"), 5425);

TcpListener server = new TcpListener(localAddress);

// server 객체는 클라이언트가 TcpClient.Connect()를 호출하여 연결을 요청해 오기를 기다리기 시작한다.
server.Start();


/* 클라이언트의 TcpListener 코드 예시 */

// 포트를 0으로 지정하면 OS에서 임의의 번호로 포트를 할당해 준다.
IPEndPoint clientAddress = new IPEndPoint(IPAddress.Parse("192.168.100.18"), 0);

TcpClient client = new TcpClient(clientAddress);

IPEndPoint serverAddress = new IPEndPoint(IPAddress.Parse("192.168.100.17"), 5425);

// 서버가 수신 댁하고 있는 IP 주소와 포트 번호를 향해 연결 요청을 수행한다.
client.Connect(serverAddress);


/* 서버 코드 예시. 서버에서 AcceptTcpClient()를 호출하면 코드는 블록되어 그 자리에서 이 메소드가 반환할 때까지 진행하지 않는다. 기다리던 연결 요청이 오면 이 메소드는 클라이언트와 통신을 수행할 수 있도록 TcpClient 형식의 객체를 반환한다. */

TcpClient client = server.AcceptTcpClient();


/* TcpClient 객체가 NetworkStream 객체를 반환하고 NetworkStream 객체를 이용하여 데이터를 읽고 쓰는 예제 */

// TcpClient를 통해 NetworkStream 객체를 얻는다.
NetworkStream stream = client.GetStream();

int length;
string data = null
byte[] bytes = new byte[256];

// NetworkStream.Read() 메소드는 상대바잉 보내 온 데이터를 읽어 들인다.
// 한편, 상대와의 연결이 끊어지면 이 메소드는 0을 반환한다. 즉, 이 루프는 연결이 끊어지기 전까지는 계속된다.
while ( (length = stream.Read(bytes, 0, bytes.Length)) != 0 )
{
    data = Encoding.Default.GetString(bytes, 0, length);
    Console.WriteLine(String.Format("수신: {0}", data));

    byte[] msg = Encoding.Default.GetByte(data);

    stream.Write(msg, 0, msg.Length);
    Console.WriteLine(String.Format("송신: {0}", data));
}
  • 참고) 127.0.0.1은 컴퓨터의 네트워크 입출력 기능을 시험하기 위해 가상으로 할당한 주소이다. 네트워크 출력에 데이터를 기록하면 실제로 패킷이 링크 계층을 거쳐 바깥으로 나가야 하지만 127.0.0.1을 향해 데이터를 기록하면 링크 계층을 거치지 않고 다시 자기 자신에게로 패킷을 보내게 된다. 이렇게 되돌아 오는 입출력 기능 때문에 루프백(Loopback) 주소라고 부르기도 한다.

흐르는 패킷

  • TCP를 처음 접하면서 가장 흔히 하게 되는 오해 중의 하나는 ‘송신측에서 Write()를 할 때마다 하나의 메시지가 만들어지며 이 메시지를 수신측에서 Read()를 통해 하나씩 읽어온다’는 것.
  • TCP는 연결 지향, 흐름 지향 프로토콜로 TCP 프로토콜의 데이터 전달 과정은 편지보다는 오히려 전기가 전달되는 모습과 더 닮았다고 할 수 있다. 전기는 전선으로 ‘연결’ 되어 있는 상태에서 전기를 갖고 있는 쪽이 전기를 받아야 하는 쪽으로 전기를 ‘흘려’ 보낸다. TCP 프로토콜도 전기처럼 양쪽이 연결되어 있어야 하고 보내는 쪽에서 받는 쪽으로 패킷을 흘려 보낸다. 둘이 다른 점이 있다면 TCP 프로토콜은 흐름 속에서 각 개별 패킷의 경계를 구분한다는 점. 시작이 어디고 끝이 어딘지를 파악해야 한다는 것이다.
  • TCP 통신 응용 프로그램도 댐과 같은 역할을 하는 버퍼(Buffer)를 갖고 있다. 응용 프로그램에서 네트워크를 향해 내보내는 데이터나 들어오는 데이터나 모두 이 버퍼를 거친다.

  • 예컨대 두 응용 프로그램이 TCP 연결을 맺고 있고, 송신 응용 프로그램이 메모리에 들고 있는 ‘a’, ‘b’, ‘c’를 수신 응용 프로그램에 보내려 한다고 가정한다. –a, b, c는 wBuffer라는 이름의 바이트 배열에 담겨 있다.

  • 송신측 응용 프로그램에서 writer.Write(wBuffer, 0, 3)을 호출하면 데이터는 아래 그림과 같이 응용 프로그램의 메모리에서 송신 버퍼로 이동한다.

  • 운영체제는 송신 버퍼에 있는 내용을 연결을 맺고 있는 수신측으로 보내기 싲가한다. 이 때 네트워크 대역폭이 넓고 품질도 좋다면 많은 데이터가 빠른 속도로 수신측으로 이동할 것이고 그렇지 않다면 다음과 같이 아주 조금씩 데이터가 이동할 수도 있다.

  • 한편 수신측 응용 프로그램에서는 데이터를 담기 위핸 rBuffer를 선언하고 reader.Read(rBuffer, 0, 16)을 호출한다. 이 코드는 16바이트를 읽어오려고 시도하지만 실제 수신 버퍼에는 a 하나 밖에 없으므로 rBuffer에는 ‘a’가 담기고 Read() 메소드는 실제로 읽은 바이트 수 1을 반환한다. 한편, 그러는 동안 수신 버퍼에는 송신측에서 보낸 b, c가 도착했다.

  • 이번에도 수신측은 reader.Read(rBuffer, 0, 16)을 호출했는데, 이번에는 b, c가 rBuffer에 담기고 Read() 메소드는 읽은 바이트 수 2를 반환한다. 이렇게 해서 송신측의 프로그램 메모리에 있던 a, b, c가 모두 수신측의 메모리로 전달된다.

  • (이하 프로토콜 설계하는 내용 생략)
It's only fair to share...Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedIn

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기