관리 메뉴

Rootable의 개발일기

단체 채팅 프로그램 개발 본문

Network

단체 채팅 프로그램 개발

dev-rootable 2024. 7. 12. 17:29

Ref: https://www.pexels.com/ko-kr/photo/whatsapp-46924/

 

Ubuntu 환경에서 단체 채팅방을 C와 MySQL을 통해 구현했다. MySQL은 채팅 내용을 저장하는 용도로 사용한다.

 

📜 기능 목록

 

  • 모든 사용자에게 채팅 메시지가 전달된다.
  • 최초 접속하면 최근 10개의 채팅을 볼 수 있다.
  • 채팅 내용은 MySQL에 저장한다.

 

📌 클라이언트 동작 흐름

 

  1. MySQL 커넥션 초기화 및 연결
  2. 서버와 통신할 소켓 생성
  3. 소켓에 연결할 서버 주소 정보 바인딩
  4. 서버에 연결 요청
  5. DB로부터 최근 10개 레코드 조회 후 각 클라이언트 화면에 출력
  6. 데이터 전송
    1. 시간 정보, 사용자 이름, 채팅 메시지 등을 포맷팅 하여 서버에 전송
    2. 클라이언트가 'exit' 명령을 입력하면 로그아웃으로 간주하여 close 수행
  7. 데이터 수신
    1. 서버에서 작성한 메시지가 있다면 읽어온 후 출력

 

📌 서버 동작 흐름

 

  1. MySQL 커넥션 초기화 및 연결
  2. 듣기 소켓 생성
  3. 듣기 소켓에 서버 주소 정보 바인딩
  4. 클라이언트 요청 대기
  5. Connect를 시도한 클라이언트 요청을 받으면 클라이언트 소켓 생성
  6. 클라이언트가 전송한 메시지를 모든 클라이언트에게 보낸 후 저장
  7. 접속한 클라이언트 IP와 Port 등 주소 정보 출력
  8. 접속한 클라이언트에게 로그인 메시지 전달
  9. 채팅 데이터를 DB에 INSERT 쿼리
  10. 종료를 요청한 클라이언트 소켓 제거
  11. 서버 소켓 종료 및 MySQL 연결 해제

 

📌 들어가기 전 알아두기

 

✍ 소켓 통신 과정

 

 

❓ 소켓을 어떻게 접근할까

 

소켓은 socket descriptor 라는 식별자를 통해 접근할 수 있다. 해당 값은 int 형으로 socket() 함수를 통해 소켓을 생성하면 반환 값으로 얻을 수 있다. 이는 UNIX에서 파일 open 후 file descriptor를 얻는 것과 같은 기능이다.

 

❓ 소켓 바인딩하는 방법

 

객체지향 프로그래밍에서는 바인딩을 위해 생성자를 사용하고, 그 생성자의 본체가 클래스다. 소켓은 sockaddr이라는 구조체가 본체이고, 해당 구조체에 바인딩하기 위해 sockaddr_in 구조체를 사용한다. 그 이유는 sockaddr 구조체 내에 주소를 담는 char 배열이 있는데 이것을 좀 더 세분화해서 나눈 것이 sockaddr_in이기 때문이다. 즉 구조적으로 허용된 작업인 것이다.

 

sockaddr 구조체는 리눅스/유닉스 시스템일 경우 <sys/socket.h> 헤더 파일, 윈도우는 <winsock.h> 헤더 파일을 통해 사용할 수 있다.

 

sys/socket.h: 리눅스/유닉스 시스템에서 소켓의 주소를 저장하거나 표현하는 데 사용하는 구조체를 제공

 

sockaddr_in 구조체 내에 주소 체계, 주소, 포트 등의 멤버에 값을 입력하고, socket 식별자에 바인딩하면 된다. 이 작업을 위한 bind() 함수는 아래와 같이 구성되어 있다.

 

int bind(int sockfd, struct sockaddr *addr, int addr_len)

 

 

여기서 주소를 바인딩하기 위해 arpa/inet.h 라는 헤더 파일이 필요하다.

 

arpa/inet.h: 숫자로 IP 주소를 조작하는 기능들을 정의한 헤더 파일

 

헤더 파일 내에 htonl(), htons(), ntohl(), ntohs() 등의 함수가 있는데 이들은 소켓을 통해 다른 기종 간에 데이터를 송수신할 수 있도록 데이터를 변환한다.

 

❓ 소켓 통신에서 주소를 변환하는 이유

 

🔸 Big Endian과 Little Endian

소켓으로 통신을 할 때 TCP/IP와 호스트는 Byte를 저장하는 순서가 다르다.

저장할 때 상위 바이트, 즉 큰 쪽을 먼저 저장하는 것빅 엔디안(Big Endian),
저장할 때 하위 바이트, 즉 작은 쪽을 먼저 저장하는 것리틀 엔디안(Little Endian)이라고 한다.

여기서 먼저 저장한다는 것은 앞 주소에 둔다는 것을 의미한다.

예를 들어, 컴퓨터에 int형 4 byte 데이터 0x010203040을 저장한다고 했을 때, 다음과 같이 저장한다.

출처: https://softtone-someday.tistory.com/20

가령 TCP/IP에 주소 정보를 전송한다고 했을 때, 네트워크에서는 빅 엔디안으로 통일하기 때문에 <arpa/inet.h>와 같은 헤더 파일의 함수를 통해 별도 처리하여 전송해야 한다.

 

❓ accept() 함수를 수행한 후 소켓이 다시 반환되는 이유

 

서버는 listen() 함수를 통해 클라이언트의 연결 요청을 대기한다. 클라이언트 요청 정보는 시스템 내부적으로 관리되는 큐에 쌓이게 되는데, 큐에 대기하고 있는 요청을 꺼내와서 연결을 완료하는 작업을 accept() 함수가 수행한다.

 

여기서 듣기 소켓연결 소켓이라는 개념을 알아야 한다.

 

socket() 함수를 통해 생성된 소켓은 연결 요청을 확인하는 또는 듣는 역할만 수행한다. 그래서 연결 요청을 받으면 즉시 수신 대기열로 넘겨 다음 요청을 대기한다. 그렇다면 accept() 함수를 통해 반환된 소켓은 무엇일까?

 

출처: https://codingfarm.tistory.com/538

 

위 그림을 보면 연결 요청 대기열이라는 것이 있는데 이것이 요청 대기 큐이다. 여기서 accept() 함수가 첫 번째 요청을 꺼내는데 이 소켓이 바로 클라이언트 소켓이다. 즉, 실제 클라이언트와 통신하는 소켓이라는 것이다. 왜 이렇게 소켓을 분리했을까. 그것은 통신하는 중에도 다른 클라이언트의 요청을 받아들일 수 있도록 하기 위함이다.

 

❓ 멀티 스레딩은 왜 필요한가

 

데이터를 송수신하기 위해서 read() 와 write() 함수를 사용한다. 여기서 write()는 데이터를 보내는 주체가 자기 자신이다. 그래서 보낼 데이터의 양을 알 수 있지만 read() 함수는 그렇지 않다. 즉, 한번 실행하면 언제 끝날지 모르는 상태가 되는 것이다.

 

read() 함수가 종료되는 순간은 클라이언트에서 close() 를 수행하여 서버에 EOF를 보냈을 때이다. 결론적으로 모든 클라이언트를 위해 시스템을 동기화할 수 없다. 그래서 송수신 작업을 하는 함수를 스레드에 올려 비동기적으로 작업해야 여러 클라이언트의 요청을 처리하고 응답할 수 있다. 유닉스 계열 POSIX 시스템에서는 pthread.h 헤더 파일을 제공하여 이를 지원한다.

 

pthread.h: POSIX Thread의 약자로 유닉스계열 POSIX 시스템에서 병렬적으로 작동하는 소프트웨어를 작성하기 위해 제공하는 API (멀티 스레딩)

 

🔨 구현

 

👨‍💻 Server, Client > MySQL  접속

 

먼저 MySQL 을 사용하기 위해 <mysql/mysql.h> 헤더 파일을 넣어야 한다. 실습 환경인 Ubuntu에서 MySQL을 사용하기 위해서는 MySQL을 설치하고 계정을 생성하는 등의 작업이 필요하다. 아래 링크를 참고하면 도움이 될 것이다.

 

https://olppaemmit.tistory.com/27

 

[Linux] Ubuntu에 MySQL 설치 및 사용(외부 접속)

1. MySQL 설치 2. conf 파일 수정 3. port 열어주기 4. MySQL 시작 5. MySQL 접속 6. MySQL 권한 부여 7. VirtualBox에서 포트포워딩 8. MySQL 외부접속 1. MySQL 설치하기 sudo apt-get install mysql-server 2. conf 파일 수정(port

olppaemmit.tistory.com

 

또한 C 에서 사용하는 MySQL 명령어는 아래 링크를 참고하면 도움이 될 것이다.

 

https://onecellboy.tistory.com/161

 

[C 언어] mysql c언어 api라이브러리 사용법 - 펌

출처 : http://koronaii.tistory.com/194 MySQL C API 1) my_ulonglong mysql_affected_rows(MYSQL* mysql) INSERT, UPDATE, DELETE 등의 query로 영향을 받은 ROW의 수를 리턴한다. 2) void mysql_close(MYSQL* mysql) 서버와의 연결을 종료한다

onecellboy.tistory.com

 

...
#include <mysql/mysql.h>

MYSQL *con;                   // SQL connection
MYSQL_RES *sql_result = NULL; // SQL 응답
MYSQL_ROW sql_row;            // SQL 결과 배열

int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    pthread_t send_thread, recv_thread; // 송신 스레드, 수신 스레드
    void *thread_return;                // pthread_join에 사용
    int str_len;
    
    // MYSQL
    con = mysql_init(NULL);

    if (con == NULL)
    {
        fprintf(stderr, "%s\n", mysql_error(con));
        exit(1);
    }

    if (mysql_real_connect(con, "localhost", "user1", "0000", "chatdb", 3306, NULL, 0) == NULL)
    {
        finish_with_error(con);
    }
    
    ...

 

C에서 MySQL이라는 타입으로 connection을 생성할 수 있다. 이를 초기화하고 생성한 계정을 통해 접속하는 코드다.

 

👨‍💻 Client > 소켓 생성

 

int main(int argc, char *argv[])
{
    ...
    sock = socket(PF_INET, SOCK_STREAM, 0); // 소켓 생성
    
    ...

 

'sock' 이라는 Socket 식별자에 socket() 함수를 통해 생성한 소켓을 저장한다. PF_INET은 프로토콜을 지정한다는 것인데 리눅스에서는 AF_INET과 서로 같은 상수를 갖고 있어 구분하지 않아도 된다. SOCK_STREAM은 TCP 통신을 의미하며, 0은 별도 프로토콜을 지정하지 않겠다는 의미다.

 

👨‍💻 Client > 서버 정보 바인딩

 

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

    // 연결할 서버 정보 할당
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));
    
    ...

 

 

inet_addr() 함수와 htons() 함수를 통해 네트워크 바이트 순서에 맞게 주소를 변환한다.

 

🗄 Server > 듣기 소켓 생성

 

...

#include <pthread.h>

/*
여러 명의 클라이언트가 접속하므로 클라이언트 소켓은 배열
멀티스레드 시, clnt_cnt와 clnt_socks 에 여러 스레드가 접속할 수 있기 때문에
두 변수를 사용하는 영역은 임계 영역
*/
int clnt_cnt = 0; // 접속한 클라이언트 수
int clnt_socks[MAX_CLNT];
pthread_mutex_t mtx; // mutex 선언 (스레드끼리 전역변수 동시 사용 방지)

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock; // serv_sock(듣기 소켓), clnt_sock(연결 소켓)
    struct sockaddr_in serv_addr, clnt_addr;
    pthread_t tid; // thread 선언
    socklen_t clnt_addr_size;
    
    ...
    
    pthread_mutex_init(&mtx, NULL); // mutex 생성

    if ((serv_sock = socket(PF_INET, SOCK_STREAM, 0)) == -1) // 듣기 소켓 생성
    {
        error_handling("socket() error");
    }
    
    ...

 

듣기 소켓을 생성하기 앞서 mutex를 알아야 한다.

 

mutex는 pthread에서 지원하는 기능으로 여러 스레드를 실행하는 환경에서 자원에 대한 접근 제한을 위한 동기화를 지원한다. 클라이언트 요청을 비동기적으로 처리하지만 모든 클라이언트가 사용하는 변수는 동기화를 해야 동시 수정으로 인한 일관성 오류를 막을 수 있다. 바로 클라이언트의 수를 카운팅하는 clnt_cnt와 앞서 그림에서 본 클라이언트 소켓들을 저장하는 clnt_socks 배열이 대상이다. 위 코드에서는 사용할 mutex를 초기화하고 있다.

 

🗄 Server > 듣기 소켓에 서버 주소 바인딩

 

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

    // 주소 정보 바인딩
    printf("set server addr...\n");
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));
    
    printf("binding...\n");
    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) // 소켓과 주소 정보 결합
    {
        error_handling("bind() error");
    }
    
    ...

 

이처럼 서버 주소를 바인딩하면 클라이언트가 요청한 서버 주소와 동일한 주소인지 듣기 소켓이 식별한다.

 

🗄 Server > 클라이언트 연결 요청 대기

 

int main(int argc, char *argv[])
{
    // 5는 큐의 크기
    // 웹 서버같이 수 천명의 클라이언트로 바쁠 경우, 15로 잡는 경우가 보통
    if (listen(serv_sock, 5) == -1)
    {
        error_handling("listen() error");
    }

    printf("waiting...\n");
    
    ...

 

큐의 크기를 설정하고 클라이언트 요청을 대기한다.

 

👨‍💻 Client > 서버에 연결 요청

 

int main(int argc, char *argv[])
{
    ...
    
    // 서버에 연결 요청
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
        perror("connect");
        return -1;
    }
    
    ...

 

connect() 함수를 통해 소켓 식별자에 서버 정보를 바인딩한 구조체(serv_addr)의 주소를 넘겼다. 만약 음수가 나온다면 연결에 실패한 것이다.

 

🗄 Server > 클라이언트 소켓 생성

 

...

#include <pthread.h>

int main(int argc, char *argv[])
{
    ...
    
    while (1)
    {
        clnt_addr_size = sizeof(clnt_addr);
        // clnt_addr -> 연결된 클라이언트의 주소 정보
        if ((clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size)) < 0)
        {
            error_handling("accept() error");
        }

        printf("[NOW] accept\n");
        
        pthread_mutex_lock(&mtx);           // 전역 변수 사용을 위해 mutex 락
        clnt_socks[clnt_cnt++] = clnt_sock; // 소켓 배정
        pthread_mutex_unlock(&mtx);         // mutex 언락
        
    ...

 

듣기 소켓을 통해 적재해 둔 연결 요청으로부터 accept() 함수를 통해 클라이언트 요청을 생성했다. 그 후에 클라이언트 소켓을 저장하는 clnt_socks 배열에 현재 클라이언트 소켓을 저장했다.

 

clnt_socks 배열은 클라이언트 전역 변수이므로 pthread_mutex_lock()과 pthread_mutex_unlock() 안에서 작업해야 한다.

 

👨‍💻 Client > 최근 채팅 10개 로그 출력 요청

 

MYSQL *con;                   // SQL connection
MYSQL_RES *sql_result = NULL; // SQL 응답
MYSQL_ROW sql_row;            // SQL 결과 배열

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

    if (mysql_query(con, "SELECT * FROM CHAT ORDER BY date DESC LIMIT 10") != 0) // 최근 10개 레코드 조회
        finish_with_error(con);

    sql_result = mysql_store_result(con); // 쿼리 결과 호출

    while ((sql_row = mysql_fetch_row(sql_result)) != NULL)
    {
        printf("%s : %s\n", sql_row[0], sql_row[1]);
    }
    
    ...

 

클라이언트는 쿼리를 통해 최근 10개 레코드를 요청했다. 테이블 상태는 아래와 같다.

 

 

MYSQL_RES 타입 변수를 통해 저장된 쿼리 결과를 받을 수 있다. 그 후에 결과를 모두 fetch 하여 MYSQL_ROW 변수로 받으면 튜플 단위로 필드마다 배열로 저장된다. 이를 출력했다.

 

로그 데이터 형식

 

👨‍💻 데이터 전송

 

...

#include <pthread.h>

#define BUF_SIZE 100
#define NAME_SIZE 30
#define TIME_SIZE 100

void *send_msg(void *arg);
void *recv_msg(void *arg);
void finish_with_error(MYSQL *con);

int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    pthread_t send_thread, recv_thread; // 송신 스레드, 수신 스레드
    void *thread_return;                // pthread_join에 사용
    int str_len;

    ...

    // 송신과 수신을 수행할 두 스레드 생성
    // 연결 요청 대상인 서버는 동일하므로 매개변수는 sock으로 동일
    pthread_create(&send_thread, NULL, send_msg, (void *)&sock);
    pthread_create(&recv_thread, NULL, recv_msg, (void *)&sock);

    // 스레드 종료 대기
    pthread_join(send_thread, NULL);
    pthread_join(recv_thread, NULL);

    close(sock); // 클라이언트 연결 종료

    mysql_free_result(sql_result); // SQL 응답 포인터 해제

    return 0;
}

 

데이터를 송수신하는 작업은 클라이언트끼리 동기화할 수 없다. 따라서 pthread_create()를 통해 데이터를 전송하고 수신하는 함수를 스레드에 올려서 동작하도록 했다.

 

스레드는 종료되어도 즉시 메모리 자원이 해제되지 않는다. 이를 위해 pthread_join() 함수를 사용한다. 이 함수를 만나면 스레드는 자원을 해제하기 때문에 메모리 누수가 발생하지 않으려면 스레드마다 해당 함수를 사용해야 한다. pthread_join()의 영향을 받는 스레드는 기본 스레드인 joinable 스레드다.

 

...
#include <time.h>

char msg[BUF_SIZE];
char name[NAME_SIZE] = "[DEFAULT]"; // 채팅창에 보여질 이름의 형태(20자 제한)
char logout[] = "님이 로그아웃했습니다.\n";

...

void *send_msg(void *arg)
{
    int sock = *((int *)arg);                        // void descriptor -> int 변환
    char name_msg[TIME_SIZE + NAME_SIZE + BUF_SIZE]; // 사용자 ID와 메시지를 합칠 것임
    char logout_msg[NAME_SIZE + strlen(logout)];     // 사용자 ID와 로그아웃 메시지를 합칠 것임
    char local_date_time[TIME_SIZE];                 // 포맷팅한 시간 정보
    time_t now = time(NULL);                         // 현재 시간
    struct tm *t = localtime(&now);                  // 시간 포맷팅

    while (1)
    {
        fgets(msg, BUF_SIZE, stdin); // 사용자 입력을 msg에 저장

        asctime_r(t, local_date_time); // 현재 시간 갱신

        if (!strcmp(msg, "exit\n"))
        {
            sprintf(logout_msg, "%s %s", name, logout);  // (사용자 ID + 로그아웃 메시지) 를 버퍼에 저장
            write(sock, logout_msg, sizeof(logout_msg)); // 서버로 전송하여 모든 클라이언트에 뿌림

            close(sock); // 서버에 EOF 보냄
            exit(1);
        }

        // 생성된 name_msg를 출력
        sprintf(name_msg, "%s > %s %s", local_date_time, name, msg); // ID가 joo이고 메시지가 "Hi" 라면, [joo] Hi

        write(sock, name_msg, sizeof(name_msg)); // 서버로 채팅을 보냄
    }

    return NULL;
}

 

사용자 입력을 fgets()를 통해 받는다. fgets()는 공백이나 '\n'에 상관없이 마지막에 NULL('\0')만 붙이기 때문에 가장 안전하다고 판단하여 사용했다.

 

현재 시간을 채팅에 포함하기 위해 time.h 헤더 파일을 사용했다. 해당 헤더 파일에서 제공하는 time() 함수를 사용하면 현재 시간을 얻을 수 있다. 앞서 sockaddr 구조체처럼 시간도 tm이라는 구조체로 받을 수 있다. 그러면 아래와 같은  tm 필드에 값이 적절히 바인딩된다.

 

tm 구조체

 

asctime()이라는 함수는 이 내용을 토대로 시간 정보를 좀 더 알아보기 좋게 포맷팅 해준다. asctime_r() 함수는 이 기능에 자동으로 시간을 갱신해 주는 기능이 추가되었다.

 

동작 방식은 채팅 출력문을 포맷팅 한 후 서버 소켓으로 write() 하는 것이다. 그리고 'exit'이라는 문자열을 적으면 로그아웃 즉 EOF를 전송하도록 했다.

 

🗄 Server > 클라이언트가 전송한 메시지를 모든 클라이언트에 전송하고 저장

 

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

    while (1)
    {
        ...

        pthread_create(&tid, NULL, handle_clnt, (void *)&clnt_sock); // handle_clnt를 작업하는 스레드 생성
        pthread_detach(tid);                                         // 해당 스레드 분리
        printf("accepted host(IP: %s, Port: %d)\n", inet_ntoa(clnt_addr.sin_addr), ntohs(serv_addr.sin_port));

        mysql_free_result(sql_result); // SQL 응답 포인터 해제
    }
    
    close(serv_sock);
    mysql_close(con);

    return 0;
}

 

서버도 클라이언트 요청을 처리하는 함수를 pthread를 통해 생성한 스레드에 올렸다. read() 작업이 종료된 후 close()를 요청한 클라이언트의 클라이언트 소켓을 close() 한 후 해당 스레드를 분리한다. 마찬가지로 스레드를 통해 동작하므로 detach 아래 출력문이 클라이언트가 접속하자마자 출력된다.

 

char login[] = "로그인을 완료하였습니다. 로그아웃 명령은 'exit' 입니다.\n";

...

void *handle_clnt(void *arg)
{
    int clnt_sock = *((int *)arg); // void descriptor -> int 변환
    int str_len = 0;
    char msg[BUF_SIZE];
    char query[QUERY_SIZE + BUF_SIZE];
    char pk[QUERY_SIZE];

    send_msg_me(clnt_sock, login, strlen(login));

    /*
    클라이언트에서 보낸 메시지 받음
    클라이언트에서 EOF를 보내 str_len 이 0이 될때까지 반복
    EOF는 클라이언트에서 소켓을 close 했을 때 보냄
    즉, 클라이언트가 접속을 하고 있는 동안에는 while 문을 벗어나지 않는다.
    */
    while ((str_len = read(clnt_sock, msg, sizeof(msg))) != 0)
    {
        send_msg(msg, str_len); // 접속한 모두에게 메시지 보내기

        // escape single quote
        for (int i = 0; i < str_len; i++)
        {
            if (msg[i] == 39)
            {
                for (int j = str_len; j > i; j--)
                {
                    msg[j] = msg[j - 1];
                }
                i += 2;
            }
        }

        now = time(NULL); // 현재 시간

        sprintf(pk, "%ld", now);
        sprintf(query, "INSERT INTO CHAT VALUES('%s', '%s')", pk, msg); // 채팅 쿼리 생성
        if (mysql_query(con, query))                                    // 채팅 저장
            finish_with_error(con);
    }

    // while 문을 탈출 -> 현재 담당하는 소켓의 연결이 끊김

    ...

}

// 접속한 모두에게 메시지 보내기
void send_msg(char *msg, int len)
{
    pthread_mutex_lock(&mtx); // 전역 변수 사용을 위해 mutex 락
    for (int i = 0; i < clnt_cnt; i++)
        write(clnt_socks[i], msg, len); // 모든 클라이언트 소켓에 메시지 전달

    pthread_mutex_unlock(&mtx); // mutex 언락
}

// 접속한 대상에만 메시지 보내기
void send_msg_me(int clnt_sock, char *msg, int len)
{
    write(clnt_sock, msg, len); // 접속 클라이언트 소켓에 메시지 전달
}

...

 

클라이언트가 전송한 데이터와 클라이언트 소켓을 처리하는 함수다.

 

send_msg_me() 함수를 통해 접속한 클라이언트에게 로그인 메시지를 전송한다.

 

서버는 read() 함수를 통해 클라이언트 측에서 write() 한 데이터가 있는지 주시한다. 그러다가 데이터가 들어오면 모든 클라이언트에게 입력한 내용을 보여준다. 이는 설계상 단체 채팅 프로그램이기 때문이다. 모든 클라이언트에게 메시지를 전송하려면 클라이언트 소켓 목록을 가져와서 반복문을 통해 메시지를 보여줘야 한다. 이 작업은 클라이언트 전역 변수를 사용하기 때문에 mutex 락을 걸고 수행해야 한다.

 

앞서 테이블의 필드를 보면 Primary Key로 'date'가 지정되어 있었다. 이는 현재 시간을 10자리 정도의  숫자로 저장한 것이며, 기본키 역할을 한다. 이렇게 완성된 하나의 튜플을 INSERT문을 통해 MySQL에 로그로서 저장한다.

 

while 문을 탈출하게 되면 EOF를 받은 것이며, 연결 종료 작업을 수행해야 한다. 그래서 다시 mutex를 열어 해당 클라이언트 소켓을 제거하는 작업을 수행했다.

 

👨‍💻 데이터 수신

 

void *recv_msg(void *arg)
{
    int sock = *((int *)arg);                        // void descriptor -> int 변환
    char name_msg[TIME_SIZE + NAME_SIZE + BUF_SIZE]; // 사용자 ID와 메시지를 합칠 것임
    int str_len = 0;

    while (1)
    {
        str_len = read(sock, name_msg, sizeof(name_msg) - 1); // 서버에서 들어온 메시지 수신

        name_msg[str_len] = 0; // 버퍼 맨 마지막 값 NULL

        fputs(name_msg, stdout); // 받은 메시지 출력 (서버에서 write 한 메시지)
    }

    return NULL;
}

 

 

서버에서 write() 한 데이터가 있다면 이를 읽어 출력한다.

 

 

🗄 Server > 종료를 요청한 클라이언트 소켓 제거

 

    // while 문을 탈출 -> 현재 담당하는 소켓의 연결이 끊김

    pthread_mutex_lock(&mtx); // 전역 변수 사용을 위해 mutex 락
    // 현재 스레드에서 담당하는 소켓(disconnected) 삭제
    for (int i = 0; i < clnt_cnt; i++)
    {
        if (clnt_sock == clnt_socks[i]) // 현재 담당하는 클라이언트 소켓의 descriptor를 찾으면
        {
            while (i++ < clnt_cnt - 1) // 해당 위치부터 클라이언트 소켓 1칸씩 당기기
                clnt_socks[i] = clnt_socks[i + 1];

            break;
        }
    }

    clnt_cnt--;                 // 클라이언트 수 감소
    pthread_mutex_unlock(&mtx); // mutex 언락

    close(clnt_sock); // 서버의 스레드에서 담당하는 클라이언트 소켓 종료
    
    return NULL;
} //handle_clnt 종료

 

🗄 Server > MySQL 해제 후 서버 소켓 종료

 

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

    while (1)
    {
        ...

        pthread_detach(tid);                                         // 해당 스레드 분리
        printf("accepted host(IP: %s, Port: %d)\n", inet_ntoa(clnt_addr.sin_addr), ntohs(serv_addr.sin_port));

        mysql_free_result(sql_result); // SQL 응답 포인터 해제
    }
    
    close(serv_sock);
    mysql_close(con);

    return 0;
}

 

🚩 결과물

 

전체 코드:

 

https://github.com/wndudrla1011/neo_c/tree/main/chat

 

neo_c/chat at main · wndudrla1011/neo_c

Contribute to wndudrla1011/neo_c development by creating an account on GitHub.

github.com

 

채팅 프로그램

 

접속 후 로그

 

References:

 

https://softtone-someday.tistory.com/20

 

[개념정리] 빅엔디안(Big Endian)과 리틀엔디안(Little Endian)

통신을 하다 보면 통신 패킷이 반대로 나갈 때가 있습니다. 예를 들면 1 2 3 4를 보냈는데 막상 받는 쪽에서 들어온 패킷은 4 3 2 1인 거죠 이는 컴퓨터 CPU의 데이터를 저장하는 순서에서 발생하는

softtone-someday.tistory.com

 

https://dev-rootable.tistory.com/163

 

소켓 프로그래밍

🤔 소켓이란 네트워크에서 데이터를 송수신할 수 있도록 네트워크 환경에 연결할 수 있게 만들어진 연결부 네트워크에 연결하기 위한 소켓은 정해진 규약, 즉 통신을 위한 프로토콜에 맞게

dev-rootable.tistory.com

 

'Network' 카테고리의 다른 글

소켓 프로그래밍  (1) 2024.07.08
SSR에 JWT 적용이 부적합한 이유  (0) 2024.06.08
동기와 비동기 통신  (0) 2024.05.04
HTTP Content-Type  (0) 2024.05.04
OAuth 2.0  (0) 2024.03.31