joo_coding 2025. 4. 11. 20:55
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>

#define BUF_SIZE 1024
#define MAX_CLIENT 30

char id[50];
int sock;
char client_ids[MAX_CLIENT][50];                                        // 클라이언트 ID를 저장하는 배열 : 50개

void *recv_msg(void *arg) {                                             // 메세지 수신 스레드
    char buf[BUF_SIZE];                                                 // 수신 버퍼
    int str_len;                                                        // 수신된 메세지 길이
    while ((str_len = read(sock, buf, BUF_SIZE - 1)) > 0) {             // 소켓에서 메세지 수신
        buf[str_len] = 0;                                               // 문자열 종료
        printf("%s", buf);                                              // 수신된 메세지 출력
        fflush(stdout);   // stdout(출력스트림) 버퍼 비우기. fflush: 입력 메세지 마지막 "\n(개행문자)" 처리를 위해
    }                                                             
    return NULL;                                                        // 메세지 수신 스레드 종료
}

void error_handling(char *message) {                                    // 에러 처리 함수 선언
    perror(message);                                                    // 에러 메세지 출력
    exit(1);                                                            // 에러 메세지 출력 후 프로그램 종료
}

void create_config(char *filename, char *ip, char *port, char *id) {    // config 파일 생성 함수(서버IP, PORT(비번), ID))
    FILE *fp = fopen(filename, "w");                                    // config 파일 쓸 수 있게 열기
    if (fp == NULL) error_handling("fopen()");                          // 파일 열기 실패 시 에러 처리
    fprintf(fp, "%s %s %s\n", ip, port, id);                            // config 파일에 서버IP, PORT, ID 저장
    fclose(fp);                                                         // config 파일 닫기
}

void read_config(char *filename, char *ip, char *port, char *id) {      // config 파일 읽기 함수
    FILE *fp = fopen(filename, "r");                                    // config 파일 읽기
    if (fp == NULL) error_handling("fopen()");                          // 파일 열기 실패 시 에러 처리
    fscanf(fp, "%s %s %s", ip, port, id);                               // config 파일에서 서버IP, PORT, ID 읽기
    fclose(fp);                                                         // config 파일 닫기
    
    printf("IP: %s PORT: %s ID: %s", ip, port, id);            // config파일이 정상 생성되었고 출력되는지 확인 여부                  
}

// main 함수
int main(int argc, char *argv[]) {
    int server_sock, client_sock;                                           // 서버 소켓, 클라이언트 소켓 정의
    struct sockaddr_in server_addr, client_addr;                            // 서버, 클라이언트 주소 구조체
    socklen_t addr_sz;                                                      // 주소 사이즈 
    int option;                                                             // 112행 소켓 옵션 설정을 위한 변수 정의
    fd_set reads, cpy_reads;                                                // fd_set 소켓에 있는 파일들을 묶을때 많이 쓰임. 129행에 소켓안에 내용을 읽기위한 
    struct timeval timeout;                                                 // timeval 시간의 간격(초)들을 사용할때 많이 사용되는 구조체. tv_sec와 같이 쓰임
    int fd_max, str_len, fd_num, i;                                         // 변수 정의
    char buf[BUF_SIZE];                                                     // 버퍼 사이즈
    char msg_with_id[BUF_SIZE + 50];                                        // 채팅에 접속한 아이디 

    if (argc < 2) {                                                         // 인자 (서버 -s/클라이언트 -c) 가 없을 경우
        printf("Usage: %s [-s|-c] options...\n", argv[0]);                  // 사용방법은 파일명 -s/ -c  이후 뒤에 추가 옵션을 적는 방식으로. 옵션은 아래 추가설명
        exit(1);                                                            // 인자 값이 없을 경우 프로그램 종료
    }

    if (strcmp(argv[1], "-s") == 0) {                                       // 파일명 0번 인자 다음 배열 2째가 서버 "-s" 모드일 경우
        // 서버 모드
        if (argc != 4 || strcmp(argv[2], "-p") != 0) {                      // argc(인자개수)가 4개가 아니거나 or 2번째 인자가 "-p"가 아닐 경우
            printf("Usage: %s -s -p <port>\n", argv[0]);                    // usage 사용방법 : 배열 0번 인자(파일명) -s -p <port> 형식으로 작성하라는 내용 공지
            exit(1);                                                        // 해당 조건에 맞지 않을 경우 프로그램 종료
        }

        server_sock = socket(PF_INET, SOCK_STREAM, 0);                      // 서버 소켓 생성 (PF_INET: IPv4, SOCK_STREAM: TCP) - 연결 지향형 소켓
        if (server_sock == -1) error_handling("socket()");                  // 서버 소켓 생성 실패시 에러 처리

        memset(&server_addr, 0, sizeof(server_addr));                       // 메모리셋 (서버 구조 구조체 초기화) - 4줄은 형식이라 외워서 사용
        server_addr.sin_family = AF_INET;                                   // 주소 체계 (IPv4)
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);                    // long값. 서버 IP 주소 (모든 IP 주소 수신). INADDR_ANY: 자동으로 이 컴퓨터에 존재하는 랜카드중 사용가능한 랜카드 IP 주소를 사용하라
        server_addr.sin_port = htons(atoi(argv[3]));                        // short값. 서버 PORT 번호 (배열 4번째 인자에 위치). hton: 호스트 바이트 순서 -> 네트워크 바이트 순서 변환. 
                                                                            // atoi : 문자열을 정수로 변환

        option = 1;                                                                         // 소켓 옵션 설정
        setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));         // setsockopt (소켓 옵션 설정하는 함수). 형식: int setsockopt(SOCKET socket, int level, int optname, const void* optval, int optlen);형식
                                                                                            // socket : 소켓의 번호
                                                                                            // level : 옵션의 종류. 보통 SOL_SOCKET(소켓과 관련된 옵션들을 지정하는 상수)와 IPPROTO_TCP 중 하나를 사용
                                                                                            // optname : 설정을 위한 소켓 옵션의 번호
                                                                                            // optval : 설정 값이 저장된 주소값.
                                                                                            // optlen : optval 버퍼의 크기
                                                                                            
                                                                                            // SO_REUSEADDR: 소켓 주소 재사용. cf) SO_RCVBUF: 수신 버퍼 크기 조정

        if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1)   // bind함수(소켓에 주소를 연결해주는 함수). 주소 할당 실패시 에러 처리
            error_handling("bind()");

        if (listen(server_sock, 5) == -1)                                                   // listen함수(클라이언트 요청을 바로 받지 않고 대기하는 함수). 대기 큐의 길이 5. -1은 실패시 에러 처리
            error_handling("listen()");
        
        // 아래 select()함수에서 사용할 fd_set 초기화하며 주로 파일 디스크립터 상태(읽기, 쓰기, 예외)를 모니터링 할때 사용. 
        FD_ZERO(&reads);                                                                    // fd_zero 매크로 함수는 fd_set 선언된 변수 초기화할때 사용                                            
        FD_SET(server_sock, &reads);                                                        // fd_set 함수안에 서버 소켓, reads를 넣어줌
        FD_SET(0, &reads);                                                                  // fd_set 0(stdin). 표준입력에서 데이터를 읽을 준비가 되었는지 감시 
        fd_max = server_sock;                                                               // 현재 감시중인 파일 디스크립터중 서버소켓이 가장 큰 값임을 나타냄.

        printf("서버 시작. [방장]\n");

        while (1) {
            cpy_reads = reads;                                                // 원본훼손방지, 복사본을 이용한다.
            timeout.tv_sec = 5;                                               // timeout을 5초로 설정
            timeout.tv_usec = 0;                                              // 마이크로 초수 초기화

            fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout);          // 검사의 범위지정과 타임아웃 설정 성공0,실패-1 반환
                                                                              // select(파일 디스크립터의수, 수신된 데이터의 존재여부, 블로킹없는 데이터 전송의 가능여부,예외상황의 발생여부,무한블로킹 상태에 빠지지 않도록 타임아웃)
            if (fd_num == -1) break;                                          // 만약 오류라면 (반환값 -1)

            for (i = 0; i <= fd_max; i++) {                                   // select함수 리턴값 만큼 반복 돌린다.
                if (FD_ISSET(i, &cpy_reads)) {                                // FD_ISSET : select 함수의 호출결과를 확인. 파일 디스트립터 정보가 있으면 양수를 반환.
                    if (i == server_sock) {                                   // sever_sock 정수면 서버
                        // 새로운 클라이언트 연결
                        addr_sz = sizeof(client_addr);                        // 클라이언트 주소의 크기를 addr_sz에 할당
                        client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &addr_sz);    //연결요청수락값을 client_sock
                        str_len = read(client_sock, buf, BUF_SIZE - 1);         // 클라이언트로부터 ID 받기
                        buf[str_len] = 0;                                       // 문자열 종료

                        // 클라이언트 ID 중복 확인
                        int duplicate = 0;
                        for (int j = 0; j <= fd_max; j++) {
                            if (FD_ISSET(j, &reads) && j != server_sock && j != 0) {    // FD_ISSET : 소켓fd에 해당하는 비트가 있으면 양수값
                                if (strcmp(client_ids[j], buf) == 0) {          // 클라이언트 아이디와 버퍼크기가 일치하면
                                    duplicate = 1;                              // duplicate = 1 할당
                                    break;
                                }
                            }
                        }

                        if (duplicate) {
                            // 중복된 ID가 있을 경우 연결 종료
                            sprintf(msg_with_id, "[오류] ID '%s'는 이미 사용 중입니다.\n", buf); // msg_with_id 에 "[오류] ID '%s'는 이미 사용 중입니다.\n" 를 담는다
                            write(client_sock, msg_with_id, strlen(msg_with_id));   // 문자열의 길이만큼 client_sock에게 msg_with_id에 담긴 문자열을 보낸다
                            close(client_sock);                                     // 소켓연결을 끊는다.
                            printf("[알림] 중복된 ID '%s'로 연결 시도. 연결 종료.\n", buf);
                        } else {
                            // 중복되지 않은 경우 클라이언트 등록
                            strncpy(client_ids[client_sock], buf, 50); // 클라이언트 ID 저장
                            client_ids[client_sock][49] = '\0'; // 안전한 문자열 종료--> 아이디 입력할때 적는 문자들의 크기의 50개 배열

                            FD_SET(client_sock, &reads);    //소켓 목록 reads에 client_sock을 추가해서 select()가 서버 소켓을 감시할수 있게함
                            if (fd_max < client_sock)
                                fd_max = client_sock;       // fd_max = 50

                            sprintf(msg_with_id, "[알림] %s 입장\n", buf);
                            printf(msg_with_id, "[알림] 클라이언트 %s가 입장하셨습니다.\n", buf);
                            for (int j = 0; j <= fd_max; j++) {
                                if (FD_ISSET(j, &reads) && j != server_sock && j != 0)
                                    write(j, msg_with_id, strlen(msg_with_id));
                            }
                        }

                    } else if (i == 0) {
                        // 서버 관리자 입력 처리
                        fgets(buf, BUF_SIZE, stdin);
                        buf[strcspn(buf, "\n")] = 0; // 줄바꿈 제거

                        if (strlen(buf) == 0) continue; // 버프가 0이면 계속

                        if (strncmp(buf, "/kick ", 6) == 0) { // 킥이라고 써있으면
                            char kick_target[50];             // 킥타겟 함수 발동
                            sscanf(buf + 6, "%s", kick_target); // 강퇴할 유저 입력

                            int kicked = 0;
                            for (int j = 0; j <= fd_max; j++) { // 현재 들어와있는 클라이언트 반복문
                                if (FD_ISSET(j, &reads) && j != server_sock && j != 0) {
                                    if (strcmp(client_ids[j], kick_target) == 0) { // 해당 유저가 있으면
                                        // 강제 퇴장 메시지 전송
                                        sprintf(msg_with_id, "[알림] %s님이 강제 퇴장되었습니다.\n", kick_target); 
                                        printf(msg_with_id, "[알림] %s님이 강제 퇴장되었습니다.\n", kick_target); 
                                        for (int k = 0; k <= fd_max; k++) {
                                            if (FD_ISSET(k, &reads) && k != server_sock && k != 0) {
                                                write(k, msg_with_id, strlen(msg_with_id));
                                            }
                                        }

                                        // 클라이언트 소켓 닫기 및 FD_SET에서 제거
                                        FD_CLR(j, &reads);
                                        close(j);                               // 해당 유저의 소켓 닫기
                                        memset(client_ids[j], 0, 50);           // ID 정보 초기화
                                        kicked = 1;                             // 강퇴횟수 = 1
                                        break;  
                                    }
                                }
                            }
                            if (!kicked) { 
                                printf("[오류] %s님을 찾을 수 없습니다.\n", kick_target);
                            }
                        } else {
                            // 방장이 메시지를 보냄
                            sprintf(msg_with_id, "[방장]: %s\n", buf);
                            printf("%s", msg_with_id);                                   // 서버 콘솔에도 출력
                            for (int j = 0; j <= fd_max; j++) {                          // 현재 연결된 클라이언트 확인
                                if (FD_ISSET(j, &reads) && j != server_sock && j != 0) { // J가 서버소켓이 아니고, 0이 아니고
                                    write(j, msg_with_id, strlen(msg_with_id));          
                                }
                            }
                        }
                    } else {
                        // 클라이언트 메시지 수신
                        str_len = read(i, buf, BUF_SIZE); // 리드함수
                        if (str_len == 0) {
                            // 클라이언트 연결 종료
                            FD_CLR(i, &reads);
                            close(i);
                            sprintf(msg_with_id, "[알림] %s 퇴장\n", client_ids[i]);
                            printf(msg_with_id, "[알림] 클라이언트 %s가 입장하셨습니다.\n", client_ids[i]);
                            for (int j = 0; j <= fd_max; j++) {                         // 현재 연결된 클라이언트 확인
                                if (FD_ISSET(j, &reads) && j != server_sock && j != 0)
                                    write(j, msg_with_id, strlen(msg_with_id));
                            }
                            memset(client_ids[i], 0, 50); // ID 정보 초기화
                        } else {
                            buf[str_len] = 0; // 버프 초기화

                            // 클라이언트가 강제 퇴장된 경우 메시지 무시
                            if (strlen(client_ids[i]) == 0) {
                                printf("[알림] 강제 퇴장된 클라이언트가 메시지를 보냈으나 무시되었습니다.\n");
                                continue;
                            }

                            // 서버에 클라이언트 메세지 출력
                            printf("[%s]: %s", client_ids[i], buf);
                            fflush(stdout); //fflush: 입력 메세지 마지막 "\n(개행문자)" 처리를 위해

                            // 클라이언트들에게 메시지 전송
                            sprintf(msg_with_id, "[%s]:  %s\n", client_ids[i], buf);
                            for (int j = 0; j <= fd_max; j++) { // 현재 연결된 클라이언트 확인
                                if (FD_ISSET(j, &reads) && j != server_sock && j != 0 && j != i) {
                                    write(j, msg_with_id, strlen(msg_with_id));
                                }
                            }
                        }
                    }
                }
            }
        }

        close(server_sock);
    } else if (strcmp(argv[1], "-c") == 0) {
        // 클라이언트 모드: 명령행에서 -c 옵션이 지정된 경우 실행됨

        char ip[20], port[10];  // 서버 IP 주소와 포트를 저장할 문자열 배열

        // 인자가 2개만 주어진 경우 (예: ./chat -c)
        // config.conf 파일이 있는지 확인하고 없으면 새로 생성
        if (argc == 2) {
            if (access("config.conf", F_OK) == -1) {
                // config.conf 파일이 없을 경우 사용자로부터 정보를 입력받아 생성
                printf("config.conf 파일이 없습니다. 생성합니다.\n");
                printf("서버 IP: "); scanf("%s", ip);
                printf("서버 PORT: "); scanf("%s", port);
                printf("ID: "); scanf("%s", id);
                create_config("config.conf", ip, port, id);  // 파일로 저장
            } else {
                // config.conf 파일이 존재하는 경우 그 안의 정보를 사용해 자동 접속
                read_config("config.conf", ip, port, id);
                printf("config.conf 로 자동 접속합니다.\n");
            }
        } else if (argc == 5) {
            // 명령행 인자로 IP, 포트, ID가 모두 전달된 경우
            strcpy(ip, argv[2]);
            strcpy(port, argv[3]);
            strcpy(id, argv[4]);
        } else {
            // 잘못된 인자 형식일 경우 사용법 안내 후 종료
            printf("Usage: %s -c <IP> <PORT> <ID>\n", argv[0]);
            exit(1);
        }

        // 클라이언트용 TCP 소켓 생성
        sock = socket(PF_INET, SOCK_STREAM, 0);
        if (sock == -1) error_handling("socket()");

        // 서버 주소 구조체 초기화 및 설정
        struct sockaddr_in server_addr;
        memset(&server_addr, 0, sizeof(server_addr));   // 0으로 초기화
        server_addr.sin_family = AF_INET;               // IPv4
        server_addr.sin_addr.s_addr = inet_addr(ip);    // 문자열 IP를 네트워크 주소로 변환
        server_addr.sin_port = htons(atoi(port));       // 포트를 문자열에서 정수 → 네트워크 바이트 순서로 변환

        // 서버에 연결 시도
        if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1)
            error_handling("connect()");

        // 서버에 나의 ID 전송 (입장 알림용)
        write(sock, id, strlen(id));

        // 서버로부터 메시지를 받을 쓰레드 생성
        pthread_t rcv_thread;
        pthread_create(&rcv_thread, NULL, recv_msg, NULL);
        pthread_detach(rcv_thread);  // 쓰레드 종료 시 자동 자원 해제

        // 사용자 입력 루프
        while (1) {
            fgets(buf, BUF_SIZE, stdin);                  // 입력 버퍼에 한 줄 입력
            buf[strcspn(buf, "\n")] = 0;                  // 개행 문자 제거

            if (strlen(buf) == 0) continue;                // 빈 문자열은 무시

            if (strncmp(buf, "/exit", 5) == 0) {
                // /exit 명령어 입력 시 연결 종료
                close(sock);
                exit(0);
            }

            // ID와 메시지를 합쳐 서버에 전송
            sprintf(msg_with_id, "%s: %s\n", id, buf);
            write(sock, msg_with_id, strlen(msg_with_id));
        }
    } else {
        // -s도 -c도 아닌 경우: 사용법 안내 출력 후 종료
        printf("Usage: %s [-s|-c] options...\n", argv[0]);
        exit(1);
    }

    return 0; // main 함수 정상 종료
}