Computer Science/네트워크 (Network)

네트워크 모델 | (4) 소켓(Socket)에 대한 이해

hyuga_ 2023. 11. 19. 19:36

소켓(Socket)

Socket? 

소켓은 네트워크상의 두 프로그램 간의 통신을 위한 엔드포인트로,

소켓 주소 정보를 사용하여 데이터 송수신을 관리하는 프로그래밍 인터페이스이다.

 

 

네트워크 기능을 사용하려는 애플리케이션(프로세스)은 소켓을 열어서, 소켓 인터페이스를 통해 네트워크 작업을 수행한다. 

소켓 자체는 TCP나 UDP에 종속된 개념이 아니다. 네트워크 통신을 할 때, 각 통신주체가 데이터를 주고 받는 출발지와 도착지 역할을 하는 대상을 통칭한다.

 

다만 현대 네트워킹에서는 TCP, UDP가 흥하면서 TCP, UDP 프로토콜을 기반으로 하는 소켓을 주로 쓰고 있고, 때문에 소켓 == TCP 소켓, UDP 소켓으로 통용되는 감이 없지않아 있다.

 

소켓의 종류

네트워크 통신을 위한 소켓을 Stream 소켓과 Datagram 소켓 유형으로 나누어 볼 수 있다. 이들은 사용하는 프로토콜과 데이터 전송 방식에서 차이가 있다. 각각 TCP와 UDP가 대명사이다. 

 

Stream 소켓 vs Datagram 소켓 요약

  • Stream 소켓 (TCP): 데이터의 정확성과 순서를 중시하는 애플리케이션에 적합하며, 더 신뢰성 있는 데이터 전송을 제공한다.
  • Datagram 소켓 (UDP): 빠른 데이터 전송을 필요로 하며, 일부 데이터 손실이나 순서 변경이 허용되는 애플리케이션에 적합하다.

Stream 소켓

  • 연결 지향적 통신 (Connection-oriented): 데이터 송수신 전에 클라이언트와 서버 간에 연결을 먼저 설정한다.
  • 데이터는 연속적인 스트림으로 전송되며, 전송된 데이터의 순서가 보장된다.
  • 높은 신뢰성을 제공하며, 손실된 데이터는 재전송된다.
  • 데이터 무결성과 순서가 중요한 애플리케이션에 적합하다.

일반적으로 TCP (Transmission Control Protocol)를 사용한다.

웹 서버, 이메일 전송, 파일 전송 등 데이터의 정확성과 순서가 중요한 애플리케이션에서 주로 사용된다.

 

Datagram 소켓

  • 연결이 없는 통신 (Connectionless): Datagram 소켓은 미리 연결을 설정할 필요 없이 데이터를 송수신한다.
  • 각 데이터 패킷은 독립적으로 전송된다. 이는 각 패킷이 서로 다른 경로로 전송될 수 있음을 의미한다.
  • 신뢰성이 낮고 순서가 보장되지 않는다. 즉, 패킷의 손실이나 순서 변경이 발생할 수 있다.
  • 대역폭을 적게 사용하며, 빠른 전송이 가능하다.

일반적으로 UDP (User Datagram Protocol)를 사용한다. 

실시간 스트리밍, 온라인 게임, 비디오 회의 등 실시간성이 중요하고 일부 패킷 손실이 허용되는 애플리케이션에서 주로 사용된다.

 

 

TCP 소켓

TCP 소켓의 정의

위에서 소켓을 한 문장으로 정의한 바는 다음과 같다. 

 

 

소켓은 네트워크상의 두 프로그램 간의 통신을 위한 엔드포인트로,
소켓 주소 정보를 사용하여 데이터 송수신을 관리하는 프로그래밍 인터페이스이다.

 

 

그러나 이것만으로는 인터넷 네트워크 통신에서의 소켓의 정체를 파악하기에 완전하지 않다. 

소켓을 여러 관점에서 바라보면 다음과 같이 정의할 수 있다. 

 

 

Socket 이란?

1. 개발자에게는, 소켓은 네트워크를 통한 데이터 입출력을 위한 file
2. 프로세스에게는 네트워크 세계로의 인터페이스 (= 포트 역할)
3. 네트워크 세계에서는, 데이터 통신의 엔드포인트
4. 각 소켓은 <프로토콜 + IP 주소 + 포트 넘버> 라는 소켓 주소로 식별된다.
(= 각 프로세스의 포트를 유니크하게 식별하기 위한 주소)


Socket 연결? (in TCP)

1. 연결 설정 시: 소켓주소 활용
= <프로토콜 + IP 주소 + 포트 넘버>
2. 연결 유지 시: 연결 정보 활용 =
<src IP 주소, src 포트, dest IP 주소, dest 포트>

아래 'Socket 연결' 내용은 TCP 통신에 한정된 얘기다. (근데 TCP가 거의 표준이니깐..)

지금부터는 위 요약 정리가 대체 뭔 말인지를 알아보자.

 

우선 소켓은 인터넷 상에서 데이터를 주고 받는 엔드포인트 역할임과 동시에, 이를 위해 인터넷 상에 존재하는 각 PORT를 유니크하게 식별하기 위한 주소이기도 하다. 

 

TCP 소켓 식별

  • 프로세스는 소켓을 열어서 통신한다고 했지?
  • 그럼 당연히, 그 소켓은 프로세스까지 닿을 수 있도록 IP 주소 뿐 아니라 포트 번호까지 담고 있어야 한다.
  • 그래서 초기에는, 소켓 = <IP 주소 + 포트 넘버> 였다. (소켓을 식별할 때 주소를 통해 식별함. 이름과 같은 것)

즉, 설계 목적부터 알 수 있듯이 각각의 socket은 인터넷 상에서 유니크하다. (뒤에서 보겠지만 사실은 유니크하게 식별되지 않을 수 있다.)

 

원래는 TCP 프로토콜에서 주로 소켓이 쓰였는데, 소켓이라는 개념이 없던 UDP에서도 소켓 개념을 가져와 쓰기 시작했고, UDP도 주류로 올라오게 되면서 프로토콜 구분도 필요하게 되었다. 

 

그래서 Socket = <프로토콜 + IP 주소 + 포트 넘버> 가 되었다. 

  • 따라서 개념상 이 세 개 요소의 조합이 유니크하면 된다. 

만약, 하나의 프로세스에서 두 개의 소켓을 열었다고 해보자. 그럼 IP와 포트 넘버가 같을 수 있다. 

그러나 만약 각각의 프로토콜을 다르게 했다면, 3개를 합친 조합은 유니크하기 때문에 소켓의 조건에 어긋나지 않는다. 

 

 

 

TCP 소켓의 작동 과정

소켓을 통한 연결(Connection)

Connection이라는 건 socket과 socket을 연결하는 것을 말한다. (src socket -> dest socket)

  • 따라서, 하나의 연결 정보를 다음과 같이 표현할 수 있다. 
  • <src IP 주소, src 포트, dest IP 주소, dest 포트>

 

 

 

위에서 소켓 식별 방법을 <프로토콜 + IP 주소 + 포트 넘버> 로 정의했는데, 

실제로는 상황에 따라 이 연결 정보도 소켓 식별의 역할을 한다. 

  • 만일 서버 소켓이라면, 하나의 소켓에 여러 클라이언트 소켓들이 연결될 수도 있다. 
  • 이때 식별을 연결 정보 = <src ip 주소, src 포트, dest ip 주소, dest 포트> 로 한다. 

 

정리하자면, TCP 소켓의 동작 과정은 다음과 같다.

  1. 연결 전: 서버측 listening 소켓에 연결 요청 (3-way handshake)
    • 이미 연결되었는지 확인. 연결되었다면 3번으로
  2. 연결 성공: 서버에서는 새로운 소켓을 만들고, 클라이언트는 새롭게 만들어진 소켓과 데이터를 주고받는다.
    • 이때, 새로운 소켓과 기존 listening 소켓은 IP와 포트 넘버가 동일할 것이다.
  3. 연결 이후 통신:
    1. 때문에 소켓 주소가 아니라 연결 정보로 데이터 경로를 식별한다.
      (클라이언트의 요청 헤더 정보에는 src와 dest의 ip와 port가 포함되어 있다.)

 

 

 

 

 

클라이언트 쪽에서도 같은 IP와 포트 넘버를 갖는 여러개의 소켓이 가능하다. 이 경우는 OS 레벨에서 알아서 포트 번호를 바인딩하는 경우에 발생 가능하다.

OS는 포트 넘버를 할당할 때 아무도 쓰고있지 않은 포트 넘버를 할당하는데, 이미 소켓이 엄청 많아서 빈 포트 넘버가 없을 수 있다. 그러면 어쩔수없이 중복된 걸 할당하도록 설계되어 있다. 

 

이때 기존 소켓과 주소가 동일한 해당 소켓으로 기존 소켓이 연결되어있던 서버에 연결을 요청하면 어떻게 될까?

TCP 스펙상 <src ip, port, dest ip, port>는 유니크해야 하므로, 이 경우에는 연결할 수 없다. 

이럴 때는 또 다른 서버를 열어서 또 다른 listening 소켓을 열어줘야 한다. 

 

 

소켓: 프로그래밍 측면에서의 이해

개발자 입장에서 소켓이란? (소켓 인터페이스)

소켓이라는 대상을 개발자의 관점에서 다시 한번 해석해보자. '프로세스는 소켓을 열어서, 소켓을 통해 네트워크 통신을 한다..' 라는 말이 무엇을 의미할까? 

 

TCP/IP 4계층을 더 뭉뚱그려 나누면 애플리케이션 영역, Kernel 영역, H/W 영역으로 나눌 수 있는데, 우리 개발자 입장에서 보면 이를 또 다시 1) 네트워크 기능을 활용하는 애플리케이션 레벨2) 이를 지원하는 시스템 레벨(커널 & 하드웨어)로 나눌 수 있다. 

 

개발자가 복잡한 네트워크 인프라의 내부 사정을 다 파악하고 컨트롤해야 한다고 생각하면 너무 비효율적이지 않나? 애플리케이션 개발자는 서비스 개발에 집중하는게 효율적이다. 따라서, 시스템은 애플리케이션이 네트워크 기능을 사용할 수 있도록 프로그래밍 인터페이스를 제공한다. 이 프로그래밍 인터페이스를 소켓이라고 한다. 

 

때문에, 반드시 소켓을 사용해야만 네트워크 기능 이용 가능하다. 이를 Socket programming이라고 부른다. 소켓 프로그래밍을 통해 개발자들은 네트워크 상의 다른 프로세스와 데이터를 주고받는 식의 무언가를 개발할 수 있다. 

 

Q. 난 지금까지 HTTP 개발했는데, 소켓 프로그래밍인지 뭔지 한 적 없는데?

ㅇㅇ. 보통 개발자가 socket을 직접 조작해서 통신 기능을 구현할 일은 적다. 

왜냐하면 socket 프로그래밍은 우리가 사용하는 라이브러리나 모듈로 추상화되어있기 때문이다. 우리는 일반적으로 application layer에서도 최상단(OSI로 치면 L7)에서 작업한다. (HTTP도 여기에 속한 프로토콜이다.) application layer의 프로토콜은 보통 라이브러리나 모듈 형태로 하위 layer의 기능을 제공한다. 

 

만일 라이브러리나 모듈의 내부의 소스코드를 열어보면 socket을 활용했다는 걸 볼 수 있을 것이다.

 

소켓은 file이다⭐️

 

'이게 대체 무슨 소리야???' 싶지만..  

 

리눅스 등 Unix 계열, 윈도우는 Socket 형태로 네트워크 기능을 제공한다. 

컴퓨터 입장에선 네트워크도 입출력 장치나 마찬가지이다. 따라서 소켓은 곧 file과 같다.

개발자는 파일 디스크립터(file descriptor)를 통해 파일을 다루는 것과 똑같은 원리로 소켓을 다루게 된다

 

 

파일이 뭘까? 

초등학생도 바보같다고 생각할 질문이지만, 생각보다 파일은 더 우아한 존재였다.

Unix 계열 OS는 '모든 것은 파일이다 (Everything is a file)'라는 철학을 가지고 있다.

아니, 우리가 평소에 더블클릭해서 실행하는 '파일'이라는게 그렇게 대단한 존재라니?

 

입출력(I/O)은 근본적으로 메인 메모리 <-> 입출력장치(디스크, 터미널, 네트워크) 간에 데이터를 복하는 작업이다. 

  • 입력 연산은 입출력장치의 데이터를 메인 메모리로 복사하는 것이고, 
  • 출력 연산은 메인 메모리의 데이터를 입출력장치로 복사하여 내보내는 것이다. 

 

Unix 시스템은 입출력(I/O) 작업을 파일로 추상화하여 컨트롤하고 관리할 수 있도록 지원한다. 이를 파일 시스템(File System)이라고 한다. 즉, 파일 시스템은 입출력 작업의 세부적인 과정을 숨기고, 사용자에게는 직관적이고 간단한 인터페이스를 제공하는 시스템이다. 

 

파일 시스템은 컴퓨터에서 데이터를 저장하고 관리하는 책장과 같다. 

우리가 문서나 정보를 파일에 정리하고 책장에 꽂아 두는 행위, 그리고 편하게 책장에서 꺼내보는 행위를 떠올리면 된다. 

 

예를 들어, 특정 .exe 파일을 더블클릭하여 프로그램을 실행하는 상황을 생각해보자.

이 과정에서 발생하는 여러 단계는 실제로는 매우 복잡하다:

  1. 하드디스크에서 프로그램 로딩: 프로그램의 실행 파일은 먼저 하드디스크에서 찾아진다.
  2. 가상 메모리 할당: 프로그램에 필요한 메모리 공간이 할당된다.
  3. 메인 메모리로의 적재: 필요한 프로그램 데이터가 메인 메모리에 적재된다.
  4. 프로세스화: 프로그램이 프로세스로 변환되어 CPU가 이를 실행할 수 있도록 한다.
  5. CPU의 명령어 실행: CPU는 코드 세그먼트의 명령어를 읽고 실행한다.

 

그러나 파일 시스템 덕분에 우리는 이 모든 복잡한 과정을 단순한 '더블 클릭' 동작으로 수행할 수 있는 것이다.

위 예시는 개발자 뿐만 아니라 컴퓨터를 사용하는 누구나 자주 사용하므로 쉽게 와닿는다. 

 

네트워크에서도 마찬가지의 법칙이 적용된다. 

예를 들어, 네트워크를 통해 데이터를 수신하는 과정은 다음과 같다:

  1. 데이터 수신: 네트워크 케이블을 통해 들어온 데이터 프레임은 먼저 네트워크 인터페이스에 도착한다.
  2. 데이터 처리: 이 프레임은 다양한 네트워크 계층을 거쳐 적절하게 처리되고, 필요한 데이터는 추출된다.
  3. 메모리에 저장: 추출된 데이터는 메인 메모리에 저장된다.
  4. 애플리케이션에서 사용: 최종적으로, 이 데이터는 애플리케이션 수준에서 사용될 준비가 되며, 애플리케이션에서 활용된다. 

이 모든 과정은 소켓 인터페이스를 통해 간단히 수행된다.

위에서 언급했지만, 프로그래밍 측면에서 '소켓'은 시스템 레벨에서 제공하는 네트워크를 위한 인터페이스이다. (= 소켓 인터페이스)

 

OS의 관점에서 보면?? 소켓은 네트워크에 의한 입출력을 조작하는 파일에 불과한 것이다. 때문에 일반 파일에 대한 개념이 대부분 적용된다. (파일 생성, 열기, 닫기, 삭제, write, read, execute)

 

아마 네트워크 프로그래밍을 하는 개발자가 아니라면 네트워크를 위한 파일 시스템을 굳이 열고 쓸 일이 없을 것이기 때문에, 이 개념이 바로 직관적으로 와닿지는 않는다. 보통 더 상위 레벨의 프로세스(서비스)가 동작하는 과정에 네트워크 기능은 숨겨져있다.

 

 

 

  • 소켓은 OS 커널에 구현되어있는 프로토콜 요소에 대한 추상화된 인터페이스
  • 장치 파일의 일종으로 이해할 수 있음
  • 일반 파일에 대한 개념이 대부분 적용된다.

 

 

파일 디스크립터 (File Descriptor) 간략 정리

파일 디스크립터(식별자)는 Unix 및 Unix 계열 OS에서 파일, 소켓, 또는 기타 I/O 리소스를 참조하기 위해 사용되는 정수 값이다.

  1. 파일 디스크립터는 열린 파일이나 소켓과 같은 입출력 리소스를 식별하는 데 사용된다. 각 파일 또는 소켓은 고유한 파일 디스크립터를 가진다.
  2. 파일 디스크립터는 정수로 표현된다.
    • 일반적으로, 0은 표준 입력(stdin), 1은 표준 출력(stdout), 2는 표준 에러(stderr)를 나타낸다. 
    • 파일 디스크립터 0: 표준 입력 (Standard Input, stdin)
      • 이 디스크립터는 프로세스의 표준 입력 스트림을 나타낸다.
      • 일반적으로 키보드 입력을 받는 데 사용된다.
      • C 프로그래밍에서 scanf, getchar, fgets 같은 입력 함수들이 이 스트림을 사용한다.
    • 파일 디스크립터 1: 표준 출력 (Standard Output, stdout)
      • 이 디스크립터는 프로세스의 표준 출력 스트림을 나타낸다.
      • 일반적으로 모니터 화면에 출력하는 데 사용된다. 
      • C 프로그래밍에서 printf, putchar, fputs 같은 출력 함수들이 이 스트림을 사용한다. 
    • 파일 디스크립터 2: 표준 에러 (Standard Error, stderr)
      • 이 디스크립터는 프로세스의 표준 에러 스트림을 나타낸다. 
      • 에러 메시지나 진단 출력을 화면에 출력하는 데 사용된다. 
      • 표준 출력과 분리되어 있기 때문에, 에러 메시지를 로그 파일이나 다른 대상으로 리디렉션 할 수 있다. 
      • C 프로그래밍에서 fprintf(stderr, ...) 함수가 이 스트림을 사용한다. 
    • 파일 디스크립터 3: 유닉스 및 유닉스 계열 시스템에서 유저가 열거나 생성한 파일이나 소켓에 대해 시스템이 할당하는 첫 번째 파일 디스크립터이다. 파일 디스크립터 3부터는 유저가 프로그램 내에서 열거나 생성한 파일, 소켓, 파이프 등에 할당된다. 

 

파일 디스크립터는 Unix 계열 시스템에서 I/O 리소스를 효율적으로 관리하는 데 중요한 역할을 한다. 파일 디스크립터를 사용하여 파일이나 소켓을 열고, 데이터를 읽고 쓰며, 리소스를 닫는 일련의 과정을 수행한다.

 

이를 통해 프로세스는 시스템 리소스에 접근하고 제어할 수 있다.

 

 

파일 디스크립터 사용하기

  1. 파일 열기: 파일을 열 때, open 시스템 호출을 사용하면 해당 파일에 대한 파일 디스크립터가 반환된다.
    • ex) fd = open("example.txt", O_RDONLY);는 읽기 전용 모드로 파일을 열고, 파일 디스크립터 fd를 반환한다.
  2. 파일 읽기 및 쓰기: readwrite 시스템 호출을 사용하여 파일 디스크립터를 통해 데이터를 읽고 쓸 수 있다.
    • ex) read(fd, buffer, size);, write(fd, buffer, size);
  3. 소켓 통신: 소켓을 생성하면 소켓에 대한 파일 디스크립터가 반환된다.
    • 이 디스크립터를 사용하여 send, recv와 같은 소켓 관련 함수로 데이터를 송수신할 수 있다.
  4. 파일 닫기: 파일이나 소켓의 작업이 완료되면, close 시스템 호출을 사용하여 파일 디스크립터를 닫는다.
    • ex) close(fd);

 

파일 디스크립터와 리소스 관리

  • 리소스 관리: 파일 디스크립터를 적절히 관리하는 것은 중요하다. 열린 파일 또는 소켓을 닫지 않으면 리소스 누수가 발생할 수 있다.
  • 동시성: 동시에 여러 파일을 다룰 때 각 파일은 고유한 파일 디스크립터를 가지므로, 병렬 처리가 용이하다.

 

 

 


 

<쉬운코드> [1부] 프로토콜 표준 스펙에서 정의한 Socket(소켓), Port(포트), TCP connection(연결) 개념

<쉬운코드> [2부] 프로토콜 표준과는 다르게 실제로는 소켓(Socket)이 어떻게 식별되는가?

<널널한 개발자 TV> 소켓 01 00 소켓의 본질에 대한 이해