이 글은 학교 네트워크 프로그래밍 과목의 기말 시험을 공부하기 위한 글
서버 프로그램은 다수의 클라이언트를 처리할 수 있어야 한다.
대표적인 다중 클라이언트 처리 기술
-- 입출력 다중화
-- 멀티 프로세스
-- 멀티 쓰레드
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();
...
}
// 클라이언트에서 온 데이터를 서버에서 받았다가 똑같이 클라이언트로 되돌려 줌
#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;
}
#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;
}
#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 표준을 따르는 pthread
-- Multi thread 프로그래밍을 위한 API 제공
뛰어난 호환성
-- 윈도우를 제외한 모든 Unix계열 운영체제 지원
pthread에 비해 뛰어난 성능이여도, 이식성과 유지/보수 상 문제로 대부분 pthread 사용
멀티 프로세스와 동일
accept 함수 호출 후 스레드 생성
-- fork 함수 대신 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);
-- 해당 메인 스레드로부터 분리
-- 스레드 종료와 동시에 자원 회수
#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이면, 이에 대응하는 파일에 대해서 입출력 함수 호출
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
nfds : 파일 테이블의 최대 크기
readfds : 읽기 이벤트를 검사할 파일 정보를 포함한 비트 테이블
writefds : 쓰기 이벤트를 검사할 파일 정보를 포함한 비트 테이블
exceptfds : 예외 이벤트를 검사할 파일 정보를 포함한 비트 테이블
timeout : 이벤트를 기다릴 시간 제한
반환 값 : 이벤트가 발생한 파일의 갯수
최대 파일 지정 번호 + 1만큼 검사
2와 7 두개의 파일만 검사할 경우에도 8개의 필드를 모두 검사해야 함
-- 열린 소켓을 저장하는 별도의 자료구조를 이용해서 루프 빈도를 줄일 수는 있음.
fd_set은 이전 상태를 기억하지 못함
매번 파일 테이블을 복사해야 함
fd_set readfds;
readfds(0, 2, 4, 7);
select(int nfds, &readfds, ....);
데이터 처리가 긴 서비스에 적합하지 않음
동시 처리가 아닌 순차적 처리
앞의 서비스 처리가 늦어지면, 그 시간만큼 지연 발생
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
);
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, ©fds, 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, ©fds)) {
readn = read(i, buf, MAXLINE);
if(readn < 1) {
close(i);
FD_CLR(sockfd, &readfds);
break;
}
write(i, buf, readn);
}
}
#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;
}
}
}
}
학교 교안보고 어지러웠는데 여기서 성불하고갑니다,,,