[포스코x코딩온] 스마트 팩토리 과정 7,8주차 회고 | C++프로젝트 회고

Dana·2023년 5월 14일
0

기능소개

  1. 회원가입
    - id(name), pw
    - id 중복체크
  2. 로그인
    - id, pw가 일치하는지 확인
    - 로그인 성공하면 채팅방 입장
  3. 암호 변경
    - 아이디를 입력하고 기존 암호 입력
    - 변경할 암호를 입력하면 암호 변경
  4. 회원 탈퇴
    - 아이디를 입력하고 암호 입력
    - 정말 탈퇴할건지 확인 후 탈퇴
  5. 단체 채팅
    - 채팅 정보 db에 저장, 시간 자동 입력
    - 채팅방에 입장했을 때 이전 채팅 기록 보여주기(최대 10개)

  6. 번외 - dm구현
    - #DM 받는사람id 보낼메세지
    - 해당 id에게만 메세지 보내기
  7. 번외 - 계산기
    - #CAL 정수 연산자 정수
    - 사칙연산 결과 출력
  8. 번외 - 게임
    - #GAME 입력시 up&down 게임 실행
    - 5번 내에 랜덤생성된 숫자 맞추기
  9. 기타 번외 기능
    - #입력시 전체 기능 출력
    - #USER 입력시 현재 접속중인 client 아이디 출력
    - #EXIT 입력시 프로그램 종료

구현방법

DB 설계


  • TIMESTAMP DEFAULT NOW() : 채팅을 디비에 저장하면 자동으로 현재 시간이 입력된다.
  • receiver에 NULL값을 가능하게 설정해서 NULL인경우 단체 채팅, NULL이 아닌 경우 DM으로 구분했다.

<server.cpp>

main()

 WSADATA wsa;
 int code = WSAStartup(MAKEWORD(2, 2), &wsa);
  • Winsock를 초기화하는 함수. MAKEWORD(2, 2)는 Winsock의 2.2 버전을 사용하겠다는 의미.
  • 실행에 성공하면 0을, 실패하면 그 이외의 값을 반환.
  • 0을 반환했다는 것은 Winsock을 사용할 준비가 되었다는 의미.
    if (!code) {
        server_init();

        std::thread th1[MAX_CLIENT];
        for (int i = 0; i < MAX_CLIENT; i++) { // 인원 수 만큼 thread 생성해서 각각의 클라이언트가 동시에 소통할 수 있도록 함.
            th1[i] = std::thread(add_client);
        }

        while (1) { // 무한 반복문을 사용하여 서버가 계속해서 채팅 보낼 수 있는 상태를 만들어 줌. 반복문을 사용하지 않으면 한 번만 보낼 수 있음.
            string text, msg = "";

            std::getline(cin, text);
            const char* buf = text.c_str();
            msg = server_sock.user + " : " + buf;
            send_msg(msg.c_str()); // 서버가 보내는 메세지
        }

        for (int i = 0; i < MAX_CLIENT; i++) {
            th1[i].join();
            //thread 작업이 끝날 때까지 main 함수가 끝나지 않도록 해줌.
        }

        closesocket(server_sock.sck);
    }
    else {
        cout << "프로그램 종료. (Error code : " << code << ")";
    }

    WSACleanup();
    return 0;

server_init()

server_sock.sck = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

    SOCKADDR_IN server_addr = {}; // 소켓 주소 설정 변수
    server_addr.sin_family = AF_INET; // 소켓은 Internet 타입 
    server_addr.sin_port = htons(7777); // 서버 포트 설정
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // Any인 경우는 호스트를 127.0.0.1로 잡아도 되고 localhost로 잡아도 되고 양쪽 다 허용하게 할 수 있다. 그것이 INADDR_ANY이다.

    bind(server_sock.sck, (sockaddr*)&server_addr, sizeof(server_addr)); // 설정된 소켓 정보를 소켓에 바인딩한다.
    listen(server_sock.sck, SOMAXCONN); // 소켓을 대기 상태로 기다린다.

    server_sock.user = "server";

    cout << "Server On" << endl;

add_client()

SOCKADDR_IN addr = {};
    int addrsize = sizeof(addr);
    char buf[MAX_SIZE] = { };

    ZeroMemory(&addr, addrsize); // addr의 메모리 영역을 0으로 초기화

    SOCKET_INFO new_client = {};

    new_client.sck = accept(server_sock.sck, (sockaddr*)&addr, &addrsize);
    recv(new_client.sck, buf, MAX_SIZE, 0); // Winsock2의 recv 함수. client가 보낸 닉네임을 받음.
    new_client.user = string(buf);

    string msg = "[공지] " + new_client.user + " 님이 입장했습니다.";
    cout << msg << endl;
    sck_list.push_back(new_client); // client 정보를 담는 sck_list 배열에 새로운 client 추가

    std::thread th(recv_msg, client_count); // 다른 사람들로부터 오는 메시지를 계속해서 받을 수 있는 상태로 만들어 두기.

    client_count++; // client 수 증가.
    cout << "[공지] 현재 접속자 수 : " << client_count << "명" << endl;
    send_msg(msg.c_str()); // c_str : string 타입을 const chqr* 타입으로 바꿔줌.

    th.join();

send_msg(const char* msg)

  • 접속해 있는 모든 client에게 메시지 전송
for (int i = 0; i < client_count; i++) { 
        send(sck_list[i].sck, msg, MAX_SIZE, 0);
    }

recv_msg(int idx)

  • DB 연결
try {
        driver = sql::mysql::get_mysql_driver_instance();
        con = driver->connect(server, username, password);
    }
    catch (sql::SQLException& e) {
        cout << "Could not connect to server. Error message: " << e.what() << endl;
        exit(1);
    }

    // 데이터베이스 선택
    con->setSchema("chattingproject");

    // db 한글 저장을 위한 셋팅 
    stmt = con->createStatement();
    stmt->execute("set names euckr");
    if (stmt) { delete stmt; stmt = nullptr; }
  • 클라이언트로부터 메세지를 받은 경우
    char buf[MAX_SIZE] = { };
    string msg = "";

    while (1) {
        ZeroMemory(&buf, MAX_SIZE);
        if (recv(sck_list[idx].sck, buf, MAX_SIZE, 0) > 0) { // 오류가 발생하지 않으면 recv는 수신된 바이트 수를 반환. 0보다 크다는 것은 메시지가 왔다는 것.
            string sbuf(buf);
            if (sbuf.compare("#") == 0)
                show_func(idx);
            else if (sbuf.compare("#USER") == 0)
                show_user(idx);
            else if (buf[0] == '#') {
                int cur_position = 0;
                int position = sbuf.find(" ", cur_position);
                int len = position - cur_position;
                string flag = sbuf.substr(cur_position, len);
                if (flag.compare("#DM") == 0)
                    send_dm(position, sbuf, idx);
                else if (flag.compare("#CAL") == 0)
                    calculator(idx, position, sbuf);
            }
            else {
                msg = sck_list[idx].user + " : " + buf;
                pstmt = con->prepareStatement("INSERT INTO chatting(sender, message) VALUES(?,?)"); // INSERT
                pstmt->setString(1, sck_list[idx].user);
                pstmt->setString(2, buf);
                pstmt->execute(); // 쿼리 실행
                pstmt = con->prepareStatement("SELECT date_format(time, '%H:%m') FROM chatting ORDER BY time DESC LIMIT 1");
                result = pstmt->executeQuery();

                while (result->next()) {
                    msg += " [" + result->getString(1) + "]\n";
                }
                    cout << msg << endl;
                send_msg(msg.c_str());
            }
        }
        else { //그렇지 않을 경우 퇴장에 대한 신호로 생각하여 퇴장 메시지 전송
            msg = "[공지] " + sck_list[idx].user + " 님이 퇴장했습니다.";
            cout << msg << endl;
            send_msg(msg.c_str());
            del_client(idx); // 클라이언트 삭제
            return;
        }
    }
  • .compare(비교할문자열) : 같은문자열이면 0 return
  • .find(찾을문자, 시작인덱스) : 시작인덱스부터 문자를 찾으면 문자 인덱스 return
  • .substr(시작인덱스,크기) : 시작인덱스부터 크기만큼 문자열 자르기

del_client(int idx)

  • 클라이언트 소켓 연결을 끊고 구조체 리스트에서 삭제
  • 현재 클라이언트 수 -1
closesocket(sck_list[idx].sck);
    sck_list.erase(sck_list.begin() + idx);
    client_count--;

show_func(int idx)

  • #입력시 전체 기능 출력
string msg = 
    "--------------------------------------------------\n" 
    "* # : FUNC LIST                                  *\n" 
    "* #USER :현재 접속중인 사용자 아이디             *\n"
    "* #DM receiver message : DM 보내기               *\n" 
    "* #CAL number operator number : 사칙연산         *\n"
    "* #GAME : UP & DOWN 게임                         *\n"
    "* #EXIT : 프로그램 종료                          *\n"
    "--------------------------------------------------\n";
    send(sck_list[idx].sck, msg.c_str(), MAX_SIZE, 0);

show_user(int idx)

  • #USER입력시 현재 접속중인 클라이언트 아이디 출력
string msg = "현재 접속중인 사람 : ";
    for (int i = 0; i < client_count; i++) {
        msg += sck_list[i].user;
        msg += " ";
    }
    send(sck_list[idx].sck, msg.c_str(), MAX_SIZE, 0);

send_dm(int position, string sbuf, int idx)

  • #DM receiver message
  • 보낸시간을 함께 보내야하기 때문에 먼저 db에 입력하고 시간을 가져와서 보낸다.
int cur_position = position + 1;
    position = sbuf.find(" ", cur_position);
    int len = position - cur_position;
    string receiver = sbuf.substr(cur_position, len);
    cur_position = position + 1;
    string dm = sbuf.substr(cur_position);
    string msg = "[DM] " + sck_list[idx].user + " : " + dm;
    pstmt = con->prepareStatement("INSERT INTO chatting(sender,receiver, message) VALUES(?,?,?)"); // INSERT
    pstmt->setString(1, sck_list[idx].user);
    pstmt->setString(2, receiver);
    pstmt->setString(3, dm);
    pstmt->execute(); // 쿼리 실행
    pstmt = con->prepareStatement("SELECT date_format(time, '%H:%m') FROM chatting ORDER BY time DESC LIMIT 1");
    result = pstmt->executeQuery();

    while (result->next()) {
        msg += " [" + result->getString(1) + "]\n";
    }
    cout << msg << endl;
    for (int i = 0; i < client_count; i++) {
        if (receiver.compare(sck_list[i].user) == 0)
            send(sck_list[i].sck, msg.c_str(), MAX_SIZE, 0);
    }

calculator(int idx, int position, string sbuf)

int cur_position = position + 1;
    position = sbuf.find(" ", cur_position);
    int len = position - cur_position;
    string num1 = sbuf.substr(cur_position, len);
    cur_position = position + 1;
    position = sbuf.find(" ", cur_position);
    string op = sbuf.substr(cur_position, 1);
    cur_position = position + 1;
    string num2 = sbuf.substr(cur_position);
    
    double result = 0;
    if (op == "+")
        result = std::stoi(num1) + std::stoi(num2);
    else if (op == "-")
        result = std::stoi(num1) - std::stoi(num2);
    else if (op == "*")
        result = std::stoi(num1) * std::stoi(num2);
    else if (op == "/")
        result = (double)std::stoi(num1) / std::stoi(num2);
    string res = std::to_string(result);
    string msg = "[RESULT] " + num1 + " " + op + " " + num2 + " = " + res;
    cout << msg << endl;
    send(sck_list[idx].sck, msg.c_str(), MAX_SIZE, 0);
  • stoi : 문자열을 정수로 변환

차별화한 점

다양한 기능

  • 실제 카카오톡과 유사하게 #으로 다양한 기능을 제공한다.

UI

  • 단조로운 콘솔 화면에 변화를 주기 위해 채팅방 배경색, 글자색을 변경했다.
콘솔 채팅방 크기, 이름 설정
    system("mode con: cols=50 lines=40 | title CodingOnTalk");
  • 채팅방 입장 시 배경색이 바뀌고 콘솔 이름이 아이디로 바뀐다.
  • 실제로 채팅방에 입장하는 효과를 주기 위해 sleep 사용
string title = "title CodingOnTalk - " + user_name;
                Sleep(1000);
                system("cls");
                system("color 60");
                system(title.c_str());
  • 콘솔 배경색, 글자색을 변경하는 함수
void textcolor(int foreground, int background)
{
    int color = foreground + background * 16;
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color);
}

textcolor(0, 6); // 기본 (글씨 검정색, 배경 노란색)
textcolor(4,6); // 서버에서 온 메세지는 빨간색으로 출력
textcolor(1,6); // DM은 파란색으로 출력

보안

  • 비밀번호를 입력할 때 *로 보이도록 함
char* pw = new char[20];
int i = 0;
    while ((pw[i] = _getch()) != 13)
    {
        if (pw[i] == 8) // 백스페이스 입력
        {
            printf("\b");
            cout << ' ';
            printf("\b");
            if (i > 0)
                i--;
        }
        else
        {
            cout << '*';
            i++;
        }
    }
    pw[i] = '\0';
    user_pw = pw;

Git 활용

  • 기능별로 브랜치를 생성하고 merge하는 과정 반복
  • 커밋 메세지 형식을 고정해서 정확한 의도 전달

파일 분리, 함수 분리

  • 소스파일과 헤더파일을 분리하여 관리
  • 기능별로 함수를 나눠 코드를 구조화하여 가독성이 좋고 리팩토링이 용이하다.

0개의 댓글