[C] Linux Multi-Process Socket Programming

박세윤·2022년 6월 4일
0
post-thumbnail

이 글은 학교 네트워크 프로그래밍 과목의 기말 시험을 공부하기 위한 글



📌 Linux Multi-Process Socket Programming

✍ 멀티 프로세스와 소켓 프로그래밍

  • 서버 프로그램은 다수의 클라이언트를 처리할 수 있어야 한다.

  • 대표적인 다중 클라이언트 처리 기술
    -- 입출력 다중화
    -- 멀티 프로세스
    -- 멀티 쓰레드


  • fork() 이용하여 클라이언트 코드를 분리

  • accept() 호출 후 fork() 함수 호출


  • accept() 함수로 "듣기 소켓"과 "연결 소켓" 분리

  • 자식 프로세스 : 연결 소켓, 클라이언트와 통신

  • 부모 프로세스 : 듣기 소켓, accept 호출



✍ 소켓 지정 번호 복사

  • 자식 프로세스를 만들 때, 부모 프로세스의 자원이 복사된다.
    -- 코드
    -- 소켓을 포함한 모든 열린 파일들
    -- 시그널



✍ 멀티 프로세스 기술의 장점

  • 단순한 프로그램 흐름
    -- accept 후 fork 함수 호출

  • 오랜 시간 검증된 흐름
    -- 유닉스는 멀티 프로세스 기반으로 시작
    -- 멀티 스레드 기술은 비교적 최근 도입

  • 안정적인 동작
    -- 독립된 프로세스로 작동
    -- 프로세스의 잘못된 작동이 다른 프로세스에 영향을 미치지 않음 (최소화)



✍ 멀티 프로세스 기술의 장점

  • 프로세스 복사에 따른 성능 문제
    -- 프로세스가 새로 생성되기 때문에 많은 CPU/Memory 비용 소모
    -- 코드 중복
    -- 연결과 종료가 빈번한 서비스에서 연결 지연 우려

  • 프로세스간 정보 교환이 어렵다.
    -- 독립된 프로세스로 작동
    -- 프로세스간 통신에 IPC 이용해야 함



✍ 멀티 프로세스 기반 소켓 프로그램 개발

  • 자식 프로세스를 wait()로 기다리지 않으면 좀비 프로세스가 됨.

  • signal()로 좀비 프로세스가 되지 않도록 막을 수 있음

// socket -> bind -> listen
signal(SIGCHLD, SIG_IGN);
for(;;) {
	connectfd = accept(listenfd...);
    pid = fork();
    ...
}



✍ echo_server.c 코드

// 클라이언트에서 온 데이터를 서버에서 받았다가 똑같이 클라이언트로 되돌려 줌

#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAXBUF 1024
#define PORTNUMB 3500

int main(int argc, char* argv[]) {
	int server_sockfd, client_sockfd;
    int client_len, n;
    char buf[MAXBUF];
    struct sockaddr_in clientaddr, serveraddr;
    client_len = sizeof(clientaddr);
    
    // IPPROTO_TCP : 0 대신에 IPPROTO_TCP를 넣으려면 #include <arpa/inet.h> 헤더파일 필요
    if((server_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
    	perror("socket error : ");
        exit(0);
    }
    
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(PORTNUM);
    
    // bind, listen 할때 -1테스트 하면 좋음 여기선 안했긴했는데
    bind(server_sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(server_sockfd, 5);
    
    while(1) {
    	memset(buf, 0x00, MAXBUF);
        client_sockfd = accept(server_sockfd, (struct sockaddr *)&clientaddr, &client_len);
        printf("New Client Connect: %s\n", inet_ntoa(clientaddr.sin_addr));
        
        if((n = read(client_sockfd, buf, MAXBUF)) <= 0) {
        	close(client_sockfd);
            continue;
        }
        printf("Read Data : %s", buf);
        
        if(write(client_sockfd, buf, MAXBUF) <= 0) {
        	perror("write error : ");
            close(client_sockfd);
        }
        close(client_sockfd);
    }
    close(client_sockfd);
    return 0;
}



✍ echo_client.c 전체 코드

#include <unistd.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>

#define MAXLINE 1024
#define PORTNUM 3500
#define ADDR "127.0.0.1" // ip주소까지 넣어버려도됨.

int main(int argc, char* argv[]) {
	struct sockaddr_in serveraddr;
    int server_sockfd;
    int client_len;
    char buf[MAXLINE];
    char rbuf[MAXLINE];
    
    if ((server_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    	perror("error : ");
        return 1;
    }
    
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(ADDR);
    serveraddr.sin_port = htons(PORTNUM);
    
    client_len = sizeof(serveraddr);
    
    if(connect(server_sockfd, (struct sockaddr *)&serveraddr, client_len) < 0) {
    	perror("connect error : ");
        return 1;
    }
    
    memset(buf, 0x00, MAXLINE);
    read(0, buf, MAXLINE);
    if(write(server_sockfd, buf, MAXLINE) <= 0) {
    	perror("write error : ");
        return 1;
    }
    
    memset(buf, 0x00, MAXLINE);
    if(read(server_sockfd, buf, MAXLINE) <= 0) {
    	perror("read error : ");
        return 1;
    }
    close(server_sockfd);
    printf("read : %s", buf);
    return 0;
}



✍ echo_server_fork.c

  • fork를 이용해서 다중 클라리언트 서비스를 해보자
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>

#define MAXLINE 1024
#define PORTNUM 3500

int main(int argc, char**argv) {
	int listen_fd, client_fd;
    pid_t pid;
    socklen_t addrlen;
    int readn;
    char buf[MAXLINE];
    struct sockaddr_in client_addr, server_addr;
    
    if((listen_fd = socket(AF_INET, SOCK_STREAM. 0)) < 0)
    	return 1;
        
    memset((void *)&server_addr, 0x00, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORTNUM);
    
    if(listen(listen_fd, 5) == -1) {
    	perror("listen error");
        return 1;
    }
    
    signal(SIGCHLD, SIG_IGN);
    
    while(1) {
    	addrlen = sizeof(client_addr);
        client_fd = accept(lisen_fd, (struct sockaddr *)&client_addr, &addrlen);
        if(client_fd == -1) {
        	printf("accept error\n");
            break;
        }
        pid = fork();
        if(pid == 0) {
        	memset(buf, 0x00, MAXLINE);
            while((readn = read(client_fd, buf, MAXLINE)) > 0) {
            	printf("Read Data %s : %s", inet_ntoa(client_addr.sin_addr), buf);
                write(client_fd, buf, strlen(buf));
                memset(buf, 0x00, MAXLINE);
            }
            close(client_fd);
            return 0;
        }
	}
    
    close(server_fd);
    return 0;
}



✍ 멀티 스레드?

  • 최소 실행단위 : 프로세스

  • but, 스위칭 : 코드 단위 실행

  • 하나의 프로세스에 여러 코드를 스위칭 할수 있도록 할수 있을까?


  • 프로세스를 새로 생성하는 대신, 스레드(코드 흐름 단위)를 만들고 이들 사이를 스위칭한다.

  • 스레드는 Light Weight Process라고 부른다.



✍ 멀티 스레드 방식의 장점

  • 빠르게 실행됨
    -- 프로세스를 새로 생성에 드는 비용을 절약할 수 있다.

  • 데이터 교환이 쉽다
    -- 파일, Heap, Static, Code의 많은 부분 공유

  • CPU를 잘 활용
    -- 멀티코어에서 코어에 스레드를 할당하는 방식으로 동작



✍ 멀티 스레드 방식의 단점

  • 프로그래밍 난이도가 올라간다
    -- 직관적이지 않음
    -- 문맥의 흐름 예상 어려움

  • 불안
    -- 스레드에서 발생한 문제가 다른 스레드에 영향을 미친다 (데이터 공유 때문)

  • 디버깅 어려움

  • 제대로 만들기 어려움
    -- 병렬 프로그래밍, 공유 자원 관리는 높은 기술 숙련도를 요구



✍ 네트워크 프로그램에서 멀티 스레드 방식의 장점

  • 대량의 데이터 처리에 적합
    -- CPU 자원을 효율적으로 사용
    -- 멀티 프로레스 방식에 비해 빠른 스레드 생성

  • 데이터 교환이 쉬움
    -- IPC를 사용하지 않고, 데이터 교환 가능

  • 기술의 성숙

  • 다른 기술과의 융합 수월



✍ POSIX Thread : pthread

  • POSIX 표준을 따르는 pthread
    -- Multi thread 프로그래밍을 위한 API 제공

  • 뛰어난 호환성
    -- 윈도우를 제외한 모든 Unix계열 운영체제 지원

  • pthread에 비해 뛰어난 성능이여도, 이식성과 유지/보수 상 문제로 대부분 pthread 사용



✍ 멀티 스레드 소켓 프로그램의 흐름

  • 멀티 프로세스와 동일

  • accept 함수 호출 후 스레드 생성
    -- fork 함수 대신 pthread_create 함수 호출



✍ 스레드 생성

  • pthread_create
#include <pthread.h>

int pthread_create(pthread_t *thread, pthread_attr_t *attr, void * (*start_routine)(void *), void * arg);
  • start_routine : 스레드 함수

  • arg : 스레드 함수로 넘길 매개 변수


void *myfunc(void *data) {
	int fd = *(int *)data;
    read(fd, ...);
    write(fd, ...);
}

int main() {
	pthread_t pt;
    int fd = socket(...);
    bind() -> listen();
    while(1) {
    	clifd = accept(...);
        pthread_create(&pt, NULL, myfunc, (void *)*clifd);
    }
}



✍ 스레드 기다리기

  • pthread_join(pthread_t th, void **thread_return);
    -- 스레드 종류 후 join 함수를 호출해야만 자원을 회수 가능

  • pthread_detach(pthread_t th);
    -- 해당 메인 스레드로부터 분리
    -- 스레드 종료와 동시에 자원 회수



✍ Thread 정보 확인

  • ps -eLf | grep count_thread



✍ echo_server_thread.c 전체 코드

#include <pthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define MAXLINE 1024
#define PORTNUM 3500

void * thread_func(void *data) {
	int sockd = *((int *)data);
    int readn;
    socklen_t addrlen;
    char buf[MAXLINE];
    struct sockaddr_in client_addr;
    memset(buf, 0x00, MAXLINE);
    addrlen = sizeof(client_addr);
    getpeername(sockfd, (struct sockaddr *)&client_addr, &addrlen); // getpeername : 실질적으로 클라이언트 정보를 얻어내는 함수
    while((readn = read(sockfd, buf, MAXLINE)) > 0) {
    	printf("Read Data %s (%d) : %s", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buf);
        write(sockfd, buf, strlen(buf));
        memset(buf, 0x00, MAXLINE);
    }
    close(sockfd);
    printf("worker thread end\n");
    return 0;
}

int main(int argc, char **argv) {
	int listen_fd, client_fd;
    socklen_t addrlen;
    int readn;
    char buf[MAXLINE];
    pthread_t thread_id;
    
    struct sockaddr_in server_addr, client_addr;
    
    if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 
    	return 1;
        
    memset((void *)&server_addr, 0x00, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORTNUM);
    
    if(bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
    	perror("bind error");
        return 1;
    }
    
    if(listen(listen_fd, 5) == -1) {
    	perror("listen error");
        return 1;
    }
    
    while(1) {
    	addrlen = sizeof(client_addr);
        client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &addrlen);
        
        if(client_fd == -1)
        	printf("accept error\n");
        else {
        	pthread_create(&thread_id, NULL, thread_func, (void *)&client_fd);
            pthread_detach(thread_id);
        }
    }
    return 0;
}



✍ 하나의 프로세스에서 여러개의 입출력을 다룰 수 없을까?

  • 멀티 프로세스 방법은 많은 비용을 필요로 한다.

  • 멀티 쓰레드 방법도 많은 비용이 소모된다.

  • 입출력을 사건(이벤트로) 다룬다면, 어떨까?

  • 관리할 파일의 그룹을 만들고 그룹의 파일에 입출력 이벤트가 있는지 확인

  • 입출력 이벤트가 발생한 파일의 목록을 일괄 처리

  • 순차처리이긴한데 event-based라서 동시에 처리하는것 처럼 보임 (병렬처리 인것처럼)



✍ 입출력 다중화

  • 하나의 프로세스에서 입력과 출력을 다룬다

  • 프로세스 혹은 스레드를 만들 필요가 없다
    -> 이때문에 비용 세이브가 좋아 많은 기술들이 io multiplexing (입출력 다중화) 기술을 사용한다.

  • 다른 많은 기술들이 입출력 다중화에 기반

  • 하나의 프로세스에서 이벤트를 handling 하는 구조



✍ 입출력 다중화 매커니즘

  • 입출력 이벤트를 검사할 파일의 정보를 가지는 비트 테이블 준비

  • 비트 테이블에 검사할 파일을 체크

  • 체크한 파일에 이벤트가 발생하면, 이벤트가 발생한 비트 필드의 값을 1로 해서 반환

  • 비트 테이블을 검사해서 비트 필드가 1이면, 이에 대응하는 파일에 대해서 입출력 함수 호출



✍ select() 함수

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
  • nfds : 파일 테이블의 최대 크기

  • readfds : 읽기 이벤트를 검사할 파일 정보를 포함한 비트 테이블

  • writefds : 쓰기 이벤트를 검사할 파일 정보를 포함한 비트 테이블

  • exceptfds : 예외 이벤트를 검사할 파일 정보를 포함한 비트 테이블

  • timeout : 이벤트를 기다릴 시간 제한

  • 반환 값 : 이벤트가 발생한 파일의 갯수


  • nfds = 최대 파일 지정 번호 + 1
    -- 비트 테이블은 비트 array로 0번째부터 시작
    -- ex) 7번 소켓이 만들어졌다면, 7+1 == 8



✍ select()의 문제점

  • 최대 파일 지정 번호 + 1만큼 검사

  • 2와 7 두개의 파일만 검사할 경우에도 8개의 필드를 모두 검사해야 함
    -- 열린 소켓을 저장하는 별도의 자료구조를 이용해서 루프 빈도를 줄일 수는 있음.


  • fd_set은 이전 상태를 기억하지 못함

  • 매번 파일 테이블을 복사해야 함

fd_set readfds;
readfds(0, 2, 4, 7);
select(int nfds, &readfds, ....);


  • 데이터 처리가 긴 서비스에 적합하지 않음

  • 동시 처리가 아닌 순차적 처리

  • 앞의 서비스 처리가 늦어지면, 그 시간만큼 지연 발생



✍ select() 함수 매크로 사용

  • select는 fd_set의 제어가 핵심

  • fd_set 비트 연산을 돕기 위한 매크로 함수 제공
    -- 비트 테이블 초기화
    -- 비트 테이블 값 설정
    -- 비트 테이블 값 검사

  • fd_set 테이블의 초기화
    -- FD_ZERO(fd_set *fds);

  • fd_set 테이블에 검사할 파일 목록 추가
    -- FD_SET(int fd, fd_set *fds);

  • fd_set 테이블에서 파일 삭제
    -- FD_CLR(int fd, fd_set *fds);

  • fd_set 테이블을 검사
    -- FD_ISSET(int fd, fd_set *fds);



✍ select() 함수 사용 예시

int main() {
	fd_set readfds;
    int maxfd;
    listen_fd = socket(..);
    bind(..);
    listen(..);
    
    // fd_set을 초기화 함 (0으로)
    FD_ZERO(&readfds);
    FD_SET(listen_fd, &readfds);
    maxfd = listen_fd + 1; // 검사할 비트 테이블 크기 설정
}

보통 0, 1, 2는 standard로 정해져있으니까 3부터 할당


fd_set copyfds; // 카피할 테이블
for(;;) {
	copyfds = readfds;
    // select 함수로 입출력 이벤트 대기
    fd_num = select(maxfd + 1, &copyfds, NULL, NULL, NULL);
    
    // 만약 listen socket으로 부터 데이터 입력이라면 
    // accept 함수를 호출하고, connected socket을 fd_set에 추가
    
    if(FD_ISSET(listen_fd, &readfds)) {
    	client_fd = accept(...);
        FD_SET(client_fd, &readfds);
        if(client_fd > maxfd)
        	maxfd = client+fd;
    }
}

// 입출력 소켓의 이벤트라면 maxfd 만큼 루프를 돌면서 이벤트가 발생한 소켓인지를 확인한 후 데이터를 처리한다.
for(i=0; i<maxfd; i++) {
	if(FD_ISSET(i, &copyfds)) {
    	readn = read(i, buf, MAXLINE);
        if(readn < 1) {
        	close(i);
            FD_CLR(sockfd, &readfds);
            break;
        }
        write(i, buf, readn);
    }
}



✍ echo_server_select.c 전체 코드

#include <sys/time.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>

#define MAXLINE 1024
#define PORTNUM 3500
#define SOCK_SETSIZE 1021

int main(int argc, char **argv) {
	int listen_fd, client_fd;
    socklen_t addrlen;
    int fd_num;
    int maxfd = 0;
    int sockfd;
    int i=0;
    char buf[MAXLINE];
    fd_set readfds, allfds;
    
    struct sockaddr_in server_addr, client_addr;
    
    if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    	perror("socket error");
        return 1;
    }
    memset((void *)&server_addr, 0x00, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY)
    server_addr.sin_port = htons(PORTNUM);
    
    if(bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
    	perror("bind error");
        return 1;
    }
    if(listen(listen_fd, 5) == -1) {
    	perror("listen error");
        return 1;
    }
    
    FD_ZERO(&readfds);
    FD_SET(listen_Fd, &readfds);
    
    maxfd = listen_fd; // 여기 +1 안해줄거면 뒤에서 select할때 +1해주면됨
    while(1) {
    	allfds = readfds;
        printf("Select Wait %d\n", maxfd);
        fd_num = select(maxafd + 1, &allfds, (fd_set *)0, (fd_set *)0, NULL); // 사실상 뒤에 null, null, null
        
        if(FD_ISSET(listen_fd, &allfds)) {
        	addrlen = sizeof(client_addr);
            client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &addrlen);
            
            FD_SET(client_fd, &readfds); // client꺼도 추가해야된다. select에서 client꺼 봐서 readwrite 해줘야 하기때문
            
            if(client_fd > maxfd) 
            	maxfd = client_fd;
                
            printf("Accept OK\n");
       	    continue;
            
        }
        
        // select의 단점 0번부터 다 뒤져봐야됨 세팅됬나안됬나
        for(i=0; i<=maxfd; i++) {
        	sockfd = i;
            if(FD_ISSET(sockfd, &allfds)) {
            	memset(buf, 0x00, MAXLINE);
                if(read(sockfd, buf, MAXLINE) <= 0) {
                	close(sockfd);
                    FD_CLR(sockfd, &readfds);
                }
                else {
                	if(strncmp(buf, "quit\n", 5) == 0) {
                    	close(sockfd);
                        FD_CLR(sockfd, &readfds);
                        break;
                    }
                    printf("Read : %s", buf); // 읽은거 출력하고
                    write(sockfd, buf, strlen(buf)); // 다시 client에게 넣어주고
                }
                if(--fd_num <= 0)
                	break;
            }
        }
    }
}
profile
개발 공부!

1개의 댓글

comment-user-thumbnail
2022년 11월 17일

학교 교안보고 어지러웠는데 여기서 성불하고갑니다,,,

답글 달기