본문 바로가기

Programming/프로그래밍 내용 정리

[네크워크] TCP와 UDP의 차이점이 뭔가요? 소켓 프로그래밍이 뭔가요?

 

 

  TCP UDP
연결방식 연결기반
- 연결 후 통신
- 1:1 통신방식
비연결기반
- 연결 없이 통신
- 1:1, 1:n, n:n 통신방식
특징 데이터의 경계를 구분하지 않는다.(byte-stream)

신뢰성 있는 데이터 전송
- 데이터의 전송 순서가 보장됨
- 데이터의 수신여부를 확인
- 패킷을 관리할 필요가 없음

UDP보다 전송속도가 느림

서버소켓은 연결만을 담당

데이터의 경계를 구분함(datagram)

신뢰성이 낮은 데이터 전송
- 데이터의 전송순서가 바뀔 수 있음
- 데이터의 수신여부를 확인안함
- 데이터가 손실되어도 알 수 없음
- 패킷을 관리해주어야함

TCP보다 전송속도가 빠름

소켓 대신 IP를 기반으로 데이터를 전송

UDP는 연결자체가 없어서 서버소켓과 클라이언트 소켓의 구분이 없음
관련
클래스
Socket
ServerSocket
DatagramSocket
DatagramPacket
MulticastSocket

 


❓ 패킷(Packet)이란?


인터넷 내에서 데이터를 보내기 위한 경로배정(라우팅)을 효율적으로 하기 위해서
데이터를 여러 개의 조각들로 나누어 전송을 하는데 이러한 조각을 패킷이라고 한다.


🎈소켓 프로그래밍

소켓 프로그래밍은 소켓을 이용한 통신 프로그래밍을 뜻한다.

"소켓(Socket)"은 사전적으로 "구멍", "연결", "콘센트" 등의 의미를 가진다.
전기를 필요로하는 부품들이 전기를 공급받을 수 있도록
전기 공급 인프라 환경에 연결할 수 있게 만들어진 연결부가 "소켓(Socket)"이다.

네트워크 프로그래밍에서의 소켓(Socket)에 대한 의미도 사전적 의미를 크게 벗어나지 않는다.
프로그램이 네트워크에서 데이터를 송수신할 수 있도록
"네트워크 환경에 연결할 수 있게 만들어진 연결부"가 바로 "네트워크 소켓(Socket)"이다.


전기 소켓이 전기를 공급받기 위해 정해진 규격(110V, 220V 등)에 맞게 만들어져야 하듯이
네트워크에 연결하기 위한 소켓 또한 정해진 규약인 프로토콜(Protocol)에 맞게 만들어져야 한다.

보통 OSI 7 Layer(Open System Interconnection 7 Layer)의 네 번째 계층
TCP(Transport Control Protocol) 상에서 동작하는 소켓을 주로 사용하는데
이를 "TCP 소켓" 또는 "TCP/IP 소켓"이라고 부른다.


🎈클라이언트 소켓(Client Socket)과 서버 소켓(Server Socket)

 


클라이언트 소켓(Client Socket)
처음 소켓(Socket)을 [1]생성(create)한 다음,
서버 측에 [2]연결(connect)을 요청한다.
그리고 서버 소켓에서 연결이 받아들여지면 데이터를 [3]송수신(send/recv)하고,
모든 처리가 완료되면 소켓(Socket)을 [4]닫는다(close).


서버 소켓(Server Socket) 또한
첫 번째 단계는 소켓(Socket)을 [1]생성(create)한다.
그 다음 서버가 사용할 IP 주소와 포트 번호를 생성한 소켓에 [2]결합(bind)시킨다.
그 후 클라이언트로부터 연결 요청이 수신되는지 [3]주시(listen)하고,
요청이 수신되면 요청을 [4]받아들여(accept) 데이터 통신을 위한 소켓을 생성한다.
일단 새로운 소켓을 통해 연결이 수립(ESTABLISHED)되면, 클라이언트와 마찬가지로 데이터를 [5]송수신(send/recv)할 수 있다.
마지막으로 데이터 송수신이 완료되면 소켓(Socket)을 [6]닫는다(close).


[ TCP Flow ]


[ UDP Flow ]

 

 


🎈클라이언트 소켓(Client Socket) 흐름

 

1. 클라이언트 소켓 생성 (socket())


소켓 통신을 위해 가장 먼저 해야 할 일은 소켓을 생성하는 것이다.
이 때 소켓의 종류를 지정할 수 있는데
TCP 소켓을 위해서는 스트림(Stream) 타입,
UDP 소켓을 위해서는 데이터그램(Datagram) 타입을 지정할 수 있다.

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

2. 연결 요청 (connect())


connect() API는 "IP주소"와 "포트 번호"로 식별되는 대상으로 연결 요청을 보낸다.
connect() API는 연결 요청에 대한 결과(성공, 거절, 시간 초과 등)가 결정되기 전에는 실행이 끝나지 않는다.
connect() API 호출이 성공하면, send() / recv API를 통해 데이터를 주고 받을 수 있다.

3. 데이터 송수신 (send() / recv())

연결된 소켓을 통해 데이터를 보낼 때는 send(), 데이터를 받을 때는 recv API를 사용한다.
send()와 recv() 두 API 모두 실행 결과(성공, 실패, 종료)가 결정되기 전까지는 API가 리턴되지 않는다.
특히 recv()는 데이터가 수신되거나 에러가 발생하기 전에는 실행이 종료되지 않는다.

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

그래서 데이터 수신을 위한 recv() API는 별도의 스레드에서 실행한다.
소켓의 생성과 연결이 완료된 후,
새로운 스레드를 하나 만든 다음 그곳에서 recv()를 실행하고 데이터가 수신되길 기다린다.

4. 소켓 닫기 (close())

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

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


🎈서버 소켓(Server Socket) 흐름

1. 서버 소켓 생성 (socket())

클라이언트 소켓과 마찬가지로, 서버 소켓을 사용하려면 최초에 소켓을 생성해야 한다.

2. 서버 소켓 바인딩 (bind())

"bind"는 "결합하다", "구속하다", "묶다" 등의 사전적 의미를 가지고 있다.
bind() API에 사용되는 인자는 소켓과 포트 번호(또는 IP 주소+포트 번호)이다.

"소켓(Socket)과 포트 번호를 결합(bind)"할 때,
"결합(bind)"이라는 동작이 구체적으로 무엇을 의미할까?

보통 시스템에는 많은 수의 프로세스가 동작한다.
만약 프로세스가 TCP 또는 UDP 프로토콜을 사용한다면
그 표준에 따라 각 소켓은 시스템이 관리하는 포트(0~65535) 중 하나의 포트 번호를 사용하게 된다.

만약 소켓이 사용하는 포트 번호가 다른 소켓의 포트 번호와 중복되어
모든 소켓이 777 이라는 동일한 포트 번호를 사용하게 된다면
네트워크를 통해 777 포트로 데이터가 수신될 때 어떤 소켓이 처리해야 하는지 결정할 수 없는 문제가 발생한다.

이런 이유로 운영체제에서는 소켓들이 중복된 포트 번호를 사용하지 않도록
내부적으로 포트 번호와 소켓 연결 정보를 관리한다.

그리고 bind() API는 해당 소켓이 지정된 포트 번호를 사용할 것이라는 것을 운영체제에 요청하는 API인 것이다.
만약 지정된 포트 번호를 다른 소켓이 사용하고 있다면, bind() API는 에러를 리턴한다.

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

3. 클라이언트 연결 요청 대기 (listen())

서버 소켓에 포트 번호(또는 IP 주소+포트 번호)를 결합(bind)하고 나면
서버 소켓을 통해 클라이언트의 연결 요청을 받아들일 준비가 된다.
listen API는 클라이언트에 의한 연결 요청이 수신될 때까지 기다리는 역할을 한다.

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

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

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

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

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

4. 클라이언트 연결 수립 (accept())

listen() API가 클라이언트의 연결 요청을 확인하고 문제없이 리턴한다고 해서
클라이언트와의 연결 과정이 모두 완료되는 것은 아니다.
최종적으로 연결 요청을 받아들이는 역할을 수행하는 것은 accept() API이다.

accept() API는 연결 요청을 받아들여(accept) 소켓 간 연결을 수립한다.
주의할 점은 최종적으로 데이터 통신을 위해 연결되는 소켓이
앞서 bind() 또는 listen() API에서 사용한 서버 소켓(Server Socket)이 아니라는 것이다.

최종적으로 클라이언트 소켓과 연결(Connection)이 만들어지는 소켓은
앞서 사용한 서버 소켓(Server Socket)이 아니라,
accept API 내부에서 새로 만들어지는 소켓(Socket)이다.

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

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

실질적인 데이터 송수신은 accept API에서 생성된
연결(Connection)이 수립(Established)된 소켓(Socket)을 통해 처리된다.

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

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

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

소켓을 닫기 위해서는 close() API를 호출한다.
단, 서버 소켓에서는 close()의 대상이 하나만 있는 것이 아니다.

최초 socket() API를 통해 생성한 서버 소켓과
accpet() API 호출에 의해 생성된 소켓도 같이 닫아야 한다.