Network NotesChapter 01
COMPUTER NETWORKS · CHAPTER 01

네트워크와
소켓 프로그래밍.

인터넷의 구조와 데이터 전달 과정을 배웁니다.
소켓의 역할을 익힌 뒤 TCP 에코 프로그램을 실행합니다.

학부생을 위한 개념 해설강의 자료 · PDF 25쪽Ubuntu · C17 실습
A MESSAGE, ACROSS THE NETWORK
응용프로그램 → 소켓 → 운영체제의 TCP/IP → 네트워크 → 상대 응용프로그램

이 장의 네 가지 주제

01 / THE INTERNET원본 PDF 3–4쪽 · 슬라이드 5–8

인터넷은 여러 네트워크의 연결

프로그램이 만든 데이터는 여러 장비를 거쳐 상대 프로그램에 도착합니다. 데이터를 주고받는 장치는 호스트, 네트워크를 잇는 장치는 라우터이며, 통신 규칙을 프로토콜이라고 합니다.

HOST

호스트

사용자의 응용프로그램이 실행되는 컴퓨터입니다. 노트북, 스마트폰, 서버처럼 데이터를 만들거나 최종적으로 사용하는 장치가 해당합니다.

ROUTER

라우터

서로 다른 네트워크를 잇고, 목적지 IP와 라우팅 정보를 보고 패킷을 다음 지점으로 넘깁니다. 한 번에 최종 목적지로 직접 보내는 것은 아닙니다.

PROTOCOL

프로토콜

데이터 형식, 주고받는 순서, 상황별 동작에 관한 약속입니다. 같은 선으로 연결되어 있어도 이 약속을 이해하지 못하면 올바른 통신이 어렵습니다.

프로그램은 운영체제에 통신을 요청한다.

프로그램은 소켓 함수를 호출해 운영체제에 데이터 전송과 수신을 요청합니다. 개발자가 랜카드를 직접 제어할 필요는 없습니다. 운영체제의 TCP/IP 기능과 장치 드라이버가 실제 통신을 처리합니다.

이 강의에서 TCP/IP는 TCP와 IP만이 아니라, 인터넷 통신에 쓰이는 여러 프로토콜을 묶어 부르는 이름입니다.

보충 · 계층을 나누는 이유
웹 요청의 문법과 Wi-Fi 신호 처리 방법을 하나의 프로그램에 묶으면 변경이 매우 어려워집니다. 계층마다 제공하는 서비스와 접점을 정하면, 아래의 네트워크 기술이 바뀌어도 위의 프로그램을 유지하기 쉬워집니다.

02 / FOUR LAYERS원본 PDF 5–6, 9쪽 · 슬라이드 9–12, 18

TCP/IP의 네 계층과
각 계층의 역할

이 강의는 TCP/IP를 네 계층으로 설명합니다. 송신할 때는 위에서 아래로, 수신할 때는 아래에서 위로 데이터를 처리합니다.

응용 계층어떤 의미의 데이터를 주고받을까?HTTP · SMTP · FTP
전송 계층어느 통신 종단점으로, 어떤 방식으로 전달할까?TCP · UDP / 포트
인터넷 계층어느 네트워크의 어느 주소로 전달할까?IP / IP 주소
네트워크 접근 계층바로 연결된 다음 지점에 어떻게 보낼까?Ethernet · Wi-Fi
아래 계층은 위 계층에 데이터 전달 서비스를 제공합니다.

1. 응용 계층 — 데이터에 의미를 부여한다.

응용 계층은 프로그램끼리 주고받을 메시지의 형식과 처리 방법을 정합니다. 웹의 HTTP 요청·응답과 메일을 보내는 SMTP, 원격 접속용 Telnet, 파일 전송용 FTP가 예입니다.

MIME은 콘텐츠의 종류와 표현 형식을 정하는 규약입니다. TCP처럼 데이터를 전달하지는 않습니다.

2. 전송 계층 — 프로그램의 통신을 구분한다.

전송 계층은 포트 번호를 사용해 통신 끝점을 구분합니다. 대표 프로토콜은 TCP와 UDP입니다.

TCP는 데이터의 순서를 관리하고 필요한 재전송을 수행합니다. UDP는 데이터그램 단위로 전달하며, 기본 기능에 재전송이나 순서 보장은 없습니다.

3. 인터넷 계층 — 네트워크를 넘어 전달한다.

IP 주소는 논리적인 주소입니다. 라우터는 라우팅 정보를 바탕으로 다음 홉(next hop)을 결정합니다. IP 자체는 도착·순서·중복 방지를 보장하지 않는 최선형(best-effort) 전달을 제공하므로, 필요한 추가 기능을 위 계층이 맡습니다.

4. 네트워크 접근 계층 — 한 구간을 실제로 건넌다.

네트워크 하드웨어와 드라이버가 실제 연결된 구간에서 프레임을 전달합니다. 이더넷에서는 보통 48비트 MAC 주소를 사용합니다. MAC 주소는 IP 주소와 역할이 다르며, 모든 링크 기술이 같은 종류의 물리 주소를 쓰는 것은 아닙니다.

보충 · 어디까지가 라우터의 역할일까?
일반적인 IP 전달에서 라우터는 인터넷 계층의 경로 결정과 네트워크 접근 계층의 프레임 전달을 맡습니다. 중간 라우터가 사용자의 TCP 연결을 매번 새로 만드는 것은 아닙니다. 방화벽·NAT는 별도의 추가 기능입니다.

03 / TCP & UDP원본 PDF 7–9쪽 · 슬라이드 13–17

TCP의 바이트 스트림과
UDP의 데이터그램

TCP는 순서가 있는 바이트 스트림을, UDP는 경계가 있는 개별 데이터그램을 전달합니다. 이에 따라 수신 코드가 데이터를 처리하는 방식도 달라집니다.

TCP와 UDP의 기본 서비스 비교
관점TCPUDP
통신 준비연결 설정 후 송수신TCP와 같은 연결 설정 절차 없음
손실·순서재전송·순서 복원·중복 처리기본적으로 복구·순서·중복 방지 보장 없음
데이터 단위바이트 스트림, 메시지 경계 없음데이터그램, 메시지 경계 유지
통신 형태유니캐스트유니캐스트, 멀티캐스트 등. IPv4에서는 브로드캐스트도 가능
설계 시 질문어디까지가 메시지 하나인가?유실·중복·순서 역전을 어떻게 다룰 것인가?

같은 데이터를 다르게 받는다면?

송신 애플리케이션 · 세 번의 송신 호출ABCDEFGHI
수신 애플리케이션 · 가능한 수신 결과ABCDEFGHI

TCP는 ABCDEFGHI의 순서를 유지하지만, recv() 한 번의 결과는 AB처럼 메시지 중간에서 끝날 수 있습니다. 이 화면은 개념 시뮬레이션이며 실제 패킷 캡처가 아닙니다.

TCP: send() 한 번 ≠ recv() 한 번.

ABC, DEF, GHI를 세 번 보내도 상대는 ABCDEFGHI를 한 번에 받을 수 있습니다. AB / CDEFG / HI처럼 다른 크기로 나눠 받을 수도 있습니다. TCP가 유지하는 것은 바이트의 순서이며, 몇 번에 나누어 보냈는지는 전달하지 않습니다.

보내는 쪽도 반환값을 확인해야 합니다. send()가 요청한 길이보다 적게 처리했다면, 남은 부분을 이어서 보내야 합니다.

FRAMING 01

구분 문자로 나누기

Hello\nWorld\n처럼 끝 표시를 넣습니다. 수신 버퍼에 바이트를 누적하다가 줄바꿈을 찾으면 메시지를 꺼냅니다. 본문에 구분 문자가 들어가는 경우의 처리와 최대 길이도 정해야 합니다.

FRAMING 02

길이를 먼저 보내기

[길이 5][Hello]처럼 헤더 뒤에 본문을 둡니다. 예를 들어 4바이트 길이 헤더를 먼저 모으고, 지정된 바이트 수만큼 본문을 모읍니다. 헤더 자체도 여러 번에 나뉘어 도착할 수 있습니다.

보충 · 길이는 문자 수가 아니라 바이트 수
UTF-8의 “안녕”은 2글자지만 6바이트입니다. 길이 헤더의 크기·바이트 순서·최대 허용 길이와 텍스트 인코딩을 양쪽이 똑같이 정해야 합니다.

UDP: 데이터그램 경계와 유실

UDP는 한 번 보낸 데이터그램의 경계를 유지합니다. 수신된 데이터그램 두 개가 하나의 메시지로 합쳐져 전달되지는 않습니다.

다만 데이터가 사라지거나, 중복되거나, 순서가 바뀔 수 있습니다. 수신 버퍼가 너무 작으면 데이터그램 일부가 잘리고 나머지는 버려질 수 있습니다. 경계를 유지한다는 것과 전달에 성공한다는 것은 다릅니다.

UDP에도 connect()를 사용할 수 있습니다. 같은 이름의 함수여도 TCP처럼 연결을 맺는 절차를 수행하는 것은 아닙니다.

TCP의 전달 보장과 응용프로그램의 처리
TCP가 있어도 네트워크 단절이나 상대 프로그램 종료는 발생합니다. TCP의 전달 기능이 “상대 앱이 데이터를 처리하고 저장했다”는 확인까지 대신하지는 않습니다. 그런 확인이 필요하면 응용 프로토콜에 응답을 설계해야 합니다.근거: Linux tcp(7) · Linux udp(7)

04 / THE JOURNEY OF DATA원본 PDF 10–12쪽 · 슬라이드 19–23

송신 시 캡슐화와
수신 시 역캡슐화

각 계층은 데이터에 필요한 제어 정보를 붙입니다. 이 과정을 캡슐화(encapsulation), 수신 측이 제어 정보를 해석하고 제거하는 과정을 역캡슐화라고 합니다.

송신 측 · 위에서 아래로 캡슐화 ↓
응용 계층응용 데이터
TCPTCP 헤더응용 데이터
IPIP 헤더TCP 헤더응용 데이터
EthernetEthernet 헤더IP 헤더TCP 헤더응용 데이터트레일러
수신 측은 아래에서 위로 처리합니다 ↑. 블록 너비는 실제 헤더 크기나 데이터 길이와 비례하지 않습니다.

헤더와 트레일러

헤더(header)는 데이터 앞의 제어 정보입니다. TCP에는 포트·순서 정보 등이, IP에는 출발지·목적지 주소 등이 들어갑니다. 트레일러(trailer)는 뒤에 붙는 정보로, 이더넷의 프레임 오류 검사용 FCS가 예입니다.

계층별 단위 이름

TCP의 단위는 세그먼트, UDP의 단위는 데이터그램, IP에서는 패킷 또는 데이터그램, 링크 계층에서는 프레임이라고 부릅니다. “패킷”을 넓은 의미의 전송 단위로 쓰는 경우도 있습니다.

중간 라우터에서는 무엇이 바뀔까?

라우터는 들어온 링크의 프레임을 처리하고, IP 목적지와 라우팅 정보를 확인한 뒤 다음 링크에 맞는 새 프레임으로 전달합니다. 따라서 링크 계층 주소는 구간마다 달라질 수 있습니다.

NAT 같은 변환이 없다면 출발지·목적지 IP는 일반적으로 유지되지만, TTL 같은 필드는 전달 과정에서 바뀝니다.

보충 · “같은 계층끼리 통신한다”는 뜻
송신 TCP와 수신 TCP가 같은 헤더 형식을 이해한다는 논리적 관계를 말합니다. 실제 바이트는 아래 계층과 물리적 연결을 거쳐 갑니다. 또한 응용 데이터 하나가 항상 프레임 하나와 대응하는 것은 아닙니다.

05 / IP ADDRESS & PORT원본 PDF 12–14쪽 · 슬라이드 24–27

IP 주소로 목적지를,
포트로 통신을 구분

IP 주소로 네트워크상의 목적지를 지정하고, 전송 프로토콜과 포트 번호로 그 안의 통신을 구분합니다. 주소와 포트는 함께 읽어야 합니다.

이 장의 실습에서 사용할 TCP 서버
127.0.0.1:9000

루프백 IP · 현재 운영체제 자신 포트 · 실습 서버의 접속 지점

IPv4 · 32 BITS

192.0.2.10

8비트씩 네 부분으로 나누어 10진수와 점으로 표시합니다. 각 부분은 0~255입니다. 이 주소는 문서 예시용입니다. 실습에서는 자기 자신을 가리키는 127.0.0.1을 사용합니다.

IPv6 · 128 BITS

2001:db8::10

16비트씩 여덟 부분을 16진수와 콜론으로 표시하며, 연속된 0은 한 번의 ::로 줄일 수 있습니다. 루프백은 ::1입니다. 포트와 함께 쓸 때는 [::1]:9000처럼 주소를 대괄호로 감쌉니다.

보충 · IP 주소가 항상 전 세계에서 유일할까?
인터넷에서 사용하는 공인 주소와 달리 사설 IPv4 주소는 서로 다른 사설망에서 반복해서 사용됩니다. 또 하나의 호스트가 여러 인터페이스와 여러 IP 주소를 가질 수 있어, “컴퓨터 한 대 = IP 하나”도 일반적으로 성립하지 않습니다.

포트 번호는 16비트. 범위는 0~65535.

범위IANA 분류이해할 점
0–1023System / Well-known널리 알려진 서비스 영역. 0은 예약된 값
1024–49151User / Registered서비스 등록에 사용되는 영역
49152–65535Dynamic / Private동적·사설 사용 영역

TCP와 UDP는 포트 공간을 각각 사용하므로 TCP 9000과 UDP 9000은 구분됩니다. 클라이언트는 일반적으로 운영체제가 고르는 임시 포트를 씁니다. IANA의 분류 범위와 운영체제의 실제 임시 포트 설정은 일치하지 않을 수 있습니다.

bind()에 포트 0을 주는 것은 보통 “사용 가능한 포트를 골라 달라”는 요청입니다. IANA 포트 분류 확인 ↗

도메인 이름은 기억하기 쉬운 이름이다.

프로그램이 도메인 이름으로 통신하려면 먼저 그 이름에 해당하는 주소를 알아야 합니다. 이 과정을 이름 해석(resolution)이라고 하며, 보통 DNS를 이용합니다.

이름 하나에 IP 주소가 여러 개 연결될 수도 있고, 여러 이름이 같은 IP 주소를 가리킬 수도 있습니다. 통신할 포트 번호는 이름과 별도로 정합니다.

06 / CLIENT & SERVER원본 PDF 14쪽 · 슬라이드 28 + 보충

접속을 기다리는 서버와
연결을 요청하는 클라이언트

서버와 클라이언트는 프로그램이 맡는 역할입니다. TCP 서버는 접속을 기다리고, 클라이언트는 서버의 주소로 연결을 요청합니다.

서버 · 먼저 실행

socket()대기용 소켓 생성
↓
bind() → listen()주소를 정하고 접속 대기 상태로
↓
accept()연결을 받아 새 통신용 소켓 반환
↓
recv() / send()클라이언트와 양방향 데이터 교환
↓
close()소켓 자원 해제

클라이언트 · 나중에 실행

socket()소켓 생성
↓
서버 IP + 포트 지정예: 127.0.0.1:9000
↓
connect()서버에 연결 요청 →
↓
send() / recv()서버와 양방향 데이터 교환
↓
close()소켓 자원 해제
보충: 블로킹 TCP 프로그램의 호출 순서이며, 양쪽의 같은 높이에 있는 호출이 동시에 실행된다는 뜻은 아닙니다.

연결된 뒤에는 양쪽 모두 데이터를 보내고 받을 수 있습니다. “서버는 받기만 하고 클라이언트는 보내기만 한다”는 뜻이 아닙니다. 같은 컴퓨터에서도 서버 프로그램과 클라이언트 프로그램을 따로 실행해 통신할 수 있습니다.

07 / WHAT IS A SOCKET?원본 PDF 15–17쪽 · 슬라이드 30–34

소켓의 식별자·종단점·API:
세 가지 관점

소켓은 코드에서 사용하는 식별자, 데이터를 주고받는 종단점, 운영체제의 통신 기능을 호출하는 API라는 세 관점으로 설명할 수 있습니다.

01 / DATA TYPE

다루기 위한 값

Linux에서 socket()은 정수형 파일 디스크립터를 반환합니다. 이 값은 운영체제가 관리하는 소켓 객체를 참조하는 식별자입니다. 숫자 자체에 모든 연결 정보가 들어 있는 것은 아닙니다.

02 / ENDPOINT

통신의 종단점

프로그램이 데이터를 주고받는 끝점입니다. 인터넷 소켓은 프로토콜, 로컬 주소·포트, 연결된 경우 상대 주소·포트 같은 정보와 관련됩니다. 생성 직후에는 주소가 아직 정해지지 않을 수 있습니다.

03 / INTERFACE

운영체제에 대한 API

socket(), connect(), send(), recv()처럼 통신 서비스를 요청하는 함수 집합입니다. 응용프로그램이 전송 계층 서비스를 사용하는 접점으로 이해할 수 있습니다.

전화 비유로 보는 소켓

강의의 전화 비유에서 사람은 응용프로그램, 전화기는 소켓에 해당합니다. 대표 전화번호는 IP 주소, 내선 번호는 포트에 비유할 수 있습니다. 다만 소켓은 물리 장치가 아니라 운영체제가 관리하는 소프트웨어 자원입니다.

Ubuntu · C17 / 개념을 보여 주는 코드 조각
int fd = socket(AF_INET, SOCK_STREAM, 0);
/* fd가 -1이면 생성 실패. 성공하면 이후 호출에서 fd로 소켓을 참조한다. */
/* AF_INET: IPv4 / SOCK_STREAM: 스트림 / 여기에서 0: 기본 TCP 선택 */

서로 통신하는 두 프로그램이 반드시 같은 프로그래밍 언어나 같은 소켓 라이브러리를 쓸 필요는 없습니다. 네트워크에서 주고받는 프로토콜과 메시지 형식이 맞아야 합니다. Linux C 서버와 다른 언어·운영체제의 클라이언트도 이 약속을 따르면 통신할 수 있습니다.

08 / WINDOWS & LINUX원본 PDF 18–21쪽 · 슬라이드 36–41

Windows Winsock과
Linux 소켓의 차이

Winsock은 Berkeley UNIX 소켓 인터페이스의 개념을 Windows에서 사용할 수 있게 발전시킨 API입니다. 함수와 흐름은 비슷하지만 초기화, 자료형, 종료와 오류 처리에는 차이가 있습니다.

항목Ubuntu / POSIX 소켓Windows / Winsock
소켓 자료형int 파일 디스크립터SOCKET
사용 준비Winsock식 전역 초기화 없음WSAStartup()
소켓 생성 실패-1INVALID_SOCKET
소켓 닫기close(fd)closesocket(s)
오류 조회errno, perror()WSAGetLastError()
전체 사용 정리각 자원 해제소켓 해제 후 WSACleanup()
대표 헤더·링크sys/socket.h, arpa/inet.h
기본 소켓에 별도 Winsock 링크 불필요
winsock2.h, ws2tcpip.h
Ws2_32.lib / MinGW의 -lws2_32

환경 구분
Windows 11 위에서 WSL의 Ubuntu 터미널을 쓰는 경우, 이 장의 Linux 예제는 Ubuntu용 실행 파일로 빌드합니다. Windows용 -lws2_32 옵션을 Ubuntu GCC 명령에 붙이지 않습니다.근거: Microsoft · Winsock 초기화 · closesocket

소켓 프로그램에서 정해야 할 사항

소켓은 범용적인 통신 인터페이스이므로 세부 제어와 이식에 유리합니다. 반면 메시지 형식·길이·종료 절차·오류 처리를 프로그램이 정해야 합니다.

서로 다른 바이트 순서와 자료형 크기를 고려해야 하며, C 구조체를 메모리 그대로 보내면 패딩·정렬·표현 차이 때문에 해석이 어긋날 수 있습니다.

Winsock의 역사와 지원 버전

강의 자료는 Windows 95의 Winsock 1.1, 이후 Winsock 2.2 지원과 IPv4·IPv6·Bluetooth·IrDA의 도입 시점을 다룹니다. 과거 발전 과정에 관한 자료이므로 현재 PC의 지원 기능과는 다를 수 있습니다.

강의의 GUI·메시지 처리·멀티스레드 확장은 Windows API의 특징을 설명합니다. Linux도 GUI와 멀티스레드를 지원합니다.

강의 구조도는 Winsock 1.x 응용프로그램, WSOCK32.DLL, Winsock 2.x의 WS2_32.DLL, 확장 API와 프로토콜 제공자의 관계를 보여 줍니다. 응용프로그램이 공개 API를 호출하면 구현 라이브러리와 하부 프로토콜이 통신을 처리합니다.

09 / YOUR FIRST CONNECTION원본 PDF 22–24쪽 · 슬라이드 43–48 + 보충 실습

“Hello, network!”
TCP 에코 실습

같은 Ubuntu 안의 두 터미널에서 TCP 에코 통신을 실습합니다. 에코 서버는 받은 바이트를 그대로 돌려주는 프로그램입니다.

1. Ubuntu에서 준비하기

Ubuntu 초기 설정을 마친 뒤 진행합니다. 아직 준비하지 않았다면 프로젝트 루트의 wsl-ubuntu-gcc-guide.html 가이드를 참고하세요. 강의 폴더만 별도로 받은 경우 아래 Ubuntu 공식 안내를 사용할 수 있습니다.

아래 명령은 Windows PowerShell이 아닌 Ubuntu 터미널에서 실행합니다.

Ubuntu · Bash / GCC가 없을 때 개발 도구 설치
sudo apt update
sudo apt install build-essential gdb
gcc --version

C 컴파일러와 기본 개발 도구를 설치합니다. 마지막 명령에서 GCC 버전이 출력되면 다음 단계로 진행합니다. Ubuntu 공식 개발 환경 안내 ↗

제공된 소스를 Linux 작업 폴더로 복사합니다. 경로의 USER는 Windows 계정 폴더 이름으로 바꾸고, 출발 경로는 소스를 저장한 위치에 맞추세요.

Ubuntu · Bash / 두 소스를 Linux 작업 폴더에 복사
mkdir -p ~/projects/network-ch01
cp /mnt/c/Users/USER/Desktop/Network_Programing/Lecture_HTML/1장/examples/echo_server.c ~/projects/network-ch01/
cp /mnt/c/Users/USER/Desktop/Network_Programing/Lecture_HTML/1장/examples/echo_client.c ~/projects/network-ch01/
cd ~/projects/network-ch01

2. 경고를 켜고 컴파일하기

Ubuntu · Bash / ~/projects/network-ch01
gcc -std=c17 -Wall -Wextra -Wpedantic -g -O0 echo_server.c -o echo_server
gcc -std=c17 -Wall -Wextra -Wpedantic -g -O0 echo_client.c -o echo_client

-std=c17은 언어 표준, -Wall -Wextra -Wpedantic은 경고, -g -O0은 디버깅을 위한 정보와 최적화 수준입니다.

성공하면 보통 별도 메시지 없이 echo_server와 echo_client 실행 파일이 생깁니다. 이 예제는 스레드나 Makefile을 사용하지 않습니다.

3. 서버를 실행한 다음 클라이언트 실행하기

Ubuntu · Bash / 터미널 A · 서버
cd ~/projects/network-ch01
./echo_server
예상 출력 · 접속 대기 중
Listening on 127.0.0.1:9000 (Ctrl+C to stop)
Ubuntu · Bash / 터미널 B · 클라이언트
cd ~/projects/network-ch01
./echo_client
예상 출력 · 서버가 돌려준 바이트
Hello, network!

서버는 클라이언트 한 명에게 받은 데이터를 계속 돌려주다가 상대가 송신을 끝내면 종료합니다. 클라이언트는 에코를 모두 받고 서버가 연결을 닫은 것을 확인하면 종료합니다. 다시 실습하려면 서버부터 실행하세요.

대기 중인 프로그램은 해당 터미널에서 Ctrl+C로 종료할 수 있습니다. 실습 주소 127.0.0.1:9000은 같은 Ubuntu 환경 안에서 접속하는 주소입니다.

4. 코드에서 읽어야 할 다섯 가지

  1. 대기와 통신을 분리한다. server는 대기용, accept()가 반환한 client는 통신용 디스크립터입니다.
  2. 포트와 주소의 바이트 순서를 맞춘다. htons(9000), htonl(INADDR_LOOPBACK)로 IPv4 주소 구조체를 채웁니다.
  3. 수신 길이를 그대로 사용한다. recv()는 끝에 NUL을 붙이지 않습니다. 서버는 반환된 바이트 수만 돌려주고 클라이언트는 fwrite()로 그 길이만 출력합니다.
  4. 부분 송신을 반복 처리한다. send()가 처리한 길이를 누적합니다. EINTR는 신호에 의한 중단으로 재시도하고, Linux의 MSG_NOSIGNAL은 상대가 닫았을 때 SIGPIPE로 바로 종료되는 것을 막습니다.
  5. 이 예제의 끝 표시는 송신 종료다. 클라이언트는 shutdown(fd, SHUT_WR)로 더 보낼 바이트가 없음을 알리되 계속 수신합니다. 서버는 남은 바이트까지 읽은 후 recv() == 0을 만나 종료합니다. 서버가 연결을 닫으면 클라이언트도 EOF를 읽고 끝납니다.

보충 · 에코는 메시지를 해석하지 않아도 된다.
이 예제는 “받은 바이트를 그대로 반환”하므로 메시지 경계를 복원할 필요가 없습니다. 한 연결에서 여러 요청을 해석하는 채팅·명령 서버로 확장한다면 앞서 배운 구분 문자나 길이 헤더를 추가해야 합니다. 블로킹·한 클라이언트·타임아웃 없는 학습용 예제이므로 다음 연결을 동시에 처리하지 않습니다.

서버 전체 코드 펼치기 · echo_server.c
Ubuntu · C17 / echo_server.c
/* Ubuntu / POSIX, C17: 한 클라이언트의 바이트를 그대로 돌려준다. */
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>

static int send_all(int fd, const char *data, size_t size)
{
    size_t sent = 0;

    /* send() 한 번이 전체 데이터를 처리한다는 보장은 없다. */
    while (sent < size) {
        /* 이미 보낸 부분을 건너뛰고 남은 바이트만 다시 보낸다.
           MSG_NOSIGNAL: 상대가 닫아도 SIGPIPE 대신 오류 반환으로 처리한다. */
        ssize_t n = send(fd, data + sent, size - sent, MSG_NOSIGNAL);

        /* 신호로 중단된 호출은 재시도하고, 다른 실패는 호출자에게 알린다. */
        if (n < 0 && errno == EINTR) {
            continue;
        }

        if (n <= 0) {
            return -1;
        }

        /* 실제로 보낸 길이만 누적한다. */
        sent += (size_t)n;
    }

    return 0;
}

int main(void)
{
    /* 1. IPv4(AF_INET)용 TCP 스트림 소켓을 만든다.
       성공하면 0 이상의 파일 디스크립터, 실패하면 -1을 반환한다. */
    int server = socket(AF_INET, SOCK_STREAM, 0);
    if (server == -1) {
        perror("socket");
        return EXIT_FAILURE;
    }

    /* 2. 종료 후 주소를 다시 바인딩하기 쉽게 한다.
       이미 대기 중인 다른 서버와 같은 주소·포트를 공유하는 옵션은 아니다. */
    int reuse = 1;
    if (setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof reuse) == -1) {
        perror("setsockopt");
        close(server);
        return EXIT_FAILURE;
    }

    /* 3. 접속을 받을 주소를 지정한다. {0}은 구조체 전체를 0으로 초기화한다.
       htons/htonl은 정수를 네트워크 바이트 순서로 변환한다.
       루프백 127.0.0.1에만 바인딩하므로 같은 Ubuntu 안에서 접속한다. */
    struct sockaddr_in address = {0};
    address.sin_family = AF_INET;
    address.sin_port = htons(9000);
    address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

    /* 4. 소켓에 주소를 연결한다. 실제 구조체 크기는 sizeof로 알려 준다. */
    if (bind(server, (struct sockaddr *)&address, sizeof address) == -1) {
        perror("bind");
        close(server);
        return EXIT_FAILURE;
    }

    /* 5. 접속 대기 상태로 전환한다. 두 번째 인자는 연결 대기열의 힌트다.
       한 클라이언트만 처리하는 이유는 accept()를 한 번만 하기 때문이다. */
    if (listen(server, 1) == -1) {
        perror("listen");
        close(server);
        return EXIT_FAILURE;
    }

    puts("Listening on 127.0.0.1:9000 (Ctrl+C to stop)");
    fflush(stdout);

    /* 6. 연결이 올 때까지 기다리고 새 통신용 소켓을 받는다.
       NULL 두 개는 상대 주소 정보를 받지 않겠다는 뜻이다. */
    int client;
    do {
        client = accept(server, NULL, NULL);
    } while (client == -1 && errno == EINTR);

    if (client == -1) {
        perror("accept");
        close(server);
        return EXIT_FAILURE;
    }

    puts("Client connected");

    /* 7. 받은 바이트를 그대로 돌려준다. TCP 메시지 경계를 가정하지 않는다. */
    char buffer[1024];
    int result = EXIT_SUCCESS;

    for (;;) {
        /* n > 0: 받은 길이 / n == 0: 상대의 송신 종료 / n < 0: 오류 */
        ssize_t n = recv(client, buffer, sizeof buffer, 0);

        if (n == 0) {
            break;
        }

        if (n < 0 && errno == EINTR) {
            continue;
        }

        if (n < 0) {
            perror("recv");
            result = EXIT_FAILURE;
            break;
        }

        /* n이 양수인지 확인한 뒤 unsigned 타입인 size_t로 변환한다. */
        if (send_all(client, buffer, (size_t)n) == -1) {
            perror("send");
            result = EXIT_FAILURE;
            break;
        }
    }

    /* 8. 통신용 소켓과 대기용 소켓은 서로 다른 자원이므로 각각 닫는다. */
    close(client);
    close(server);

    puts("Connection closed; server finished");
    return result;
}
클라이언트 전체 코드 펼치기 · echo_client.c
Ubuntu · C17 / echo_client.c
/* Ubuntu / POSIX, C17: 송신을 끝내고 서버의 에코를 EOF까지 읽는다. */
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>

int main(void)
{
    /* 1. 서버와 같은 IPv4/TCP 방식의 소켓을 만든다. */
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1) {
        perror("socket");
        return EXIT_FAILURE;
    }

    /* 2. 연결할 서버의 주소·포트를 정한다. 클라이언트 자신의 포트가 아니다.
       주소와 포트는 네트워크 바이트 순서로 저장한다. */
    struct sockaddr_in address = {0};
    address.sin_family = AF_INET;
    address.sin_port = htons(9000);
    address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

    /* 3. 서버가 먼저 실행되어 있어야 한다. 로컬 임시 포트는 OS가 고른다. */
    if (connect(fd, (struct sockaddr *)&address, sizeof address) == -1) {
        perror("connect");
        close(fd);
        return EXIT_FAILURE;
    }

    /* 4. C 문자열 끝의 NUL은 보내지 않고 줄바꿈까지의 바이트만 보낸다.
       message는 배열이므로 sizeof message에 NUL 한 바이트도 포함된다. */
    const char message[] = "Hello, network!\n";
    size_t sent = 0;

    while (sent < sizeof message - 1) {
        /* 부분 송신에 대비해 전송 위치와 남은 길이를 매번 갱신한다. */
        ssize_t n = send(fd, message + sent, sizeof message - 1 - sent, MSG_NOSIGNAL);

        /* EINTR는 신호로 중단된 경우다. 그 외의 실패는 정리 후 종료한다. */
        if (n < 0 && errno == EINTR) {
            continue;
        }

        if (n <= 0) {
            perror("send");
            close(fd);
            return EXIT_FAILURE;
        }

        sent += (size_t)n;
    }

    /* 5. 더 보낼 데이터가 없음을 알린다. 수신 방향은 계속 열려 있다.
       서버는 남은 바이트를 읽은 뒤 EOF를 받아 종료 절차로 들어간다. */
    if (shutdown(fd, SHUT_WR) == -1) {
        perror("shutdown");
        close(fd);
        return EXIT_FAILURE;
    }

    /* 6. 서버가 돌려주는 데이터를 여러 번에 나누어 받을 수도 있다. */
    char buffer[1024];

    for (;;) {
        ssize_t n = recv(fd, buffer, sizeof buffer, 0);

        /* 서버가 송신을 끝냈고 더 읽을 바이트가 없으면 수신을 마친다. */
        if (n == 0) {
            break;
        }

        if (n < 0 && errno == EINTR) {
            continue;
        }

        if (n < 0) {
            perror("recv");
            close(fd);
            return EXIT_FAILURE;
        }

        /* recv()는 NUL을 붙이지 않는다. 문자열이 아닌 받은 길이로 출력한다. */
        if (fwrite(buffer, 1, (size_t)n, stdout) != (size_t)n) {
            perror("fwrite");
            close(fd);
            return EXIT_FAILURE;
        }
    }

    /* 7. 수신까지 마쳤으므로 소켓 자원을 해제한다. */
    close(fd);

    return EXIT_SUCCESS;
}

예상과 다를 때 확인하기

현상확인할 내용
gcc: command not foundUbuntu에 개발 도구를 설치했는지 확인합니다. Windows의 컴파일러 설치와 별개입니다.
Connection refused서버가 먼저 실행 중인지, 동일한 Ubuntu 환경과 주소·포트를 사용하는지 확인합니다.
Address already in use이전 서버 등 다른 프로세스가 포트를 사용 중일 수 있습니다. 자신이 띄운 서버 터미널에서 Ctrl+C로 종료한 뒤 다시 실행합니다.
서버가 멈춘 것처럼 보임대기 메시지 이후 accept()가 클라이언트를 기다리는 것은 정상입니다. 터미널 B에서 클라이언트를 실행합니다.
No such file or directory현재 위치, 소스 복사 경로, 컴파일 오류 여부를 확인합니다. 실행은 ./echo_server처럼 경로를 명시합니다.

실행 후에는 보낸 바이트와 돌려받은 바이트가 같은지, 송신 종료 뒤 양쪽 프로그램이 종료되는지 확인합니다. 서버 없이 클라이언트를 실행하면 접속 오류가 발생해야 합니다.

10 / CHECK YOUR UNDERSTANDING보충 · 자기 점검

이해도
체크

먼저 자신의 말로 답해 보세요. 질문을 누르면 해설이 펼쳐집니다.

01. TCP로 3번 보냈는데 recv()가 2번만 반환해도 정상일까?

그럴 수 있습니다. TCP는 송신 호출 횟수가 아니라 바이트의 순서를 보존합니다. 받은 바이트를 누적하고, 응용 프로토콜의 규칙으로 메시지를 분리해야 합니다.

02. UDP가 메시지 경계를 유지한다면 재전송도 해 줄까?

아닙니다. 경계 유지와 신뢰성은 다른 특성입니다. 도착한 데이터그램은 구분되지만 유실될 수도 있습니다. 필요하다면 응용 계층이 복구 정책을 정해야 합니다.

03. 같은 서버의 9000번 포트에 여러 TCP 연결이 가능할까?

가능합니다. 서버는 대기용 소켓으로 연결을 받고, 각 연결에 별도 소켓을 사용합니다. 연결은 양쪽 IP 주소와 포트 조합으로 구분됩니다. 단, 이 장의 실습 코드는 의도적으로 한 연결만 처리합니다.

04. IP 주소가 목적지를 가리키는데 왜 MAC 주소도 필요할까?

역할의 범위가 다릅니다. IP는 네트워크를 넘는 논리적 목적지와 경로 결정에 사용하고, 이더넷의 MAC 주소는 현재 링크에서 프레임을 다음 지점으로 전달하는 데 사용합니다.

05. Ubuntu 프로그램에 WSAStartup()과 -lws2_32가 필요할까?

필요하지 않습니다. 두 항목은 Windows Winsock 사용과 관련됩니다. WSL의 Ubuntu에서 빌드하는 Linux 소켓 코드는 POSIX/Linux API를 사용합니다.

06. 실습 클라이언트에서 shutdown(SHUT_WR)을 빼면 어떻게 될까?

클라이언트는 응답을 받은 뒤에도 EOF를 기다리고, 서버는 다음 데이터를 기다릴 수 있습니다. 양쪽이 상대의 다음 동작을 기다리는 상황입니다. 데이터 형식뿐 아니라 “누가 언제 송신을 끝내는가”도 프로토콜의 일부입니다.

07. recv() 결과를 printf("%s", buffer)로 바로 출력해도 될까?

안전하다고 볼 수 없습니다. recv()는 문자열 종료 문자를 넣지 않습니다. 버퍼 밖을 읽지 않도록 받은 길이를 사용하거나, 충분한 공간을 확보하고 적절하게 NUL 종료를 처리해야 합니다. 바이너리 데이터는 문자열과 다르게 다룹니다.

통신 흐름 정리
응용프로그램은 메시지의 의미와 경계를 정하고 소켓 API로 운영체제에 통신을 요청합니다. 전송 계층은 포트로 통신을 구분하고, IP는 네트워크를 넘어 목적지로 데이터를 전달합니다. 네트워크 접근 계층은 연결된 한 구간에서 프레임을 전달합니다.
11 / REFERENCES제공 강의 25쪽 기준

학습 범위와 참고 자료

아래 표는 각 주제에 해당하는 강의 자료의 위치입니다. TCP 에코 코드와 실행 절차는 보충 실습입니다.

주제별 강의 출처

원본 PDF주요 내용·시각요소HTML 위치
1–2쪽제목, 학습 목표, 4개 주제 목차학습 지도
3–6쪽호스트·라우터, TCP/IP 구조, 계층 도식인터넷 · 네 계층
7–9쪽TCP/UDP 비교, 스트림·프레이밍, 데이터그램TCP와 UDP
10–12쪽헤더와 트레일러, 송수신 캡슐화, 계층별 경로데이터 이동
12–14쪽IPv4/IPv6, 포트 표, 도메인, 서버·클라이언트주소 · 통신 역할
15–17쪽전화 비유, 파일과 소켓 비교, 종단점·API소켓의 세 관점
18–21쪽Winsock 역사, 차이·장단점, DLL 구조도플랫폼 비교
22–24쪽Dev-C++ 설정, 서버·Telnet·Linux 실행 화면첫 소켓 프로그램
25쪽마무리 슬라이드별도 학습 개념 없음

보충 설명의 참고 문서

출처: 1장 PDF와 참고 문서(2026-09-14 확인).

서버 없이 이 HTML을 직접 열 수 있습니다. 다른 위치로 옮길 때는 Lecture_HTML 폴더 전체를 복사하세요. 외부 참고 문서는 인터넷 연결이 필요합니다.