web, network 등 전송이 필요한 모든 곳에서 쓰이는 TCP, UDP에 대해 알아보자.
📤 Transport Layer
한 마디로 정의하면?
- End Point 간 신뢰성있는 데이터 전송을 담당하는 계층.
- 신뢰성 : 데이터를 순차적, 안정적으로 전달.
- 전송 : 포트 번호에 해당하는 프로세스에 데이터 전달.
- TCP와 UDP는 ‘포트 번호’라는 숫자를 이용하여 컴퓨터 안의 어떤 서비스(애플리케이션)에게 데이터를 전달하면 좋은지를 식별함.
- TCP와 UDP는 OSI 표준모델과 TCP/IP 모델의 전송계층에서 사용되는 프로토콜.

잠깐 전송 계층에 대해 설명하자면
- 전송계층은 송신자와 수신자를 연결하는 통신 서비스를 제공하고 IP에 의해 전달되는 패킷의 오류를 검사하며 재전송 요구 제어등을 담당하는 계층.
- 쉽게 말해 데이터의 전달을 담당.
- TCP와 UDP는 포트 번호를 이용하여 주소를 지정하는것과 데이터 오류검사를 체크하는 두가지 공통점을 가지고 있음.
- 정확성(TCP)을 추구할지 신속성(UDP)을 추구할지를 구분하여 나뉨.
전송 계층이 왜 필요한지 아직 모르겠다면
- 데이터의 순차 전송이 원활하지 않음. ex)
송신자 1-2-3, 수신자 2-3-1
- 송수신자 간의 데이터 처리 속도가 차이가 나기 때문에 발신자는 무한으로 전송할 수 있는 반면 수신자는 뇌처럼 처리할 수 있는 데이터량의 한계가 있어 초과할 위험이 있음.
- 송수신자는 문제가 없지만 중간에 네트워크가 데이터를 늦게 처리한다면(네트워크가 혼잡하다면) 전송에 문제가 될 수 있음.
- 결국 전송 계층이 없다면 데이터의 손실이 발생함.
🛡️ TCP


- 데이터를 중요하게 생각하여 확실히 주고받고 싶을 때는 TCP(Transmission Control Protocol)를 사용함.
- TCP는 통신할 컴퓨터끼리
보냈습니다
,도착했습니다
라고 서로 확인 메시지를 보내면서 데이터를 주고받음으로써 통신의 신뢰성을 높임. - 이는 TCP의 가장 중요한 특징으로, 연결의 설정(3-way handshaking)과 해제(4-way handshaking)이 사용됨.
- TCP는 연결 지향적 프로토콜임.
- 클라이언트와 서버가 연결된 상태에서 데이터를 주고받는 프로토콜을 의미.
- 클라이언트가 연결 요청(SYN 데이터 전송)을 하고, 서버가 연결을 수락하면 통신 선로가 고정되고, 모든 데이터는 고정된 통신 선로를 통해서 순차적으로 전달.
- 그렇기 때문에 TCP는 데이터를 정확하고 안정적으로 전달.
- TCP는 호스트간 신뢰성 있는 데이터 전달과 흐름을 제어함.
- TCP는 패킷을 성공적으로 전송하면(ACK) 라는 신호를 날리고 만약에 ACK 신호가 제 시간에 도착하지 않으면 Timeout이 발생하여 패킷 손실이 발생한 패킷을 다시 전송.
- 데이터를 송신할때마다 확인 응답을 주고받는 절차가 있으므로 통신의 신뢰성이 올라감.
앞서 언급한 전송 계층이 없다면 발생할 문제
데이터의 순차 전송 보장
, 흐름 제어
, 혼잡 제어
를 해결할 수 있음.- 웹이나 메일, 파일 공유 등과 같이 데이터를 누락시키고 싶지 않은 서비스가 사용 중.
TCP 헤더

- SYN : 연결 설정 요구. TCP 에서 세션을 성립할 때 가장먼저 보내는 패킷, 시퀀스 번호를 임의적으로 설정하여 세션을 연결하는 데에 사용되며 초기에 시퀀스 번호를 보냄.
- ACK : 응답 번호 필드가 유효한지 설정할때 사용하며 상대방으로부터 패킷을 받았다는 걸 알려주는 패킷. 클라이언트가 보낸 최초의 SYN 패킷 이후에 전송되는 모든 패킷은 이 플래그가 설정되어야 함.
3 way handshake
- TCP 통신을 위한 네트워크 연결은 3 way handshake 이라는 방식으로 연결.
- 3 way handshake 방식은 서로의 통신을 위한 관문(port)을 확인하고 연결하기 위하여 3번의 요청/응답 후에 연결이 되는 것을 말함.
- 이 과정에서 가장 많은 시간이 소요되어 UDP방식보다 속도가 느려지는 주요 원인으로 지목.

- Client에서 Server에 연결 요청을 하기위해
SYN
데이터를 보낸다.
- Server에서 해당 포트는
LISTEN
상태에서SYN
데이터를 받고SYN_RCV
로 상태가 변경된다.
- 요청을 정상적으로 받았다는 대답(
ACK
)와 Client도 포트를 열어달라는SYN
을 같이 보낸다.
- Client에서는
SYN+ACK
를 받고ESTABLISHED
로 상태를 변경하고 서버에ACK
를 전송한다.
ACK
를 받은 서버는 상태가ESTABLSHED
로 변경된다.
→ 위와 같이 3번의 통신이 정상적으로 이루어지면, 서로의 포트가 ESTABLISHED 되면서 연결됨.
TCP state
- LISTEN : 접속 요청을 기다리는 상태.
- SYN_RECEIVED : 서버가 원격 클라이언트로부터 접속 요구를 받아 클라이언트에게 응답을 하였지만 아직 클라이언트에게 확인 메시지는 받지 않은 상태.
- ESTABLISHED : 3 way-handshaking 이 완료된 후 서로 연결된 상태.
- CLOSED : 완전히 종료
TCP 데이터 전송 방식

- Client가 패킷 송신.
- Server에서 ACK 송신.
- ACK를 수신하지 못하면 재전송.
4 way handshake

- 데이터를 전부 송신한 Client가 FIN 송신.
- Server에서 ACK 송신.
- Server에서 남은 패킷 송신.
- Server가 FIN 송신.
- Clinet가 ACK 송신.
예시
const net = require('net'); (() => { // When null, we are waiting for the first player to connect, after which we will // create a new game. After the second player connects, the game can be fully set // up and played, and this variable immediately set back to null so the future // connections make new games. let game = null; net.createServer((socket) => { console.log('Connection from', socket.remoteAddress, 'port', socket.remotePort); if (game === null) { game = new Game(); game.playerX = new Player(game, socket, 'X'); } else { game.playerO = new Player(game, socket, 'O'); game = null; } }).listen(58901, () => { console.log('Tic Tac Toe Server is Running'); }); })(); class Game { // A board has nine squares. Each square is either unowned or it is owned by a // player. So we use a simple array of player references. If null, the corresponding // square is unowned, otherwise the array cell stores a reference to the player that // owns it. constructor() { this.board = Array(9).fill(null); } hasWinner() { const b = this.board; const wins = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]]; return wins.some(([x, y, z]) => b[x] != null && b[x] === b[y] && b[y] === b[z]); } boardFilledUp() { return this.board.every(square => square !== null); } move(location, player) { if (player !== this.currentPlayer) { throw new Error('Not your turn'); } else if (!player.opponent) { throw new Error('You don’t have an opponent yet'); } else if (this.board[location] !== null) { throw new Error('Cell already occupied'); } this.board[location] = this.currentPlayer; this.currentPlayer = this.currentPlayer.opponent; } } class Player { constructor(game, socket, mark) { Object.assign(this, { game, socket, mark }); this.send(`WELCOME ${mark}`); if (mark === 'X') { game.currentPlayer = this; this.send('MESSAGE Waiting for opponent to connect'); } else { this.opponent = game.playerX; this.opponent.opponent = this; this.opponent.send('MESSAGE Your move'); } socket.on('data', (buffer) => { const command = buffer.toString('utf-8').trim(); if (command === 'QUIT') { socket.destroy(); } else if (/^MOVE \d+$/.test(command)) { const location = Number(command.substring(5)); try { game.move(location, this); this.send('VALID_MOVE'); this.opponent.send(`OPPONENT_MOVED ${location}`); if (this.game.hasWinner()) { this.send('VICTORY'); this.opponent.send('DEFEAT'); } else if (this.game.boardFilledUp()) { [this, this.opponent].forEach(p => p.send('TIE')); } } catch (e) { this.send(`MESSAGE ${e.message}`); } } }); socket.on('close', () => { try { this.opponent.send('OTHER_PLAYER_LEFT'); } catch (e) {} }); } send(message) { this.socket.write(`${message}\n`); } }
🤔 TCP의 단점


- 데이터로 보내기 전에 반드시 연결이 형성되어야 함(3 way-handshake로 시간 손실 발생).
- 1 : 1 통신만 가능함.
- 패킷을 조심만 손실해도 재전송.
- 별로 티가 안나는 손실임에도 불구하고 재전송을 함. 서비스에 따라 불합리한 방식일 수 있음.
🏃 UDP


- 그에 반해 데이터의 신뢰성은 제쳐두고 어쨌든 빨리 보내고 싶을 때는 UDP(User Datagram Protocol)를 사용.
- UDP는 데이터를 보내면 그것으로 끝이므로 신뢰성은 없지만 확인 응답과 같은 절차를 생략할 수 있으므로 통신의 신속성을 높임.
- 순차 전송 X, 흐름 제어 X, 혼잡 제어X, 3 way-handshake X
- 패킷을 순차적으로 보내더라도 이 패킷들은 서로 다른 통신 선로를 통해 전달될 수 있음.
- 최악의 경우 잘못된 선로로 전송되어 유실될 수도 있음.
- 패킷이 유실이나 변조가 되어도 재전송을 하지 않음.
- UDP는 전송계층의 비연결 지향적 프로토콜.
- 데이터를 주고받을 때 연결 절차를 거치지 않고 발신자가 일방적으로 데이터를 발신하는 방식.
- VoIP(Voice over IP)나 시간 동기, 영상 스트리밍 등과 같이 무엇보다 속도를 필요로 하는 서비스가 사용 중.
UDP 헤더

🤔 UDP의 단점
- 데이터의 신뢰성이 없음.
- 의미있는 서버를 구축하기위해서는 일일이 패킷을 관리해주어야 함.
🤩 결론
- TCP, UDP의 특성을 파악하고 상황에 따라 적절한 프로토콜을 사용하자.
- TCP, UDP의 헤더에 대해 파악하고 성능 개선에 이용하자.
참고자료 :