#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 함수 정상 종료
}
주코딩일지