프로그래밍/컴퓨터 네트워크

[리눅스 네트워크] - 2 : 채팅 서버 만들기 (feat. multi thread)

KimuGamJa 2024. 4. 5. 02:59

 

 

https://ballbigdiary.tistory.com/2

 

[리눅스 네트워크] - 1 : echo 서버 만들기

네트워크 프로그래밍이란? 네트워크 프로그래밍은 시스템 프로그래밍의 연장선이다! 컴퓨터의 경우, 랜선이라 부르는 이더넷 케이블을 통해 Data가 송/수신 되고 있다. 이때, 케이블은 H/W 장치이

ballbigdiary.tistory.com

 

  • 이전에는 1:1 통신으로, 한 클라이언트에서 보낸 메시지를 다시 해당 클라이언트로 보내주는 echo 서버를 제작하였다.
  • 이번 시간에는 multi thread를 이용해 1:N 통신을 구현하고,
  • 한 클라이언트가 보낸 메시지를 연결된 N개의 모든 클라이언트에게 전송하는 채팅 서버를 만들어 볼 것이다.

 

💡주의할점!!
본 코드의 경우, thread를 생성하는 pthread_create()함수가 포함되어 있기 때문에, 
gcc를 이용한 빌드시, -lpthread 옵션을 추가해 주어야 한다!!!!!

 

 

server 코드 분석

 

//chat_server.c

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

#define MAX_CLIENT_CNT 500

char PORT[6]; //포트번호 저장.
int server_sock; //서버소켓
int client_sock[MAX_CLIENT_CNT] = { 0 }; 
//서버와 연결될 client_socket의 각 id별 사용 여부 저장

struct sockaddr_in client_addr[MAX_CLIENT_CNT] = { 0 };
//client들의 socket 구조체 저장.

pthread_t tid[MAX_CLIENT_CNT]; //각 client를 관리할 각각의 thread
int exitFlag[MAX_CLIENT_CNT]; //각 client들의 thread가 종료되었는지 관리

pthread_mutex_t mlock; //context switching에 의한 오류를 막기 위한 mutex변수

void interrupt(int arg) { //서버 종료 Ctrl + C
	printf("\nYou typed Ctrl + C\n");
	printf("Bye\n");

	//서버 종료시, 연결된 client를 관리하는 thread 모두 종료시킴.
	for (int i = 0; i < MAX_CLIENT_CNT; i++) { 
		if (client_sock[i] != 0) {
			pthread_cancel(tid[i]);
			pthread_join(tid[i], 0);
			close(client_sock[i]); //socket도 종료
		}
	}

	//client정리 완료 후 서버 소켓도 종료
	close(server_sock);
	exit(1);
}

void removeEnterChar(char* buf) { //개행문자를 널문자로 변환해줌
	int len = strlen(buf);
	for (int i = len - 1; i >= 0; i--) {
		if (buf[i] == '\n') {
			buf[i] = '\0';
			break;
		}
	}
}

int getClientID() {  //새로운 client에게 사용중이지 않은 ID를 부여해줌.
	for (int i = 0; i < MAX_CLIENT_CNT; i++) {

		if (client_sock[i] == 0) //사용중이지 않다면, 해당 ID를 부여한다.
			return i;
	}

	return -1; //이미 다 사용중이다. == 할당 가능한 ID가 없다.
}


//서버의 각 thread가 해줄 동작 = client가 보낸 메시지를 연결된 모든 client에게 전달
void* client_handler(void* arg) {
	int id = *(int*)arg;//클라이언트의 아이디

	char client_IP[100];

	strcpy(client_IP, inet_ntoa(client_addr[id].sin_addr));
	//네트워크 바이트 오더의 32bit 값을 XXX.XXX.XXX.XXX 형식으로 변경

	printf("INFO :: Connect new Client (ID : %d, IP : %s)\n", id, client_IP);

	char buf[MAX_CLIENT_CNT] = { 0 };


	while (1) {

		memset(buf, 0, MAX_CLIENT_CNT);
		int len = read(client_sock[id], buf, MAX_CLIENT_CNT);
		//각 client가 보낸 메시지를 읽어 buffer에 저장.

		if (len == 0) { //해당 클라이언트와 연결이 끊겼다.
			printf("INFO :: Disconnect with client.. BYE\n");
			exitFlag[id] = 1;
			break;
		}

		
		if (!strcmp("exit", buf)) {//만약 클라이언트가 보낸 메시지가 exit라면,
			printf("INFO :: Client want close.. BYE\n");
			exitFlag[id] = 1; //종료로 설정
			break;
		}

		//연결이 유지되있고, exit도 아니라면
		removeEnterChar(buf);//개행문자를 널 문자로 변환

		pthread_mutex_lock(&mlock); //mutex lock걸기

		//해당 클라이언트가 보낸 메시지를 연결된 모든 클라이언트에게 전송
		for (int i = 0; i < MAX_CLIENT_CNT; i++) {
			if (client_sock[i] != 0) { //연결되어 있는 클라이언트에게만 전송
				write(client_sock[i], buf, strlen(buf));
			}
		}

		pthread_mutex_unlock(&mlock);//작업이 끝난 후, mutex 풀기
	}

	//클라이언트가 종료되었다면, 해당 소켓을 닫음
	close(client_sock[id]);
}



int main(int argc, char* argv[]) {
	if (argc < 2) { //Port번호를 받을 수 있게함.
		printf("ERROR Input Port Num\n");
		exit(1);
	}

	//포트번호 저장
	strcpy(PORT, argv[1]);

	//SIGINT신호를 받으면, interrupt함수를 실행
	signal(SIGINT, interrupt);

	//mutex 초기화
	pthread_mutex_init(&mlock, NULL);

	//서버 소켓 생성
	server_sock = socket(AF_INET, SOCK_STREAM, 0);

	if (server_sock == -1) {//서버 소켓 생성 실패
		printf("ERROR :: 1_Socket Create Error\n");
		exit(1);
	}

	//서버 소켓 생성 성공
	printf("Server On..\n");


	//서버 소켓 구조체 설정
	int optval = 1;
	setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&optval, sizeof(optval));

	struct sockaddr_in server_addr = { 0 };
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	server_addr.sin_port = htons(atoi(PORT));


	//bind()함수를 이용해 server 소켓에 주소 할당
	if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
		printf("ERROR :: 2_bind Error\n");
		exit(1);
	}
	printf("Bind Success\n");

	//클라이언트의 연결 요청을 수신할 수 있도록 소켓을 대기상태로 둠
	if (listen(server_sock, 5) == -1) {
		printf("ERROR :: 3_listen Error");
		exit(1);
	}
	printf("Wait Client...\n");

	//서버와 연결될 클라이언트 소켓 설정
	socklen_t client_addr_len = sizeof(struct sockaddr_in);
	int id_table[MAX_CLIENT_CNT];


	while (1) {

		int id = getClientID();//사용중이지 않은 유효한 id를 받음.

		id_table[id] = id;

		if (id == -1) { //유효한 id가 없는 경우, 1초 기다리고 다시 id 받기를 시도함
			printf("WARNING :: Client FULL\n");
			sleep(1);
			continue;
		}

		memset(&client_addr[id], 0, sizeof(struct sockaddr_in));

		//클라이언트의 연결 요청을 수락하고, 클라이언트와 통신하기 위한 새로운 클라이언트 소켓을 생성
		client_sock[id] = accept(server_sock, (struct sockaddr*)&client_addr[id], &client_addr_len);
		
		if (client_sock[id] == -1) {
			printf("ERROR :: 4_accept Error\n");
			break;
		}

		//해당 클라이언트에 대한 동작을 수행해줄 thread 생성
		//해당 thread는 client_handler 함수를 수행함.
		pthread_create(&tid[id], NULL, client_handler, (void*)&id_table[id]);
		//&tid[id] : 생성된 스레드의 식별자를 저장할 변수의 주소
		//NULL : 기본적인 스레드 속성을 사용
		//client_handler : 새로운 스레드에서 실행될 함수의 포인터
		//(void*)&id_table[id] :  함수에 전달할 매개변수를 전달. 
		// 여기서는 해당 클라이언트의 id를 client_handler함수에게 전달함.
		//이때, void* 형태로 전달하며, 함수 내부에서는 해당 포인터를 다시 적절한 형으로 변환하여 사용


		//종료된 client가 있다면, exitFlag를 초기화해주고,
		//pthread_join을 통해 해당 client를 관리하는 thread가 종료될때까지 기다리고,
		//종료되었다면 socket을 반환해줌
		for (int i = 0; i < MAX_CLIENT_CNT; i++) {
			if (exitFlag[i] == 1) {
				exitFlag[i] = 0;
				pthread_join(tid[i], 0);
				client_sock[i] = 0;
			}
		}
	}

	//서버 동작끝.
	//mutext를 삭제하고, 서버 소켓 종료.
	pthread_mutex_destroy(&mlock);
	close(server_sock);
	return 0;
}

 

 

이전 echo 서버와 달라진 점을 위주로 분석해보면 다음과 같다.

 

💡 서버가 하나인 것은 동일하기 때문에, 서버 소켓의 설정은 이전 echo 서버와 동일하다...!

 

 

 

수정된/추가된 변수 목록
  • 다수의 client 연결을 위해 client 소켓 디스크립터(client_sock), 구조체 (client_addr)를 저장하는 변수를 모두 배열형태로 선언해 주었다.
    • int client_sock[MAX_CLIENT_CNT] = { 0 }; 
    • struct sockaddr_in client_addr[MAX_CLIENT_CNT] = { 0 };
  • 각 client를 관리할 thread의 id를 저장해주기 위한 배열이 추가됨
    • pthread_t tid[MAX_CLIENT_CNT]; 
  • multi thread로 동작하므로, 한 thread가 종료되었음을 다른 thread에게 알려야 한다. 이를 위한 exitFlag를 각 client마다 가진다. 이때, 해당 변수는 각 client의 thread가 공유할 수 있도록 data영역에 저장하기 위해 전역 변수로 선언해주어야 한다!
    • int exitFlag[MAX_CLIENT_CNT];

 

추가된 함수

 

  • getClientID()
    • 새로운 client와 연결할 때, 사용중이지 않은 client_sock을 얻기 위한 함수이다.
    • client_sock 배열을 탐색하면서 값이 0인 = 사용중이지 않은 배열 인덱스를 반환한다.
    • 만약, 모든 sock을 사용하고 있다면, -1을 반환하여 할당 가능한 sock이 없음을 알린다.
int getClientID() {  //새로운 client에게 사용중이지 않은 ID를 부여해줌.
	for (int i = 0; i < MAX_CLIENT_CNT; i++) {

		if (client_sock[i] == 0) //사용중이지 않다면, 해당 ID를 부여한다.
			return i;
	}

	return -1; //이미 다 사용중이다. == 할당 가능한 ID가 없다.
}

 

 

  • client_handler()
    • 각 client에 대해 서버의 각 thread가 해줄 동작을 정의한다.
    • 이때, 함수는 클라이언트의 id를 전달받기 때문에 각 client 별로 동작을 정의해줄 수 있다.
    • (client 관련 변수들이 배열이므로, 어떤 client에 대해 동작할지는 id를 인덱스로 하여 접근해준다!)
    • 서버가 해주는 역할은 이전 echo서버와 유사하다.
    • 다만, 클라이언트가 보낸 메시지를 수신하고 (read),
    • 해당 메시지를 모든 클라이언트에게 송신하기 위해(write) for문을 이용해 연결된 (client_sock != 0) 모든 클라이언트에게 보내는 코드로 수정되었을 뿐이다!
    • => 중간에 context switching이 일어나 buf가 수정되거나 client 목록이 바뀌거나 하는 등의 불상사가 발생하지 않도록, write() 코드는 mutext_lock & mutex_unlock으로 감싸주어야 한다!
void* client_handler(void* arg) {
	int id = *(int*)arg;//클라이언트의 아이디

	char client_IP[100];

	strcpy(client_IP, inet_ntoa(client_addr[id].sin_addr));
	//네트워크 바이트 오더의 32bit 값을 XXX.XXX.XXX.XXX 형식으로 변경

	printf("INFO :: Connect new Client (ID : %d, IP : %s)\n", id, client_IP);

	char buf[MAX_CLIENT_CNT] = { 0 };


	while (1) {

		memset(buf, 0, MAX_CLIENT_CNT);
		int len = read(client_sock[id], buf, MAX_CLIENT_CNT);
		//해당 id의 client가 보낸 메시지를 읽어 buffer에 저장.

		if (len == 0) { //해당 클라이언트와 연결이 끊겼다.
			printf("INFO :: Disconnect with client.. BYE\n");
			exitFlag[id] = 1;
			break;
		}

		
		if (!strcmp("exit", buf)) {//만약 클라이언트가 보낸 메시지가 exit라면,
			printf("INFO :: Client want close.. BYE\n");
			exitFlag[id] = 1; //종료로 설정
			break;
		}

		//연결이 유지되있고, exit도 아니라면
		removeEnterChar(buf);//개행문자를 널 문자로 변환

		pthread_mutex_lock(&mlock); //mutex lock걸기

		//해당 클라이언트가 보낸 메시지를 연결된 모든 클라이언트에게 전송
		for (int i = 0; i < MAX_CLIENT_CNT; i++) {
			if (client_sock[i] != 0) { //연결되어 있는 클라이언트에게만 전송
				write(client_sock[i], buf, strlen(buf));
			}
		}

		pthread_mutex_unlock(&mlock);//작업이 끝난 후, mutex 풀기
	}

	//클라이언트가 종료되었다면, 해당 소켓을 닫음
	close(client_sock[id]);
}

 

 

 

thread 생성
  • main함수에서 바로 client에 대한 동작을 수행해 주지 않고, 각 client를 담당하는 thread를 생성하여 처리해줌
    • 해당 thread는 전달받은 client_handler 함수를 수행함.
		pthread_create(&tid[id], NULL, client_handler, (void*)&id_table[id]);
  • 기능 : thread 생성
  • 입력값
    • &tid[id] : 생성된 스레드의 식별자를 저장할 변수의 주소
    • NULL : 기본적인 스레드 속성을 사용
    • client_handler : 새로운 스레드에서 실행될 함수의 포인터
    • (void*)&id_table[id] :  함수에 전달할 매개변수를 전달. (여기서는 해당 클라이언트의 id(=index)를 client_handler함수에게 전달함. 이때, void* 형태로 전달하며, 함수 내부에서 다시 적절한 형으로 변환하여 사용.할 수 있게 함.)

 

 

exitFlag 초기화 
  • 종료된 client에 대한 theread가 있다면, exitFlag를 0으로 초기화 시켜주고 interrupt를 통해 완전히 종료되기 기다렸다가 (pthread_join()) 종료 되었다면 해당 id를 유효한 상태로(0) 전환해줌.
		for (int i = 0; i < MAX_CLIENT_CNT; i++) {
			if (exitFlag[i] == 1) {
				exitFlag[i] = 0;
				pthread_join(tid[i], 0);
				client_sock[i] = 0;
			}
		}

 

 

interrupt 수정
  • 서버에서 interrupt가 발생했을 때 socket만 close() 시킬 것이 아니라, 연결된 client를 담당하는 모든 thread들도 cancle시킬 수 있도록 코드를 수정!
void interrupt(int arg) { //서버 종료 Ctrl + C
	printf("\nYou typed Ctrl + C\n");
	printf("Bye\n");

	//서버 종료시, 연결된 client를 관리하는 thread 모두 종료시킴.
	for (int i = 0; i < MAX_CLIENT_CNT; i++) { 
		if (client_sock[i] != 0) {
			pthread_cancel(tid[i]);
			pthread_join(tid[i], 0);
			close(client_sock[i]); //socket도 종료
		}
	}

	//client정리 완료 후 서버 소켓도 종료
	close(server_sock);
	exit(1);
}

 

 

 

 

 

Client 코드 분석

 

//chat_client.c

#include <stdio.h>
//표준 입력 및 출력 함수 (printf, fgets 등)를 사용하기 위한 헤더
#include <unistd.h>
//Unix 표준 파일 함수 (close, read, write 등)를 사용하기 위한 헤더
#include <stdlib.h>
//(exit, atoi 등)를 사용하기 위한 헤더
#include <signal.h>
//시그널 처리 함수 (signal, kill 등)를 사용하기 위한 헤더
#include <sys/types.h>
//시스템 자료형 및 상수를 사용하기 위해
#include <sys/socket.h>
//소켓 프로그래밍을 위한 함수와 자료형을 사용하기 위해 
#include <netinet/in.h>
//인터넷 프로토콜을 위한 자료형 및 상수를 사용하기 위해
#include <arpa/inet.h>
//인터넷 주소 변환 함수 (inet_addr, inet_ntoa 등)를 사용하기 위해
#include <string.h>
//문자열 처리 함수 (memset, strcmp, strcpy 등)를 사용하기 위해
#include <pthread.h>
//POSIX 스레드 라이브러리 함수를 사용하기 위해 포함


#define NAME_SIZE 20 // client 이름 최대 크기
#define MSG_SIZE 100 // 메시지 최대 크기

char name[NAME_SIZE]; // client 이름 저장
char msg[MSG_SIZE]; // 메시지 저장

pthread_t send_tid; // 메시지 송신용
pthread_t receive_tid; // 메시지 수신용
int exitFlag;
//thread가 종료되었는지 판별하는 전역 변수 
// data 영역에 저장되어 있기 때문에 모든 thread가 공유한다.
//*data, file은 각 thread가 공유.
//register, stack은 각 thread가 개별적으로 가짐.

char SERVER_IP[20]; //연결할 Server의 IP저장
char SERVER_PORT[6]; //연결할 Server의 PORT저장

int client_sock; //client의 socket

void interrupt(int arg) { //Ctrl + C를 통해 종료
	printf("\nYou typped Ctrl + C\n");
	printf("Bye\n");

	//해당 client의 송/수신 thread 종료
	pthread_cancel(send_tid);
	pthread_cancel(receive_tid);

	//각 thread가 종료되기를 기다림.
	//완전히 종료될 때까지 blcok되어 코드가 진행되지 않음.
	pthread_join(send_tid, 0);
	pthread_join(receive_tid, 0);

	//모든 thread 종료 완료 후 소켓을 닫음
	close(client_sock);
	exit(1);
}

void* sendMsg() { //사용자로부터 메시지를 받고, server에게 송신하는 thread

	char buf[NAME_SIZE + MSG_SIZE + 1]; //클라이언트 정보 + 메시지를 저장할 buffer

	while (!exitFlag) { //종료된 thread가 있는지 확인
		memset(buf, 0, NAME_SIZE + MSG_SIZE); //buffer초기화

		fgets(msg, MSG_SIZE, stdin);
		//사용자가 입력을 하고 있는 중간에도, 메시지를 수신 받을 수 있도록
		//scanf()함수 대신 fgets()함수를 사용함!

		//thrad종료
		//1. 본인이 종료됨
		if (!strcmp(msg, "exit\n")) { //만약 사용자가 입력한 문자열이 exit라면, 
			exitFlag = 1; //thread 종료로 설정
			write(client_sock, msg, strlen(msg)); 
			//client 소켓에 작성하여 서버에도 exit사실을 알림
			break; //while문 종료
		}
			
		//2. 다른 thread가 종료됨
		if (exitFlag) break; //종료된 thread가 있다면, while문 종료

		//thread 종료 상태가 아니라면, 사용자가 입력한 값을
		//sprinf() 표준 함수를 이용하여 name, msg을 buf에 저장
		sprintf(buf, "%s %s", name, msg);
		//client 소켓에 buf 내용을 작성
		write(client_sock, buf, strlen(buf));
	}
}

void* receiveMsg() {//서버에게 메시지를 받고, 해당 메시지를 출력

	char buf[NAME_SIZE + MSG_SIZE]; //클라이언트 정보 + 메시지를 저장할 buffer

	while (!exitFlag) {//종료된 thread가 있는지 확인
		
		//종료된 thread가 없다면
		memset(buf, 0, NAME_SIZE + MSG_SIZE);
		int len = read(client_sock, buf, NAME_SIZE + MSG_SIZE - 1);
		//client 소켓으로부터 메시지를 읽어 buf에 저장함

		if (len == 0) {//read()함수의 반환값이 0이다 = 연결이 종료되었다.
			printf("INFO :: Server Disconnected\n");
			kill(0, SIGINT); //SIGINT 시그널을 보내 client를 kill시킴
			//0 : 현재 프로세스 그룹을 의미. 속한 모든 스레드 및 프로세스에게 전달
			// SIGINT : 인터럽트 시그널을 의미.

			exitFlag = 1; //thread도 종료 상태로 전환.
			break;
		}

		//연결이 유효하다면, buf의 내용을 출력
		printf("%s\n", buf);
	}
}

int main(int argc, char* argv[]) {

	if (argc < 4) { //서버 IP, Port번호 및 클라이언트 이름 3가지를 모두 받게 함.
		printf("ERROR Input [IP Addr] [Port Num] [User Name]\n");
		exit(1);
	}


	strcpy(SERVER_IP, argv[1]); //서버 IP저장
	strcpy(SERVER_PORT, argv[2]); //서버 PORT저장
	sprintf(name, "[%s]", argv[3]); //서버 name저장. [name]형식으로!

	signal(SIGINT, interrupt); 
	//SIGINT시그널을 받는다면, interrupt 함수가 동작하여 client 종료시킴

	client_sock = socket(AF_INET, SOCK_STREAM, 0); //client 소켓 생성

	if (client_sock == -1) { //socket생성 실패
		printf("ERROR :: 1_Socket Create Error\n");
		exit(1);
	}
	//printf("Socket Create!\n");

	//연결할 server 구조체 정의
	struct sockaddr_in server_addr = { 0 };
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
	server_addr.sin_port = htons(atoi(SERVER_PORT));
	socklen_t server_addr_len = sizeof(server_addr);


	//서버와의 연결 시도
	//실패했다면
	if (connect(client_sock, (struct sockaddr*)&server_addr, server_addr_len) == -1) {
		printf("ERROR :: 2_Connect Error\n");
		exit(1);
	}
	//printf("Connect Success!\n");

	//------서버와 연결 성공------

	//멀티 스레드 생성. 송/수신용 총 2개의 스레드 생성
	pthread_create(&send_tid, NULL, sendMsg, NULL); //send용 스레드
	pthread_create(&receive_tid, NULL, receiveMsg, NULL); //recieve용 스레드

	//thread 종료를 기다림
	//각 thread는 각각의 함수(sendMsg, recieveMsg)를 수행하고 있음.
	pthread_join(send_tid, 0);
	pthread_join(receive_tid, 0);

	close(client_sock);
	return 0;
}

 

client 단에서도, 메시지 송 + 수신을 동시에 해야하므로, 각각을 담당하는 2개의 thread를 생성해주어야 한다!

 

 

추가된 변수
  • 메시지 송신용 thread : pthread_t send_tid;
  • 메시지 수신용 thread : pthread_t receive_tid;
  • thread가 닫혔는지 나타내는 변수 : int exitFlag;

 

 

추가된 함수

 

  • sendMsg() : 메시지 송신용 함수
    • fgets()로 사용자입력을 받고, write()함수를 통해 메시지를 송신함.
    • 이때, 메시지 입력 중에도 메시지를 수신 받을 수 있도록 scanf()함수 대신 fgets()함수를 이용한다.
    • exitFlag == 1이라면, 다른 thread가 종료되었다는 뜻이므로 현재 thread도 더이상 진행하지 않는다.
void* sendMsg() { //사용자로부터 메시지를 받고, server에게 송신하는 thread

	char buf[NAME_SIZE + MSG_SIZE + 1]; //클라이언트 정보 + 메시지를 저장할 buffer

	while (!exitFlag) { //종료된 thread가 있는지 확인
		memset(buf, 0, NAME_SIZE + MSG_SIZE); //buffer초기화

		fgets(msg, MSG_SIZE, stdin);
		//사용자가 입력을 하고 있는 중간에도, 메시지를 수신 받을 수 있도록
		//scanf()함수 대신 fgets()함수를 사용함!

		//thrad종료
		//1. 본인이 종료됨
		if (!strcmp(msg, "exit\n")) { //만약 사용자가 입력한 문자열이 exit라면, 
			exitFlag = 1; //thread 종료로 설정
			write(client_sock, msg, strlen(msg)); 
			//client 소켓에 작성하여 서버에도 exit사실을 알림
			break; //while문 종료
		}
			
		//2. 다른 thread가 종료됨
		if (exitFlag) break; //종료된 thread가 있다면, while문 종료

		//thread 종료 상태가 아니라면, 사용자가 입력한 값을
		//sprinf() 표준 함수를 이용하여 name, msg을 buf에 저장
		sprintf(buf, "%s %s", name, msg);
		//client 소켓에 buf 내용을 작성
		write(client_sock, buf, strlen(buf));
	}
}

 

 

  • receiveMsg() : 메시지 수신용 함수
    • read() 함수를 통해 소켓통신으로 메시지가 올 때까지 대기함.
    • len == 0이다 = 연결이 끊어졌다 이므로 kill()함수를 이용해 SIGINT 시그널을 보냄 
    • 연결이 유효하다면, 수신한 메시지를 출력함 printf()
void* receiveMsg() {//서버에게 메시지를 받고, 해당 메시지를 출력

	char buf[NAME_SIZE + MSG_SIZE]; //클라이언트 정보 + 메시지를 저장할 buffer

	while (!exitFlag) {//종료된 thread가 있는지 확인
		
		//종료된 thread가 없다면
		memset(buf, 0, NAME_SIZE + MSG_SIZE);
		int len = read(client_sock, buf, NAME_SIZE + MSG_SIZE - 1);
		//client 소켓으로부터 메시지를 읽어 buf에 저장함

		if (len == 0) {//read()함수의 반환값이 0이다 = 연결이 종료되었다.
			printf("INFO :: Server Disconnected\n");
			kill(0, SIGINT); //SIGINT 시그널을 보내 client를 kill시킴
			//0 : 현재 프로세스 그룹을 의미. 속한 모든 스레드 및 프로세스에게 전달
			// SIGINT : 인터럽트 시그널을 의미.

			exitFlag = 1; //thread도 종료 상태로 전환.
			break;
		}

		//연결이 유효하다면, buf의 내용을 출력
		printf("%s\n", buf);
	}
}

 

 

 

수정된 함수

 

  • interrupt()
    • 이전에는, 단순히 sock만 close() 시켰다면 multi thread를 이용한 경우, socket을 종료시키기 전 thread를 먼저 cancle 시켜준 뒤, cancle이 완료 된 후 socket을 close() 시킴!
void interrupt(int arg) { //Ctrl + C를 통해 종료
	printf("\nYou typped Ctrl + C\n");
	printf("Bye\n");

	//해당 client의 송/수신 thread 종료
	pthread_cancel(send_tid);
	pthread_cancel(receive_tid);

	//각 thread가 종료되기를 기다림.
	//완전히 종료될 때까지 blcok되어 코드가 진행되지 않음.
	pthread_join(send_tid, 0);
	pthread_join(receive_tid, 0);

	//모든 thread 종료 완료 후 소켓을 닫음
	close(client_sock);
	exit(1);
}