Socket 통신의 기본 - TCP / UDP

강정우·2024년 3월 25일
0

네트워크

목록 보기
32/32

소켓통신

소켓(SOCKET)이란?

소켓(Socket)은 프로세스가 드넓은 네트워크 세계로 데이터를 내보내거나 혹은 그 세계로부터 데이터를 받기 위한 실제적인 "창구" 역할을 한다.

그러므로 프로세스가 데이터를 보내거나 받기 위해서는 반드시 소켓을 열어서 소켓에 데이터를 써보내거나 소켓으로부터 데이터를 읽어들여야 한다.

즉,

  1. 소켓은 네트워크 상에서 통신을 위한 엔드포인트를 추상화한 것이다.
  2. IP 주소와 포트 번호의 조합으로 정의된다.
  3. 소켓을 통해 데이터를 송수신할 때, 그 데이터는 패킷의 형태로 네트워크를 통해 이동하지만, 소켓 자체는 데이터의 실제 내용이 아니라 통신의 채널 또는 인터페이스를 의미한다.
  4. 소켓은 특정 IP 주소와 포트 번호에 바인딩되며, TCP나 UDP 프로토콜을 사용하여 데이터를 전송한다.

소켓은 프로토콜, IP 주소, 포트 번호로 정의된다.

프로토콜

소켓 통신은 전송 제어 프로토콜(TCP) 또는 사용자 데이터그램 프로토콜(UDP) 같은 특정 프로토콜을 사용한다. TCP는 연결 지향적이고 신뢰성 있는 데이터 전송을 제공하는 반면, UDP는 연결을 맺지 않고 데이터를 전송하는 비연결성 프로토콜이다.

IP

네트워크 상의 각 장치는 고유한 IP 주소를 가지고 있어, 데이터가 올바른 목적지로 전송될 수 있도록 한다.

포트

하나의 호스트 내에서 실행 중인 여러 애플리케이션을 구분하기 위해 사용된다. 포트 번호는 해당 호스트의 구체적인 애플리케이션 또는 프로세스에 데이터를 전달하는 데 사용된다.

다시말해 소켓이은 떨어져 있는 두 호스트를 연결해주는 도구로써 인터페이스의 역할을 하는데 데이터를 주고 받을 수 있는 구조체로 소켓을 통해 데이터 통로가 만들어 진다. 이러한 소켓은 역할에 따라 서버 소켓, 클라이언트 소켓으로 구분된다.

소켓통신의 흐름, 데이터 전송과정

🖥 서버 (Server)

클라이언트 소켓의 연결 요청을 대기하고, 연결 요청이 오면 클라이언트 소켓을 생성하여 통신이 가능하게 한다.

1) socket() 함수를 이용하여 소켓을 생성

2) bind() 함수로 ip와 port 번호를 설정하게 됨

소켓(Socket)에 IP 주소와 포트 번호를 결합하는 함수가 bind() API 이다.

일반적으로 서버 소켓은 고정된 포트 번호를 사용한다. 그리고 그 포트 번호로 클라이언트의 연결 요청을 받아들인다. 그래서 운영체제가 특정 포트 번호를 서버 소켓이 사용하도록 만들기 위해 소켓과 포트 번호를 결합(bind)해야 하는데, 이 때 사용하는 API가 바로 bind() 이다.

이때 만약 지정된 포트 번호를 다른 소켓이 사용하고 있다면, bind() API는 에러를 리턴한다.

3) listen() 함수로 클라이언트의 접근 요청에 수신 대기열을 만들어 몇 개의 클라이언트를 대기 시킬지 결정

서버 소켓(Server Socket)에 포트 번호(또는 IP 주소+포트 번호)를 결합(bind)하고 나면, 서버 소켓(Server Socket)을 통해 클라이언트의 연결 요청을 받아들일 준비가 되었다.

이제 할 일은, 클라이언트에 의한 연결 요청이 수신될 때까지 기다리는 것인데, listen() API 가 그 역할을 수행한다.

listen() API는 서버 소켓(Server Socket)에 바인딩된 포트 번호로 클라이언트의 연결 요청이 있는지 확인하며 대기 상태에 머무른다. 클라이언트에서 호출된 connect() API에 의해 연결 요청이 수신되는지 listen하고 있다가, 요청이 수신되면, 그 때 대기 상태를 종료하고 리턴한다.

listen() API가 대기 상태에서 빠져나오는 경우는 크게 두 가지이다.
1. 클라이언트 요청이 수신되는 경우
2. 에러가 발생(소켓 close() 포함)하는 경우

그런데 listen() API가 성공한 경우라도 리턴 값에 클라이언트의 요청에 대한 정보는 들어 있지 않는다. listen()의 리턴 값으로 판단할 수 있는 것은 클라이언트 연결 요청이 수신되었는지(SUCCESS), 그렇지 않고 에러가 발생했는지(FAIL) 뿐이다.

대신 클라이언트 연결 요청에 대한 정보는 시스템 내부적으로 관리되는 큐(Queue)에서 쌓이게 되는데, 이 시점에서 클라이언트와의 연결은 아직 완전히 연결되지 않은(not ESTABLISHED state) 대기 상태이다.

대기 중인 연결 요청을 큐(Queue)로부터 꺼내와서, 연결을 완료하기 위해서는 accept() API를 호출해야 한다.

4) accept() 함수를 사용하여 클라이언트와의 연결을 기다림

아직 실질적인 소켓 연결(Connection)을 수립하는 절차가 남아 있다. 결구 최종적으로 연결 요청을 받아들이는 역할을 수행하는 것은 accept() API 이다.

accept() API는 그 사전적 의미만큼 직관적인 역할을 수행한다. 연결 요청을 받아들여(accept) 소켓 간 연결을 수립하는 것이다.

그런데 주의할 점은 최종적으로 데이터 통신을 위해 연결되는 소켓이, 앞서 bind() 또는 listen() API에서 사용한 서버 소켓(Server Socket)이 아니다.

앞에서 언급했듯, 서버 소켓(Server Socket)의 핵심 역할은 클라이언트의 연결 요청을 수신하는 것고 이를 위해 bind()listen()을 통해 소켓에 포트 번호를 바인딩하고 요청 대기 큐를 생성하여 클라이언트의 요청을 대기하였다.
그리고 accept() API에서, 데이터 송수신을 위한 새로운 소켓(Socket)을 만들고 서버 소켓의 대기 큐에 쌓여있는 첫 번째 연결 요청을 매핑시킨다.

여기까지, 하나의 연결 요청을 처리하기 위한 서버 소켓(Server Socket)의 역할은 끝났다.
서버 소켓의 입장에서 남은 일은, 또 다른 연결 요청을 처리하기 위해 다시 대기(listen)하거나, 서버 소켓(Socket)을 닫는(close) 것 뿐이ek.

5) send()/recv() 함수로 데이터 송수신

그래서 위 accept() API 로 생성된 연결(Connection)이 수립(Established)된 소켓을 통해 실질적인 데이터 송수신이 이뤄진다.

6) close() 소켓 연결 종료.

서버 소켓에서는 close()의 대상이 하나만 있는 것이 아니라는 것에 주의해야 한다.
최초 socket() API를 통해 생성한 서버 소켓에 더해, accpet() API 호출에 의해 생성된 소켓도 관리해야한다는 의미이다.


🙋‍♂️ 클라이언트 (Client)

1) socket() 함수로 가장먼저 소켓을 연다.

이 때 여기서 소켓의 종류를 지정할 수 있는데, TCP 소켓을 위해서는 스트림(Stream) 타입, UDP 소켓을 위해서는 데이터그램(Datagram) 타입을 지정할 수 있다.

최초 소켓이 만들어지는 시점에는 어떠한 "연결 대상"에 대한 정보도 들어 있지 않다. 껍데기 뿐인 소켓이 하나 만들어진 것 뿐인데, 연결 대상(IP:PORT)을 지정하고 연결 요청을 전달하기 위해서는, 여기서 생성한 소켓을 사용하여 connect() API를 호출해야 한다.

2) connect() 함수를 이용하여 통신 할 서버의 설정된 ip와 port 번호에 통신을 시도

connect() API는 "IP주소"와 "포트 번호"로 식별되는 대상(Target)으로 연결 요청을 보낸다.

connect() API는 블럭(Block) 방식으로 동작한다. 즉, 연결 요청에 대한 결과(성공, 거절, 시간 초과 등)가 결정되기 전에는 connect()의 실행이 끝나지 않는다는 것이다. 그러므로 connect() API가 실행되지마자 실행 결과와 관계없이 무조건 바로 리턴될 것이라 가정하면 안 된다.

3) 통신을 시도 시, 서버가 accept() 함수를 이용하여 클라이언트의 socket descriptor를 반환

4) 이제 send() / recv API를 통해 데이터를 주고 받을 수 있다.

연결된 소켓을 통해 데이터를 보낼 때는 send(), 데이터를 받을 때는 recv() API를 사용한다.

언뜻보면 API를 호출하는 방법이 매우 단순할 것 같지만, 한 가지 중요한 사실이 있는데, send()recv() API 역시 모두 블럭(Block) 방식으로 동작한다는 것이다. 즉, 두 API 모두 실행 결과(성공, 실패, 종료)가 결정되기 전까지는 API가 리턴되지 않는다.

특히 recv()는 데이터가 수신되거나, 에러가 발생하기 전에는 실행이 종료되지 않기 때문에, 데이터 수신 작업을 생각만큼 단순하게 처리하기 쉽지 않다.

왜냐하면 send()의 경우 데이터를 보내는 주체가 자기 자신이기 때문에, 얼마만큼의 데이터를 보낼 것인지, 언제 보낼 것인지를 알 수 있는데 데이터를 수신하는 경우엔 통신 대상이 언제, 어떤 데이터를 보낼 것인지를 특정할 수 없기 때문에 recv() API가 한번 실행되면 언제 끝날지 모르는 상태가 되기 때문이다.

이게 Block 방식으로 동작한는 뜻인데 이를 극복자고자 데이터 수신을 위한 recv() API는 별도의 스레드에서 실행한다.
소켓의 생성과 연결이 완료된 후, 새로운 스레드를 하나 만든 다음 그곳에서 recv()를 실행하고 데이터가 수신되길 기다리는 것이다.

5) 더 이상 데이터 송수신이 필요없게되면, 소켓을 닫기 위해 close() API를 호출

close()에 의해 닫힌 소켓은 더 이상 유효한 소켓이 아니기 때문에, 해당 소켓을 사용하여 데이터를 송수신할 수 없다.

그리고 만약 소켓 연결이 종료된 후 또 다시 데이터를 주고 받고자 한다면, 또 한번의 소켓 생성(socket())과 연결(connect()) 과정을 통해, 소켓이 데이터를 송수신할 수 있는 상태가 되어야 합니다.


소켓종류

1. 스트림 (TCP) 소켓

  • 양방향으로 바이트 스트림을 전송, 연결 지향성
  • 오류 수정, 정송처리, 흐름제어 보장
  • 송신된 순서에 따라 중복되지 않게 데이터를 수신 → 오버헤드가 발생
  • 소량의 데이터보다 대량의 데이터 전송에 적합 → TCP를 사용

TCP가 연결 지향 방식이라는 것은 패킷을 전송하기 위한 논리적 경로를 배정한다는 뜻이다.

그리고 3-way handshaking과정은 목적지와 수신지를 확실히 하여 정확한 전송을 보장하기 위해서 세션을 수립하는 과정을 의미한다. 이로하여 바로 TCP는 연결형 서비스로 신뢰성을 보장한다. 대신 이러한 과정을 거쳐야하기 때문에 UDP보다 속도가 느리다.

  • 참고로 이러한 기능은 CPU를 사용하기 때문에 속도에 영향을 주는 것이다.

그렇기에 TCP는 연속성보다 신뢰성있는 전송이 중요할 때에 사용하는 프로토콜로 예를 들면 파일 전송과 같은 경우에 사용된다.

TCP 서버의 특징

  1. 서버소켓(Server Socket)은 연결만을 담당한다.
  2. 연결과정에서 반환된 클라이언트 소켓은 데이터의 송수신에 사용된다
  3. 서버와 클라이언트는 1대1로 연결된다.
  4. 스트림 전송으로 전송 데이터의 크기가 무제한이다.
  5. 패킷에 대한 응답을 해야하기 때문에(시간 지연, CPU 소모) 성능이 낮다.
  6. Streaming 서비스에 불리하다.(손실된 경우 재전송 요청을 하므로)

2. 데이터그램 (UDP) 소켓

  • 비연결형소켓
  • 데이터의 크기에 제한이 있음
  • 확실하게 전달이 보장되지 않음, 데이터가 손실돼도 오류가 발생하지 않음
  • 실시간 멀티미디어 정보를 처리하기 위해 주로 사용 ex) 전화

UDP는 비연결형 서비스이기 때문에, 데이터그램 방식을 사용한다.

즉, 연결을 설정하고 해제하는 과정이 존재하지 않는다. 서로 다른 경로로 독립적으로 처리함에도 패킷에 순서를 부여하여 재조립을 하거나 흐름 제어 또는 혼잡 제어와 같은 기능도 처리하지 않기에 TCP보다 속도가 빠르며 네트워크 부하가 적다는 장점이 있지만 신뢰성있는 데이터의 전송을 보장하지는 못한다.

대신 UDP헤더의 CheckSum 필드를 통해 최소한의 오류만 검출한다. 따라서 신뢰성보다는 연속성이 중요한 서비스 예를 들면 실시간 서비스(streaming)에 자주 사용된다.

UDP 서버의 특징

  1. UDP에는 연결 자체가 없어서(connect 함수 불필요) 서버 소켓과 클라이언트 소켓의 구분이 없다.
  2. 소켓 대신 IP를 기반으로 데이터를 전송한다.
  3. 서버와 클라이언트는 1대1, 1대N, N대M 등으로 연결될 수 있다.
  4. 데이터그램(메세지) 단위로 전송되며 그 크기는 65535바이트로, 크기가 초과하면 잘라서 보낸다.
  5. 흐름제어(flow control)가 없어서 패킷이 제대로 전송되었는지, 오류가 없는지 확인할 수 없다.
  6. 파일 전송과 같은 신뢰성이 필요한 서비스보다 성능이 중요시 되는 경우에 사용된다.

reference

https://mangkyu.tistory.com/15
https://recipes4dev.tistory.com/153

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글