들어가기 전에
백엔드 소프트웨어 개발이라는 강좌를 학교에서 듣고 있는데 너무 유익해서 꼼꼼히 정리해보려고 합니다. 이번 포스트에서는 Socket Programming에 대해서 써보겠습니다.
Socket이란?
뭔가를 집어넣기 위해서 자연적 혹은 인위적으로 만든 구멍
어떤 기능을 추가하기 위해서 혹은 제공되는 기능을 쓰기 위해서 만든 구멍
예를 들어서 TCP 혹은 UDP를 쓰고 싶다면 Transport에 있는 socket을 만들면 됩니다.
소켓은 어떤 계층이 되었든 간에 자기가 그 서비스를 쓰고 싶으면 만들 수 있습니다.
Network Socket Library
- network layer를 손쉽게 접근할 수 있게 도와주는 library (ex: 프로토콜 옵션 조정, 헤더 자동 채우기 등)
- 1980년대 Berkeley socket이 근간
- 현재 거의 모든 OS가 Berkeley Socket API를 구현하고 있습니다.(C언어로)
- 거의 모든 최근의 프로그래밍 언어가 이를 직간접적으로 제공합니다.
- 직접적: C 언어로 된 함수들을 거의 1:1로 대응해서 제공합니다.
- 간접적: C 언어로 된 함수들을 감싸거나 framework 형태로 바꿔서 제공합니다.
Berkeley Socket API 패턴
- 각 socket을 위한 자료 구조를 내부(library, kernel)에서 관리합니다.
- API 함수를 호출할 때는 내부에 있는 특정 소켓을 지칭할 수 있어야 합니다.
- 이 때문에 "어떤 소켓"인지를 지칭하는 (opaque: 불투명한) handle을 같이 넘기는 방식입니다.
- 이 handle은 그냥 숫자입니다. 이 숫자는 어떤 숫자인지 모르지만 그냥 갖다 주기만 하면 됩니다.
- handle을 넘기면 커널 내부에서 자료 구조가 존재하고 아래 그림처럼 123이 어떤 것을 가리키는지 관리를 하면 됩니다.
- 이 handle이 어떤 숫자인지 모르지만 다음 함수를 호출할 때 그냥 넘겨주면 내부 자료 구조에서 이 숫자를 참조하는 방식입니다.
Layer 별 Socket Library 지원
- Layer 2(datalink): Raw Socket(PF_PACKET 옵션)
- Layer 3(network): Raw Socket(AF_INET 옵션)
- Layer 4(transport)
- TCP: AF_INET + SOCK_STREAM
- UDP: AF_INET + SOCK_DGRAM
Socket 에서 기대할 수 있는 것
- Protocol Header를 자동으로 채워 줍니다.
- Ethernet frame header
- IP packet header
- TCP segment header
- UDP datagram header
- State 관리 (TCP)
- SEQ/ACK
- Receiver window size
- Send/Receive time-out
- 등등...
- 동작 방식의 제어
- 데이터를 모았다가 보낼지 바로 보낼지 (Nagle's algorithm, TCP_NODELAY)
- 포트 번호 재활용할지 (SO_REUSEADDR)
Client Socket vs Server Socket
- 식당에서 손님들은 들락날락 하지만, 서빙하는 종업원(영어로 "server")은 늘 손님이 찾을 수 있는 위치에 있습니다.
- 네트워크 프로그래밍에서도 server는 늘 찾을 수 있는 위치에 있어야 합니다.
- L3와 L4의 주소 요소는 "IP 주소"와 "포트 번호" 입니다.
- 따라서 server는 고정된 "IP 주소"와 "포트 번호"에 "묶여 있어야" 합니다.
- "묶여 있다"는 영어로 bind입니다.
- 따라서 server socket에게는 bind 과정이 필수입니다.
Q) 그럼 Client Socket은 bind를 할 수 없을까?
A) 음식점에서도 손님이 늘 같은 테이블을 고집하는 경우가 있습니다. 유사하게 client socket도 특정 IP와 port를 고집할 수 있습니다.
Bind 여부는 client/server socket을 구분하지 않습니다.
다만 client에게는 선택이지만, 서버에게는 필수입니다.
UDP Client의 흐름도
- 클라이언트에서 socket을 만듭니다.
- 위에서 설명한 것처럼 opaque handle을 얻어낸 후 그걸 가지고 sendto()와 recvfrom()을 합니다.
UDP Server의 흐름도
- 서버는 자신이 서비스를 한다는 것을 알려야 되니 bind()가 필수입니다. 클라이언트와 bind()를 제외하고는 동일합니다.
- UDP는 매번 내가 누구한테 보낼 건지 누구한테 받을 건지 지정을 해야 해서 보낼 때는 sendto()로 누구에게 보낼지 알려줍니다.
- 받을 때는 recvfrom()으로 누구한테 받았는지 알려줍니다.
TCP Client의 흐름도
- 클라이언트는 UDP와 동일하게 opaque handle을 얻어내지만 가상의 연결을 맺어야 하므로 connect()로 연결을 맺습니다.
- 한 번 연결을 맺을 때 pipe를 뚫었으니 UDP와 달리 매번 그걸 지정할 필요가 없어집니다. 그래서 sendto()가 아닌 send()를 사용합니다.
- 받는 것도 연결을 맺은 상대한테만 받게 되므로 recvfrom()이 아닌 recv()를 사용합니다.
TCP Server의 흐름도
- TCP Server는 연결을 받기 위한 소켓과 특정 상대하고 통신을 위한 소켓 두 개로 구분합니다.
- 연결을 위한 소켓은 Passive Socket, 통신을 위한 소켓은 Active Socket이라고 부릅니다.
- 통상적으로 자기가 어디에 있는지 알려야 하니 opaque hadle을 만들고 bind 하는 것은 UDP와 같습니다.
- 여기서 listen()이 Passive Socket 역할을 합니다. 이 Socket은 연결을 받기만 합니다.
TCP, UDP 차이 정리
- UDP는 소켓 하나로 여러 사람과 통신할 수 있습니다. 이는 등대를 세운 것과 같습니다. 등대에서 빛을 여러 군데 비추듯이 소켓을 하나 만들고 여러 군데 뿌립니다. receive도 마찬가지입니다. 안테나를 하나 세우고 신호를 받는 것과 같습니다.
- TCP는 connect()라는 가상의 연결을 만드는 과정이 들어가고 연결이 되었으니 sendto() 대신 send()를 사용합니다. passive socket으로 연결을 받아들이면 연결이 만들어진 socket을 별도의 socket으로 취급을 해서 이 socket을 가지고 send와 receive를 합니다.
Socket 생성
UDP Socket 생성
int socket(AF_INET, SOCK_DGRAM, 0 또는 IPPROTO_UDP);
- AF_INET: Address Family InterNET의 약자입니다.
- SOCK_DGRAM: Datagram Socket을 의미합니다.
- 0: L4 Datagram 중 프로토콜 타입. UDP만 쓰므로 0 (0 대신 IPPROTO_UDP로 명시적으로 사용 가능)
TCP Socket 생성
int socket(AF_INET, SOCK_STREAM, 0 또는 IPPROTO_TCP)
- AF_INET: Address Family InterNET의 약자입니다.
- SOCK_STREAM: Stream 기반 Socket을 의미합니다.
- 0: L4 Datagram 중 프로토콜 타입. TCP만 쓰므로 0 (0 대신 IPPROTO_TCP로 명시적으로 사용 가능)
TCP의 Active Socket / Passive Socket
- TCP는 연결지향(connection-oriented)이므로 "연결을 맺는" 과정을 거칩니다.
- 연결이 맺어진 상태의 socket을 "active socket"이라고 합니다.
- 클라이언트 측: connect()가 성공한 뒤의 socket
- 서버 측: accept()가 성공한 뒤의 socket
- 연결을 맺기 위해 대기 중인 socket을 "passive socket"이라고 합니다.
- 서버 측: listen() 성공 한 뒤의 socket
TCP Passive Socket으로 전환
int listen(sock, backlog);
- sock: socket()으로 얻어진 소켓 핸들
- backlog
- 연결 완료를 기다리는 대기열의 크기
- 즉, 상대가 연결 요청을 해왔는데 아직 완료되지 않은 것을 몇 개까지나 기억하고 있을지를 결정(그 이상의 연결 요청은 자동으로 거절된 것으로 처리됨)
- ex) 10과 같은 적당한 숫자
Byte Order: Big Endian / Little Endian
예를 들어 2바이트 메모리 공간에 정수 0x1234를 저장한다고 하고 메모리 주소는 X번지와 (X+1) 번지 이렇게 둘을 사용한다고 가정하겠습니다. 이때, 시작 주소 X에 높은 바이트의 수 0x12를 저장할지, 낮은 바이트의 수 0x34를 저장할지 선택할 수 있습니다.
- Big endian: 시작 주소에 높은(big) 바이트 수를 저장합니다.
- Little endian: 시작 주소에 낮은(little) 바이트 수를 저장합니다.
- 2바이트뿐만 아니라 4바이트, 8바이트 등 multi-byte의 경우 모두 해당됩니다.
- CPU마다 메모리에 멀티바이트 정수를 저장하는 방식이 다를 수 있습니다.
- 이는 메모리를 덤프 해서 그대로 다른 호스트에 보내면 문제가 발생함을 의미합니다.
Network Byte Order: Big Endian
- 위에서 2바이트 이상의 정수가 저장된 메모리를 덤프해서 서로 다른 endian 방식을 쓰는 CPU의 호스트에 보내면 문제가 발생한다고 했습니다.
- 이 때문에 네트워크로 전송할 때는 byte order를 big endian으로 고정합니다.
- 유틸리티 함수들
- htons(): host - to - network - short (2 bytes)
- ntohs(): network - to -host - short (2 bytes)
- htonl(): host - to - network - long (4 bytes)
- ntohl(): network - to - host - long (4 bytes)
Loop-back Address 127.0.0.1
- 127.0.0.1은 loopback address라고 불립니다. 보통 local host, 즉 "나 자신의 주소"라는 의미로 쓰입니다.
- 데이터를 보낼 때 127.0.0.1을 주소로 사용하면 "내 실제 IP가 뭔지 모르지만 어쨌든 나 자신에게 보낸다"라는 의미입니다.
- bind()에 사용하면 "127.0.0.1이라는 주소"에 묶이는 것을 의미합니다. 이 때는 나 자신에게 트래픽을 보낸다고 하더라도 실제 IP를 명시하면 안 되고, 반드시 127.0.0.1을 써야 합니다.
- 이런 특성을 이용해서 해당 서버 안에 있는 프로그램들만 접속할 수 있는 내부 프로그램은 일부러 127.0.0.1을 bind 합니다.
No Specific Address: 0.0.0.0
- 0.0.0.0은 "특별히 정한 것 없이 모든 주소"라는 의미입니다.
- AWS의 Security Group Rule에서 source가 0.0.0.0/0 이면 ANY source였습니다.
- bind 할 때 0.0.0.0을 명시하면 "내가 가진 주소 모두 다 bind 한다"라는 의미입니다. 즉, 내가 가진 IP 어떤 걸 명시하더라도 다 받아준다는 의미입니다.
'CS' 카테고리의 다른 글
[Network] TCP Socket Programming - 데이터 전송하기 (1) | 2023.10.22 |
---|---|
[Network] Protobuf, JSON (0) | 2023.10.22 |
[Network] Socket Programming 메모리 관리 (1) | 2023.10.21 |
[Network] UDP Socket 만들어보기 - 데이터 받기 (1) | 2023.10.20 |
[Network] UDP Socket 만들어보기 - 데이터 보내기 (3) | 2023.10.15 |