- 0
- 144 words
네트워크를 공부하다 보면 IP 주소와 포트 번호라는 말을 계속 만나게 된다.
IP 주소는 어떤 컴퓨터와 통신할 것인지 구분하고, 포트 번호는 그 컴퓨터에서 어떤 프로그램이나 서비스를 대상으로 통신할 것인지 구분한다.
그런데 여기서 한 가지 궁금한 점이 생긴다.
그렇다면 실제 프로그램은 네트워크와 어떻게 연결되는 걸까?
웹 브라우저나 게임, 채팅 프로그램 같은 애플리케이션이 데이터를 주고받으려면 운영체제의 네트워크 기능을 이용해야 한다. 이때 프로그램과 운영체제의 네트워크 기능 사이에서 중요한 역할을 하는 것이 바로 소켓(Socket)이다.
처음 소켓을 접하면 단순히 “IP 주소 + 포트 번호”라고 외우기 쉽지만, 실제로는 조금 더 넓은 개념이다.
이번 글에서는 소켓이 무엇인지, 포트와는 어떤 차이가 있는지, 그리고 서버와 클라이언트가 소켓을 이용해 통신하는 과정을 정리해보자.
소켓은 프로그램과 네트워크를 연결해주는 인터페이스
소켓을 가장 쉽게 이해하는 방법은 프로그램이 네트워크 통신을 사용하기 위한 창구라고 생각하는 것이다.
프로그램이 직접 랜카드나 TCP/IP 프로토콜을 제어하면서 데이터를 보내는 것은 아니다.
대신 운영체제가 제공하는 소켓 인터페이스를 이용한다.
대략적인 구조를 보면 다음과 같다.
[애플리케이션]
│
│ 소켓 API
▼
[운영체제의 네트워크 기능]
│
▼
[TCP / UDP / IP]
│
▼
[네트워크 인터페이스]
│
▼
[네트워크]
예를 들어 웹 브라우저가 서버와 통신한다고 생각해보자.
브라우저는 운영체제에 소켓을 생성하고, 이 소켓을 통해 데이터를 보내거나 받는다.
운영체제는 그 요청을 TCP, IP 등의 네트워크 프로토콜을 이용해 실제 네트워크로 전달한다.
즉, 애플리케이션 입장에서는 복잡한 네트워크 장비나 패킷 처리 과정을 직접 관리하지 않고 소켓이라는 인터페이스를 통해 네트워크를 사용할 수 있다.
소켓과 포트 번호는 같은 개념일까?
여기서 71번 글에서 설명했던 포트 번호와 소켓이 헷갈릴 수 있다.
둘은 같은 것이 아니다.
포트 번호는 통신 대상을 구분하기 위한 번호이고, 소켓은 프로그램이 네트워크 통신을 하기 위해 사용하는 인터페이스 또는 통신 끝점이라고 이해하면 된다.
예를 들어 서버의 주소가 다음과 같다고 해보자.
192.168.0.10:8080
여기서
192.168.0.10 → IP 주소
8080 → 포트 번호
이다.
그런데 프로그램이 실제로 8080번 포트에서 통신하려면 운영체제에 소켓을 생성하고 해당 포트에 연결하는 과정이 필요하다.
따라서 관계를 단순하게 표현하면 다음과 같다.
IP 주소
+
포트 번호
↓
통신 대상을 식별
소켓
↓
프로그램이 실제 네트워크 통신을 수행하는 인터페이스
그래서 “포트가 프로그램에 연결되는 과정에서 소켓이 사용된다”고 생각하면 이해하기 쉽다.
서버에서는 소켓을 어떻게 사용할까?
서버 프로그램은 일반적으로 특정 포트에서 클라이언트의 연결을 기다린다.
예를 들어 웹 서버가 TCP 8080 포트를 사용한다고 해보자.
개념적인 흐름은 다음과 같다.
소켓 생성
↓
IP 주소와 포트에 연결(bind)
↓
연결 요청 대기(listen)
↓
클라이언트 연결 수락(accept)
↓
데이터 송수신
각 단계가 어떤 의미인지 하나씩 보면 이해하기 쉽다.
1. socket()
먼저 운영체제에 네트워크 통신을 위한 소켓을 만들어 달라고 요청한다.
socket()
이 단계에서는 아직 특정 서버와 연결된 것은 아니다.
말 그대로 통신을 위한 소켓을 하나 만든 것이다.
2. bind()
그 다음 소켓을 특정 IP 주소와 포트 번호에 연결한다.
bind()
예를 들어 서버가
192.168.0.10:8080
에서 연결을 기다린다고 하면 이 주소와 소켓을 연결하는 과정이다.
이렇게 해야 운영체제가 8080번 포트로 들어오는 연결을 해당 서버 프로그램과 연결할 수 있다.
3. listen()
TCP 서버는 클라이언트의 연결 요청을 기다리는 상태가 필요하다.
listen()
이 단계에서 서버는 새로운 TCP 연결 요청을 받아들일 준비를 한다.
4. accept()
클라이언트가 실제로 연결을 요청하면 서버는 이를 받아들인다.
accept()
여기서 중요한 부분이 있다.
listen을 하고 있는 소켓과 실제 통신에 사용하는 소켓은 역할이 다르다.
서버는 하나의 포트에서 연결 요청을 계속 기다리면서, 연결이 성립된 클라이언트와는 별도의 연결 소켓을 사용해 데이터를 주고받는다.
예를 들어 다음과 같은 상황이 가능하다.
서버
192.168.0.10:8080
│
┌───────┼───────┐
│ │ │
연결1 연결2 연결3
│ │ │
Client A Client B Client C
서버는 8080번 포트를 사용하고 있지만 여러 클라이언트와 동시에 통신할 수 있다.
이것이 가능한 이유 중 하나가 각각의 TCP 연결을 서로 구분할 수 있기 때문이다.
TCP 연결에서는 4개의 정보가 중요하다
TCP 연결을 구분할 때는 일반적으로 다음 네 가지 정보를 사용한다.
출발지 IP
출발지 Port
목적지 IP
목적지 Port
예를 들어 클라이언트가 다음과 같이 서버에 연결했다고 해보자.
Client
192.168.0.20:51000
↓ TCP
Server
192.168.0.10:8080
이 연결은 다음과 같이 표현할 수 있다.
192.168.0.20:51000
↓
192.168.0.10:8080
다른 클라이언트가 접속하면 출발지 포트가 달라질 수 있다.
192.168.0.21:51001
↓
192.168.0.10:8080
목적지 서버의 포트는 둘 다 8080이지만 각각 다른 TCP 연결로 구분할 수 있다.
그래서 TCP 연결을 설명할 때 흔히 4-tuple(4-튜플)이라는 표현을 사용한다.
(출발지 IP, 출발지 Port,
목적지 IP, 목적지 Port)
여기에 TCP라는 프로토콜까지 포함해서 생각하면 각각의 연결을 더욱 명확하게 구분할 수 있다.
클라이언트의 소켓은 어떻게 동작할까?
클라이언트도 기본적으로 소켓을 생성해서 서버와 연결한다.
흐름은 서버와 조금 다르다.
소켓 생성
↓
connect()
↓
서버와 TCP 연결
↓
send / recv
↓
데이터 송수신
예를 들어 웹 브라우저가 서버에 접속한다고 생각해보자.
브라우저가 소켓을 만들고 서버의 주소로 연결을 요청한다.
Client
192.168.0.20:51000
│
│ connect()
▼
Server
192.168.0.10:8080
이때 클라이언트의 51000번 포트처럼 통신을 위해 임시로 사용되는 포트는 운영체제가 자동으로 할당할 수 있다.
반면 서버는 일반적으로 외부에서 접속해야 하는 서비스 포트를 미리 정해놓는다.
예를 들면 웹 서버는 80 또는 443, SSH 서버는 22번 포트를 사용하는 식이다.
소켓으로 데이터를 주고받는 과정
연결이 완료되면 이제 실제 데이터를 주고받는다.
개념적으로는 다음과 같은 구조다.
Client Application
│
│ send()
▼
Socket
│
▼
TCP / IP
│
▼
Network
│
▼
TCP / IP
│
▼
Socket
│
│ recv()
▼
Server Application
프로그램이 데이터를 보내면 소켓을 통해 운영체제에 전달되고, 운영체제는 TCP/IP 등의 프로토콜에 따라 데이터를 처리한 뒤 네트워크로 전송한다.
상대방에서는 받은 데이터를 다시 운영체제의 네트워크 스택이 처리하고 소켓을 통해 프로그램에 전달한다.
그래서 프로그램 입장에서는 복잡한 패킷의 분할이나 재전송 같은 작업을 직접 처리하지 않고 소켓을 이용해 데이터를 주고받을 수 있다.
물론 애플리케이션이 사용하는 프로토콜에 따라 데이터 처리 방식은 달라질 수 있다.
TCP 소켓과 UDP 소켓은 다르다
소켓이라고 해서 모두 같은 방식으로 동작하는 것은 아니다.
대표적으로 TCP와 UDP에서 사용하는 소켓 방식이 다르다.
| 구분 | TCP 소켓 | UDP 소켓 |
|---|---|---|
| 연결 방식 | 연결 지향 | 비연결 |
| 대표 API 흐름 | connect / listen / accept | sendto / recvfrom |
| 데이터 전달 | 신뢰성 있는 바이트 스트림 | 데이터그램 |
| 순서 보장 | O | X |
| 재전송 | O | 기본 제공 X |
| 연결 설정 | 필요 | 필요 없음 |
TCP에서는 앞에서 본 것처럼 서버가 listen()과 accept()를 사용해 연결을 받아들이는 구조가 일반적이다.
반면 UDP는 연결을 설정하지 않고 데이터를 바로 특정 목적지로 보낼 수 있다.
그래서 UDP에서는 TCP처럼 연결을 수락하는 accept() 과정이 필요하지 않다.
이 차이는 앞에서 살펴본 TCP와 UDP의 차이와도 연결된다.
소켓은 실제로 어디에서 볼 수 있을까?
소켓은 프로그램 내부에서만 존재하는 추상적인 개념처럼 보이지만 운영체제에서도 확인할 수 있다.
리눅스에서는 다음과 같은 명령어를 사용할 수 있다.
ss -tuln
또는 현재 연결 상태까지 확인하려면
ss -tun
같은 명령을 사용할 수 있다.
실행 결과에서는 다음과 같은 정보들을 볼 수 있다.
Local Address:Port
Foreign Address:Port
State
예를 들어
192.168.0.20:51000
192.168.0.10:8080
ESTAB
와 같은 정보가 나온다면 두 주소와 포트 사이에 TCP 연결이 성립되어 있다는 의미로 볼 수 있다.
이런 명령어를 사용하면 실제 서버에서 어떤 포트가 열려 있는지, 어떤 연결이 만들어져 있는지 확인할 수 있기 때문에 네트워크 문제를 분석할 때도 유용하다.
소켓을 전화에 비유하면 이해하기 쉽다
소켓을 이해하기 어려울 때는 전화 통화에 비유해보는 것도 괜찮다.
IP 주소 → 어느 집에 전화할 것인가
포트 → 그 집에서 어느 서비스와 연결할 것인가
소켓 → 실제 통화를 하기 위한 통신 창구
연결 → 통화 연결
데이터 → 통화 내용
물론 완전히 똑같은 개념은 아니지만 전체적인 관계를 이해하기에는 도움이 된다.
IP 주소만 알아서는 충분하지 않고, 특정 서비스와 통신하려면 포트 번호가 필요하다.
그리고 실제 프로그램이 네트워크를 이용하기 위해서는 운영체제가 제공하는 소켓 인터페이스를 사용한다.
소켓을 이해하면 서버 구조가 조금 더 명확해진다
웹 서버를 예로 들어보면 지금까지 배운 내용이 하나로 연결된다.
사용자
│
│ 도메인 입력
▼
DNS
│
│ IP 주소 확인
▼
서버 IP
│
│ TCP 연결
▼
서버의 Port
│
▼
Server Socket
│
▼
웹 서버 프로그램
│
▼
HTTP 요청 처리
브라우저가 서버의 IP 주소를 알아내고 특정 포트로 연결하면, 운영체제는 해당 포트에서 대기하고 있는 서버 프로그램의 소켓과 연결을 만들어준다.
그 이후 HTTP 같은 애플리케이션 프로토콜을 이용해 실제 데이터를 주고받는다.
이렇게 보면 DNS, IP, 포트, TCP, 소켓, HTTP가 각각 완전히 별개의 개념이 아니라 하나의 통신 과정에서 이어지는 요소라는 것을 알 수 있다.
마무리
소켓은 단순히 “IP 주소와 포트 번호”라고 외우는 것보다 프로그램이 운영체제의 네트워크 기능을 이용하기 위한 통신 인터페이스라고 이해하는 것이 좋다.
정리하면 다음과 같다.
IP 주소
→ 어떤 컴퓨터와 통신할지 구분
포트 번호
→ 그 컴퓨터의 어떤 서비스와 통신할지 구분
소켓
→ 프로그램이 실제 네트워크 통신을 하기 위한 인터페이스
TCP 연결
→ 출발지와 목적지의 IP/포트 정보를 이용해 연결을 구분
특히 TCP 서버의 기본 흐름인
socket()
→ bind()
→ listen()
→ accept()
→ send/recv
이 구조를 이해해두면 나중에 웹 서버나 백엔드 프로그램을 공부할 때도 도움이 된다.
결국 우리가 사용하는 웹 브라우저나 서버 프로그램도 네트워크와 직접 대화하는 것이 아니라, 운영체제가 제공하는 소켓이라는 통신 창구를 통해 데이터를 주고받는다고 보면 된다.
다음 글에서는 여기서 한 단계 더 들어가서 DNS는 도메인 이름을 어떻게 IP 주소로 바꾸는지 알아볼 예정이다. 소켓까지 이해했다면 이제 브라우저에 주소를 입력했을 때 실제로 어떤 과정이 시작되는지도 조금씩 연결해서 볼 수 있다.