null
vuild_
Nodes
Flows
Hubs
Login
MENU
GO
Notifications
Login
☆ Star
TCP/IP 스택 구조 — 연결의 신뢰성을 만드는 방법
#tcp
#udp
#3wayhandshake
#프로토콜스택
#ethernet
@devpc
|
2026-05-06 05:26:55
|
GET /api/v1/nodes/639?nv=1
History:
v1 (2026-05-06) (Latest)
0
Views
1
Calls
# TCP/IP 스택 구조 — 연결의 신뢰성을 만드는 방법 ## TCP/IP란 정확히 무엇인가 "TCP/IP"라는 말은 두 가지 의미로 혼용된다. 첫째, 좁은 의미: TCP 프로토콜 + IP 프로토콜 조합을 가리킨다. 이 경우 UDP/IP와 구분해서 쓴다. 둘째, 넓은 의미: 인터넷을 위한 프로토콜 전체 집합을 가리킨다. Ethernet PHY/MAC, IPv4, IPv6, TCP, UDP, DHCP, HTTP 등 모든 레이어를 포괄한다. 임베디드 개발 문서를 보다 보면 두 가지 의미가 섞여 쓰이는 경우가 많다. 해당 문서가 TCP와 UDP를 구분해서 언급하고 있다면 좁은 의미, 그렇지 않다면 넓은 의미로 보면 된다. ## TCP vs UDP — 목적이 다른 두 프로토콜 | 항목 | TCP | UDP | |------|-----|-----| | 연결 | 3-Way Handshake 필요 | 비연결 | | 신뢰성 | ACK + 재전송 | 없음 | | 순서 보장 | 있음 | 없음 | | 속도 | 상대적으로 느림 | 빠름 | | 용도 | 파일 전송, HTTP | 실시간 스트림, 제어 | 임베디드에서는 두 가지 모두 쓰인다. XCP on Ethernet은 TCP를 쓰고, 일부 실시간 제어 데이터는 UDP를 선택한다. 요구사항에 따라 프로토콜을 선택해야 한다. ## TCP 3-Way Handshake TCP는 데이터를 보내기 전에 연결을 먼저 맺는다. 이 과정이 3-Way Handshake다. ``` Client ─── SYN ──────────────────────────────────────────► Server Client ◄── SYN + ACK ──────────────────────────────────── Server Client ─── ACK ──────────────────────────────────────────► Server [연결 완료] ``` 1. **SYN**: 클라이언트가 "연결하자"는 요청을 보낸다 2. **SYN + ACK**: 서버가 요청을 받았고 수락한다는 응답을 보낸다 3. **ACK**: 클라이언트가 서버의 응답에 확인을 보낸다 왜 이 절차가 필요한가? 노이즈나 패킷 손실이 있는 환경에서 "양쪽이 서로 통신 가능한 상태"를 확인하기 위해서다. 현대 칩은 신뢰성이 높아졌지만, 네트워크 환경이 불안정하거나 패킷 지연이 심한 경우에는 여전히 의미 있다. ## 실전에서의 프로토콜 선택 책에서는 "신뢰성이 필요하면 TCP, 속도가 필요하면 UDP"라고 단순화한다. 하지만 실제 프로젝트에서는: - **TCP를 쓰면서도 Latency를 줄여야 할 때**: TCP_NODELAY 옵션으로 네이글 알고리즘을 비활성화한다 - **UDP를 쓰면서도 일부 신뢰성이 필요할 때**: 애플리케이션 레이어에서 재전송 로직을 직접 구현한다 - **RTOS 없이 lwIP를 쓸 때**: 동기 방식으로 처리하면 설계가 단순해지고 오히려 성능이 좋아지는 경우도 있다 표준 문서가 말하는 것과 실제 환경이 다른 경우는 늘 있다. 경험하지 않은 상태에서 문서만 보고 프로토콜을 선택하는 건 위험하다. ## lwIP에서 TCP 소켓 초기화 — RAW API lwIP는 세 가지 API 레벨을 제공한다: RAW API, Netconn API, Socket API. 그 중 RAW API가 가장 낮은 오버헤드를 갖는다. 콜백 기반이지만 임베디드 환경에서 메모리 효율이 가장 좋다. ```c /* TCP 에코 서버 예시 (RAW API) */ static err_t tcp_recv_callback(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { if (p == NULL) { /* 연결 종료 */ tcp_close(pcb); return ERR_OK; } /* 받은 데이터 그대로 송신 (에코) */ tcp_write(pcb, p->payload, p->len, TCP_WRITE_FLAG_COPY); tcp_output(pcb); pbuf_free(p); return ERR_OK; } static err_t tcp_accept_callback(void *arg, struct tcp_pcb *newpcb, err_t err) { tcp_recv(newpcb, tcp_recv_callback); return ERR_OK; } void tcp_server_init(void) { struct tcp_pcb *pcb = tcp_new(); tcp_bind(pcb, IP_ADDR_ANY, 7); /* 포트 7 = Echo */ pcb = tcp_listen(pcb); tcp_accept(pcb, tcp_accept_callback); } ``` `tcp_write()`는 데이터를 버퍼에 넣는 것이고, `tcp_output()`이 실제로 전송을 트리거한다. 네이글 알고리즘이 활성화되어 있다면 `tcp_output()` 호출에도 즉시 나가지 않을 수 있다. ## UDP 소켓 초기화 UDP는 연결 없이 바로 전송하므로 구조가 훨씬 단순하다. ```c static void udp_recv_callback(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { /* 수신한 데이터 처리 */ char buf[128]; memcpy(buf, p->payload, p->len); buf[p->len] = '\0'; printf("UDP Received: %s\n", buf); pbuf_free(p); /* 응답 전송 */ struct pbuf *pResp = pbuf_alloc(PBUF_TRANSPORT, 5, PBUF_RAM); memcpy(pResp->payload, "OK\r\n", 4); udp_sendto(pcb, pResp, addr, port); pbuf_free(pResp); } void udp_server_init(void) { struct udp_pcb *pcb = udp_new(); udp_bind(pcb, IP_ADDR_ANY, 8888); udp_recv(pcb, udp_recv_callback, NULL); } ``` 다음 챕터에서는 IP 주소 체계를 다룬다. 공인 IP와 사설 IP가 어떻게 분리되고, 임베디드 장비가 이 구조 안에서 어디에 위치하는지를 이해해야 네트워크 설계가 가능하다.
// COMMENTS
Newest First
ON THIS PAGE