네트워크 프로그래밍이란 결국 프로세스 간의 통신을 의미하며, 이를 가능하게 하는 핵심 도구가 바로 소켓(Socket)이다.
파이썬 socket 모듈을 활용하면 리눅스 환경에서 서버와 클라이언트를 직접 구현하고 데이터를 주고받는 과정을 명확히 이해할 수 있다.
1. 소켓 통신의 기본 개념
소켓은 네트워크상의 두 프로그램이 서로 데이터를 주고받을 수 있도록 연결해 주는 통신 종착점(Endpoint)을 의미한다.
서버와 클라이언트는 각각 역할에 맞는 소켓을 생성하여 통신에 참여한다.
🟢 서버와 클라이언트의 역할
- 서버(Server): 클라이언트의 연결 요청을 기다리다가 요청이 오면 수락하여 통신용 소켓을 생성한다. 연결을 기다리는 소켓을 리스닝 소켓, 연결된 클라이언트와 통신하는 소켓을 커넥션 소켓이라고 한다.
- 클라이언트(Client): 서버에 연결을 요청하고 데이터를 전송하는 역할을 한다.
🟢 TCP 소켓 통신 흐름도
소켓 프로그래밍은 서버와 클라이언트의 역할에 따라 특정 시스템 콜(System Call)을 순서대로 호출하는 과정이다.
아래의 시퀀스 다이어그램은 TCP 연결의 전체적인 흐름을 보여준다.

2. 파이썬 소켓 모듈의 주요 메소드
소켓 프로그래밍을 위해 파이썬에서는 기본적으로 제공하는 socket 모듈을 임포트하여 사용한다.
파이썬에서 import socket을 통해 사용하는 주요 함수들 다음과 같다.
| 메소드 | 설명 | 주요 매개변수 및 리턴값 |
| socket() | 통신을 위한 소켓 객체 생성 | AF_INET(IPv4), SOCK_STREAM(TCP) 등 설정 |
| bind() | 생성된 소켓에 IP 주소와 포트 번호를 할당 | (host, port) 튜플 형태로 전달 |
| listen() | 클라이언트의 접속 요청 대기 상태로 전환 | backlog: 최대 연결 대기 큐의 크기 |
| accept() | 클라이언트의 접속을 수락 | (새로운 소켓, 주소) 쌍을 반환 |
| connect() | 서버 측에 연결을 요청 | 서버의 IP와 포트 주소 필요 |
| send() | 연결된 상대방에게 데이터를 송신 | encode()된 바이트 데이터 전달 |
| recv() | 상대방으로부터 데이터를 수신 | bufsize: 한 번에 받을 최대 바이트 수 |
| close() | 사용이 끝난 소켓 자원을 시스템에 반환 | 통신 종료 |
3. 실습 환경 준비
1️⃣ 가상 IP 추가
루프백 주소(127.0.0.1)를 가지고 서버-클라이언트 통신을 실습하는 대신, 서로 다른 IP 주소 간의 통신을 재현하기 위해 리눅스 서버에 가상 IP를 추가한다.
하나의 리눅스 서버 내에서 가상 IP를 추가해 실습할 때는 인터페이스 설정을 주의해야 한다.
루프백 인터페이스에 가상 IP를 추가할 때는 반드시 /32 마스크를 사용해서 특정 IP 하나만 지정해야 한다.
# 서버용 가상 IP 추가
sudo ip addr add 192.168.100.10/32 dev lo
추가된 IP가 정상적으로 인식되는지 확인한다.

2️⃣ 파이썬 설치 및 실행
# 파이썬 3.12 설치
dnf -y install python3.12
# 파이썬 3.12 실행
python3.12
3️⃣ net-tools 패키지 설치
sudo dnf -y install net-tools
4. 실습 : 파이썬으로 소켓 통신 구현하기
🟢 프로그램
1️⃣ 서버 측 : port open
첫 번째 터미널 (서버 측) 에서 다음과 같이 파이썬으로 실행한다.
python3.12
>>> import socket
>>> host = "192.168.100.10"
>>> port = 8000
>>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
>>> s.bind((host,port))
>>> s.listen(5)
>>> conn, addr = s.accept()
- bind()의 host: 서버 자신의 IP (또는 모든 IP를 받는 0.0.0.0)
- connect()의 host: 접속하려는 대상(서버)의 IP
2️⃣ 포트 상태 확인 : LISTEN
세번째 터미널(리눅스서버)에서 netstat -nat 명령으로 8000번 포트가 대기중인지 확인한다.

서버가 연결 요청을 기다리고 있는 상태다.
지금 첫 번째 창에서 실행한 파이썬 코드는 conn, addr = s.accept() 줄에서 멈춰 있다.

accept()는 클라이언트가 실제로 접속 버튼을 누를 때까지 프로그램을 잠시 멈추고 기다리는 '블로킹(Blocking)' 함수다.
이제 서버는 준비가 끝났으니, 클라이언트가 들어오기만 하면 블로킹이 해제되어 다음 코드를 입력할 수 있다.
3️⃣ 클라이언트 측 : 서버와 연결
두 번째 터미널 (클라이언트 측) 에서 다음과 같이 파이썬으로 실행한다.
python3.12
>> import socket
>> host = "192.168.100.10"
>> port = 8000
>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
>> s.connect((host,port)) #서버 IP로 바로 연결)

4️⃣ 포트 상태 확인 : ESTABLISHED
서버와 클라이언트가 서로 연결이 되었으므로 ESTABLISHED 상태인 연결이 두 개가 뜬다.
- 서버는 클라이언트가 찾아올 수 있도록 8000번처럼 정해진 포트를 사용해야 하지만, 클라이언트는 굳이 특정 번호를 고집할 필요가 없기 때문에 동적으로 포트가 할당된다.
- 클라이언트가 서버에 connect() 요청을 보낼 때, 운영체제(OS)는 현재 사용 중이지 않은 임의의 높은 번호(보통 49152~65535 사이)를 자동으로 할당해 주는데, 이걸 동적 포트라고 한다.
- 44718은 리눅스 커널이 이 통신을 위해 클라이언트에게 임시로 빌려준 번호인 것이다.

서버 측 프로그램의 blocking이 해제된 것을 볼 수 있다.

5️⃣ 서버 측 : 데이터 송신
data 변수에 클라이언트에 보낼 메시지를 담아서 클라이언트에 전송한다.
>>> data = "Hello client!"
>>> conn.send(data.encode('utf-8'))
13
6️⃣ 클라이언트 측 : 데이터 수신 및 송신
>>> data = s.recv(1024).decode('utf-8')
>>> print(data)
Hello client!
>>> s.send("Hello server!".encode('utf-8'))
13
5️⃣ 서버 측 : 데이터 수신
>>> print(conn.recv(1024).decode('utf-8'))
Hello server!
7️⃣ 클라이언트 측 : 연결 해제
>>> s.close()
8️⃣ 서버 측: 연결 해제
>>> conn.close()
>>> s.close()
9️⃣ 포트 상태 확인 : TIME_WAIT
TIME_WAIT은 TCP 연결이 종료된 후, 해당 소켓이 완전히 사라지기 전에 잠시 머무르는 종료 유예 상태이다.
TCP는 데이터 전송의 신뢰성을 보장하는 프로토콜이므로 연결을 종료하는 과정(4-Way Handshake)에서 내가 보낸 마지막 확인 신호(ACK)가 상대방에게 전달되지 않았을 경우를 대비한다.
상대방이 신호를 못 받으면 다시 종료 요청을 보낼 텐데, 이때 내가 이미 소켓을 없애버렸다면 제대로 응답해줄 수 없기 때문이다.
먼저 close()를 호출하여 능동적 종료(Active Close)를 한 쪽이 TIME_WAIT 상태가 된다.
아래 상태를 보면 클라이언트 프로그램이 먼저 종료되었기 때문에 51094 포트가 TIME_WAIT으로 나오는 것을 볼 수 있다.
일정 시간이 지나면 상태 리스트에서 사라진다.

🟢 소켓 객체 형성 및 메서드 작동 과정

① 객체의 형성 (Instance Creation)
- 서버/클라이언트 공통 (s 객체): socket.socket() 메서드를 호출하면 운영체제로부터 네트워크 자원을 할당받은 소켓 객체 s가 메모리에 생성된다. 이때 AF_INET(IPv4)과 SOCK_STREAM(TCP)이라는 속성이 메서드 인자를 통해 객체에 부여되는 것이다.
- 서버 전용 (conn 객체): 서버가 s.accept()를 호출하고 클라이언트가 접속하면, 기존 대기용 소켓 s와는 별도로 실제 통신만 전담하는 새로운 객체 conn이 생성된다. conn, addr = s.accept() 부분이 이 새로운 통신용 객체를 변수에 할당하는 과정이다.
② 메서드 부착 및 전송 (send)
- conn.send(data.encode('utf-8')) :
- encode('utf-8') : 텍스트인 "Hello client!"를 컴퓨터가 이해하는 바이트(Byte) 배열로 변환한다.
- send() : conn 객체에 붙은 이 메서드는 변환된 바이트 데이터를 소켓의 출력 버퍼에 넣는다. 이후 운영체제가 TCP/IP 프로토콜을 통해 상대방에게 물리적으로 전송한다.
③ 메서드 부착 및 수신 (recv)
- data = s.recv(1024).decode('utf-8') :
- recv(1024) : 소켓 객체 s의 수신 버퍼에 데이터가 들어올 때까지 기다렸다가(Blocking), 데이터가 오면 최대 1024바이트만큼 꺼내온다.
- decode('utf-8') : 수신된 바이트 데이터를 다시 사람이 읽을 수 있는 유니코드 문자열로 디코딩한다.
'Journey to Security > 리눅스' 카테고리의 다른 글
| Kali 리눅스 IP주소 설정 방법 (0) | 2026.05.13 |
|---|---|
| 구형 노트북에 Rocky Linux 9 서버 설치하기 (+wifi 연결) (0) | 2026.05.01 |
| OpenSSL로 Apache HTTPS 인증서 생성하기 (2) 루트 인증서 등록 (0) | 2026.05.01 |
| OpenSSL로 Apache HTTPS 인증서 생성하기 (1) SAN, 와일드카드 인증서 (0) | 2026.04.30 |
| [시스템] C 프로그램으로 이해하는 멀티쓰레드 (0) | 2026.04.27 |