ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 네트워크] - 2 : 채팅 서버 만들기 (feat. multi thread)
    프로그래밍/컴퓨터 네트워크 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);
    }
Designed by Tistory.
-->