개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › 네트워크 › 전송 계층

TCP와 UDP

세 번에 나눠 보낸 데이터는 세 번에 나뉘어 도착하나

TCP와 UDP는 프로그램의 데이터를 상대 프로그램까지 실어 나르는 두 가지 방식입니다. TCP는 먼저 연결을 맺고 바이트를 순서대로 빠짐없이 전하지만, 어디서 끊어 보냈는지는 전해 주지 않습니다. UDP는 연결 없이 덩어리째 그냥 보내고, 도착했는지는 책임지지 않습니다.

일부만 실행으로 확인 · Node.js 24의 net, dgram 모듈로 127.0.0.1에서 여덟 가지 조합 가운데 여섯 가지를 실행해 대조. TCP의 유실과 순서 바뀜 두 가지는 RFC 9293 기준

이 실습에 나오는 개념

TCP와 UDP
인터넷에서 데이터를 전송하는 두 가지 대표적인 방식이다. TCP는 통신 전에 연결을 맺고, 데이터가 빠짐없이 순서대로 도착하도록 보장하며, 문제가 생기면 다시 보낸다. UDP는 연결 없이 데이터를 보내며 도착 여부나 순서를 보장하지 않는 대신 가볍고 빠르다. HTTP/1.1과 HTTP/2, 데이터베이스 연결은 TCP를, 실시간 영상·음성이나 DNS는 UDP를 주로 쓴다.
바이트 스트림과 데이터그램
데이터를 실어 나르는 두 가지 모양이다. 바이트 스트림은 바이트가 순서대로 끊김 없이 이어지는 흐름으로, TCP가 이 방식이다. 보내는 쪽이 몇 번에 나눠 썼는지는 전달되지 않아서, 세 번에 나눠 보낸 것이 한 번에 읽히거나 한 번에 보낸 것이 여러 번에 나뉘어 읽힐 수 있다. 데이터그램은 한 번에 보낸 덩어리가 그대로 하나의 단위로 전달되는 방식으로, UDP가 이 방식이며 덩어리끼리 합쳐지거나 쪼개져 읽히지 않는다. 그래서 TCP 위에서 메시지를 주고받는 프로그램은 길이를 먼저 적거나 구분 글자를 넣어 메시지의 끝을 스스로 표시해야 한다.
소켓
프로그램이 네트워크로 데이터를 주고받을 때 쓰는 통신의 끝점이며, 운영체제가 만들어 프로그램에 건네준다. 프로그램은 소켓에 쓰고 소켓에서 읽기만 하고, 실제로 데이터를 나누어 보내고 받는 일은 운영체제가 맡는다. TCP 연결 하나는 양쪽 끝의 소켓 한 쌍으로 이루어지고, 운영체제는 보내는 IP 주소, 보내는 포트, 받는 IP 주소, 받는 포트의 네 값으로 연결을 구분한다. 그래서 서버는 포트 하나로 여러 클라이언트의 연결을 함께 받을 수 있고, 연결마다 소켓이 하나씩 따로 생긴다. 다 쓴 소켓을 닫지 않으면 운영체제의 자원이 계속 묶인다.
포트
한 컴퓨터에서 돌고 있는 여러 프로그램 가운데 누가 데이터를 받을지 구분하는 0부터 65535까지의 번호다. IP 주소가 컴퓨터를 가리킨다면 포트는 그 컴퓨터 안의 프로그램을 가리키므로, 접속할 곳은 "IP 주소:포트"로 적는다. 서버 프로그램은 정해 둔 포트에서 연결을 기다리며, 웹 서버의 80번(HTTP)과 443번(HTTPS)처럼 널리 약속된 번호도 있다. 한 포트에서 연결을 기다릴 수 있는 프로그램은 보통 하나뿐이어서, 이미 쓰이는 포트에 서버를 또 띄우면 "주소가 이미 사용 중"이라는 오류가 난다. TCP의 포트와 UDP의 포트는 따로 관리되므로 번호가 같아도 서로 다른 것이다.
3-way handshake
TCP 연결을 맺을 때 거치는 세 단계의 과정이다. 클라이언트가 연결 요청(SYN)을 보내고, 서버가 수락과 함께 자신의 연결 요청(SYN+ACK)을 보내며, 클라이언트가 이를 확인(ACK)한다. 양쪽이 서로 보내고 받을 수 있는 상태임을 확인한 뒤 데이터 전송을 시작한다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. net.connect(9000, '127.0.0.1'): TCP는 데이터를 보내기 전에 먼저 연결을 맺습니다(3-way handshake). 받는 프로그램이 9000번 포트에서 기다리고 있으므로 성공합니다. 이제 양쪽 운영체제가 이 연결로 오가는 바이트에 번호를 붙여 관리합니다.
  2. socket.write('AB'): 2바이트가 받는 쪽 운영체제에 도착해 수신 버퍼에 쌓입니다. 받는 프로그램은 아직 읽지 않았으므로 운영체제가 맡아 둡니다. TCP의 버퍼는 바이트가 차례로 쌓이는 한 줄입니다.
  3. socket.write('CD'): 2바이트가 받는 쪽 운영체제에 도착해 수신 버퍼에 쌓입니다. 앞의 바이트 바로 뒤에 이어 붙습니다. 어디까지가 한 번의 write였는지는 기록되지 않습니다.
  4. socket.write('EF'): 2바이트가 받는 쪽 운영체제에 도착해 수신 버퍼에 쌓입니다. 역시 앞의 바이트 뒤에 이어 붙어, 버퍼에는 6바이트가 한 줄로 놓입니다.
  5. 받는 프로그램이 읽습니다: 'data' 이벤트 한 번에 6바이트 "ABCDEF"가 모두 나옵니다. 세 번에 나눠 보냈다는 흔적은 없습니다. TCP가 전하는 것은 순서가 맞는 바이트의 흐름이지, 덩어리의 경계가 아닙니다.

실행 결과

코드

// Node.js (net 모듈): TCP
const socket = net.connect(9000, '127.0.0.1');   // 먼저 연결을 맺는다
socket.on('connect', async () => {               // 연결된 뒤에야 보낼 수 있다
  socket.write('AB');  await sleep(50);           // 0.05초 간격으로 세 번
  socket.write('CD');  await sleep(50);
  socket.write('EF');
});
socket.on('error', e => console.log(e.code));    // 연결 실패는 여기로 온다

// 받는 프로그램: 세 번이 다 도착한 뒤에 읽기 시작한다
net.createServer({ pauseOnConnect: true }, conn => {
  conn.on('data', d => console.log(d.toString()));
  setTimeout(() => conn.resume(), 500);
}).listen(9000, '127.0.0.1');

이 모형의 전제

"AB", "CD", "EF"를 0.05초 간격으로 보내고 받는 프로그램은 세 번이 다 도착한 뒤에 읽는 상황입니다. 여덟 가지 조합 가운데 여섯 가지를 이 PC(윈도, Node.js 24)의 127.0.0.1에서 net과 dgram 모듈로 실제로 실행해 대조했습니다(조합마다 다섯 번 실행해 매번 같은 결과). UDP의 유실과 순서 바뀜은 보내는 쪽과 받는 쪽 사이에 데이터그램을 버리거나 늦게 넘기는 중계 프로그램을 두어 만들었고, 양 끝의 UDP 소켓이 이를 바로잡지 않는다는 것을 확인했습니다. TCP에서 유실과 순서 바뀜이 일어난 두 가지 조합은 이 PC에서 만들 수 없어 실행하지 않았으며, TCP 표준(RFC 9293 3.4절의 순서 번호, 3.8절의 확인 응답과 재전송)에 따라 "프로그램이 읽는 결과는 아무 일 없을 때와 같다"고 적었습니다. TCP에서 한 번의 읽기로 6바이트가 모두 나온 것은 이 PC에서 본 결과이고, 표준이 보장하는 것은 바이트의 순서와 내용뿐입니다. 읽기가 몇 번으로 나뉘는지는 상황에 따라 달라집니다. UDP가 데이터그램 단위로 전하고 도착을 보장하지 않는다는 것은 RFC 768과 RFC 1122 4.1절의 내용입니다. UDP 헤더에는 순서를 나타내는 번호가 없고(RFC 768), 그 아래의 IP도 순서를 지켜 주지 않습니다(RFC 791 1.2절). 받는 프로그램이 없을 때 운영체제가 ICMP 메시지로 알려 주기도 하지만, 이 실행에서 send의 결과나 오류 이벤트로 돌아온 것은 없었습니다. 운영체제의 버퍼가 가득 차서 버려지는 경우는 다루지 않았습니다.

앞 실습: TCP 연결과 재전송 · 다음 실습: 흐름 제어와 혼잡 제어

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요