본문 바로가기
CS/Network

[Network] 소켓

by 그냥하는거지뭐~ 2024. 9. 6.
목차
- 소켓은 통신에 필요한 데이터를 담는 파일이다!
- TCP socket 동작 방식 
- UDP socket 동작 방식 

 

1. 소켓은 통신에 필요한 데이터를 담는 파일이다!

1.1. L4(전송 계층)와 포트 번호

전송 계층(L4)는 네트워크 계층(L3)에서 전송된 데이터를 애플리케이션 계층으로 전달하는 역할을 한다. 여기서 포트 번호를 통해 송신지와 수신지의 특정 애플리케이션을 식별한다. 즉, 포트 번호는 애플리케이션에서 동작하는 부분과 시스템 레벨에서 동작하는 부분을 연결해주는 중요한 역할을 한다. TCP나 UDP 프로토콜은 패킷의 헤더에 포함된 포트 번호를 통해 송신자와 수신자의 애플리케이션을 구분하여 네트워크 상의 데이터를 올바른 애플리케이션에 전달하게 된다. 

 

 

1.2. 소켓과 애플리케이션 - 시스템 연결

애플리케이션은 직접 시스템의 네트워크 기능에 접근할 수 없다. 시스템의 리소스는 여러 애플리케이션이 공유하기 때문에, 어떤 한 애플리케이션에서 시스템 코드에 직접 접근했다가 문제가 발생한다면, 이 애플리케이션 뿐만 아니라 시스템 위에서 동작하고 있는 다른 모든 애플리케이션들에게도 영향이 갈 것이다. 

이를 방지하고 안전한 접근을 제공하기 위해 시스템은 인터페이스를 제공하는데, 이게 바로 소켓이다. 애플리케이션은 소켓을 통해 데이터를 주고받고, 개발자는 socket programming을 통해 네트워크 상의 다른 프로세스와 데이터를 주고받을 수 있도록 구현한다. 

 

예를 들어, 컴퓨터 A에서 컴퓨터 B로 데이터를 보내는 상황을 가정해보자. A의 애플리케이션은 소켓을 열어서 이곳을 통해 운영체제에게 네트워크 연결을 요청한다. B도 소켓을 열어 통신을 수락할 준비를 한다. 이때 A의 소켓과 B의 소켓이 한 쌍을 이루어 unique한 connection을 형성한다. 

 

 

1.3. 소켓에 들어가는 정보

소켓은 인터넷 상에 존재하는 각 포트를 unique하게 식별하기 위한 주소를 담은 파일이라고 생각하면 쉽다. 소켓은 <protocol, ip addr, port num>로 정의된다. 

소스코드를 잠시 살펴보자. 

// 1. 소켓 생성
sock = socket(AF_INET, SOCK_STREAM, 0);

// 2. 주소 구조체 초기화
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("111.0.0.2");  // IP 주소 설정
servaddr.sin_port = htons(8080);  // 포트 설정

// 3. 소켓 바인딩
bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr));
1. 소켓 생성
- socket(AF_INET, SOCK_STREAM, 0)으로 소켓을 생성한다.
- AF_INET은 IPv4를 의미한다.
- SOCK_STREAM은 TCP 프로토콜을 의미한다. (UDP는 SOCK_DGRAM)
2. 주소 설정
- servaddr 구조체에서 sin_family, sin_addr.s_addr, sin_port를 통해 IP 주소와 포트 번호를 설정한다.
3. 바인딩
- bind()함수로 소켓을 지정된 IP 주소와 포트에 바인딩한다.

 


2. TCP socket 동작 방식 

먼저 TCP echo 서비스 작동 순서를 보자. (<protocol, ip addr, port>에서 protocol은 TCP로 고정되어 있으므로 생략하겠다.) 

[SERVER]

1. 서버는 connection을 맺는 요청을 기다리는 listening socket을 연다. socket() -> bind() -> listen()

<50.50.50.50, 8080>

 

[CLIENT]

1. 소켓을 열고, 서버(listening socket)에 connect 요청을 보낸다. 

<77.77.77.77, 49999>

 

[SERVER]

1. 요청을 받은 서버는 accept() 함수를 호출하는데, 이 함수는 새로운 소켓(data socket)을 return해서 클라이언트와 통신하도록 한다. 클라이언트가 여러 명이라면, 각 클라이언트마다의 data socket을 생성한다. 이때, 서버의 data socket들의 ip addr, port num는 listening socket과 동일하다. 

 

정리해보면, 소켓 종류는 3개
- listening socket(접속 대기용)
- data socket(클라이언트와 통신하는 소켓)
- 클라이언트 소켓(서버와 통신하는 소켓)

 

여기까지 정리해보자. 서버에서 두 클라이언트와 connection이 맺어져 있다고 가정해보자. 그러면 서버에 열려 있는 소켓은 listening socket을 포함해서 3개가 열려 있을 것이다. 이 3개의 소켓은 모두 동일한 Ip addr, port num를 가지고 있다. 

 

🤔 엇? 서버에 열려 있는 소켓들이 모두 동일한 주소를 가지고 있다면 어떻게 알맞은 애플리케이션을 찾아갈 수 있는 것일까? 소켓은 unique해야 한 것 아니었나? 

TCP connection 요청을 할 때는 listening socket으로 데이터를 보낸다. 이미 connection이 이루어진 후에는 서버의 소켓에 있는 정보만으로는 어떤 애플리케이션으로 데이터를 보내야 하는지 식별할 수 없기 때문에, 추가적으로 src ip addr, src port까지 확인하게 된다. (socket pair는 unique하므로)

 

=> 즉, TCP socket은 <ip addr, port>만으로는 unique하게 식별할 수 없으므로, unique한 socket pair <src ip addr, src port, dest ip addr, dest port>로 목적지를 식별한다. 

 

여기에서 알 수 있는 사실은, 이론적으로는 한 서버에서 65535개의 포트를 사용할 수 있지만, 실질적으로는 socket pair를 이용해 그 이상의 소켓을 열 수 있다. 


3. UDP socket 동작 방식 

UDP 특징 short review
- connection 개념이 없다. (connectionless)
- 신뢰할 수 없는 불안정한 프로토콜이다.

 

UDP는 TCP와 다르게 <ip addr, port>만으로 식별이 가능하다. connection 개념이 없으므로, TCP처럼 listen()이나 accept()가 필요하지 않다. UDP socket에서 데이터를 보낼 때 sendTo() 함수로 어느 UDP socket으로 보낼지 지정할 수 있으며, 받은 쪽에서는 recvfrom() 함수로 어느 UDP socket으로부터 왔는지 알 수 있다. (각 전송이 독립적이므로)

 


Reference

- 쉬운코드 https://www.youtube.com/watch?v=WwseO8l8rZc

- 널널한 개발자 TCP echo https://www.youtube.com/watch?v=EMDTnK3uUYg&list=PLXvgR_grOs1A4C8-Q64yizWA49iN1rgtv&index=8

- 널널한 개발자 소켓의 본질 https://www.youtube.com/watch?v=3jQ2dBpiqPo&list=PLXvgR_grOs1A4C8-Q64yizWA49iN1rgtv&index=6