포트폴리오/[IoT 기반 피지컬 AI 교육일지]

[ C언어 IoT 프로젝트 ] #3 MySQL 연동 및 멀티스레드 TCP 소켓 구축(27일차 기록)

개발자혜콩 2026. 3. 24. 09:50

1. 오늘 한 것

① 시스템 데이터베이스(MariaDB/MySQL) 설계 및 구축

데이터의 성격과 보존 주기에 따라 3개의 핵심 테이블 구조를 설계함.

  • sensor_logs: 아두이노 센서 수치 실시간 기록 (주기적 삭제 필요)
CREATE TABLE sensor_logs (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    sensor_type VARCHAR(20) NOT NULL,  -- TEMP, HUMI, PIR, ULTRA, FALL, SOS, FAN
    value       FLOAT       NOT NULL,
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

 

  • chat_logs: 보호자 ↔ 거주자 간 양방향 통신 메시지 이력 기록
CREATE TABLE chat_logs (
    id         INT AUTO_INCREMENT PRIMARY KEY,
    sender     ENUM('S', 'G') NOT NULL,  -- S: Senior, G: Guardian
    message    TEXT           NOT NULL,
    sent_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

 

  • alert_logs: 고온, 낙상, SOS 등 위험 이벤트 발생 이력 영구 보관
CREATE TABLE alert_logs (
    id         INT AUTO_INCREMENT PRIMARY KEY,
    alert_type VARCHAR(20)  NOT NULL,   -- 고온, 주의, 경보, 비상
    detail     VARCHAR(200) NOT NULL,
    status     VARCHAR(20)  DEFAULT '확인필요',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

② 리눅스 서버 - 윈도우 클라이언트 간 TCP 소켓 통신 구현

  • 서버(Port 9000)는 클라이언트 접속 시마다 개별 스레드를 생성(tcpAcceptThread)하여 다중 접속을 처리하는 구조 구현.
  • 접속 시 첫 패킷으로 역할(SENIOR 또는 GUARDIAN)을 부여받아 클라이언트 배열에 등록.
  • 특정 역할의 모든 클라이언트에게 패킷을 브로드캐스팅하는 sendToRole() 함수 구현.

③ 통신 프로토콜 상세 설계 및 데이터 중계(Relay)

  • 채팅 중계: 거주자의 메시지는 보호자에게, 보호자의 메시지는 거주자에게 교차 포워딩하는 로직 구현.

         : 거주자가 메시지를 보내면 서버가 보호자에게 포워딩하고, 보호자가 메시지를 보내면 거주자에게 포워딩한다.

if (pkt.type == PT_CHAT_MSG) {
    char sender = (clients[idx].role == ROLE_SENIOR) ? 'S' : 'G';
    saveChat(db, sender, pkt.payload.chat.message);

    int target = (clients[idx].role == ROLE_SENIOR)
                 ? ROLE_GUARDIAN : ROLE_SENIOR;

    NetPacket relay = pkt;
    snprintf(relay.sender_id, 16, "%s",
        sender == 'S' ? "SENIOR" : "GUARDIAN");
    sendToRole(target, &relay);
}

 

           sender_id에 발신자 정보를 담아서 중계하면, 수신 측에서 "거주자 : 메시지" 형태로 구분해 출력할 수 있다.


  • HISTORY 프로토콜: 과거 기록 조회 시 패킷 제한(128Byte)을 우회하기 위해 단일 문자열 대신 HISTORY_START ➔ 데이터 개별 전송 ➔ HISTORY_END 형태로 행(Row) 단위 분할 전송 기법 적용.
서버 → 클라이언트 전송 순서:
  "HISTORY_START"
  "HISTORY|03/19 14:30|비상|SOS 호출|확인필요"
  "HISTORY|03/19 11:20|경보|낙상위험 경보|확인필요"
  ... (최대 20건)
  "HISTORY_END"

 

      클라이언트는 HISTORY_START를 받으면 벡터를 초기화하고,

       HISTORY|로 시작하는 패킷을 파싱해 행 단위로 누적한다.

       HISTORY_END를 받으면 ListView 팝업을 띄운다.

if (msg == "HISTORY_START") {
    g_history.clear();
} else if (msg.rfind("HISTORY|", 0) == 0) {
    // pipe로 분리해서 HistRow에 저장
    auto parts = splitPipe(msg.substr(8));
    g_history.push_back({ parts[0], parts[1], parts[2], parts[3] });
} else if (msg == "HISTORY_END") {
    PostMessage(g_hWnd, WM_SHOWHISTORY, 0, 0);
}

       -> 128바이트 제한을 행 단위 분리로 우회.


  • CARD 프로토콜: UI 상단 센서 위젯의 갱신을 위해 CARD:TEMP:21.5 형태의 접두사 패킷을 전송. 수신 측에서는 해당 접두사를 필터링해 채팅 로그창을 오염시키지 않고 위젯만 조용히 갱신(WM_UPDATESENSOR)하도록 분리.

   ▼ 서버에서 CARD: 접두사 패킷으로 업데이트

서버 → 보호자:
  "CARD:TEMP:21.5"
  "CARD:HUMI:56.0"
  "CARD:PIR:0"
  "CARD:FALL:0.0"

 

   ▼  클라이언트는 CARD:를 감지하면 로그창에 출력하지 않고 카드 값만 갱신

if (msg.rfind("CARD:", 0) == 0) {
    std::string body = msg.substr(5);
    if (body.rfind("TEMP:", 0) == 0) {
        float v = atof(body.substr(5).c_str());
        swprintf(buf, 32, L"%.1f C", v);
        g_valTemp = buf;
    }
    // ... HUMI, PIR, FALL 동일
    PostMessage(g_hWnd, WM_UPDATESENSOR, 0, 0);
    continue;  // 로그창 출력 스킵
}

  -> 이 분리 덕분에 대화 로그가 센서 데이터로 오염되지 않음.


2. 문제 / 헷갈린 점

① 멀티스레드 환경의 MySQL 크래시 (세그멘테이션 폴트)

  • 상황: 시리얼 스레드와 클라이언트 스레드가 하나의 DB 연결 객체(MYSQL*)를 공유한 상태에서 동시에 쿼리를 날리자 서버가 다운됨.
  • 해결: MySQL의 연결 객체는 스레드 안전(Thread-Safe)하지 않음.
    1. main() 최상단에서 mysql_library_init(0, NULL, NULL); 최초 1회 의무 호출.
    2. 각 스레드 내부에서 mysql_thread_init() 및 mysql_init()을 통해 스레드별 독립적인 DB 커넥션을 생성하여 충돌 완벽 해결.
// main()에서 최초 1회
mysql_library_init(0, NULL, NULL);

// 각 스레드에서 독립 연결
MYSQL* dbConnect() {
    mysql_thread_init();
    MYSQL *conn = mysql_init(NULL);
    mysql_real_connect(conn, ...);
    return conn;
}

void dbDisconnect(MYSQL *conn) {
    if (conn) mysql_close(conn);
    mysql_thread_end();
}

② 상태조회기능 오류 : MySQL 8.0 only_full_group_by 모드 오류

  • 상황: 센서별 최신값을 조회하기 위해 GROUP BY sensor_type을 사용했으나, MySQL 8.0의 엄격한 모드 설정으로 인해 문법 에러(ERROR 1055) 발생.
ERROR 1055: Expression #2 of SELECT list is not in GROUP BY clause
and contains nonaggregated column — incompatible with sql_mode=only_full_group_by
  • 해결: 서브 쿼리와 INNER JOIN을 활용해 SQL 모드에 의존하지 않는 표준 쿼리로 튜닝.
SELECT s1.sensor_type, s1.value, s1.created_at
FROM sensor_logs s1
INNER JOIN (
    SELECT sensor_type, MAX(id) AS max_id
    FROM sensor_logs
    GROUP BY sensor_type
) s2 ON s1.sensor_type = s2.sensor_type AND s1.id = s2.max_id;

 


③ 공유 자원(클라이언트 배열) 동시 접근 이슈

  • 상황: 여러 스레드가 동시에 클라이언트 배열에 접근하여 패킷을 전송할 때 데이터가 꼬일 위험 존재.
  • 해결: pthread_mutex_t를 활용하여 sendToRole() 함수 내 루프 실행 구간을 임계 영역(Critical Section)으로 묶어 상호배제(Mutex) 보장.

- TCP 소켓 통신 구조

Windows UI → connect(서버IP:9000)
           → 첫 패킷으로 "SENIOR" 또는 "GUARDIAN" 전송
           → 이후 NetPacket 주고받기

 

- 서버는 클라이언트마다 별도 스레드를 생성

void *tcpAcceptThread(void *arg) {
    while (1) {
        int client_fd = accept(server_fd, ...);
        
        ClientArg *ca = malloc(sizeof(ClientArg));
        ca->idx = addClient(client_fd);
        
        pthread_t tid;
        pthread_create(&tid, NULL, clientThread, ca);
        pthread_detach(tid);
    }
}

 

- 클라이언트 배열은 여러 스레드가 동시에 접근하므로 뮤텍스로 보호

pthread_mutex_t clients_lock = PTHREAD_MUTEX_INITIALIZER;

void sendToRole(int role, NetPacket *pkt) {
    pthread_mutex_lock(&clients_lock);
    for (int i = 0; i < MAX_CLIENTS; i++) {
        if (clients[i].active && clients[i].role == role)
            send(clients[i].fd, pkt, sizeof(NetPacket), MSG_NOSIGNAL);
    }
    pthread_mutex_unlock(&clients_lock);
}

 

sendToRole(ROLE_GUARDIAN, &pkt)을 호출하면 현재 접속 중인 모든 보호자 클라이언트에게 패킷이 전송


3. 오늘 배운 핵심

  • 멀티스레드는 항상 의심해야 한다: DB 커넥션, 클라이언트 배열 등 스레드가 공유 자원을 건드릴 때는 반드시 독립 객체를 할당하거나 뮤텍스로 잠가야 시스템 크래시를 막을 수 있다.
  • 데이터 한계를 설계로 극복하라: 패킷 사이즈(128Byte) 한계를 늘리는 대신, START/END 플래그를 활용한 데이터 분할 전송(HISTORY 프로토콜) 구조를 고안하여 네트워크 효율과 한계를 동시에 해결했다.
  • 데이터의 목적지에 따라 채널을 분리하라: 센서 데이터(CARD:)와 채팅 데이터(CHAT)의 패킷 헤더를 분리하여, UI 상단 위젯과 하단 로그창이 독립적으로 동작하도록 설계하는 것이 깔끔한 클라이언트 렌더링의 핵심이다.

4. 다음 목표

  • 한글 인코딩(UTF-8 ↔ CP949) 해결하기.
  • Win32 API를 활용하여 Windows 클라이언트(SeniorUI, GuardianUI) 화면 디자인 및 컴포넌트(ListView 등) 구성.
  • 수신된 CARD: 패킷과 통신하여 UI 화면의 센서 수치가 실시간으로 변하는지 연동 테스트 진행.