지난번에 멀티 스레드 채팅 서버를 만든 적이 있다. (궁금하면 여기를 참고)
이번 포스팅은 멀티 프로세스 채팅 서버를 C로 구현해봤다.
(사실 친구의 수업에서 진행하는 프로젝트인데 재미있어 보여서 따라 했다.)
C언어 + fork의 조합 때문에 전체적인 코드가 길어지기 때문에 주요 로직에 대해서만 정리를 해야겠다.
서버 코드
서버의 코드에서 사용한 공유 메모리 종류이다.
전체적인 채팅 알고리즘을 아래와 같이 설계했다.
- 메세지 헤더, 메세지 유저, 메세지 데이터에 해당하는 공유메모리 영역을 선언한다.
- 메세지 헤더에는 현재 메세지의 번호가 담기며, 코드를 실행하면 0으로 초기화된다.
- 메인 코드 : 클라이언트 들의 접속을 허가하고 fork를 통해 채팅 알고리즘을 수행하는 프로세스를 생성한다.
- 채팅 프로세스 : 내부에서 또 한 번의 fork를 수행한다.
- 수신 프로세스 : 클라이언트에서 메세지를 받으면 메세지 번호가 올라가고 메세지 유저와 메세지 데이터를 갱신한다.
- 발신 프로세스 : 메세지 헤더를 항상 참조하며(바쁜 대기) 메세지 헤더가 변경될 시 새로운 메세지를 각 클라이언트에게 전송한다.
mutex 변수를 통해 세 개의 공유 메모리에 접근하는 임계 영역에 대해 상호 배제를 구현하였다.
(파이썬은 스레드 GIL이 있기 때문에 이런 작업이 필요 없었지만, C언어에서는 해주는 게 좋다!)
메모리 해제는 필수!
- 채팅을 담당하는 프로세스 코드
*실제 채팅에 관여하는 프로세스가 실행하는 함수이기 때문에 프로세스 코드라고 설명.
void chat_func(char *id, char *pw, char *msg, char *buf, int new_fd){
char *exit_flag;
int temp, r_byte;
exit_flag = mmap(NULL, sizeof(char), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
*exit_flag = 0;
if(fork() == 0){
//child -> only read shared mem
temp = *chat_header;
while(*exit_flag == 0){
if(temp != *chat_header){
//read
temp = *chat_header;
if (strcmp(id, chat_user) != 0){
if(strcmp("Log-out", chat_data) == 0){
while(*mutex){}
*mutex = 1;
sprintf(msg, "##### %s %s #####", chat_user, chat_data);
send(new_fd, msg, strlen(msg) + 1, 0);
*mutex = 0;
} else {
while(*mutex){}
*mutex = 1;
sprintf(msg, "%s : %s", chat_user, chat_data);
send(new_fd, msg, strlen(msg) + 1, 0);
*mutex = 0;
}
}
}
}
} else {
//parent -> only recieve client's data
while(*exit_flag == 0){
memset(buf, 0, 512);
r_byte = recv(new_fd, buf, MAX_LEN, 0);
if(r_byte != -1){
if(strcmp(buf, "/exit") == 0){
*exit_flag = 1;
while(*mutex){}
*mutex = 1;
memset(chat_data, 0, MAX_LEN);
strncpy(chat_data, "Log-out", strlen("Log-out") + 1);
strncpy(chat_user, id, strlen(id) + 1);
printf("##### %s %s #####\n", chat_user, chat_data);
*chat_header += 1;
*mutex = 0;
} else {
while(*mutex){} // A
*mutex = 1; // ---------------------- B --------------
memset(chat_data, 0, MAX_LEN);
strncpy(chat_data, buf, strlen(buf) + 1);
strncpy(chat_user, id, strlen(id) + 1);
printf("%s : %s\n", chat_user, chat_data);// ---------
*chat_header += 1;// C
*mutex = 0;
}
}
}
}
munmap(exit_flag, sizeof(char));
}
- 부모 프로세스
1. 클라이언트의 입력을 대기한다.
2. 클라이언트에서 데이터가 수신되면 공유 메모리 진입을 대기한다. - (A)
3. 공유 메모리에 접근하기 전 mutex의 값을 1로 해주고 메세지 유저와 메세지 데이터를 공유메모리에 복사한다. - (B)
4. 메세지 헤더에 1을 더해주고 mutex의 값을 0으로 바꿔준다. - (C)
*예를 들면, 직전의 메세지가 43번째 메세지였다면 이제 44번째 메세지가 된다는 뜻이다.
5. 만약 수신된 메세지가 "/exit"라면 프로세스를 종료한다.
- 자식 프로세스
1. 자식 프로세스가 실행되면서 현재 메세지 헤더의 값을 저장시킨다. (채팅 순서 동기화)
2. 반복 문 안에서 메세지 헤더의 값과 현재 프로세스의 채팅 번호(변수 temp)를 비교한다.
3. 새로운 채팅이 입력된 것이 확인되면 클라이언트에게 메세지 유저와 메세지 데이터를 전송한다.
4. 이때 자신이 보낸 메세지는 송신되지 않는다.
결과
잘 동작하는군..
전체 코드는 너무 길어서 여기에 따로 올려두었다.
유저에 대한 아이디, 비밀번호 정보와 그 정보를 확인하는 알고리즘을 구축하는 게 주요 목표가 아니기 때문에 하드코딩으로 진행하였다.