동시 파일 전송 프로그램
github 주소: https://github.com/HeesangJin/adv-sys-programming
1. 프로그램 설명
1) 1개의 서버와 20개의 클라이언트, 20개의 1MB 파일이 있다.
2) 각각의 클라이언트는 1MB 파일을 한 줄씩 읽어 서버에게 전송한다.
3) 서버는 이 파일들을 전송받음과 동시에 20개의 클라이언트에게 전송해준다.
4) 모든 클라이언트는 이 파일들을 각각 하나의 파일들로 저장한다.
5) 결과적으로 20개의 20MB 파일이 생성된다.
2. 프로그램 동작 흐름
쉬운 설명을 위해 클라이언트, 파일의 수는 3개로 가정한다.
A. 1개의 서버와 3개의 클라이언트 3개의 1MB 파일 3개가 있다.
B. 각 클라이언트는 파일의 일부분을 서버로 보낸다.
C. 서버는 이 파일들을 받는다.
(그림처럼 파일을 하나로 모으진 않는다.)
D. 서버는 받은 파일들을 모든 클라이언트에게 보낸다.
E. 클라이언트는 서버에게 받은 파일들을 저장함과 동시에 아직 보내지 못한 파일의 일부분을 보낸다.
F. 마찬가지로 서버는 이 파일을 모든 클라이언트에게 보낸다.
G. 모든 파일들에 대해 이 작업이 이루어질 때까지 위 작업을 반복한다.
H. 모든 파일의 대해 작업이 이루어지면 프로그램을 마친다.
이 때 각 out파일들은 같은 파일구성을 갖는다.
실제로는 그림과 다르게 out파일의 순서(색깔)가 무작위이다.
3. 프로그램 세부 동작 흐름
- 라즈베리파이에서 채팅 서버를 실행
- 서버: main 시작하자마자 시작 시간 기록
- 20개의 클라이언트를 실행 (독립적인 프로세스 또는 thread)
- 모든 클라이언트가 서버에 연결
- 서버는 20개의 클라이언트가 연결된 것을 확인하고
모든 클라이언트에 문자 ‘$’를 보냄 - 각 클라이언트는 1MB의 Text 데이터를 서버에 한줄씩 송신함과 동시에
수신된 메시지를 각자 파일에 저장
서버는 각 클라이언트에서 수신된 text 데이터를 모든 20개의 클라이언트에 송신 - 클라이언트는 모든 데이터를 송신한 뒤 ‘@’를 송신 (이 ‘@’는 서버가 수신만 하고 클라이언트에 전달하지 않음)
- 서버는 모든 클라이언트에 수신된 Text를 모두 보낸 뒤에, 20개 클라이언트에 ‘%’를 송신
- 클라이언트는 ‘%’를 수신하면 연결을 종료
- 서버는 모든 클라이언트와의 연결이 끊어진 것을 확인하고 연결 종료
- 서버: 종료 시간 측정
- 최종적으로 소요된 시간을 출력하고 종료
4. 실행 예시
1) Input
20개의 1M 파일이 존재한다.
2) Processing
3) Output
20개의 20M 파일이 만들어진다.
5. 시도한 것
1) select 대신 epoll 사용
어떤 I/O들에 대한 요청이 왔는지 알기 위해 등록 된 모든 I/O를 탐색하여 처리하는 select와 달리
요청 된 I/O들 만을 탐색하여 처리하는 epoll을 사용하였다.
2) pthread 사용
다수의 프로세스를 실행하는 대신, 단일 프로세스에서 여러개의 스레드를 생성하도록 하였다.
1개의 서버와 20개의 클라이언트가 모두 단일 프로세스에서 돌아간다.
즉, 21개의 스레드가 만들어진다.
3) NON BLOCKING 함수
이 프로그램의 클라이언트 부분(launch_chat())을 보면
nfds = epoll_wait(epollfd, events, MAX_EVENTS, 0)
이런 부분이 있다.
epoll_wait()는 현재 등록된 I/O요청을 기다리는 함수이다.
이때, 이 함수의 마지막 인자값에 들어간 0은 NON-BLOCKING 모드를 뜻한다.
만약 -1 이라면 BLOCKING 모드를 뜻한다.
BLOCKING 모드일때는 epoll_wait() 함수가 I/O요청이 하나도 없을 경우, I/O요청이 올 때까지 기다린다. 즉, 이 함수의 다음 부분이 실행되지 않는 다는 뜻이다.
NON-BLOCKING 모드일때는 epoll_wait() 함수가 I/O요청이 하나도 없을 경우, 즉시 실패값을 반환한다. 즉, 이 함수의 다음 부분이 실행된다는 다는 뜻이다.
이 프로그램은 클라이언트의 while(1){...} 에서
while문이 한번 돌 때마다, 파일을 줄씩 읽고 나서 서버로 부터의 I/O요청을 epoll_wait()로 검사한다.
만약 BLOCKING 모드일 때, 서버로 부터의 I/O요청이 없을 경우(이미 다른 클라이언트들이 각자의 파일을 모두 읽었을 경우 or 시간이 잠깐 비는 경우)에 epoll_wait()에서 프로그램이 멈춘다.
그렇기 때문에 I/O요청이 없더라도 파일을 계속 한줄 씩 읽도록 epoll_wait()함수를 NON-BLOCKING 모드로 설정하였다.
6. 성능 측정
프로그램의 "종료 시간 - 시작 시간" 을 이용하여 프로그램 실행시간을 측정하였다.
정확한 측정을 위해 3번의 실행시간을 평균내도록 한다.
환경: 라즈베리파이2
평균 Processing time 153초
7. 결론 및 느낀 점
기존에는 파일 입출력에 대해
무조건 한번에 읽어다가 한번에 처리하는 방식을 처리했다면,
버퍼를 이용해 한번에 처리할 수 없는 대용량 파일에 대해
A. BLOCKING, NON-BLOCKING 모드
B. read(), write(), send(), recv()에 대한 return값 활용
활용이 필수적이라는 것이 굉장히 인상적이었다.
8. 코드 설명
1) 서버 launch_server(void)
serverSock = socket(PF_INET, SOCK_STREAM, 0)
bind(serverSock, (struct sockaddr *)&Addr,sizeof(Addr)
listen(serverSock, 20)
A. 소켓을 열고 이 소켓을 통해 클라이언트의 요청을 기다린다.
epollfd = epoll_create(20);
ev.events = EPOLLIN;
ev.data.fd = serverSock;
epoll_ctl(epollfd, EPOLL_CTL_ADD, serverSock, &ev
B. epoll을 이용해 단일 스레드(서버)에서 다중 I/O요청을 처리한다.
위에서 열었던 소켓을 epoll에 등록한다.
이를 이용해 다수(20개)의 클라이언트로 부터 한꺼번에 요청을 받을 수 있다.
while (num_accepted<TOTAL_CLIENT) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1)
for(j=0; j<nfds; j++){
if ((acceptedSock[num_accepted++] = accept(serverSock, (struct sockaddr*)&Addr, &AddrSize)) < 0) {
goto error;
}
ev.data.fd = acceptedSock[num_accepted-1];
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, acceptedSock[num_accepted-1], &ev) == -1) {
exit(EXIT_FAILURE);
}
}
}
C. 20개의 클라이언트로 부터 연결요청을 받아 acceptedSock배열에 각 소켓 디스크립터를 저장한다.
for(j=0; j<num_accepted; j++){
int ch = '$';
if ((ret = send(acceptedSock[j], &ch, 1, 0)) < 0)
goto error;
}
D. 20개의 클라이언트에 파일 전송 요청($)을 보낸다.
E. epoll을 이용한 I/O 처리 부분
while (1) {
//1. 모든 클라이언트로 부터 연결이 끊기면 while문을 종료한다.
if (num_disconnect == num_accepted)
break;
//2. @를 20개 수신받으면 모든 클라이언트에게 %송신한다.
if(end_server == 0&& num_end == num_accepted){
for(j=0; j<num_accepted; j++){
int ch = '%';
if ((ret = send(acceptedSock[j], &ch, 1, 0)) < 0) {
perror("send");
close(acceptedSock[j]);
break;
}
}
end_server = 1;
}
//3. I/O 요청을 기다린다.
if ((nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1)) == -1) {
perror("epoll_pwait");
exit(EXIT_FAILURE);
}
//4. I/O요청을 하나씩 처리한다.
for(j=0; j<nfds; j++){
//4.1 특정 클라이언트쪽으로 부터 연결이 종료 되면
if (!(ret = count = recv(events[j].data.fd, data, MAX_DATA, 0))) {
//fprintf(stderr, "Connect Closed by Client\n");
num_disconnect++;
//해당 클라이언트의 epoll 등록을 취소한다.
ev.events = EPOLLIN;
ev.data.fd = events[j].data.fd;
if (epoll_ctl(epollfd, EPOLL_CTL_DEL, events[j].data.fd, &ev) == -1) {
perror("epoll_ctl: events[j].data.fd");
exit(EXIT_FAILURE);
}
break;
}
//4.2 클라이언트로 부터 메시지를 받으면
else{
// 버퍼에 @가 있는지 체크한다. -> 20개의 @를 받을 때까지
for (i = 0; i < count; i++)
if(data[i]=='@'){
num_end++;
}
//모든 클라이언트에 메시지를 쏴준다.
for(j=0; j<num_accepted; j++){
int temp_count = count;
p = data;
while (temp_count) {
if ((ret = send(acceptedSock[j], p, temp_count, 0)) < 0) {
perror("send");
close(acceptedSock[j]);
break;
}
temp_count -= ret;
p = p + ret;
}
}
}
}
2) 클라이언트 launch_chat(int cindex)
sprintf(file_name, "/tmp/file_%.4d",cindex);
file_in = fopen(file_name,"r");
if (file_in == NULL) perror ("Error opening file");
sprintf(file_name, "output_%.4d",cindex);
file_out = fopen(file_name,"w");
if (file_out == NULL) perror ("Error opening file");
A. input, output 파일을 open한다.
clientSock = socket(PF_INET, SOCK_STREAM, 0)
connect(clientSock, (struct sockaddr*)&serverAddr, sizeof(serverAddr))
B. 소켓을 열어 서버에 연결 요청을 보낸다.
epollfd = epoll_create(5);
ev.events = EPOLLIN;
ev.data.fd = clientSock;
epoll_ctl(epollfd, EPOLL_CTL_ADD, clientSock, &ev);
C. 위 소켓을 epoll에 등록한다.
E. epoll을 이용한 I/O 처리 부분
while (1) {
//1. 모든 작업이 끝나면 종료한다.
if(end_program == 1)
break;
//2. 파일을 read해 한 줄 씩 읽는다.
if(start_read == 1 && end_read == 0){
//2.1 파일의 한 줄을 읽어 서버로 보낸다.
if(fgets(rdata, MAX_DATA, file_in)){
int temp_count = strlen(rdata);
p = rdata;
while (temp_count) {
if ((ret = send(clientSock, rdata, temp_count, 0)) < 0) {
perror("send");
close(clientSock);
break;
}
temp_count -= ret;
p = p + ret;
}
}
//2.2 만약 파일을 읽었으면 서버로 '@'문자를 보낸다.
else{
int ch = '@';
if ((ret = send(clientSock, &ch, 1, 0)) < 0) {
perror("send");
close(clientSock);
break;
}
fclose(file_in);
end_read = 1;
}
}
//3. I/O 요청을 기다린다.(NON BLOCKING MODE)
if ((nfds = epoll_wait(epollfd, events, MAX_EVENTS, 0)) == -1) {
perror("epoll_pwait");
exit(EXIT_FAILURE);
}
//4. I/O 요청을 하나씩 처리한다..
for(j=0; j<nfds; j++){
if (events[j].data.fd == clientSock){
int k;
int remain;
char *p;
//4.1 에러: 서버가 먼저 종료됐을 경우
if (!(remain = ret = recv(clientSock, rdata, MAX_DATA, 0))) {
printf("Connection closed by remote host\n");
goto leave1;
}
p = rdata;
remain=0;
//4.2 서버로 부터 전송받은 데이터에 대한 처리
for (k = 0; k < ret; k++){
//a. '$' 가 왔을 경우: 파일 읽기를 시작한다.
if(rdata[k]=='$'){
start_read = 1;
}
//b. '%' 가 왔을 경우: 파일 읽기를 종료한다.
else if(rdata[k]=='%'){
end_program = 1;
}
//c. '$', '%', '@' 심볼이 왔을 경우에는 이를 무시하고 나머지 데이터만 out파일에 저장한다.
if(rdata[k]=='@' || rdata[k]=='$' || rdata[k]=='%'){
fwrite(p, 1, remain, file_out); //이 전 위치까지 파일에 쓰고(이전까지: remain개)
p = &rdata[k] + 1; //다음 위치 부터 다시 카운트
remain=0;
continue;
}
remain++;
}
fwrite( p, 1, remain, file_out);
}
}
}
'옛날' 카테고리의 다른 글
Load balancing 알고리즘 비교&분석 (0) | 2016.12.19 |
---|---|
linux커널 2.4 context-switch 과정 (수정중) (0) | 2016.12.19 |
댓글