본문 바로가기
Project/socket_chat_server

[CPP] 소켓 통신 채팅 서버 만들기 - 클라이언트 구현하기

by 블로블로글 2024. 3. 21.

이제 클라이언트 쪽 코드를 만들면된다.

기능은 서버에 연결, 메시지 송수신 정도로 간단하게 구현했다.

#ifndef CLIENT_HPP
#define CLIENT_HPP


#include <iostream>
#include <string>
#include <WinSock2.h>
#include <ws2tcpip.h>
#include <thread>

class ChatClient {
public:
    ChatClient(const std::string& ip, int port, const std::string& nickname)
        : ip_(ip), port_(port), nickname_(nickname) {
        initializeWinsock();
        connectToServer();
    }

    ~ChatClient() {
        cleanup();
    }

    void startChat();
    void start();

private:
    std::string ip_;
    bool running = true;
    int port_;
    std::string nickname_;
    SOCKET serverSocket_ = INVALID_SOCKET;
    void startReceiveThread();
    void initializeWinsock();
    void connectToServer();
    void sendMessage(const std::string& message);
    void receiveMessage();
    void cleanup();
};

#endif

initializeWinsock

  - Winsock API 사용을 위해 필요한 초기화 작업을 수행한다.

  - 이는 네트워크 통신을 시작하기 전에 필수적으로 수행되어야 하는 단계

connectToServer

  - 지정된 IP 주소와 포트를 사용하여 서버에 연결을 시도한다.

  - 연결 실패 시 적절한 에러 메시지를 출력하고 프로그램을 종료합니다.

startReceiveThread

  - 별도의 스레드에서 서버로부터 메시지를 수신하기 위한 메소드이다.

  - 이 스레드는 running 변수가 true인 동안 계속해서 실행

start 및 startChat

  - 사용자로부터 메시지 입력을 받고 서버에 메시지를 전송한다.

  - /x 입력 시 채팅을 종료하고 프로그램을 종료

sendMessage

  - 지정된 메시지를 서버에 전송한다

receiveMessage

  - 서버로부터 메시지를 수신하고 콘솔에 출력한다.

  - 서버 연결이 종료되거나 수신 중 에러가 발생할 경우 적절한 처리를 수행

cleanup

  - 열려 있는 소켓을 닫고 Winsock API 사용을 정리

void ChatClient::initializeWinsock() {
    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (result != 0) {
        std::cerr << "Winsock initialization failed: " << result << std::endl;
        exit(1);
    }
}

initializeWinsock

  - Winsock 라이브러리를 초기화

 

void ChatClient::connectToServer() {
    serverSocket_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (serverSocket_ == INVALID_SOCKET) {
        std::cerr << "Failed to create socket: " << WSAGetLastError() << std::endl;
        WSACleanup();
        exit(1);
    }

    sockaddr_in serverAddr = {};
    serverAddr.sin_family = AF_INET;
    InetPtonA(AF_INET, ip_.c_str(), &serverAddr.sin_addr);
    serverAddr.sin_port = htons(static_cast<u_short>(port_));

    if (connect(serverSocket_, reinterpret_cast<SOCKADDR*>(&serverAddr), sizeof(serverAddr)) == SOCKET_ERROR) {
        std::cerr << "Failed to connect to server: " << WSAGetLastError() << std::endl;
        closesocket(serverSocket_);
        WSACleanup();
        exit(1);
    }
}

ChatClient::connectToServer

  - 클라이언트가 채팅 서버에 연결을 시도하는 과정을 담당

  - 서버와 마찬가지로 소켓을 생성하고, 주소를 설정해서 연결을 시도한다.

  - 다만 InetPtonA를 사용하여 ip주소를 네트워크 바이트 순서로 변환한다는 것이 다르다.

 

void ChatClient::startReceiveThread() {
    while (running) {
        receiveMessage();
    }
}

while 루프

  - running 변수가 true로 설정되어 있는 동안, 즉 클라이언트가 실행 중인 동안 무한 루프를 통해 receiveMessage 메서드를 반복 호출하여 새로운 메시지가 서버로부터 도착할 때마다 즉시 처리한다.

 

void ChatClient::sendMessage(const std::string& message) {
    send(serverSocket_, message.c_str(), static_cast<int>(message.length()), 0);
}

void ChatClient::receiveMessage() {
    char buffer[1024];

    while (running) {
        memset(buffer, 0, sizeof(buffer));
        int bytesReceived = recv(serverSocket_, buffer, sizeof(buffer), 0);
        if (bytesReceived > 0) {
            std::cout << "Received: " << std::string(buffer, 0, bytesReceived) << std::endl;
        }
        else if (bytesReceived == 0) {
            std::cout << "Server closed the connection." << std::endl;
            break;
        }
        else {
            std::cerr << "recv failed: " << WSAGetLastError() << std::endl;
            break;
        }
    }
}

서버와 클라이언트 간의 메시지 송수신을 담당하는 함수이다.

sendMessage

  - 사용자가 입력한 메시지를 서버로 보낸다.

receiveMessage

  - 서버로부터 메시지를 수신하는 역할을 한다.

  - recv함수를 사용하여 serverSocket을 통해 데이터를 수신하고, 수신한 데이터는 버퍼에 저장한다.

  - 수신에 성공하면, 수신된 메시지를 콘솔에 출력한다.

  - string생성자를 통해 버퍼의 내용을 문자열로 변환하고 출력 범위를 수신된 바이트 수로 제한한다.

 

ChatClient 구현은 TCP/IP 기반의 채팅 개발을 하면서, 실시간 통신이 필요한 애플리케이션에서 사용자 입력 처리와 네트워크 I/O 작업을 효율적으로 관리하는 방법을 배울 수 있었다.

 

이로써 모든 과정이 끝났다... 어려운 점이 많았지만 그래도 완성하고 나니 뿌듯한 것 같다!!
서버의 데이터 베이스 부분을 class화해서 정보를 은닉하기, 로그인 구현, 데이터 베이스 스키마 강화 등 아직 개선점은 많다. 언젠가 시간이 되면 다시 코드를 개선해봐야겠다.