네트워크에 대한 개론
이론적인 부분은 다룬 적이 있어서 다음 글을 참고하기를 바란다.
https://messy-developing-diary.tistory.com/168
IT 엔지니어를 위한 네트워크 입문
CH 1 : 네트워크 시작하기프로토콜 : 네트워크 상의 통신에 필요한 규약 대표적으로 이더넷-TCP/IP 기반 프로토콜이 있음 물리적 측면 : 데이터 전송 매체, 신호 규약, 회선 규격 등 이더넷에서 널리
messy-developing-diary.tistory.com
TCP/IP와 UDP 통신에 대해서 보다 실전적인 부분을 다루고자 한다.
주요 주소 체계
TCP/IP : MAC 주소(48 bit), IP 주소(IPv4 - 32bit, IPv6 - 128bit), 도메인 네임(서버나 단말을 구분하기 위한 문자열)
포트 번호 : 서비스들을 구분하기 위한 식별자(하나의 IP 주소를 chrome과 firefox가 같이 사용한다고 생각하면 편하다!)
- etc/service : 유닉스에서 잘 알려진 포트 번호를 찾아볼 수 있다.
service : 부팅 시 자동으로 켜지고, 백그라운드에 계속 실행되는 프로세스, 리눅스에서는 daemon이라 부르기도 함(윈도우 운영체제에서는 service, 리눅스에서는 daemon이라 불렀는데 요즘에는 혼용해서 사용)
BSD 소켓
소켓 : 프로세스 간의 상호 양방향 통신 방식 - 소프트웨어로 작성된 통신 접속점
조작을 위한 파일/소켓 디스크립터를 제공한다.
함수 이름 헤더 파일 설명 주요 인자
socket() | <sys/socket.h> | 소켓 생성 | domain, type, protocol |
bind() | <sys/socket.h> | 소켓에 로컬 주소 지정 | sockfd, addr, addrlen |
listen() | <sys/socket.h> | 소켓을 수신 대기 상태로 전환 (서버용) | sockfd, backlog |
accept() | <sys/socket.h> | 수신 대기 중인 연결 수락 (클라이언트 소켓 생성) | sockfd, addr, addrlen |
connect() | <sys/socket.h> | 원격 호스트에 연결 요청 (클라이언트용) | sockfd, addr, addrlen |
send(), write() | <sys/socket.h> | 연결된 소켓에 데이터 전송 | sockfd, buf, len, flags |
recv(), read() | <sys/socket.h> | 연결된 소켓으로부터 데이터 수신 | sockfd, buf, len, flags |
sendto() | <sys/socket.h> | 연결 없는 소켓에 데이터 전송 (UDP 등) | sockfd, buf, len, flags, dest_addr, addrlen |
recvfrom() | <sys/socket.h> | 연결 없는 소켓으로부터 데이터 수신 | sockfd, buf, len, flags, src_addr, addrlen |
close() | <unistd.h> | 소켓 종료 | sockfd |
shutdown() | <sys/socket.h> | 소켓의 송수신 기능 종료 | sockfd, how |
getsockname() | <sys/socket.h> | 로컬 주소 확인 | sockfd, addr, addrlen |
getpeername() | <sys/socket.h> | 연결된 원격 주소 확인 | sockfd, addr, addrlen |
setsockopt() | <sys/socket.h> | 소켓 옵션 설정 | sockfd, level, optname, optval, optlen |
getsockopt() | <sys/socket.h> | 소켓 옵션 확인 | sockfd, level, optname, optval, optlen |
select() | <sys/select.h> | 다중 소켓의 상태 검사 (입출력 가능 여부) | nfds, readfds, writefds, exceptfds, timeout |
poll() | <poll.h> | 다중 소켓 상태 검사 (보다 유연함) | fds, nfds, timeout |
inet_pton() | <arpa/inet.h> | IP 문자열을 이진 형태로 변환 | af, src, dst |
inet_ntop() | <arpa/inet.h> | IP 이진 데이터를 문자열로 변환 | af, src, dst, size |
소켓을 활용한 IPC : named pipe와 유사한 모습을 보인다.(실습 추가할 것)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>
#include <sys/socket.h>
int main(){
int ret, fd[2];
int status;
char buf[] = "Hello World", line[BUFSIZ];
pid_t pid;
ret = socketpair(AF_LOCAL, SOCK_STREAM, 0, fd);
if(ret == -1){
return -1;
}
printf("socket 1 : %d\n", fd[0]); //하나가 read면 다른 하나가 write인 방식, rw가 정해져 있지는 않다.
printf("socket 2 : %d\n", fd[1]);
if((pid = fork()) < 0){
return -1;
}
else if(pid == 0){
write(fd[1], buf, strlen(buf)+1);
printf("Data send : %s\n", buf);
close(fd[1]);
}
else{
wait(&status); /* 자식 프로세스의 종료 대기 */
read(fd[0], line, BUFSIZ); /* 자식 프로세스에서 온 데이터 읽기 */
printf("Received data : %s\n", line);
close(fd[0]);
}
return 0;
}
UDP 네트워크 프로그래밍
UDP : 데이터 전송 시 신뢰성은 없지만 속도가 빨라 일반적인 LAN에서 많이 사용된다.(패킷의 분실 여부를 확인하는 절차가 부재 - TCP와 대조되는 사항)
소켓 함수
int socket(int domain, int type, int protocol);
//성공 시 파일 디스크립터, 실패 시 -1
소켓 도메인
AF_INET | IPv4 인터넷 프로토콜 사용 | 대부분의 인터넷 애플리케이션 |
AF_INET6 | IPv6 인터넷 프로토콜 사용 | IPv6 기반 네트워크 |
AF_UNIX 또는 AF_LOCAL | 로컬(유닉스) 프로세스 간 통신 | 동일 시스템 내의 IPC |
AF_PACKET | 로우 레벨 패킷 접근 (Linux 전용) | 네트워크 모니터링, 패킷 스니핑 |
AF_NETLINK | 커널과 사용자 공간 간 통신 (Linux 전용) | 라우팅 테이블 변경 등 시스템 관리 |
AF_BLUETOOTH | 블루투스 통신 | Bluetooth 장치 제어 |
소켓 타입
SOCK_STREAM | 스트림 기반 (연결 지향) | 신뢰성 보장, 순차적 전송, 연결 필요 (TCP 기반) | 웹 서버, FTP 등 |
SOCK_DGRAM | 데이터그램 기반 (비연결형) | 비신뢰성, 순서 미보장, 빠름 (UDP 기반) | DNS, VoIP 등 |
SOCK_RAW | 원시 소켓 | 헤더까지 직접 제어 가능 | ping, traceroute |
SOCK_SEQPACKET | 순서화된 고정 패킷 스트림 | 신뢰성 있음, 메시지 경계 보존 | 일부 특수 IPC |
SOCK_RDM | 신뢰성 있는 메시지 전송 (순서 없음) | 거의 사용되지 않음 | 실험적 용도 |
프로토콜
IPPROTO_TCP | TCP 프로토콜 사용 (SOCK_STREAM과 함께) |
IPPROTO_UDP | UDP 프로토콜 사용 (SOCK_DGRAM과 함께) |
IPPROTO_ICMP | ICMP 프로토콜 사용 (SOCK_RAW과 함께) |
바인드 함수
소켓에 로컬 주소를 할당하는 데 사용되는 함수, 서버 측에 주로 사용된다. 명시적으로 bind()를 호출해야 클라이언트가 접속할 수 있는 고정 주소를 가질 수 있다.
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// return 0(success), -1(error)
sockfd | socket()으로 생성한 소켓의 파일 디스크립터 |
addr | 소켓에 할당할 로컬 주소 정보가 들어 있는 구조체 포인터 (struct sockaddr *) |
addrlen | addr 구조체의 크기 (바이트 단위) |
sockaddr_in(IPv4), sockaddr_in6 (IPv6) , sockaddr_un (유닉스 도메인) => sockaddr과 호환 가능하다.
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); // 구조체 초기화
addr.sin_family = AF_INET; // IPv4 주소 체계 지정
addr.sin_port = htons(8080); // 포트 번호 8080 (네트워크 바이트 순서)
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 루프백 주소 (localhost)
#include <sys/socket.h>
// 범용 소켓 주소 구조체 (모든 주소 구조체와 호환되도록 설계됨)
struct sockaddr {
sa_family_t sa_family; // 주소 체계 (AF_INET, AF_INET6, AF_UNIX 등)
char sa_data[14]; // 주소 정보를 담는 데이터 (각 주소 체계별로 다르게 해석됨)
};
little endian(작은 값 바이트 먼저 표기), big endian(큰 값 바이트 먼저 표기)의 차이를 고려해야 함(기기의 차이에 따라 메모리 저장 방식, 네트워크 데이터 순서 등이 달라질 수 있음) => 유닉스는 호스트 바이트 순서와의 변환을 위한 함수들을 제공하고 있다.
htons() | Host → Network | 16비트 (short) | 호스트 바이트 순서의 16비트 값을 네트워크 바이트 순서로 변환 |
htonl() | Host → Network | 32비트 (long) | 호스트 바이트 순서의 32비트 값을 네트워크 바이트 순서로 변환 |
ntohs() | Network → Host | 16비트 (short) | 네트워크 바이트 순서의 16비트 값을 호스트 바이트 순서로 변환 |
ntohl() | Network → Host | 32비트 (long) | 네트워크 바이트 순서의 32비트 값을 호스트 바이트 순서로 변환 |
네트워크 주소를 변환해주는 함수 역시 제공하고 있다.
inet_addr() | 문자열 → 이진 | IPv4 (in_addr) | 192.168.0.1 → uint32_t (네트워크 바이트 순서) |
inet_aton() | 문자열 → 이진 | IPv4 (in_addr) | 192.168.0.1 → struct in_addr로 저장 (성공 여부 반환) |
inet_ntoa() | 이진 → 문자열 | IPv4 (in_addr) | struct in_addr → dotted string (192.168.0.1) |
inet_pton() | 문자열 → 이진 | IPv4/IPv6 | AF_INET/AF_INET6에 따라 in_addr 또는 in6_addr로 변환 |
inet_ntop() | 이진 → 문자열 | IPv4/IPv6 | in_addr/in6_addr → char * 형태로 주소 반환 |
gethostbyname() | 도메인 → IP | IPv4 | 호스트 이름을 IP 주소로 변환 (구식, thread-safe 아님) |
gethostbyaddr() | IP → 도메인 | IPv4 | IP 주소를 도메인 이름으로 변환 (구식) |
getaddrinfo() | 도메인 → IP/포트 | IPv4/IPv6 | 최신 API, 도메인과 서비스 이름을 소켓 주소로 변환 (다중 프로토콜 지원) |
getnameinfo() | 소켓주소 → 문자열 주소 | IPv4/IPv6 | 소켓 주소에서 도메인 이름 및 서비스명 추출 (역변환) |
바이트 조작 함수 : memset, memcpy, memcmp 등 함수=> C에서 다루는 내용
Listen 함수
TCP 서버 소켓 프로그래밍에서 클라이언트로부터의 연결 요청을 대기하기 위해 사용되는 함수
#include <sys/socket.h>
int listen(int sockfd, int backlog);
파라미터 | 설명 |
sockfd | socket()으로 생성하고, bind()까지 완료된 소켓 디스크립터 |
backlog | 커넥션 큐의 길이 (대기 큐 크기): 클라이언트가 동시에 연결 요청할 수 있는 수 |
- 완료되지 않은 연결 큐 (SYN_RCVD 상태)
클라이언트가 연결 요청을 보냈지만 3-way handshake가 완료되지 않음 - 완료된 연결 큐 (ESTABLISHED 대기 상태)
3-way handshake가 완료된 연결이 대기 중이며 accept()를 기다리는 상태
UDP 송수신 실습
/*server*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define UDP_PORT 5100
int main(int argc, char **argv)
{
int sockfd,n;
struct sockaddr_in servaddr, cliaddr;
socklen_t len;
char mesg[1000];
sockfd = socket(AF_INET, SOCK_DGRAM, 0); /* UDP를 위한 소켓 생성 */
/* 접속되는 클라이언트를 위한 주소 설정 후 운영체제에 서비스 등록 */
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(UDP_PORT);
bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
/* 클라이언트로부터 메시지를 받아서 다시 클라이언트로 전송 */
do {
len = sizeof(cliaddr);
n = recvfrom(sockfd, mesg, 1000, 0, (struct sockaddr *)&cliaddr, &len);
sendto(sockfd, mesg, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
mesg[n] = '\0';
printf("Received data : %s\n", mesg);
} while(strncmp(mesg, "q", 1));
close(sockfd); /* 사용이 끝난 후 소켓 닫기 */
return 0;
}
/*client*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define UDP_PORT 5100
int main(int argc, char **argv){
int sockfd, n;
socklen_t clisize;
struct sockaddr_in servaddr, cliaddr;
char mesg[BUFSIZ];
if(argc != 2) {
printf("usage : %s <IP address>\n", argv[0]);
return -1;
}
sockfd = socket(AF_INET, SOCK_DGRAM, 0); /* UDP를 위한 소켓 생성 */
/* 서버의 주소와 포트 번호를 이용해서 주소 설정 */
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
/* 문자열을 네트워크 주소로 변경 */
inet_pton(AF_INET, argv[1], &(servaddr.sin_addr.s_addr));
servaddr.sin_port = htons(UDP_PORT);
/* 키보드로부터 문자열을 입력받아 서버로 전송 */
do {
fgets(mesg, BUFSIZ, stdin);
sendto(sockfd, mesg, strlen(mesg), 0, (struct sockaddr *)&servaddr,
sizeof(servaddr));
clisize = sizeof(cliaddr);
/* 서버로부터 데이터를 받아서 화면에 출력 */
n = recvfrom(sockfd, mesg, BUFSIZ, 0, (struct sockaddr*) &cliaddr, &clisize);
mesg[n] = '\0';
fputs(mesg, stdout);
} while(strncmp(mesg, "q", 1));
close(sockfd);
return 0;
}
TCP 서버와 클라이언트 프로그래밍
앞선 예제들과 유사한 메커니즘을 보인다!
/*server*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define TCP_PORT 5100
int main(int argc, char **argv)
{
int ssock;
socklen_t clen;
int n;
struct sockaddr_in servaddr, cliaddr;
char mesg[BUFSIZ];
if ((ssock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket()");
return -1;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(TCP_PORT);
if (bind(ssock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
return -1;
if (listen(ssock, 8) < 0) // 동시 접속하는 대기 큐 설정
return -1;
clen = sizeof(cliaddr);
do
{
/* 클라이언트가 접속하면 접속을 허용하고 클라이언트 소켓 생성 */
int n, csock = accept(ssock, (struct sockaddr *)&cliaddr, &clen);
/* 네트워크 주소를 문자열로 변경 */
inet_ntop(AF_INET, &cliaddr.sin_addr, mesg, BUFSIZ);
printf("Client is connected : %s\n", mesg);
if ((n = read(csock, mesg, BUFSIZ)) <= 0)
perror("read()");
printf("Received data : %s", mesg);
/* 클라이언트로 buf에 있는 문자열 전송 */
if (write(csock, mesg, n) <= 0)
perror("write()");
close(csock);
} while (strncmp(mesg, "q", 1));
return 0;
}
/*client*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define TCP_PORT 5100
int main(int argc, char **argv)
{
int ssock;
struct sockaddr_in servaddr;
char mesg[BUFSIZ];
if(argc < 2) {
printf("Usage : %s IP_ADRESS\n", argv[0]);
return -1;
}
/* 소켓을 생성 */
if((ssock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket()");
return -1;
}
/* 소켓이 접속할 주소 지정 */
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
/* 문자열을 네트워크 주소로 변경 */
inet_pton(AF_INET, argv[1], &(servaddr.sin_addr.s_addr));
servaddr.sin_port = htons(TCP_PORT);
/* 지정한 주소로 접속 */
if(connect(ssock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("connect()");
return -1;
}
fgets(mesg, BUFSIZ, stdin);
if(send(ssock, mesg, BUFSIZ, MSG_DONTWAIT) <= 0) { /* 데이터를 소켓에 쓴다. */
perror("send()");
return -1;
}
memset(mesg, 0, BUFSIZ);
if(recv(ssock, mesg, BUFSIZ, 0) <= 0) { /* 데이터를 소켓으로부터 읽는다. */
perror("recv()");
return -1;
}
printf("Received data : %s", mesg); /* 받아온 문자열을 화면에 출력 */
close(ssock); /* 소켓을 닫는다. */
return 0;
}
shutdown() : 서버에서 읽기용 소켓 또는 쓰기용 소켓 둘 중 하나만을 닫을 수 있다. 우아한 close() 함수
멀티 쓰레드 서버-클라이언트 예제
앞선 예제의 경우, 다른 클라이언트가 접속할 경우, 통신이 완료될 때까지 대기 상태가 된다. 쓰레드를 이용하여 병렬 처리를 해줌으로써 이 문제를 해결할 수 있다. 멀티 스레드 방식 외에도 I/O 멀티플랙싱 방식도 사용하는데, 이 경우 클라이언트의 요구 사항이 많지 않다면 하나의 소켓 디스크립터를 효율적으로 사용할 수 있게 해준다.
방식명 | 지원 플랫폼 | FD 수 제한 | 내부 | 구조 성능 (FD 수 증가 시) | 이벤트 등록 방식 | 경량성 / 무거움 | 재사용성 | 스레드 적합성 | 사용 난이도 | 대표 사용 예 |
select | UNIX 계열 (POSIX) | 1024개 (FD_SETSIZE 제한) | 선형 검색 | 낮음 (O(n)) | 매 호출 시 전체 FD 전달 | 경량 | 낮음 | 낮음 | 낮음 | 초기 네트워크 서버 |
poll | UNIX 계열 (POSIX) | 제한 없음 (OS 자원 한도) | 선형 검색 | 낮음 (O(n)) | 매 호출 시 전체 FD 전달 | 중간 | 낮음 | 낮음 | 낮음 | 중기형 네트워크 서버 |
epoll | Linux 전용 | 제한 없음 | 이벤트 기반 비동기 큐 | 높음 (O(1)) | 사전 등록 후 대기 | 경량 | 높음 | 높음 | 중간 | 리눅스 고성능 서버 |
kqueue | BSD 계열, macOS | 제한 없음 | 이벤트 기반 비동기 큐 | 높음 (O(1)~O(log n)) | 사전 등록 후 대기 | 경량 | 높음 | 높음 | 중간~높음 | macOS, FreeBSD 서버 |
IOCP | Windows 전용 | 제한 없음 | 완료 기반 비동기 모델 | 매우 높음 | 비동기 작업 등록 | 무거움 | 높음 | 매우 높음 | 높음 | Windows 고성능 서버 |
select() 함수
파일 디스크리터 변화를 확인하고, 데이터 송수신이 가능한 상태인지 확인
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
nfds | 감시할 FD 중 가장 큰 값 + 1 (예: FD가 4, 7, 10이면 → nfds = 11) : 전체 개수가 아닌 열린 파일 디스크립터의 제일 큰 수 |
readfds | 읽기 감시 대상 FD 집합 (데이터가 들어올 수 있는지 확인) |
writefds | 쓰기 감시 대상 FD 집합 (쓰기 가능 여부 확인) |
exceptfds | 예외 감시 대상 FD 집합 (일반적으로 OOB(out-of-band) 데이터 감지) |
timeout | 대기 시간 설정 - NULL: 무한 대기 - 0: 즉시 반환 - 값 지정: 해당 시간만큼 대기 |
리턴 값
> 0 | 이벤트가 발생한 FD의 개수 |
0 | 타임아웃 발생 (이벤트 없음) |
-1 | 오류 발생 (예: 잘못된 FD, 시그널 인터럽트 등) |
FD 조작 : fd_set에 fd를 추가하거나 삭제한다.
void FD_ZERO(fd_set *set); // FD 집합 초기화
void FD_SET(int fd, fd_set *set); // FD 추가
void FD_CLR(int fd, fd_set *set); // FD 제거
int FD_ISSET(int fd, fd_set *set); // FD가 이벤트 발생했는지 확인
select 사용 예제 - TCP_SELECT_SERVER, TCP_SELECT_CLIENT
폴링 방식을 이용하였다.
/*select server*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define SERVER_PORT 5100
int main(int argc, char **argv){
int ssock;
socklen_t clen;
int n;
struct sockaddr_in servaddr, cliaddr;
char mesg[BUFSIZ];
fd_set readfd;
int maxfd, client_index, start_index;
int client_fd[5] = {0, };
/* 서버 소켓 디스크립터 연다. */
if((ssock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket()");
return -1;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERVER_PORT);
if(bind(ssock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("bind()");
return -1;
}
if(listen(ssock, 8) < 0) { //클라이언트 접속 대기 최대 8개
perror("listen()");
return -1;
}
FD_ZERO(&readfd);
maxfd = ssock;
client_index = 0;
do{
FD_SET(ssock, &readfd);
for(start_index = 0; start_index < client_index; start_index++) {
FD_SET(client_fd[start_index], &readfd);
if(client_fd[start_index] > maxfd)
maxfd = client_fd[start_index]; /* 가장 큰 소켓의 번호를 저장 */
}
maxfd++; //순회 하면서 setup 해주기(폴링을 위하여)
/* select( ) 함수에서 읽기가 가능한 부분만 조사 */
select(maxfd, &readfd, NULL, NULL, NULL); /* 읽기가 가능해질 때까지 블로킹 */
if(FD_ISSET(ssock, &readfd)) { /* 읽기가 가능한 소켓이 서버 소켓인 경우 */
clen = sizeof(struct sockaddr_in); /* 클라이언트의 요청 받아들이기 */
int csock = accept(ssock, (struct sockaddr*)&cliaddr, &clen); //연결 가능한 클라이언트에 대한 정보 제공
if(csock < 0) {
perror("accept()");
return -1;
} else {
/* 네트워크 주소를 문자열로 변경 */
inet_ntop(AF_INET, &cliaddr.sin_addr, mesg, BUFSIZ);
printf("Client is connected : %s\n", mesg);
/* 새로 접속한 클라이언트의 소켓 번호를 fd_set에 추가 */
FD_SET(csock, &readfd);
client_fd[client_index] = csock;
client_index++;
continue;
}
if (client_index == 5) break;
}
/* 읽기 가능했던 소켓이 클라이언트였던 경우 */
for(start_index = 0; start_index < client_index; start_index++) {
/* for 루프를 이용해서 클라이언트들을 모두 조사 */
if(FD_ISSET(client_fd[start_index], &readfd)) {
memset(mesg, 0, sizeof(mesg));
/* 해당 클라이언트에서 메시지를 읽고 다시 전송(Echo) */
if((n = read(client_fd[start_index], mesg, sizeof(mesg))) > 0) {
printf("Received data : %s", mesg);
write(client_fd[start_index], mesg, n);
close(client_fd[start_index]); /* 클라이언트 소켓을 닫는다. */
/* 클라이언트 소켓을 지운다. */
FD_CLR(client_fd[start_index], &readfd);
client_index--;
}
}
}
}while(strncmp(mesg, "q", 1));
close(ssock);
return 0;
}
client는 위의 tcp 클라이언트 프로그램을 그대로 사용하면 된다.
epoll 방식(비동기 처리 방식)
select는 모든 fd에 대해서 송수신 여부를 확인하기 때문에 속도가 느리다. 이러한 단점을 해결하기 위해 만든 방법에 epoll이다.
기존의 방식이 사용자가 fd를 관리하였다면, epoll은 커널이 fd를 관리한다. 순회를 해야했던 select와 다르게 커널이 이벤트가 발생한 파일 디스크립터들만을 구조체 배열로 넘겨 주므로 메모리 카피에 대한 비용이 줄어든다.
함수명 | 설명 |
epoll_create() 또는 epoll_create1() | epoll 인스턴스 생성 |
epoll_ctl() | 감시할 FD를 epoll 인스턴스에 등록/수정/삭제 |
epoll_wait() | 이벤트 발생을 대기하고 반환 |
epoll_ctl() 파라미터
epfd | epoll_create()로 얻은 epoll 인스턴스의 FD |
op | 동작 종류: EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL |
fd | 감시할 파일 디스크립터 |
event | 감시할 이벤트 정보 (struct epoll_event) |
이벤트 구조체
struct epoll_event {
uint32_t events; // 감시할 이벤트 (예: EPOLLIN, EPOLLOUT, EPOLLET)
void *data; // 사용자 정의 데이터 포인터 (예: FD 값 저장 등)
};
EPOLLIN = 읽기 이벤트
EPOLLOUT = 쓰기 가능
EPOLLET = 엣지 트리거 (기본은 레벨 트리거)
epoll_wait() 파라미터
epfd | epoll 인스턴스의 FD |
events | 이벤트 정보가 저장될 배열 포인터 |
maxevents | events 배열의 크기 |
timeout | 대기 시간 (ms 단위)-1: 무한 대기, 0: 즉시 반환 |
epoll_wait event 비트 마스크
비트 마스크 상수 | 16진수 값 | 설명 |
EPOLLIN | 0x00000001 | 읽기 이벤트: 읽을 수 있는 데이터가 있음 (예: 수신 버퍼) |
EPOLLPRI | 0x00000002 | 우선순위 높은 데이터 수신 가능 (Out-of-band data 등) |
EPOLLOUT | 0x00000004 | 쓰기 이벤트: 데이터를 쓸 수 있음 (송신 버퍼에 여유 공간 있음) |
EPOLLRDNORM | 0x00000040 | 일반 데이터 읽기 가능 (EPOLLIN과 유사) |
EPOLLRDBAND | 0x00000080 | 우선순위 밴드 데이터 읽기 가능 |
EPOLLWRNORM | 0x00000100 | 일반 데이터 쓰기 가능 (EPOLLOUT과 유사) |
EPOLLWRBAND | 0x00000200 | 우선순위 밴드 데이터 쓰기 가능 |
EPOLLMSG | 0x00000400 | 사용되지 않음 (무시됨, 호환성 목적) |
EPOLLERR | 0x00000008 | 에러 상태 발생 (자동 감지됨, 명시하지 않아도 감지됨) |
EPOLLHUP | 0x00000010 | 연결 끊김(Hang-up) 발생 (자동 감지됨) |
EPOLLRDHUP | 0x00002000 | 연결의 읽기 종료 감지 (peer가 shutdown(SHUT_WR) 또는 close 호출한 경우) |
EPOLLET | 0x80000000 | Edge Triggered 모드 설정 (기본은 Level Triggered) |
EPOLLONESHOT | 0x40000000 | 이벤트 한 번만 감지 후 자동 해제 (재등록 필요) |
EPOLLEXCLUSIVE | 0x10000000 | 다중 쓰레드 epoll 대기에서 이벤트 독점 감시 (epoll 인스턴스 경쟁 방지) |
epoll 서버 예제
클라이언트는 전에 구현했던 tcp_client를 사용하면 된다.
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define SERVER_PORT 5100
#define MAX_EVENT 32
void setnonblocking(int fd){
int opts = fcntl(fd, F_GETFL);
opts |= O_NONBLOCK;
fcntl(fd, F_SETFL, opts);
}
int main(){
int ssock, csock;
socklen_t clen;
int n, epfd, nfds = 1; /* 기본 서버 추가 */
struct sockaddr_in servaddr, cliaddr;
struct epoll_event ev;
struct epoll_event events[MAX_EVENT];
char mesg[BUFSIZ];
/* 서버 소켓 디스크립터를 연다. */
if((ssock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket()");
return -1;
}
setnonblocking(ssock); /* 서버를 넌블로킹 모드로 설정 */
memset(&servaddr, 0, sizeof(servaddr)); /* 운영체제에 서비스 등록 */
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERVER_PORT);
if(bind(ssock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("bind()");
return -1;
}
if(listen(ssock, 8) < 0) { /* 클라이언트의 소켓들을 위한 큐 생성 */
perror("listen()");
return -1;
}
/* epoll_create() 함수를 이용해서 커널에 등록 */
epfd = epoll_create(MAX_EVENT);
if(epfd == -1) {
perror("epoll_create()");
return 1;
}
/* epoll_ctl() 함수를 통해 감시하고 싶은 서버 소켓을 등록 */
ev.events = EPOLLIN;
ev.data.fd = ssock;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, ssock, &ev) == -1) {
perror("epoll_ctl()");
return 1;
}
do {
epoll_wait(epfd, events, MAX_EVENT, 500);
for(int i = 0; i < nfds; i++) {
if(events[i].data.fd == ssock) { /* 읽기가 가능한 소켓이 서버 소켓인 경우 */
clen = sizeof(struct sockaddr_in);
/* 클라이언트의 요청 받아들이기 */
csock = accept(ssock, (struct sockaddr*)&cliaddr, &clen);
if(csock > 0) {
//새로운 event 추가
/* 네트워크 주소를 문자열로 변경 */
inet_ntop(AF_INET, &cliaddr.sin_addr, mesg, BUFSIZ);
printf("Client is connected : %s\n", mesg);
setnonblocking(csock); /* 클라이언트를 넌블로킹 모드로 설정 */
/* 새로 접속한 클라이언트의 소켓 번호를 fd_set에 추가 */
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = csock;
epoll_ctl(epfd, EPOLL_CTL_ADD, csock, &ev);
nfds++;
continue;
}
} else if(events[i].events & EPOLLIN) { /* 클라이언트의 입력 */
if(events[i].data.fd < 0) continue; /* 소켓이 아닌 경우의 처리 */
memset(mesg, 0, sizeof(mesg));
/* 해당 클라이언트에서 메시지를 읽고 다시 전송(Echo) */
if((n = read(events[i].data.fd, mesg, sizeof(mesg))) > 0) {
printf("Received data : %s", mesg);
write(events[i].data.fd, mesg, n);
/* 클라이언트 소켓을 닫고 지운다. */
close(events[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
nfds--;
}
}
}
} while(strncmp(mesg, "q", 1));
close(ssock); /* 서버 소켓을 닫는다. */
return 0;
}
책에 HTTP 웹 서버를 구현하는 코드가 나오는데 별도의 글로 다룰려고 한다. 글이 너무 길어질 것 같다.
서버를 실행시키는 방법(daemon)
전원을 껐다 켜도 프로그램을 자동으로 실행시키고 싶을 수 있다. 백그라운드에서 자동으로 실행되는 프로그램들을 리눅스에서는 데몬(daemon)이라 부른다.
방법은 .service 파일을 작성하고 systemctl enable daemon.service 방식으로 등록 후 start해주면 실행된다.
서비스 파일
[Unit]
Description=My Custom Daemon
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/mydaemon
Restart=on-failure
User=nobody
Group=nogroup
[Install]
WantedBy=multi-user.target
섹션 | 주요 항목 | 설명 |
[Unit] | Description | 서비스 설명 |
After | 서비스가 시작되기 전 의존 대상 (예: network.target 이후 실행) | |
[Service] | Type | 프로세스 유형: simple, forking, oneshot, notify 등 |
ExecStart | 실행할 명령 또는 바이너리 경로 | |
Restart | 실패 시 재시작 정책 (always, on-failure 등) | |
User, Group | 해당 유저/그룹으로 서비스 실행 | |
[Install] | WantedBy | enable 시 연결될 대상 runlevel (multi-user.target 등) |
/etc/systemd/system 폴더에 저장
#systemd에 서비스 파일 리로드
sudo systemctl daemon-reexec # 또는
sudo systemctl daemon-reload
#서비스 등록 및 시작
sudo systemctl enable mydaemon.service # 부팅 시 자동 실행
sudo systemctl start mydaemon.service # 즉시 실행
#서비스 상태 확인
sudo systemctl status mydaemon.service
#서비스 수동 정지 및 재시작
sudo systemctl stop mydaemon.service
sudo systemctl restart mydaemon.service
정리하자면 통신은 결국
setting -> open -> read/write -> close 이거나,
setting -> open -> bind -> listen -> read/write -> close (for server)
setting -> open -> connect -> read/write -> close (for client)
모두 파일 입출력 때 했던 내용과 매우 유사한 모습을 보이는 것을 알 수 있다.(리눅스의 특징이기도 하다!!)
출처 : https://github.com/valentis/LinuxProgrammingWithRaspberryPi/tree/master
GitHub - valentis/LinuxProgrammingWithRaspberryPi: Linux Programming With Raspberry Pi
Linux Programming With Raspberry Pi. Contribute to valentis/LinuxProgrammingWithRaspberryPi development by creating an account on GitHub.
github.com
'컴퓨터 과학 > 리눅스 프로그래밍' 카테고리의 다른 글
[이론]리눅스 드라이버 원리 (1) | 2025.05.19 |
---|---|
[이론] 스레드(Thread)와 뮤텍스 스레드(Mutex Thread) (0) | 2025.05.08 |
[이론] 세마포어(Semaphore) (0) | 2025.05.08 |
[이론]리눅스 프로그래밍 - 프로세스와 쓰레드 (1) | 2025.05.08 |
[이론] 리눅스 프로그래밍 - 입출력 함수 (0) | 2025.05.06 |