목표: 소켓을 사용하여 통신하는 클라이언트/서버 애플리케이션을 구축하는 방법을 배우는 것입니다.
소켓(Sockets): 애플리케이션 프로세스와 엔드 투 엔드 전송 프로토콜 사이의 문과 같은 역할을 합니다.

Socket interface

Socket Descriptor: 각 애플리케이션 프로세스는 고유의 소켓 디스크립터를 가지며, 이를 통해 특정 애플리케이션과 소켓이 식별됩니다.
Port Number: TCP나 UDP가 특정 애플리케이션의 소켓으로 데이터를 전달할 수 있도록 각 소켓에 포트 번호가 지정됩니다.
IP Address: 소켓은 IP 주소와 연동되어 네트워크 상에서 위치를 식별할 수 있습니다.

각 소켓에 포함된 5가지 구성 요소
1. 프로토콜: 데이터 전송에 사용할 프로토콜을 지정합니다.
+) 프로토콜 패밀리: IPv4나 IPv6 같은 네트워크 프로토콜 유형을 의미합니다.
2. 출발지 주소 및 포트: 데이터 전송을 시작하는 소켓의 IP 주소와 포트 번호입니다.
3. 목적지 주소 및 포트: 데이터를 수신할 상대방 소켓의 IP 주소와 포트 번호입니다.
TCP 서버/클라이언트 함수 호출 흐름

함수들 이름 및 사용법 정리
socket() 함수는 네트워크 통신을 위해 소켓을 생성하는 함수입니다.
함수 형식: int socket(int domain, int type, int protocol)
domain: 프로토콜 패밀리를 지정합니다. 예를 들어, PF_INET은 IPv4를, PF_INET6은 IPv6를 나타냅니다.
type: 서비스의 유형을 지정합니다. 예를 들어, SOCK_STREAM은 TCP를, SOCK_DGRAM은 UDP를 의미합니다.
protocol: 특정 프로토콜을 지정합니다. 일반적으로 0을 사용해 기본 프로토콜을 선택합니다. (예: IPPROTO_TCP 또는 IPPROTO_UDP)
반환 값:
성공 시, 새로 생성된 소켓의 파일 디스크립터를 반환합니다.
실패 시, -1을 반환합니다.
bind() 함수는 서버 소켓에 특정 IP 주소와 포트를 설정하여 클라이언트가 해당 주소로 요청을 보낼 수 있도록 합니다. 서버 소켓은 클라이언트 요청을 받을 준비가 되어야 하므로, 이 작업이 필수적입니다.
함수 형식: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
sockfd: socket() 함수로 생성한 소켓의 파일 디스크립터입니다. bind()는 이 소켓에 특정 IP 주소와 포트를 할당합니다.
addr: 소켓에 할당할 주소 정보(IP 주소와 포트 번호)를 담고 있는 sockaddr 구조체의 포인터입니다. 이 구조체에 IP 주소와 포트 번호를 설정하여 bind()에 전달합니다. 보통 sockaddr_in 구조체를 사용하여 sin_family에 주소 패밀리, sin_addr에 IP 주소, sin_port에 포트를 지정합니다.
sin_family: 주소 체계를 지정합니다. IPv4 주소를 사용하려면 AF_INET으로 설정합니다.
sin_addr.s_addr: 할당할 IP 주소입니다. 서버가 모든 네트워크 인터페이스에서 클라이언트의 연결을 수신하려면 INADDR_ANY를 사용합니다. 특정 IP로 제한할 수도 있습니다.
sin_port: 네트워크 바이트 순서로 지정된 포트 번호입니다. 일반적으로 htons()를 사용해 호스트 바이트 순서를 네트워크 바이트 순서로 변환하여 설정합니다.
addrlen: addr 구조체의 크기를 바이트 단위로 나타냅니다. 보통 sizeof(struct sockaddr_in)으로 설정합니다
반환 값:
성공 시 0을 반환합니다.
실패 시 -1을 반환합니다.
+) 주요 hton() 함수들
htons(): 16비트(2바이트) 정수를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환합니다.
htonl(): 32비트(4바이트) 정수를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환합니다.
각 함수는 네트워크 통신에서 사용되는 데이터의 바이트 순서를 일관되게 유지하기 위해 사용됩니다.
정리)
이 함수는 서버 소켓을 네트워크에서 식별 가능한 주소에 연결하여, 이후의 listen() 및 accept() 함수 호출이 가능하도록 설정합니다.
TCP 및 UDP 서버 모두에 필수적인 작업입니다.
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
/*
구조체 초기화: memset() 함수를 통해 serv_addr 구조체를 0으로 초기화합니다.
이는 구조체의 모든 필드를 초기화하여 잠재적인 쓰레기 값(garbage values)을 방지합니다.
주소 체계 설정: serv_addr.sin_family = AF_INET;을 통해 IPv4 주소 체계를 사용하도록 지정합니다.
IP 주소 설정: serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);를 통해
서버 소켓이 모든 네트워크 인터페이스에서 들어오는 요청을 수신할 수 있도록 합니다.
INADDR_ANY를 사용하면 모든 사용 가능한 IP 주소에서 요청을 받을 수 있게 하여, 특정 IP로 제한하지 않습니다.
포트 설정: serv_addr.sin_port = htons(atoi(argv[1]));에서 명령줄 인수로 전달받은 포트 번호를 설정합니다.
htons() 함수는 호스트 바이트 순서에서 네트워크 바이트 순서로 포트 번호를 변환합니다.
*/
sockfd: 서버 소켓의 파일 디스크립터입니다. 클라이언트의 연결 요청을 대기하는 소켓이며, bind() 함수로 특정 IP와 포트에 연결된 소켓이어야 합니다. 또한, 이 소켓은 SOCK_STREAM(TCP) 타입이어야 합니다.
backlog: 연결 대기열의 최대 크기를 지정합니다. 이 숫자만큼의 연결 요청을 큐에 쌓아두고, 서버가 준비되는 대로 처리하게 됩니다. 큐가 가득 차면 이후의 연결 요청은 거부되거나 대기 상태에 놓일 수 있습니다.
반환 값
성공 시: 0을 반환하며, 소켓이 클라이언트 요청을 받을 준비가 되었음을 나타냅니다.
실패 시: -1을 반환하며, 에러 처리가 필요합니다.
int accept(int sockfd, struct sockaddr addr, socklen_t addrlen);
sockfd: 클라이언트의 연결 요청을 대기하는 서버 소켓의 파일 디스크립터입니다. 이는 socket()과 bind()로 설정한 서버 소켓입니다.
addr: 연결을 요청한 클라이언트의 주소 정보를 저장하는 sockaddr 구조체 포인터입니다. 이 구조체에 클라이언트의 IP 주소와 포트 번호가 저장됩니다.
addrlen: addr 구조체의 크기(바이트 단위)를 저장합니다. 이 포인터가 가리키는 변수에는 accept()가 완료되면 클라이언트 주소의 실제 크기가 저장됩니다.
클라이언트에서 connect() 함수를 호출하여 서버 주소로 연결 요청을 보내면, 서버 측에서는 accept() 함수가 이 요청을 받아들입니다. 이때, 클라이언트의 주소 정보(IP 주소와 포트 번호)가 서버의 accept() 함수에 전달된 addr 매개변수에 저장됩니다.
즉, accept() 함수의 두 번째 인자인 client_addr(이 예제에서는 clnt_addr)는 연결을 요청한 클라이언트의 IP 주소와 포트를 담게 됩니다.
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 클라이언트 소켓의 파일 디스크립터입니다. 이 소켓은 socket() 함수로 생성된 클라이언트 소켓입니다.
addr: 서버의 IP 주소와 포트 번호가 설정된 sockaddr 구조체 포인터입니다. 클라이언트는 이 주소를 통해 서버에 연결합니다.
addrlen: addr 구조체의 크기(바이트 단위)입니다.
성공 시: 0을 반환하며, 클라이언트가 서버와 성공적으로 연결되었음을 의미합니다.
실패 시: -1을 반환하며, 이 경우 오류 처리가 필요합니다.
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 서버 IP 주소
serv_addr.sin_port = htons(8080); // 서버 포트 번호
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
perror("connect() error");
}
ssize_t read(int fd, void *buf, size_t count);
read() 함수는 소켓이나 파일 디스크립터에서 데이터를 버퍼에 읽어오기 위해 사용됩니다. 소켓 통신에서 클라이언트나 서버로부터 수신된 데이터를 읽어오는 역할을 합니다.
주로 블로킹 함수로 동작하기 때문에, 읽을 데이터가 준비될 때까지 호출한 프로그램의 실행을 잠시 멈추고 기다립니다.
fd (파일 디스크립터):
데이터를 읽어올 소켓의 파일 디스크립터입니다. 클라이언트와 서버 간 통신에서 사용되는 소켓의 파일 디스크립터를 전달해야 합니다.
buf (버퍼 포인터):
데이터를 저장할 버퍼의 주소입니다. 수신한 데이터는 이 버퍼에 저장됩니다. 함수 호출 후, 이 버퍼에 수신된 데이터가 채워지므로, 데이터를 읽고 나서도 사용할 수 있습니다.
count (읽을 최대 바이트 수):
버퍼에 읽을 수 있는 최대 바이트 수를 지정합니다. 버퍼의 크기와 같거나 더 작게 설정하는 것이 일반적입니다.
예를 들어, count 값을 버퍼의 크기와 같게 설정하면 read() 함수는 최대한 버퍼가 다 채워질 때까지 데이터를 읽습니다.
반환 값
성공 시: 실제로 읽은 바이트 수를 반환합니다. 반환값이 0이면 연결이 종료되었음을 의미합니다(상대방 소켓이 닫힌 경우).
실패 시: -1을 반환하며, 이 경우 오류 처리가 필요합니다.
char message[1024];
int str_len = read(clnt_sock, message, sizeof(message) - 1);
if (str_len == -1) {
error_handling("read() error");
}
message[str_len] = '\0'; // 문자열의 끝에 널 문자 추가
write() 함수는 소켓 파일 디스크립터를 통해 데이터를 전송하는 함수로, 주로 클라이언트나 서버가 데이터를 상대방에게 보낼 때 사용됩니다.
이 함수는 데이터를 버퍼에서 소켓으로 전송하는 역할을 합니다.
ssize_t write(int fd, const void *buf, size_t count);
매개변수 설명
fd: 데이터를 전송할 소켓의 파일 디스크립터입니다.
buf: 전송할 데이터가 저장된 버퍼의 포인터입니다.
count: 전송할 데이터의 크기(바이트 단위)입니다. 이 크기만큼 데이터를 전송합니다.
반환 값
성공 시: 실제로 전송한 바이트 수를 반환합니다.
실패 시: -1을 반환하며, 오류가 발생한 경우에 해당합니다.
char message[] = "Hello, World!";
int bytes_sent = write(clnt_sock, message, sizeof(message));
if (bytes_sent == -1)
error_handling("write() error");
int close(int fd);
매개변수 설명
fd: 닫을 소켓의 파일 디스크립터입니다. socket(), accept() 등의 함수로 생성된 소켓 파일 디스크립터를 전달합니다.
반환 값
성공 시: 0을 반환하여 소켓이 성공적으로 닫혔음을 알립니다.
실패 시: -1을 반환하며, 오류가 발생한 경우에 해당합니다.
close() 함수는 소켓 연결을 안전하게 종료하는 역할을 하며, 서버나 클라이언트가 통신을 종료할 때 반드시 호출하여 자원을 해제합니다.
practice랑 그 전에 있는 건 일단 넘어갈 예정
IP 주소 (IP Address)
컴퓨터 식별: IP 주소는 인터넷에서 컴퓨터를 식별하는 데 사용됩니다.
IPv4와 IPv6: 두 가지 종류가 있으며, IPv4는 4바이트 주소(32비트), IPv6는 16바이트 주소(128비트)를 사용합니다.
기본 프로토콜 지정: 소켓을 생성할 때 IP 주소와 함께 기본 프로토콜을 지정해야 합니다.
포트 (Port)
소켓 식별: 포트는 컴퓨터 내의 소켓을 식별하는 데 사용됩니다.
16비트 표현: 포트 번호는 16비트로 표현되며, 범위는 0에서 65535입니다.
Well-known 포트 (0~1023): 이미 용도가 정해져 있는 포트들입니다. 예를 들어, 22번은 SSH, 80번은 웹 서버에 사용됩니다.
Ephemeral 포트: 임시 포트로, IP 스택 소프트웨어가 미리 정해진 범위 내에서 자동으로 할당합니다. 예를 들어, 리눅스의 경우 32768에서 60999 범위입니다.
IP 주소 (IP Address)
컴퓨터 식별: IP 주소는 인터넷에서 컴퓨터를 식별하는 데 사용됩니다.
IPv4와 IPv6: 두 가지 종류가 있으며, IPv4는 4바이트 주소(32비트), IPv6는 16바이트 주소(128비트)를 사용합니다.
기본 프로토콜 지정: 소켓을 생성할 때 IP 주소와 함께 기본 프로토콜을 지정해야 합니다.
포트 (Port)
소켓 식별: 포트는 컴퓨터 내의 소켓을 식별하는 데 사용됩니다.
16비트 표현: 포트 번호는 16비트로 표현되며, 범위는 0에서 65535입니다.
Well-known 포트 (0~1023): 이미 용도가 정해져 있는 포트들입니다. 예를 들어, 22번은 SSH, 80번은 웹 서버에 사용됩니다.
Ephemeral 포트: 임시 포트로, IP 스택 소프트웨어가 미리 정해진 범위 내에서 자동으로 할당합니다. 예를 들어, 리눅스의 경우 32768에서 60999 범위입니다.
// Defined in <netinet/in.h>
struct sockaddr {
u_short sa_family; // Address family
char sa_data[14]; // address
};
struct sockaddr_in {
sa_family_t sin_family; // Address family: AF_INET
uint16_t sin_port; // Port number (16bits), Network byte order
struct in_addr sin_addr; // IP address (32bits), Network byte order
char sin_zero[8]; // Unused
};
struct in_addr {
in_addr_t s_addr; // IPv4 Internet address (32bits)
주요 내용
struct sockaddr_in:
주소 체계: sin_family 필드는 주소 체계를 지정하며, IPv4의 경우 AF_INET으로 설정합니다.
포트 번호: sin_port 필드는 포트 번호를 저장하며, 네트워크 바이트 순서로 변환(htons() 함수 사용)되어야 합니다.
IP 주소: sin_addr 필드는 in_addr 구조체를 통해 IP 주소를 저장합니다.
struct in_addr:
이 구조체는 IP 주소를 저장하는 s_addr 필드를 포함하며, IP 주소는 네트워크 바이트 순서로 저장됩니다.
네트워크 바이트 순서 변환:
포트와 IP 주소는 네트워크 바이트 순서(빅 엔디안)로 변환해야 하므로, htons()와 htonl() 함수를 사용하여 호스트 바이트 순서에서 변환합니다.
바이트 순서의 필요성: 컴퓨터는 각기 다른 바이트 순서를 사용할 수 있기 때문에, 네트워크 통신에서 데이터의 일관성을 유지하기 위해 네트워크 바이트 순서(빅 엔디안)로 통일해야 합니다.
변환 함수:
ntohs()와 ntohl(): 각각 네트워크 바이트 순서를 호스트 바이트 순서로 변환합니다.
사용 예: 소켓 프로그래밍에서 IP 주소나 포트 번호를 설정할 때, 필요한 변환을 거쳐 올바른 순서로 데이터를 전송할 수 있게 합니다.
-> 빅오 :: 빅엔디안 ::> 오름차순..

차이점: inet_addr()는 IP 주소 문자열을 네트워크 바이트 순서의 정수로 변환하는 데 특화되어 있고, htonl()은 IP 주소와 관계없이 정수를 네트워크 바이트 순서로 변환합니다.
공통점: 두 함수 모두 네트워크 바이트 순서(빅 엔디안)로 변환하며, 주로 네트워크 통신에서 일관된 데이터 전송을 보장하기 위해 사용됩니다.
설명 : htonl() 이런 부류는 숫자를 받아서, 네트워크 바이트 순서로 바꿔주는 거.
inet_addr() : IP 주소 문자열을 네트워크 바이트 순서로 변환하여, 32bit 정수로 변환
inet_aton() : (동일), 구조체에 저장
inet_nota() : (동일), 문자열로 변환
// 밑의 두 함수는 이걸 반환 타입으로 함.
#define h_addr haddr_list[0]
struct hostent {
char *h_name; // official name of host
char **h_alias; // alias list
int h_addrtype; // host address type
int h_length; // length of address
char **h_addr_list; // list of addresses
};
struct hostent gethostbyname(const char hostname);
이 함수는 호스트 이름(도메인 이름)을 통해 해당 호스트의 공식 이름, 별칭, IP 주소 목록을 가져오는 역할을 합니다.
매개변수:
hostname: 호스트 이름(예: "www.example.com") 또는 점으로 구분된 표기법의 IPv4 주소를 나타내는 문자열입니다.
반환값:
성공 시: 주어진 호스트 이름에 대한 정보가 담긴 hostent 구조체의 포인터를 반환합니다.
실패 시: NULL을 반환합니다.
hostent 구조체의 주요 필드:
h_name: 호스트의 공식 이름.
h_aliases: 호스트의 별칭 목록.
h_addrtype: 주소 유형(예: AF_INET은 IPv4).
h_addr_list: 호스트의 IP 주소 목록.
이 함수는 IP 주소를 통해 호스트 정보를 가져옵니다. 즉, IP 주소를 사용하여 해당 호스트의 이름, 별칭 등을 조회할 수 있습니다.
struct hostent gethostbyaddr(const void addr, socklen_t len, int type);
매개변수:
addr: 조회할 IP 주소를 가리키는 포인터입니다.
len: IP 주소의 길이입니다. IPv4의 경우 sizeof(struct in_addr)를 전달합니다.
type: IP 주소의 유형을 지정합니다. IPv4의 경우 AF_INET을 사용합니다.
반환값:
성공 시: 호스트 정보를 담은 hostent 구조체 포인터를 반환합니다.
실패 시: NULL을 반환합니다.
Point-to-Point:
TCP는 1:1 통신 방식을 사용하여, 하나의 송신자와 하나의 수신자 사이에 데이터를 전송합니다.
Reliable, In-Order Byte Stream:
TCP는 신뢰할 수 있는 순차적인 바이트 스트림을 제공합니다.
데이터는 메시지 경계 없이 연속적인 바이트 스트림으로 전송되며, 수신자는 데이터가 순서대로 도착하도록 보장받습니다.
Full Duplex Data:
TCP는 양방향 데이터 흐름을 허용합니다. 즉, 동일한 연결 내에서 송신자와 수신자가 동시에 데이터를 주고받을 수 있습니다.
MSS (Maximum Segment Size): TCP가 전송할 수 있는 최대 세그먼트 크기를 정의합니다.
Cumulative ACKs:
TCP는 누적 확인 응답 (ACK) 방식을 사용하여 수신자가 마지막으로 성공적으로 수신한 바이트의 번호를 송신자에게 알려줍니다.
Pipelining:
혼잡 제어와 흐름 제어를 통해 윈도우 크기를 설정하고, 여러 세그먼트를 동시에 송신할 수 있는 파이프라이닝을 지원합니다.
Connection-Oriented:
TCP는 연결 지향적입니다. 송신자와 수신자가 데이터를 교환하기 전에 3-way 핸드셰이크 과정을 통해 연결을 설정하고, 각자의 상태를 초기화합니다.
Flow Controlled:
TCP는 흐름 제어를 통해 송신자가 수신자의 처리 용량을 초과하지 않도록 조절합니다. 이를 통해 수신자가 감당할 수 있는 속도로 데이터가 전송되도록 합니다.
TCP의 연결 지향적 특징
3-way 핸드셰이크 (3-way Handshake):
TCP는 3-way 핸드셰이크라는 절차를 통해 송신자와 수신자가 서로를 인식하고 연결 상태를 초기화한 후에 데이터 전송을 시작합니다.
3-way 핸드셰이크 과정:
SYN: 송신자가 수신자에게 연결 요청(SYN 패킷)을 보냅니다.
SYN-ACK: 수신자가 송신자의 요청을 수락하고 응답(SYN-ACK 패킷)을 보냅니다.
ACK: 송신자가 수신자의 응답을 확인하고(ACK 패킷) 연결이 설정됩니다.
이 과정이 끝나면, 송신자와 수신자는 데이터 전송을 시작할 준비가 완료됩니다.
상태 관리:
TCP는 연결을 설정한 후 양측의 상태 정보를 유지합니다. 이를 통해 데이터가 순서대로 도착했는지, 손실된 데이터가 없는지 등을 추적할 수 있습니다.
송신자와 수신자는 각각의 연결에 대해 시퀀스 번호와 확인 응답 번호를 관리하여 데이터가 순서대로 전송되고 수신될 수 있도록 보장합니다.
연결 종료:
데이터 전송이 끝난 후에는 4-way 핸드셰이크를 통해 연결을 종료합니다. 이 과정은 연결을 안전하게 해제하여 네트워크 자원을 효율적으로 사용할 수 있게 합니다.
신뢰성 보장:연결을 설정한 상태에서 데이터의 순서 보장, 오류 검사 및 재전송을 통해 신뢰성 있는 데이터 전송을 보장합니다. 이를 통해 데이터가 손실되거나 순서가 뒤바뀌지 않도록 합니다.
정리) 3-way 핸드셰이크 : 설정할 떄, 4-way 핸드셰이크 : 해제할 때
TCP는 한 번 연결이 설정되면 지속적으로 데이터를 보낼 수 있는 특성이 있으며, 이 때문에 메시지 경계(Message Boundary)가 없다고 표현합니다.
이유 설명
연속된 바이트 스트림: TCP는 데이터를 메시지 단위로 전송하는 것이 아니라, 연속된 바이트 스트림으로 전송합니다. 따라서, 여러 개의 메시지를 송신자가 각각 보내더라도, 수신자는 이를 하나의 연속된 데이터로 받을 수 있습니다.
메시지 경계를 보존하지 않음: TCP는 UDP와 달리 메시지 경계를 구분하지 않습니다. 송신자가 두 개의 write() 호출을 통해 두 메시지를 보냈더라도, 수신자는 한 번의 read()로 모든 데이터를 하나의 덩어리로 수신할 수 있습니다.
예시로 본 TCP의 Message Boundary 부재
서버 코드에서 write()를 두 번 호출하여 두 개의 메시지를 전송했음에도 불구하고, 클라이언트는 한 번의 read() 호출로 모든 데이터를 연속된 스트림으로 받게 됩니다.
이로 인해 수신자는 어떤 부분이 첫 번째 메시지이고, 어떤 부분이 두 번째 메시지인지 별도로 알 수 없습니다. 즉, 명확한 경계가 없는 바이트 흐름으로 데이터가 전달됩니다.
server type
웹 서버와 TCP
지속적인(persistent) HTTP
지속적인 연결의 존속 기간에 클라이언트와 서버는 같은 서버 소켓을 통해 HTTP 메시지를 교환한다.
비지속적인(non-persistent) HTTP
모든 요청/응답마다 새로운 TCP 연결이 생성되고 종료된다.
TCP 소켓의 입력 버퍼와 출력 버퍼
TCP 소켓에는 데이터 송수신을 위한 입력 버퍼와 출력 버퍼가 각각 존재하며, 소켓이 생성될 때 이들 버퍼가 함께 생성됩니다.
출력 버퍼의 데이터는 소켓이 닫힌 후에도 전송이 완료될 때까지 유지되지만, 입력 버퍼의 데이터는 소켓이 닫히면 사라집니다. -> 신뢰성 유지 가능!
Half-Close
주요 내용
close()의 의미:
close() 함수는 소켓을 완전히 종료시키는 것을 의미합니다.
소켓이 닫히면 더 이상 입출력이 불가능해집니다.
만약 데이터 송수신이 완료되지 않은 상태에서 소켓을 닫으면, 데이터 전송이 중단될 수 있어 문제가 발생할 수 있습니다.
Half-Close (반쪽 닫기):
Half-Close는 입출력 스트림 중 하나만 닫는 것을 의미합니다.
예를 들어, 송신을 종료하고 수신을 계속 유지하거나, 반대로 수신을 종료하고 송신을 유지할 수 있습니다.
이를 통해, 데이터를 송신한 후에도 수신을 계속하여 클라이언트 또는 서버로부터 추가적인 데이터를 받을 수 있습니다.
동작 예시:
왼쪽 시스템은 write()를 통해 데이터를 송신하고, 오른쪽 시스템은 read()로 데이터를 수신합니다.
Half-Close를 사용하면, 한쪽의 출력 스트림만 닫아 송신은 멈추되 수신은 계속할 수 있습니다.
개념 정리
서버 타입 (Iterative Server와 Concurrent Server)
Iterative Server (반복 서버):
UDP에서 주로 사용됩니다. UDP는 연결을 설정하지 않고 메시지를 전송하기 때문에 반복 서버가 적합합니다.
한 번에 하나의 요청만 처리할 수 있어 단순한 구조를 가지며, Message Boundary가 있는 UDP의 특성 덕분에 각각의 메시지가 독립적으로 처리됩니다.
Message Boundary가 있음: UDP는 각 메시지를 독립적인 단위로 처리할 수 있으므로, 서버가 순차적으로 처리하더라도 각 메시지가 별도의 요청으로 인식됩니다.
Concurrent Server (동시 서버):
TCP에서 주로 사용됩니다. TCP는 연결 지향적이어서 클라이언트마다 개별 연결을 유지해야 하므로 동시 서버가 적합합니다.
각 클라이언트에 대해 별도의 프로세스나 스레드를 생성하여 다수의 클라이언트 요청을 동시에 처리할 수 있습니다.
Message Boundary가 없음: TCP는 데이터를 연속된 바이트 스트림 형태로 전송하므로, 여러 클라이언트가 동시에 연결되어 있더라도 데이터를 구분하기 위해 추가적인 처리가 필요합니다. 이를 위해 HTTP 메시지의 시작과 끝을 헤더 등으로 구분합니다.
HTTP와 TCP
Persistent HTTP (지속적인 HTTP):
지속적인 연결에서 여러 HTTP 요청과 응답을 하나의 TCP 연결로 주고받습니다. 이 경우 TCP의 바이트 스트림 방식으로 데이터가 전송되며, Message Boundary가 없기 때문에 각 HTTP 메시지를 구분하기 위해 헤더와 같은 데이터 구분 방식을 사용합니다.
동일한 연결을 유지하면서 여러 메시지를 주고받기 때문에 Half-Close와 같은 기능을 사용할 수 있습니다. 예를 들어, 서버가 더 이상 송신할 데이터가 없을 때 송신 스트림을 닫고, 클라이언트로부터 응답을 계속 수신할 수 있습니다.
Non-persistent HTTP (비지속적인 HTTP):
각 요청과 응답마다 새로운 TCP 연결이 생성되고 종료됩니다.
이 방식에서는 각 요청/응답이 개별 TCP 연결로 처리되므로 Message Boundary가 없는 TCP의 특성을 보완할 필요가 없습니다. 연결이 종료되면 해당 요청/응답의 데이터 흐름이 끝난 것으로 처리됩니다.
TCP 소켓의 입력 버퍼와 출력 버퍼
TCP 소켓에는 데이터를 송수신할 때 사용하는 입력 버퍼와 출력 버퍼가 있습니다.
출력 버퍼: 소켓이 닫혀도 버퍼에 남아 있는 데이터는 전송이 완료될 때까지 계속 전송됩니다. 이는 TCP의 신뢰성을 보장하는 역할을 합니다.
입력 버퍼: 소켓이 닫히면 데이터가 사라지기 때문에, 수신이 완료되지 않은 데이터는 손실될 수 있습니다. 따라서 데이터를 완전히 수신한 후 소켓을 닫아야 합니다.
Half-Close를 활용하면, 송신을 종료하고 수신을 계속 유지할 수 있어 데이터의 손실을 줄일 수 있습니다.
Half-Close와 Message Boundary
TCP에서 Half-Close는 한쪽 방향만 닫을 수 있어, 예를 들어 서버가 더 이상 송신할 데이터가 없을 때 송신 스트림을 닫고 클라이언트의 응답을 계속 받을 수 있습니다.
Message Boundary와의 연관성:
TCP는 Message Boundary가 없고 바이트 스트림 방식이기 때문에, 송신과 수신이 모두 필요한 경우 Half-Close로 송수신을 독립적으로 관리할 수 있습니다.
반면 UDP는 각 메시지가 독립적이어서 별도로 Half-Close 같은 기능이 필요 없습니다. UDP는 송신과 수신을 연결 없이 독립적으로 처리하기 때문입니다.
int shutdown(int socket, int how);
socket: 소켓 파일 디스크립터입니다.
how: 종료할 작업의 종류를 지정하는 값입니다.
종료 종류 (how 값):
SHUT_RD (0): 수신 작업을 종료합니다. 이 값을 지정하면 더 이상 데이터를 수신할 수 없습니다.
SHUT_WR (1): 송신 작업을 종료합니다. 더 이상 데이터를 전송할 수 없습니다. 송신을 종료하더라도 남아 있는 수신 데이터는 읽을 수 있습니다.
SHUT_RDWR (2): 송수신 작업을 모두 종료합니다. 모든 데이터 전송이 중단됩니다.
shutdown()과 close()의 차이점:
shutdown()은 송신 또는 수신 작업을 부분적으로 종료할 수 있지만, 소켓 디스크립터를 해제하지는 않습니다. 소켓 디스크립터를 해제하려면 close()를 사용해야 합니다.
반환 값:
성공 시: 0을 반환합니다.
실패 시: -1을 반환하며, 오류가 발생했음을 나타냅니다.

ssize_t recvfrom(int sockfd, void buffer, size_t length, int flags, struct sockaddr src_addr, socklen_t *addrlen);
sockfd: 데이터 수신에 사용할 소켓의 파일 디스크립터.
buffer: 수신된 데이터를 저장할 버퍼.
length: 수신할 데이터의 최대 바이트 크기.
flags: 메시지 수신 방식에 관한 플래그(보통 0으로 설정).
src_addr: 송신자의 주소 정보를 저장할 sockaddr 구조체 포인터.
addrlen: src_addr의 크기를 나타내며, 함수가 호출된 후 실제 주소 크기가 저장됩니다.
동작 방식:
데이터가 수신될 때까지 대기하며, 데이터가 도착하면 이를 buffer에 저장합니다.
src_addr에는 데이터를 보낸 송신자의 주소가 저장되므로, 메시지를 보낸 클라이언트의 주소 정보를 확인할 수 있습니다.
반환값:
성공 시: 수신한 바이트 수를 반환합니다.
종료(EOF): 0을 반환합니다.
오류 시: -1을 반환하며, 오류가 발생했음을 나타냅니다.
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_addr, &clnt_addr_sz);
서버 소켓 serv_sock으로부터 데이터를 수신하여 message 버퍼에 저장합니다.
clnt_addr에는 송신자의 주소 정보가 저장되며, clnt_addr_sz는 실제 주소 크기를 나타냅니다.
ssize_t sendto(int sockfd, const void buffer, size_t length, int flags, const struct sockaddr dest_addr, socklen_t addrlen);
sockfd: 소켓 파일 디스크립터, 데이터 전송에 사용될 소켓을 지정합니다.
buffer: 전송할 메시지를 담고 있는 버퍼.
length: 전송할 데이터의 바이트 크기.
flags: 메시지 전송에 사용되는 플래그로, 일반적인 경우 0으로 설정합니다.
dest_addr: 데이터를 전송할 목적지 주소 (sockaddr 구조체로 지정).
addrlen: dest_addr의 구조체 크기(바이트 단위).
동작 방식:
sendto() 함수는 지정된 목적지 주소로 데이터를 전송합니다.
주로 UDP 소켓에서 사용되며, 연결을 설정하지 않고 데이터를 보내는 데 적합합니다.
반환값:
성공 시: 전송된 바이트 수를 반환합니다.
오류 시: -1을 반환하여 오류 발생을 나타냅니다.
주요 내용
sendto()의 내부 절차:
UDP의 비연결 특성으로 인해, sendto() 함수는 매번 데이터 전송 시 다음 절차를 수행합니다:
목적지 IP 주소와 포트를 할당: 데이터 전송 시마다 UDP 소켓에 목적지 IP와 포트를 할당합니다.
데이터 전송: 할당된 목적지 IP와 포트를 사용하여 데이터를 전송합니다.
할당 해제: 전송이 끝나면 목적지 IP와 포트를 해제합니다.
이 과정을 매번 수행해야 하기 때문에 반복적으로 주소를 설정하고 해제하는 부담이 있습니다.
Connected UDP:
Connected UDP는 특정 목적지 IP와 포트에 연결된 UDP 소켓을 사용하는 방식입니다.
목적지 주소를 미리 설정하여, 매번 sendto()를 호출할 때마다 목적지 주소를 지정하지 않아도 됩니다.
이점:
주소 할당 및 해제 절차를 생략할 수 있어 성능이 향상됩니다.
sendto()와 recvfrom() 대신 write()와 read() 함수를 사용할 수 있습니다. 이는 TCP와 유사한 방식으로 데이터를 전송하고 수신할 수 있게 해줍니다.
주의 사항:
Connected UDP는 TCP와 같은 연결 지향적 소켓을 의미하지 않습니다. UDP의 비연결성 특성은 유지되지만, 특정 목적지 주소와 연결된 방식으로 사용할 수 있습니다.
UDP 특성상 여전히 데이터의 신뢰성, 순서 보장 등의 기능은 제공되지 않습니다.
그냥 Connected UDP를 사용하면, 원하는 IP로 데이터를 보낼 수 있다. 따라서 신뢰성과 순서 보장은 되지 않는다.
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr));