Book/COMPUTER NETWORKING A TOP-DOWN-APPROACH

CH2_Application Layer(2.5~2.7)

S0LL 2024. 10. 19. 15:45

2.5 Peer to Peer File Distribution

 

이 챕터에서는 P2P 파일 분배가 무엇인지 설명하고 있다. 먼저, 웹, 이메일, DNS 등은 클라이언트-서버 아키텍처를 사용하는데, 이 경우 항상 연결된 인프라 서버에 크게 의존한다. 반면, P2P 아키텍처에서는 항상 켜져 있는 서버에 대한 의존도가 매우 적거나 아예 없다. **피어(peer)**라고 불리는 간헐적으로 연결된 호스트들이 서로 직접 통신하며 데이터를 주고받는다. 이러한 피어들은 서비스 제공자가 소유한 것이 아니라 사용자들의 PC, 노트북, 스마트폰과 같은 장치들이다.

 

2.5.1 자연스러운 P2P 애플리케이션: 파일 분배

 

P2P의 자연스러운 적용 예시로 대규모 파일 분배를 들고 있다. 예를 들어, 운영 체제 업데이트 파일이나 대용량 비디오 파일을 여러 사용자에게 배포할 때 P2P 모델을 사용할 수 있다. 클라이언트-서버 파일 분배에서는 서버가 모든 피어에게 파일을 전송해야 하기 때문에 서버에 큰 부담이 된다. 반면 P2P 파일 분배에서는 각 피어가 자신이 받은 파일의 일부를 다른 피어에게 재전송할 수 있기 때문에, 서버의 부담이 줄어들고 분배 속도가 빨라진다. P2P 분배 프로토콜의 대표적인 예로 BitTorrent가 있다.

 

2.5.2 P2P 아키텍처의 확장성

 

이 부분에서는 클라이언트-서버 아키텍처와 P2P 아키텍처의 확장성을 비교한다. 확장성 측면에서 P2P 아키텍처는 자체 확장성(self-scalability)을 가진다. 즉, 피어의 수가 늘어날수록 파일을 더 빠르게 배포할 수 있다. 이를 수학적으로 모델링해서 설명하고 있는데, P2P 방식에서는 파일을 배포할 때 서버만 모든 작업을 하는 것이 아니라, 피어들이 서로 파일을 공유하면서 시간을 단축시킨다.

 

 

2.5.3 클라이언트-서버와 P2P 아키텍처의 분배 시간 계산

 

이 섹션에서는 클라이언트-서버 모델과 P2P 모델에서 파일을 모든 피어에게 배포하는 데 걸리는 분배 시간을 계산한다.

 

클라이언트-서버 분배 시간:

서버는 모든 피어에게 파일을 전송해야 하므로, 시간이 서버의 업로드 속도에 따라 선형적으로 증가한다. 공식은 다음과 같다:

 

여기서 은 피어의 수, 는 파일의 크기, 는 서버의 업로드 속도, 은 피어 중 가장 느린 다운로드 속도이다.

P2P 분배 시간:

P2P에서는 피어들이 파일의 일부를 다른 피어에게 재전송하기 때문에 시간이 더 단축된다. 공식은 다음과 같다:

 

여기서 는 각 피어의 업로드 속도를 의미한다. 이 공식은 P2P에서 서버와 피어들이 함께 파일을 배포하는 방식을 반영한다.

 

2.5.4 BitTorrent

 

BitTorrent는 P2P 파일 분배 프로토콜 중 하나로, 파일을 작은 청크(chunks) 단위로 나눠서 분배한다. 사용자가 토렌트에 참여하면, 각 청크를 다른 피어로부터 다운로드하고, 동시에 자신이 받은 청크를 다른 피어에게 업로드한다. 이를 통해 네트워크의 모든 참여자가 파일을 조금씩 나눠가질 수 있다. 또한, BitTorrent는 **트래커(tracker)**라는 서버를 사용하여 피어들이 서로를 찾을 수 있도록 도와준다.

 

BitTorrent에서 중요한 개념은 **희귀 청크 우선 다운로드(rarest first)**이다. 즉, 피어는 다른 피어들이 가장 적게 가지고 있는 파일의 청크를 우선적으로 요청하여 파일의 고르게 분배되는 것을 목표로 한다.

 

2.5.5 BitTorrent의 세부 메커니즘

 

BitTorrent는 티포탯(tit-for-tat) 전략을 통해 자신에게 데이터를 가장 빠르게 보내주는 피어에게 보답하는 방식으로 동작한다. 일정 시간마다 데이터를 많이 주고받는 피어를 우선순위에 두고 데이터를 교환하며, 이를 언초크(unchoked) 상태라고 한다. 또한, **옵티미스틱 언초크(optimistically unchoke)**라는 방식을 사용하여, 새로운 피어에게도 기회를 제공함으로써 네트워크 전반의 데이터 흐름을 원활하게 유지한다.

 

2.5.6 분산 해시 테이블 (DHT)

 

P2P의 또 다른 중요한 기술인 **분산 해시 테이블(DHT)**에 대해 간략히 설명하고 있다. DHT는 분산된 피어들 사이에서 데이터를 효율적으로 찾고 관리할 수 있는 시스템이다. BitTorrent도 DHT를 사용하여 데이터를 분산 저장하고 찾는 과정을 더욱 최적화한다.

 

P2P의 핵심은 파일을 직접 공유함으로써 서버의 부담을 줄이고, 분배 속도를 향상시키는 데 있다. BitTorrent와 같은 P2P 프로토콜은 이를 잘 구현한 사례이다.


2.6 Video Streaming and Content Distribution Networks

 

2.6.1 인터넷 비디오

 

인터넷 비디오 스트리밍은 사전 녹화된 비디오 콘텐츠(영화, TV 프로그램, 스포츠 경기, 사용자 생성 콘텐츠 등)를 서버에 저장하고, 사용자가 이 서버에 요청을 보내 비디오를 재생하는 방식이다. 예를 들어, 넷플릭스, 유튜브, 아마존 등이 그러한 서비스를 제공하는 대표적인 회사다.

 

비디오는 기본적으로 프레임이라는 연속된 이미지들의 시퀀스로, 초당 24 또는 30프레임으로 표시된다. 각 이미지는 픽셀로 이루어져 있으며, 각 픽셀은 색상과 밝기를 나타내는 비트로 인코딩된다. 압축 기술을 이용해 비디오의 파일 크기를 줄일 수 있는데, 압축이 많이 될수록 화질이 낮아지지만 파일 크기가 줄어든다. 이처럼 비디오는 화질비트레이트 간의 **트레이드오프(서로 상충하는 관계)**를 가진다.

 

네트워크 관점에서 가장 중요한 비디오의 특징은 높은 비트레이트이다. 예를 들어, 고화질 영상을 스트리밍하려면 4Mbps 이상의 속도가 필요하며, 4K 영상을 스트리밍할 경우 10Mbps 이상의 속도가 필요하다. 이는 네트워크를 통해 많은 데이터를 전송해야 함을 의미한다.

 

2.6.2 HTTP 스트리밍과 DASH

 

HTTP 스트리밍에서는 비디오가 HTTP 서버에 저장되며, 사용자가 비디오를 요청할 때 TCP 연결을 통해 해당 URL에 대한 HTTP GET 요청을 보낸다. 서버는 가능한 한 빠르게 비디오 데이터를 전송하며, 클라이언트 측에서는 데이터를 받아 버퍼에 쌓은 후 재생을 시작한다.

 

하지만, 이 방식에는 문제가 있다. 모든 사용자가 같은 비디오 인코딩을 받게 되는데, 이는 각 사용자의 네트워크 대역폭에 맞지 않을 수 있다는 점이다. 그래서 등장한 것이 **DASH(Dynamic Adaptive Streaming over HTTP)**이다. DASH는 사용자의 네트워크 상태에 따라 비디오의 화질을 자동으로 조정하는 기술이다. 비디오가 여러 버전으로 인코딩되어 있으며, 사용자는 현재 대역폭에 맞는 화질을 선택해 비디오를 끊김 없이 재생할 수 있다.

 

2.6.3 콘텐츠 분배 네트워크(CDN)

 

전 세계 사용자들에게 대용량 비디오 데이터를 전송하는 것은 매우 어려운 과제다. 한 가지 방법은 모든 비디오를 하나의 거대한 데이터 센터에 저장한 후, 그곳에서 전 세계에 스트리밍하는 것이다. 그러나 이는 여러 가지 문제가 있다. 예를 들어, 사용자가 데이터 센터에서 멀리 떨어져 있으면 네트워크 경로가 길어지며 지연과 끊김이 발생할 수 있다. 이를 해결하기 위해 **콘텐츠 분배 네트워크(CDN)**를 사용한다.

 

CDN은 여러 지리적으로 분산된 서버에 비디오 데이터를 복사하여 저장하고, 사용자의 위치에 가장 가까운 서버에서 비디오를 전송해준다. 이를 통해 지연을 줄이고 네트워크 성능을 향상시킬 수 있다. CDN은 특정 콘텐츠 제공업체가 소유한 개인 CDN일 수도 있고, 여러 콘텐츠 제공업체를 대신해 콘텐츠를 배포하는 제3자 CDN일 수도 있다.

 

CDN의 서버 배치 전략으로는 Enter DeepBring Home이라는 두 가지 방식이 있다. Enter Deep 방식은 수많은 소규모 서버 클러스터를 인터넷 서비스 제공업체(ISP)에 배치해 사용자와 가까운 곳에서 콘텐츠를 제공하는 방식이고, Bring Home 방식은 상대적으로 적은 수의 대형 서버 클러스터를 IXP(인터넷 교환 지점)에 배치하는 방식이다.

 

 

2.6.4 사례 연구: 넷플릭스와 유튜브

 

넷플릭스아마존 클라우드와 자체 CDN을 이용해 비디오를 제공한다. 넷플릭스는 비디오를 여러 버전으로 변환해 다양한 장치에서 적합한 형식으로 재생할 수 있도록 하고, 생성된 비디오를 CDN에 업로드한다. 넷플릭스는 자체 CDN을 통해 비디오를 전송하며, 네트워크 지점에 설치된 서버를 사용해 가장 가까운 곳에서 데이터를 제공한다.

 

유튜브구글의 CDN을 이용해 비디오를 전송한다. 유튜브는 HTTP 스트리밍을 사용하여 비디오를 전송하며, 유저가 직접 화질을 선택할 수 있도록 한다. 유튜브는 주로 DNS 리다이렉션을 사용해 최적의 서버 클러스터로 유저를 연결하고, 풀 캐싱 전략을 사용해 자주 사용되지 않는 데이터를 캐싱하지 않는 방식으로 작동한다.

 


2.7 Socket Programming: Creating Network Applications

소켓 프로그래밍은 네트워크 애플리케이션을 개발할 때 매우 중요한 개념이다. **소켓(Socket)**은 네트워크를 통해 데이터를 주고받을 수 있는 소프트웨어 인터페이스로, 컴퓨터 간의 통신을 담당한다. 이 섹션에서는 UDP와 TCP를 이용해 간단한 클라이언트-서버 애플리케이션을 어떻게 구현하는지 설명한다.

 

2.7.1 UDP를 이용한 소켓 프로그래밍

 

**UDP(유저 데이터그램 프로토콜)**는 신뢰성보다 빠른 전송을 우선시하는 비연결형 프로토콜이다. 연결 설정 없이 데이터를 보내며, 데이터가 제대로 도착하는지 확인하지 않는다. 그럼에도 불구하고 많은 네트워크 애플리케이션에서 UDP가 사용되는데, 이는 속도가 빠르기 때문이다.

 

UDP 클라이언트-서버 예제: 대문자 변환 애플리케이션

 

이 예제에서는 클라이언트가 서버에 소문자로 된 문장을 보내면, 서버가 이 문장을 대문자로 변환해 클라이언트에게 다시 보내는 매우 간단한 애플리케이션을 구현한다. 이를 통해 UDP 소켓 프로그래밍을 익힐 수 있다.

 

1. UDP 클라이언트(UDPClient.py)

 

from socket import *

serverName = 'hostname'

serverPort = 12000

clientSocket = socket(AF_INET, SOCK_DGRAM)

message = input('Input lowercase sentence: ')

clientSocket.sendto(message.encode(), (serverName, serverPort))

modifiedMessage, serverAddress = clientSocket.recvfrom(2048)

print(modifiedMessage.decode())

clientSocket.close()

 

from socket import *: Python에서 소켓 모듈을 가져온다. 소켓 모듈은 네트워크 통신에 필요한 다양한 기능을 제공한다.

clientSocket = socket(AF_INET, SOCK_DGRAM): 소켓을 생성한다. AF_INET은 IPv4 주소 체계를 의미하고, SOCK_DGRAM은 UDP 소켓을 의미한다.

clientSocket.sendto(message.encode(), (serverName, serverPort)): 클라이언트가 입력한 메시지를 바이트 형식으로 변환해 서버로 전송한다. 여기서 서버의 IP 주소와 포트 번호를 함께 지정한다.

modifiedMessage, serverAddress = clientSocket.recvfrom(2048): 서버로부터 대문자로 변환된 메시지를 수신한다. 이때 2048 바이트까지 수신할 수 있다.

 

2. UDP 서버(UDPServer.py)

 

from socket import *

serverPort = 12000

serverSocket = socket(AF_INET, SOCK_DGRAM)

serverSocket.bind(('', serverPort))

print("The server is ready to receive")

while True:

    message, clientAddress = serverSocket.recvfrom(2048)

    modifiedMessage = message.decode().upper()

    serverSocket.sendto(modifiedMessage.encode(), clientAddress)

 

serverSocket = socket(AF_INET, SOCK_DGRAM): 서버 소켓을 생성한다. 클라이언트와 마찬가지로 UDP 소켓을 사용한다.

serverSocket.bind(('', serverPort)): 서버 소켓에 포트 번호를 지정한다. 이 포트 번호를 통해 서버는 클라이언트로부터 데이터를 받을 준비를 한다.

message, clientAddress = serverSocket.recvfrom(2048): 클라이언트로부터 메시지를 수신한다. 메시지와 함께 클라이언트의 주소 정보(IP와 포트 번호)가 전달된다.

modifiedMessage = message.decode().upper(): 받은 메시지를 대문자로 변환한다.

serverSocket.sendto(modifiedMessage.encode(), clientAddress): 변환된 메시지를 클라이언트에게 다시 전송한다.

 

동작 과정 요약

 

1. 클라이언트가 서버에게 소문자 문장을 보낸다.

2. 서버는 그 문장을 받아 대문자로 변환한다.

3. 서버는 변환된 문장을 클라이언트에게 다시 보내고, 클라이언트는 이를 출력한다.

 

UDP는 단순하고 빠르지만, 신뢰성을 보장하지 않기 때문에 데이터가 손실될 수 있다. 이 예제에서는 간단한 데이터 전송만을 다루므로, UDP의 빠른 전송 속도가 중요한 경우에 유용하다.

 

2.7.2 TCP를 이용한 소켓 프로그래밍

 

**TCP(전송 제어 프로토콜)**는 UDP와 달리 연결형 프로토콜로, 데이터의 신뢰성, 순서 보장, 흐름 제어 등을 제공한다. TCP는 3-way 핸드셰이크라는 과정을 통해 클라이언트와 서버 간의 연결을 설정하고, 데이터를 주고받기 전에 안정적인 연결을 보장한다.

 

TCP 클라이언트-서버 예제: 대문자 변환 애플리케이션

 

1. TCP 클라이언트(TCPClient.py)

 

from socket import *

serverName = 'servername'

serverPort = 12000

clientSocket = socket(AF_INET, SOCK_STREAM)

clientSocket.connect((serverName, serverPort))

sentence = input('Input lowercase sentence: ')

clientSocket.send(sentence.encode())

modifiedSentence = clientSocket.recv(1024)

print('From Server: ', modifiedSentence.decode())

clientSocket.close()

 

clientSocket = socket(AF_INET, SOCK_STREAM): TCP 소켓을 생성한다. SOCK_STREAM을 사용해 TCP 연결을 생성한다.

clientSocket.connect((serverName, serverPort)): 클라이언트와 서버 간의 TCP 연결을 설정한다. 연결이 설정되면 데이터를 주고받을 수 있다.

clientSocket.send(sentence.encode()): 클라이언트가 입력한 문장을 서버로 전송한다.

modifiedSentence = clientSocket.recv(1024): 서버로부터 대문자로 변환된 문장을 수신한다.

 

2. TCP 서버(TCPServer.py)

 

from socket import *

serverPort = 12000

serverSocket = socket(AF_INET, SOCK_STREAM)

serverSocket.bind(('', serverPort))

serverSocket.listen(1)

print('The server is ready to receive')

while True:

    connectionSocket, addr = serverSocket.accept()

    sentence = connectionSocket.recv(1024).decode()

    capitalizedSentence = sentence.upper()

    connectionSocket.send(capitalizedSentence.encode())

    connectionSocket.close()

 

serverSocket = socket(AF_INET, SOCK_STREAM): TCP 소켓을 생성한다.

serverSocket.bind(('', serverPort)): 서버 소켓에 포트 번호를 지정한다.

serverSocket.listen(1): 서버가 클라이언트로부터의 연결 요청을 기다린다. 여기서 1은 대기할 수 있는 연결 요청의 최대 개수이다.

connectionSocket, addr = serverSocket.accept(): 클라이언트의 연결 요청을 수락하고, 새로운 소켓을 생성해 데이터를 주고받는다.

connectionSocket.recv(1024): 클라이언트로부터 데이터를 수신한다.

connectionSocket.send(capitalizedSentence.encode()): 대문자로 변환한 데이터를 클라이언트로 다시 보낸다.

 

TCP 동작 과정 요약

 

1. 클라이언트가 서버와 TCP 연결을 설정한다.

2. 클라이언트가 서버에게 소문자 문장을 보낸다.

3. 서버는 그 문장을 받아 대문자로 변환한다.

4. 서버는 변환된 문장을 클라이언트에게 다시 보내고, 클라이언트는 이를 출력한다.

 

TCP는 UDP와 달리 데이터의 신뢰성순서 보장을 제공하므로, 데이터가 손실되거나 순서가 뒤바뀌지 않는다. 이 예제는 데이터 전송 과정에서 TCP가 제공하는 안정성과 신뢰성을 잘 보여준다.

 

UDP와 TCP의 차이점 요약

 

UDP: 연결 설정 없이 데이터를 빠르게 전송하며, 신뢰성을 보장하지 않는다. 속도가 중요할 때 사용된다.

TCP: 연결을 설정한 후 데이터를 주고받으며, 데이터의 신뢰성과 순서를 보장한다. 신뢰성이 중요할 때 사용된다.

 

이로써 UDPTCP를 이용한 소켓 프로그래밍을 더 자세히 설명했다. 각 예제는 네트워크를 통해 데이터를 주고받는 과정을 단순하게 보여주며, 소켓 프로그래밍의 기본 개념을 익힐 수 있게 돕는다. 추가로 궁금한 부분이 있으면 언제든지 질문하세요! .