학교
[컴퓨터 네트워크] Socket Programming (Part 1) - (1)
1. Introduction
[1] Socket Programming

(1) 목적
: 소켓을 사용하여 통신하는 클라이언트/서버 애플리케이션을 구축하는 방법을 배운다.
(2) 소켓이란?
: 애플리케이션 프로세으와 end-end 전송 프로토콜 사이의 문과 같다.
[2] Socket
(1) Socket Interface
: 애플리케이션과 TCP/UDP 사이에 위치한다.

(2) 소켓과 TCP/UDP의 관계도

Socket Description
Port number
IP Address
(3) 특징
각 소켓은 5가지 컴포넌트와 연관되어 있다. (is associated with)
Protocol (Protocol family and protocol)
Source address
Source port
Destination address
Destination port
컴포넌트를 지정하는 위치
Protocol : socket();
Source address and port : bind()
Destination address and port : connect(), sendto()
2. TCP Socket Programming #1
[1] TCP Server / Client의 Fnction Call

(1) Server
socket() : 소켓 생성하기
bind() : 할당하기
listen() : 요청 기다리기
Client의 Connection Requestaccept() : 수락하기
read() / write() : 읽고 쓰기
Client와 Server 간의 Send / Receiveclose() : 소켓 닫기
(2) Client
socket() : 소켓 생성하기
connect() : 요청하기
Client의 Connection Requestread() / write() : 읽고 쓰기
Client와 Server 간의 Send / Receiveclose() : 소켓 닫기
[2-1] Example : tcp_server.c
(1) 코드
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
// 에러 메시지를 출력하고 프로그램을 종료하는 함수
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock; // 서버 소켓
int clnt_sock; // 클라이언트 소켓
struct sockaddr_in serv_addr; // 서버 주소 구조체
struct sockaddr_in clnt_addr; // 클라이언트 주소 구조체
socklen_t clnt_addr_size;
char message[] = "Hello World!";
// 포트 번호 입력 확인
if (argc != 2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 1. 서버 소켓 생성
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket() error");
// 2. 서버 주소 초기화 및 설정
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]));
// 3. 소켓과 주소 바인딩
if (bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
// 4. 클라이언트 연결 대기
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
// 5. 클라이언트 연결 수락
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
if (clnt_sock == -1)
error_handling("accept() error");
// 6. 클라이언트에 메시지 전송
write(clnt_sock, message, sizeof(message));
// 7. 소켓 종료
close(clnt_sock);
close(serv_sock);
return 0;
}
// 에러 메시지 출력 함수
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}(2) 특징
주요 흐름
socket()으로 서버 소켓을 생성하고,bind()로 서버 주소를 설정하여 연결 요청을 받을 준비를 한다.listen()으로 클라이언트 연결 요청을 대기한 후,accept()함수로 연결을 수락하여 클라이언트와 통신한다.마지막으로,
write()함수로 클라이언트에 메시지를 전송하고 소켓을 닫아 연결을 종료한다.
주요 함수:
socket(): 서버 소켓 생성.bind(): 소켓과 서버 주소를 연결.listen(): 클라이언트 연결 요청 대기.accept(): 클라이언트 연결 요청 수락.write(): 클라이언트에 메시지 전송.
(3) 코드 실행 및 실행 결과
$ gcc -o tcp_server tcp_server.c
$ ./tcp_server 12345
(따로 없고, 잘 전송하면 끝남)[2-2] Example : tcp_client.c
(1) 코드
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
// 에러 메시지를 출력하고 프로그램을 종료하는 함수
void error_handling(char *message);
int main(int argc, char* argv[])
{
int sock; // 클라이언트 소켓
struct sockaddr_in serv_addr; // 서버 주소 구조체
char message[30];
int str_len = 0;
int idx = 0, read_len = 0;
// IP와 포트 번호 입력 확인
if (argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
// 1. 클라이언트 소켓 생성
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("socket() error");
// 2. 서버 주소 설정
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
// 3. 서버에 연결 요청
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error!");
// 4. 서버로부터 데이터 수신
while ((read_len = read(sock, &message[idx++], 1)) > 0)
{
if (read_len == -1)
{
error_handling("read() error!");
break;
}
str_len += read_len;
}
// 5. 수신한 메시지 출력 및 함수 호출 횟수 출력
printf("Message from server: %s \n", message);
printf("Function read call count: %d \n", str_len);
// 6. 소켓 종료
close(sock);
return 0;
}
// 에러 메시지 출력 함수
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}(2) 특징
주요 흐름
socket()으로 클라이언트 소켓을 생성하고connect()를 통해 서버와 연결을 시도한다.연결 성공 후
read()를 통해 서버로부터 메시지를 읽어와message버퍼에 저장하고, 모든 데이터를 수신하면 메시지를 출력한다.
주요 함수:
socket(): 클라이언트 소켓 생성.connect(): 서버와 연결 시도.read(): 서버로부터 메시지 수신.
(3) 코드 실행 및 실행 결과
$ gcc -o tcp_client tcp_client.c
$ ./tcp_client 127.0.0.1 12345
Message from server: Hello World!
Function read call count: 12[3-1] socket()
#include <sys/socket.h>
int socket(int domain, int type, int protocol)1) 역할
통신을 위한 소켓을 생성합니다.
2) 파라미터 설명
domain: 사용할 프로토콜 패밀리PF_INET: IPv4PF_INET6: IPv6
type: 소켓의 서비스 유형SOCK_STREAM: TCPSOCK_DGRAM: UDPSOCK_RAW: Raw IP
protocol: 특정 프로토콜 지정보통
0으로 설정 (기본값)IPPROTO_TCP,IPPROTO_UDP와 같은 값으로 설정 가능
Return value:
성공 시: 새 소켓의 파일 디스크립터 반환
실패 시:
-1반환
Example
TCP 소켓
code// TCP 소켓 생성 sock = socket(PF_INET, SOCK_STREAM, 0); if(sock == -1) error_handling("socket() error");UDP 소켓
code// UDP 소켓 생성 sock = socket(PF_INET, SOCK_DGRAM, 0); if(sock == -1) error_handling("socket() error");
[3-2] bind()
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);1) 역할
소켓에 IP 주소와 포트를 할당합니다.
2) 파라미터 설명
sockfd: 소켓의 파일 디스크립터addr: 소켓에 할당할 IP 주소와 포트 번호를 포함한 주소 구조체addrlen:addr이 가리키는 주소 구조체의 크기 (바이트 단위)Return value:
성공 시:
0반환실패 시:
-1반환
Example
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");
[3-3] listen()
#include <sys/socket.h>
int listen(int sockfd, int backlog);1) 역할
소켓을 연결 요청을 수신할 수 있는 상태로 만듭니다. (주로 TCP 서버에서 사용)
2) 파라미터 설명
sockfd: 연결 요청을 대기할 소켓의 파일 디스크립터 (SOCK_STREAM타입 필요)backlog: 연결 대기열의 최대 크기Return value:
성공 시:
0반환실패 시:
-1반환
Example
codeif(listen(serv_sock, 5) == -1) error_handling("listen() error");
[3-4] accept()
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);1) 역할
클라이언트의 연결 요청을 수락합니다. (주로 TCP 서버에서 사용)
2) 파라미터 설명
sockfd: 연결 요청을 수락할 소켓의 파일 디스크립터 (SOCK_STREAM타입 필요)addr: 클라이언트 소켓의 주소 정보를 저장할sockaddr구조체 포인터addrlen: 클라이언트 주소의 실제 크기를 저장할 포인터Return value:
성공 시: 연결된 클라이언트 소켓의 파일 디스크립터 반환
실패 시:
-1반환
Example
codeclnt_addr_size = sizeof(clnt_addr); clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); if(clnt_sock == -1) error_handling("accept() error");
[3-5] connect()
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);1) 역할
소켓을 서버와 연결합니다.
2) 파라미터 설명
sockfd: 연결할 소켓의 파일 디스크립터addr: 연결할 서버의 주소 정보가 저장된sockaddr구조체 포인터addrlen:addr이 가리키는 주소 구조체의 크기Return value:
성공 시:
0반환실패 시:
-1반환
Example
codememset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) error_handling("connect() error!");
[3-6] read()
#include <sys/socket.h>
ssize_t read(int fd, void *buf, size_t count);1) 역할
파일 디스크립터에서 데이터를 읽습니다.
2) 파라미터 설명
Return value:
성공 시: 읽은 바이트 수 반환, 파일 위치가 읽은 바이트 수만큼 이동
끝에 도달하면 0 반환
실패 시:
-1반환
Example
codewhile(read_len = read(sock, &message[idx++], 1)) { if(read_len == -1) { error_handling("read() error!"); break; } str_len += read_len; }
[3-7] write()
#include <sys/socket.h>
ssize_t write(int fd, const void *buf, size_t count);1) 역할
파일 디스크립터에 데이터를 씁니다.
2) 파라미터 설명
Return value:
성공 시: 기록한 바이트 수 반환
실패 시:
-1반환
Example
codechar message[] = "Hello World!"; write(clnt_sock, message, sizeof(message));
[3-8] recv()
#include <sys/socket.h>
ssize_t recv(int socket, void * buffer, size_t length, int flags);1) 역할
연결된 소켓에서 메시지를 수신합니다.
2) 파라미터 설명
socket: 소켓의 파일 디스크립터buffer: 수신된 메시지를 저장할 버퍼의 포인터length: 버퍼의 최대 길이 (바이트 단위)flags: 메시지 수신 방식 (0은 일반 데이터)Return value:
성공 시: 수신된 바이트 수 반환,
0은 연결 종료 의미실패 시:
-1반환
Example
codewhile((str_len = recv(recv_sock, buf, sizeof(buf), 0)) != 0) { if(str_len == -1) continue; buf[str_len] = 0; puts(buf); }
[3-9] send()
#include <sys/socket.h>
ssize_t send(int socket, const void * buffer, size_t length, int flags);1) 역할
소켓을 통해 메시지를 전송합니다.
2) 파라미터 설명
socket: 소켓의 파일 디스크립터buffer: 전송할 메시지를 담은 버퍼length: 메시지의 길이 (바이트 단위)flags: 메시지 전송 방식 (0은 일반 데이터)Return value:
성공 시: 전송된 바이트 수 반환
실패 시:
-1반환
Example
codewrite(sock, "123", strlen("123")); send(sock, "4", strlen("4"), MSG_OOB); // 긴급 메시지 전송
[3-10] close()
#include <unistd.h>
int close(int fd);1) 역할
파일 디스크립터를 닫습니다.
2) 파라미터 설명
Return value:
성공 시:
0반환실패 시:
-1반환
Example
codeclose(clnt
_sock);
close(serv_sock);
[tip - 1] read / write와 recv / send의 차이점
read와 write는 일반 파일 입출력 함수이고, recv와 send는 소켓 전용 함수로 사용되는 데 차이가 있습니다.
1. read / write
용도: 파일, 표준 입출력, 소켓 등 다양한 파일 디스크립터에 적용 가능
기능: 파일 디스크립터에서 데이터 읽기(
read) 또는 쓰기(write)플래그 지원: 별도의 플래그 지원이 없어 단순한 데이터 입출력 작업에 적합
2. recv / send
용도: 소켓에만 사용
기능: 소켓을 통한 네트워크 데이터 송수신 작업을 수행
플래그 지원:
flags파라미터로 메시지 송수신 옵션을 지정 가능예를 들어,
send에서MSG_OOB플래그를 사용하면 긴급 데이터를 전송할 수 있습니다.
비교 요약
함수 | 파일/소켓 호환성 | 플래그 지원 | 사용 예시 |
|---|---|---|---|
read / write | 파일과 소켓 모두 사용 가능 | 플래그 없음 | 파일 입출력, 단순 데이터 전송 |
recv / send | 소켓 전용 |
| 네트워크 통신에서 메시지 송수신 |
일반적으로 소켓 통신에서는 recv와 send가 소켓 전용 기능을 활용할 수 있어 더 적합하며, 특정 네트워크 통신 설정이 필요 없는 단순 데이터 송수신에서는 read와 write도 사용될 수 있습니다.
[tip - 2] Listen/accept는 주로 TCP에서 쓴다면, UDP에서는?
listen과 accept는 TCP에서만 사용하고 UDP에서는 쓰지 않는데, 그 이유는 두 프로토콜의 연결 방식 차이에 있습니다.
1. TCP와 UDP의 연결 방식
TCP는 연결 지향 프로토콜로, 서버와 클라이언트가 데이터를 주고받기 전에 연결을 확립합니다.
listen함수는 서버가 클라이언트의 연결 요청을 대기열에 넣고 기다리게 하며,accept함수는 이 요청을 받아들여 연결을 수립하는 데 사용됩니다.UDP는 비연결형 프로토콜로, 서버와 클라이언트 사이에 사전에 연결을 확립하지 않습니다. 즉, 클라이언트가 데이터를 보내면 서버가 이를 바로 수신하고, 필요 시 클라이언트로 답을 보내는 방식으로 작동합니다.
2. UDP에서의 서버/클라이언트 통신 방식
UDP에서는 서버가 listen과 accept를 통해 연결을 기다릴 필요가 없습니다. 클라이언트는 서버의 IP 주소와 포트 번호를 사용해 직접 데이터를 보낼 수 있고, 서버는 recvfrom 같은 함수를 사용해 데이터를 수신합니다.
즉, UDP에서는 '연결'이라는 개념이 없기 때문에 listen과 accept가 필요하지 않으며, 이 덕분에 UDP 통신이 더 간단하고 속도가 빠른 장점이 있습니다.
[tip - 3] 10개의 함수를 간단하게 요약한 내용
socket(int domain, int type, int protocol)->socket(프로토콜 패밀리, 서비스 타입, 특정 프로토콜)예시:sock = socket(PF_INET, SOCK_STREAM, 0);bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)->bind(소켓의 파일 디스크립터, 소켓에 할당할 주소, 주소 구조체의 크기)예시:bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));listen(int sockfd, int backlog)->listen(소켓의 파일 디스크립터, 시스템이 대기할 수 있는 최대 연결 요청 수)예시:listen(serv_sock, 5);accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)->accept(소켓의 파일 디스크립터, 클라이언트 소켓의 주소를 채우기 위한 포인터, 클라이언트 주소의 실제 크기)예시:clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)->connect(소켓의 파일 디스크립터, 연결할 주소, 주소 구조체의 크기)예시:connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));read(int fd, void *buf, size_t count)->read(파일 디스크립터, 데이터를 저장할 버퍼, 읽을 바이트 수)예시:read_len = read(sock, buf, sizeof(buf));write(int fd, const void *buf, size_t count)->write(파일 디스크립터, 전송할 버퍼, 전송할 바이트 수)예시:write(clnt_sock, message, sizeof(message));recv(int sockfd, void *buf, size_t len, int flags)->recv(소켓의 파일 디스크립터, 메시지를 저장할 버퍼, 버퍼의 길이, 수신 플래그)예시:recv_len = recv(recv_sock, buf, sizeof(buf), 0);send(int sockfd, const void *buf, size_t len, int flags)->send(소켓의 파일 디스크립터, 전송할 메시지가 담긴 버퍼, 메시지의 길이, 전송 플래그)예시:send(sock, "Hello", strlen("Hello"), 0);close(int fd)->close(파일 디스크립터)예시:close(sock);
3. Internet Address and Domain Name
: 3. 인터넷 주소와 도메인 이름
[1] 인터넷 주소
1) IP 주소
인터넷에서 컴퓨터를 식별하는 데 사용됨
IPv4 (4바이트 주소)와 IPv6 (16바이트 주소)가 있음
소켓을 생성할 때 기본 프로토콜을 지정해야 함
2) Port
컴퓨터에서 소켓을 식별하는 데 사용됨
포트 번호는 16비트로 표현됨-> 총 범위 0 ~ 65535
3) Port의 종류
1. 잘 알려진 포트: 0 ~ 1023

이미 사용 용도가 결정됨
예: ssh의 경우 22, 웹의 경우 80 등
2. 임시 포트: 단기적으로 사용되는 전송 프로토콜 포트

IP 스택 소프트웨어에 의해 미리 정의된 범위에서 자동으로 할당됨
예: 리눅스: 32768 ~ 60999
[2] 주소 구조
1) <netinet/in.h>에 정의되어 있다.
1. sockaddr
struct sockaddr {
u_short sa_family; // 주소 패밀리
char sa_data[14]; // 주소
};2. sockaddr_in
struct sockaddr_in {
sa_family_t sin_family; // 주소 패밀리: AF_INET
uint16_t sin_port; // 포트 번호 (16비트), 네트워크 바이트 오더
struct in_addr sin_addr; // IP 주소 (32비트), 네트워크 바이트 오더
char sin_zero[8]; // 사용되지 않음
};3. in_addr
struct in_addr {
in_addr_t s_addr; // IPv4 인터넷 주소 (32비트)
};2) 예시
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");※ INADDR_ANY: 소켓이 모든 로컬 인터페이스에 바인딩됨.
[3] 바이트 순서 변환
1) 바이트 순서
리틀 엔디안: 가장 낮은 중요도의 바이트가 먼저
빅 엔디안: 가장 높은 중요도의 바이트가 먼저
2) 네트워크 바이트 순서 = 빅 엔디안

가장 중요한 바이트가 단어의 최하위 주소에 위치
호스트 바이트 순서는 호스트의 CPU에 따라 다름
[4] htons(), ntohs(), htonl(), ntohl()
1) 각 함수 별 정의
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort); // 호스트 바이트 순서에서 네트워크 바이트 순서로 변환하여 포트 번호로 변환하여 사용
uint16_t ntohs(uint16_t netshort); // 네트워크 바이트 순서에서 호스트 바이트 순서로 변환하여 포트 번호로 변환하여 사용
uint32_t htonl(uint32_t hostlong); // 호스트 바이트 순서에서 네트워크 바이트 순서로 변환하여 IP 주소로 변환하여 사용
uint32_t ntohl(uint32_t netlong); // 네트워크 바이트 순서에서 호스트 바이트 순서로 변환하여 IP 주소로 변환하여 사용2) 역할
: 호스트 바이트 순서와 네트워크 바이트 순서 간의 값 변환 (이는 서로 다른 컴퓨터 시스템 간에 데이터를 정확하게 주고받기 위해 필요하다.)
3-1) 예시: endian_conv.c
unsigned short host_port = 0x1234;
unsigned short net_port;
unsigned long host_addr = 0x12345678;
unsigned long net_addr;
net_port = htons(host_port);
net_addr = htonl(host_addr);
printf("호스트 바이트 순서 포트: %#x \n", host_port);
printf("네트워크 바이트 순서 포트: %#x \n", net_port);
printf("호스트 바이트 순서 주소: %#lx \n", host_addr);
printf("네트워크 바이트 순서 주소: %#lx \n", net_addr);3-2) 결과
>> 호스트 바이트 순서 포트: 0x1234
>> 네트워크 바이트 순서 포트: 0x3412
>> 호스트 바이트 순서 주소: 0x12345678
>> 네트워크 바이트 순서 주소: 0x785634124) Long은 addr, short는 port에 쓰이는 이유
long은 IPv4 주소와 같은 더 큰 값을 표현하기 위해 사용되며,short는 포트 번호와 같이 상대적으로 작은 값을 표현하기 위해 사용된다.IPv4 주소는 32비트로 구성되어 있어 long 타입이 필요하고, 포트 번호는 16비트로 충분하기 때문에 short 타입을 사용한다.
[5] inet_addr(), inet_aton()
#include <arpa/inet.h>
in_addr_t inet_addr(const char *string); // 점으로 구분된 10진수 IP 주소를 32비트 빅 엔디안 정수로 변환
int inet_aton(const char *string, struct in_addr *addr); // 점으로 구분된 10진수 IP 주소를 32비트 빅 엔디안 정수로 변환하여 struct in_addr에 저장1) 역할
: IPv4 주소 조작
점으로 구분된 10진수 IP 주소(문자열)를 32비트 빅 엔디안 정수로 변환
2) 함수 별 특징
inet_addr()
반환 값
성공: 인터넷 주소 (32비트 정수)
오류: -1
inet_aton()
inet_addr()와 유사하지만 결과 값을 인자를 통해 반환
반환 값
성공: 1 (참)
오류: 0 (거짓)
3-1) Example (inet_addr.c)
char *addr1 = "1.2.3.4";
char *addr2 = "127.232.124.79";
unsigned long conv_addr = inet_addr(addr1);
if (conv_addr == INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n", conv_addr);
conv_addr = inet_addr(addr2);
if(conv_addr == INADDR_NONE)
printf("Error occureded \n");
else
printf("Network ordered integer addr: %#lx \n\n", conv_addr);3-2) 실행 결과
>> Network ordered integer addr: 0x4030201
>> Network ordered integer addr: 0x4f7ce87f[6] inet_ntoa()
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr adr); // IPv4 주소를 점으로 구분된 10진수 IP 주소로 변환하는 함수1) 역할
: IPv4 주소 조작
32비트 빅 엔디안 정수를 점으로 구분된 10진수 IP 주소(String)로 변환
2) 함수 별 특징
inet_ntoa()
문자열 IP 주소를 점으로 구분된 10진수 IP 주소로 변환
반환 값
성공: 변환된 점으로 구분된 IP 주소(String)
오류: -1
3-1) 예시 (inet_ntoa.c)
struct sockaddr_in addr1, addr2; // sockaddr_in 구조체를 위한 변수 선언
char *str_ptr; // IP 주소 문자열을 저장할 포인터
char str_arr[20]; // IP 주소 문자열을 저장할 배열
addr1.sin_addr.s_addr = htonl(0x1020304); // addr1에 IP 주소를 설정
addr2.sin_addr.s_addr = htonl(0x1010101); // addr2에 IP 주소를 설정
str_ptr = inet_ntoa(addr1.sin_addr); // addr1의 sin_addr을 점으로 구분된 문자열로 변환
strcpy(str_arr, str_ptr); // 변환된 문자열을 str_arr에 복사
printf("Dotted-Decimal notation1: %s \n", str_ptr); // 변환된 IP 주소 출력3-2) 실행 결과
>> Dotted-Decimal notation1: 1.2.3.4[7] gethostbyname()
#include <netdb.h>
struct hostent *gethostbyname(const char *hostname); // 호스트 이름으로 호스트 정보를 가져오는 함수1) 역할
: 호스트 이름으로 호스트 정보를 가져옴
호스트 이름을 통해 호스트의 공식 이름, 별칭 및 IP 주소를 얻을 수 있음
hostname: 호스트 이름 또는 표준 점 표기법의 IPv4 주소
2) 반환 값
성공: 주어진 호스트 이름에 대한 hostent 구조체
오류: NULL
3-1) 예시 (gethostbyname.c)
struct hostent *host; // hostent 구조체 포인터 선언
…
host = gethostbyname(argv[1]); // 인자로 받은 호스트 이름으로 호스트 정보를 가져옴
printf("Official name: %s \n", host->h_name); // 공식 이름 출력
for (i = 0; host->h_aliases[i]; i++) // 별칭을 출력
printf("Aliases %d: %s \n", i+1, host->h_aliases[i]);
printf("Address type: %s \n", (host->h_addrtype==AF_INET)?"AF_INET":"AF_INET6"); // 주소 타입 출력
for (i = 0; host->h_addr_list[i]; i++) // IP 주소를 출력
printf("IP addr %d: %s \n", i+1, inet_ntoa(*(struct in_addr*)host->h_addr_list[i]));3-2) 결과
# 실행 결과는 호스트 이름에 따라 달라짐[8] hostent의 구조

1) netdb.h에 정의되어 있다.
#define h_addr h_addr_list[0] // h_addr을 h_addr_list의 첫 번째 주소로 정의
struct hostent {
char *h_name; // 호스트의 공식 이름
char **h_aliases; // 별칭 목록
int h_addrtype; // 호스트 주소 타입
int h_length; // 주소 길이
char **h_addr_list; // 주소 목록
};[9] gethostbyaddr()
#include <netdb.h>
struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family);1) 역할
: 네트워크 바이트 순서 주소를 통해 호스트 정보를 가져온다.
주어진 주소(addr)를 통해 호스트의 공식 이름, 별칭(aliases), IP 주소를 얻을 수 있다 (네트워크 바이트 주소).
2) 파라미터 설명
addr : 호스트의 주소 (type = struct in_addr)
len : 주소의 길이 (IPv4 = 4, IPv6 = 16)
family : 주소 패밀리 (AF_INET = IPv4, AF_INET6 = IPv6)
3) 반환 값
성공: 주어진 호스트 이름에 대한 hostent 구조체 타입
오류: NULL
4) 예시 (gethostbyaddr.c)
struct hostent *host; // hostent 구조체 포인터
struct sockaddr_in addr; // sockaddr_in 구조체
memset(&addr, 0, sizeof(addr)); // addr 초기화
addr.sin_addr.s_addr = inet_addr(argv[1]); // 입력된 IP 주소를 addr에 저장
host = gethostbyaddr((char*)&addr.sin_addr, 4, AF_INET); // 주소를 통해 host 정보 조회
if (!host) // 호스트 정보가 없으면 오류 처리
error_handling("gethost... error");
printf("Official name: %s \n", host->h_name); // 공식 이름 출력
for (i = 0; host->h_aliases[i]; i++) // 별칭 목록 출력
printf("Aliases %d: %s \n", i+1, host->h_aliases[i]);
printf("Address type: %s \n",
(host->h_addrtype==AF_INET)?"AF_INET":"AF_INET6"); // 주소 타입 출력
for (i = 0; host->h_addr_list[i]; i++) // IP 주소 목록 출력
printf("IP addr %d: %s \n", i+1,
inet_ntoa(*(struct in_addr*)host->h_addr_list[i]));4. TCP Socket Programming #2
: 4. TCP 소켓 프로그래밍 #2
[1] TCP: 개요 (RFC: 793, 1122, 2018, 5681, 7323)
1) 특징
포인트 투 포인트 (Point-to-Point):
송신자와 수신자가 1:1로 연결됨.
?신뢰성(Reliable) 있고 순서 보장된 바이트 스트림 (Reliable, In-Order Byte Stream):?
"메시지 경계(message boundaries)"가 없음.
전이중 데이터 (Full Duplex Data):
동일한 연결 내에서 양방향 데이터 흐름 가능.
MSS (Maximum Segment Size): 최대 세그먼트 크기.
누적 ACK (Cumulative ACK):
수신한 모든 데이터에 대한 ACK를 전송.
파이프라이닝 (Pipelining):
TCP의 혼잡 제어 및 흐름 제어가 윈도우 크기를 설정함.
?연결 지향적 (Connection-Oriented): ?
데이터 전송 전 송신자와 수신자의 상태를 초기화하기 위해 핸드셰이킹(제어 메시지 교환) 수행.
흐름 제어 (Flow Controlled):
송신자가 수신자를 압도하지 않도록 조절됨.
[2] TCP의 메시지 경계 없음
1) 특징
"메시지 경계(Message boundary)"는 프로토콜을 통해 전송되는 두 메시지 간의 구분을 의미한다.
UDP는 메시지 경계를 보존한다.
예를 들어, 서버가
write()를 두 번 호출하면 클라이언트는read()를 두 번 호출해야 한다.
TCP는 메시지 경계를 보존하지 않는다.
예를 들어, 서버가
write()를 두 번 호출하더라도 클라이언트는 모든 데이터를 한 번에 읽을 수 있다.
2-1) 예시
서버
code// Server char message1[] = "Hello World!111"; // 첫 번째 메시지 char message2[] = "Hello World!222"; // 두 번째 메시지 … write(clnt_sock, message1, sizeof(message1)); // 첫 번째 메시지 전송 write(clnt_sock, message2, sizeof(message2)); // 두 번째 메시지 전송클라이언트
code// Client sleep(1); // 잠시 대기 read_len = read(sock, message, sizeof(message)); // 서버로부터 데이터 읽기 printf("Message from server: "); // 서버로부터의 메시지 출력 for (i = 0; i < sizeof(message); i++) printf("%c", message[i]); // 읽은 메시지 출력 printf("Read_len: %d \n", read_len ); // 읽은 길이 출력
2-2) 결과
$ ./hclient2 127.0.0.1 9191
Message from server: Hello World!111Hello World!222**p^*
Read_len: 32[3] 서버 종류
1) 반복 서버 (Iterative Server)
단일 서버 프로세스가 "잘 알려진" 포트에서 들어오는 요청을 수신하고 처리한다.
대부분의 UDP 서버는 반복적이다.
UDP 서버는 간단하고 효율적인 처리를 위해 하나의 프로세스와 하나의 스레드로 운영된다.
2) 동시 서버 (Concurrent Server)
각 클라이언트를 처리하기 위해 별도의 프로세스를 생성한다.
주요 서버 프로세스가 각 클라이언트를 처리하기 위해 새로운 서비스 프로세스를 생성한다.
대부분의 TCP 서버는 연결 관리의 용이성을 위해 동시적이다.
[4] Iterative vs Concurrent servers
1) Iterative server skeleton
int sockfd, newsockfd;
// 소켓 생성
if ((sockfd = socket(…)) < 0)
err_sys(“socket error”);
// 소켓에 주소 바인딩
if ((bind(sockfd, …) < 0)
err_sys(“bind error”);
// 클라이언트 요청을 대기
if (listen(sockfd, 5))
err_sys(“listen error”);
for (;;) {
// 클라이언트 연결 수락
newsockfd = accept(sockfd, …);
if (newsockfd < 0)
err_sys(“accept error”);
// 요청 처리 함수 호출
doit(newsockfd);
// 연결 종료
close(newsockfd);
}설명:
단일 프로세스: 하나의 프로세스가 클라이언트 요청을 순차적으로 처리한다.
간단한 구조: 요청을 하나씩 처리하므로 코드가 간단하다. 하지만 동시에 여러 클라이언트를 처리할 수 없다.
2) Concurrent server skeleton
int sockfd, newsockfd;
// 소켓 생성
if ((sockfd = socket(…)) < 0)
err_sys(“socket error”);
// 소켓에 주소 바인딩
if ((bind(sockfd, …) < 0)
err_sys(“bind error”);
// 클라이언트 요청을 대기
if (listen(sockfd, 5))
err_sys(“listen error”);
for (;;) {
// 클라이언트 연결 수락
newsockfd = accept(sockfd, …);
if (newsockfd < 0)
err_sys(“accept error”);
// 자식 프로세스 생성하여 클라이언트 요청 처리
if (fork() == 0) {
close(sockfd); // 자식 프로세스에서 기존 소켓 닫기
doit(newsockfd); // 요청 처리 함수 호출
exit(0); // 자식 프로세스 종료
}
else {
close(newsockfd); // 부모 프로세스에서 연결 소켓 닫기
}
}설명:
다중 프로세스: 각 클라이언트 연결에 대해 새로운 프로세스를 생성하여 요청을 병렬로 처리한다.
효율적인 동시 처리: 여러 클라이언트의 요청을 동시에 처리할 수 있어 성능이 향상된다. 하지만, 프로세스를 관리하는 오버헤드가 증가할 수 있다.
[5] TCP 소켓의 입력 및 출력 버퍼

입력 버퍼와 출력 버퍼: TCP 소켓에는 입력 버퍼와 출력 버퍼가 존재한다.
버퍼 생성 시점: 소켓이 생성될 때 입력 버퍼와 출력 버퍼가 생성된다.
소켓 종료 시 데이터 전송: 소켓이 닫혀도 출력 버퍼에 남아 있는 데이터는 계속 전송된다.
입력 버퍼의 데이터 소멸: 소켓이 닫히면 입력 버퍼에 남아 있는 데이터는 파괴된다.
1) 설명
입력 버퍼(Input Buffer)
클라이언트가 보낸 데이터를 수신하기 위한 공간이다.
클라이언트가 데이터를 보낼 때, 이 버퍼에 데이터가 쌓이며, 이후 애플리케이션이 이 데이터를 읽어 처리한다.
클라이언트 소켓이 닫히면 이 버퍼의 데이터는 사라지기 때문에 중요한 데이터가 소실되지 않도록 주의해야 한다.
출력 버퍼(Output Buffer)
서버가 클라이언트에게 보내기 위한 데이터가 저장되는 공간이다.
애플리케이션이 데이터를 소켓을 통해 전송할 때, 이 버퍼에 데이터를 쌓고, 소켓이 실제로 데이터를 전송할 때 사용된다.
서버 소켓이 닫혀도 이 버퍼에 있는 데이터는 계속해서 전송되므로, 소켓의 종료가 데이터 전송에 영향을 미치지 않는다.
[6] Half-close
: 반닫힘 (Half-close)
close()의 의미
소켓의 완전한 파괴를 의미한다.
더 이상 I/O 작업이 불가능하다.
데이터 송수신이 완료되지 않은 경우 문제를 일으킬 수 있다.
반닫힘 (Half-close)
입력 스트림 또는 출력 스트림 중 하나만 닫는다.
[7] shutdown()
#include <unistd.h>
int shutdown(int socket, int how);1) 역할
: 소켓의 송신 및 수신 작업을 종료한다.
이 함수는 소켓의 전이중(full-duplex) 연결에서 모든 또는 일부를 종료한다.
shutdown()은 실제로 소켓 설명자(socket descriptor)를 해제하지 않으며, 설명자를 해제하려면close()를 사용해야 한다.
2) 파라미터 설명
socket: 소켓 파일 설명자
how: 종료 유형
SHUT_RD (0): 더 이상의 수신 작업을 비활성화한다.
SHUT_WR (1): 더 이상의 송신 작업을 비활성화한다.
SHUT_RDWR (2): 더 이상의 송신 및 수신 작업을 비활성화한다.
3) 반환 값
성공: 0
오류: -1
4) 예시
shutdown(clnt_sd, SHUT_WR); // 클라이언트 소켓의 송신 작업 종료
read(clnt_sd, buf, BUF_SIZE); // 클라이언트 소켓에서 메시지를 읽는다
printf("Message from client : %s \n", buf); // 클라이언트로부터 받은 메시지를 출력한다
close(clnt_sd); // 클라이언트 소켓을 닫는다
close(serv_sd); // 서버 소켓을 닫는다섹션별 정리 및 추가 설명 (심화)
1. Introduction (소개)
소켓 프로그래밍의 개념: 네트워크를 통해 애플리케이션 간에 데이터를 주고받는 방법을 다루는 프로그래밍 기법입니다. 소켓은 이러한 통신을 위한 추상적인 끝점(endpoint) 역할을 합니다.
소켓의 역할: 애플리케이션과 운영체제 사이에서 네트워크 통신을 위한 인터페이스를 제공합니다.
TCP/UDP 프로토콜: TCP는 연결 지향, 신뢰성 있는 프로토콜이고, UDP는 비연결형, 비신뢰성 프로토콜입니다. 각 프로토콜의 특징에 따라 적절한 소켓을 사용해야 합니다.
소켓의 구성 요소: 소켓은 프로토콜, 소스 주소, 소스 포트, 목적지 주소, 목적지 포트 등 다섯 가지 컴포넌트로 구성됩니다.
추가 설명:
소켓 종류: 스트림 소켓(TCP), 데이터그램 소켓(UDP) 외에도 RAW 소켓 등 다양한 종류의 소켓이 있습니다.
소켓 주소: IP 주소와 포트 번호로 구성되며, 이를 통해 다른 컴퓨터의 특정 애플리케이션과 통신할 수 있습니다.
소켓 함수: 소켓을 생성하고, 연결하고, 데이터를 주고받는 등 다양한 작업을 수행하기 위한 함수들이 있습니다.
2. TCP Socket Programming #1 (TCP 소켓 프로그래밍 1)
TCP 서버/클라이언트의 기능 호출: TCP 서버는
listen을 통해 연결 요청을 대기하고,accept로 연결을 수락하며,read와write로 데이터를 주고받습니다. TCP 클라이언트는connect를 통해 서버에 연결하고,read와write로 데이터를 주고받습니다.예제 코드 분석: 제공된 코드를 통해 TCP 서버와 클라이언트의 구현 방법을 자세히 살펴볼 수 있습니다. 각 함수의 역할과 코드의 흐름을 이해하는 것이 중요합니다.
TCP의 특징: TCP는 연결 지향, 신뢰성, 순서 보장 등의 특징을 가지고 있어 안정적인 데이터 전송에 적합합니다.
추가 설명:
TCP 3-way handshake: TCP 연결을 설정하기 위한 과정입니다. SYN, SYN-ACK, ACK 패킷을 주고받으며 연결을 확립합니다.
TCP 4-way handshake: TCP 연결을 종료하기 위한 과정입니다. FIN 패킷을 주고받으며 연결을 해제합니다.
TCP 윈도우 크기: 송신자가 수신자에게 한 번에 보낼 수 있는 데이터의 양을 제한하는 값입니다. 흐름 제어에 사용됩니다.
3. Internet Address and Domain Name (인터넷 주소와 도메인 이름)
IP 주소: 컴퓨터를 식별하기 위한 숫자 형태의 주소입니다. IPv4와 IPv6가 있으며, IPv4는 32비트, IPv6는 128비트로 구성됩니다.
포트 번호: 컴퓨터 내에서 실행되는 프로세스를 식별하기 위한 번호입니다.
도메인 이름: 사람이 기억하기 쉬운 형태로 표현된 컴퓨터의 이름입니다. DNS 서버를 통해 IP 주소로 변환됩니다.
바이트 순서: 빅 엔디안과 리틀 엔디안이 있으며, 네트워크 통신에서는 빅 엔디안을 사용합니다.
주소 변환 함수:
htons,htonl,inet_addr,inet_ntoa등의 함수를 사용하여 호스트 바이트 순서와 네트워크 바이트 순서를 변환하거나, IP 주소를 문자열로 변환하고, 그 반대의 작업을 수행할 수 있습니다.
추가 설명:
DNS (Domain Name System): 도메인 이름을 IP 주소로 변환해주는 시스템입니다.
IP 주소 클래스: A, B, C, D, E 클래스로 나뉘며, 각 클래스마다 네트워크 부분과 호스트 부분의 비율이 다릅니다.
서브네팅: 하나의 네트워크를 여러 개의 작은 네트워크로 나누는 기술입니다.
4. TCP Socket Programming #2 (TCP 소켓 프로그래밍 2)
TCP의 특징: TCP의 신뢰성, 순서 보장, 연결 지향 등의 특징을 심층적으로 다룹니다.
서버 종류: 반복 서버와 동시 서버의 차이점을 비교하고, 각각의 장단점을 설명합니다.
소켓 버퍼: 소켓의 입력 버퍼와 출력 버퍼의 역할과 중요성을 설명합니다.
반닫힘: TCP 연결을 부분적으로 종료하는 방법으로,
shutdown함수를 사용하여 구현합니다.
추가 설명:
TCP 윈도우 흐름 제어: 송수신 버퍼의 크기를 이용하여 흐름을 제어하는 방식입니다.
TCP 혼잡 제어: 네트워크 혼잡을 해결하기 위한 다양한 알고리즘(Slow Start, Congestion Avoidance 등)이 존재합니다.
TCP Keep-Alive: 연결 유지를 위한 메커니즘으로, 주기적으로 데이터를 주고받지 않아도 연결이 유지될 수 있도록 합니다.
