[DEVELOG] 콘솔 채팅 프로그램 1 - Server 코딩

이성훈·2023년 3월 31일
0

DEVELOG

목록 보기
13/14

C++의 windock2헤더파일 이용하여 간단한 채팅 프로그램을 만들어보았다.



가장먼저, 헤더파일과 ws2_32.lib 라는 라이브러리를 링크해주어야한다.

#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")




윈도우소켓의 통신 과정은 아래의 5가지과정을거친다.

윈속초기화 -> 소켓생성 -> 통신 -> 소켓닫기 -> 윈속종료


첫단계인 윈속초기화 이전에 사용할 객체들을 선언해주자.
WSADATA wsa; //winsock초기화 정보를 저장하는 구조체
SOCKET server_socket, client_socket; //서버와 클라이언트정보를 담는 소켓
sockaddr_in server_addr, client_addr; //각각 소켓마다의 주소를 저장
int client_addr_size = sizeof(client_addr); //클라이언트 소켓의 주소길이




이제 윈속라이브러리를 초기화해주는 WSAStartup함수를 호출해주자.
만약에 초기화에 실패한경우 경고메시지를 띄우기위해 아래와 같이 코딩하였다.

if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0){
    printf("윈속 초기화 실패\n");
    return 1; //오류표시
}




먼저 첫번째인자로 MAKEWORD(2,2)로 사용할 winsock라이브러리의 버전을 전달하고, 두번째인자로 초기화된 winsock정보를 담을 구조체의 주소를 전달하는 모습이다.
여기서 에러가뜨면 return 1로 프로그램을 종료한다.
(지금 main()내에서 코드가 진행중이다)




다음으로 서버소켓을 초기화하는과정이다.

server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (server_socket == INVALID_SOCKET){
    printf("소켓생성 실패\n");
    return 1;
}

socket()함수로 소켓을 초기화하는데, 첫번째인자로 사용할 프로토콜체계(IPv4를사용할것임으로 AF_INET) 두번째인자로 TCP방법을 사용하기위한 SOCK_STREAM, 세번째인자로 SOCK_STREAM을 사용하기위한 IPPROTO_TCP를 선택해준다.(0을 전달하면 시스템이 자동결정해준다.)


그리고 서버 소켓을 바인딩할 주소와 포트를 지정하는 server_adder구조체를 초기화해주자.

server_addr.sin_family = AF_INET; //주소체계. 위에서 AF_INET을 사용했음
server_addr.sin_addr.s_addr = INADDR_ANY; //우리 채팅서버는 모든 네트워크 인터페이스로부터의 연결을 받아들임.
server_addr.sin_port = htons(8888); //서버 포트

2번째줄의 addr.s_addr에서 특정 IPv4주소를 저장하면 해당 PC만이 접속가능하게 설정 할 수 있다.



이제 만든 서버소켓과 주소구조체를 이용하여 바인딩을 해보자.

if (bind(server_socket, (sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR){
    return 1;
}

bind()함수로 간단하게 바인딩가능하다. 인자로 소켓, 주소구조체포인터, 구조체크기를 전달한다.
원래 두번째인자로 sockaddr구조체 포인터를 전달해야한다는데, 좀더 쉬운 sockaddr_in구조체(필드가 3개뿐임)를 전달해도 무방하다.

다음으로 클라이언트로부터 연결요청을 받아들이도록 설정해주면된다.

const int MAX_CLIENTS = 10;
if (listen(server_socket, MAX_CLIENTS) == SOCKET_ERROR){
    std::cout << "listen failed." << std::endl;
    return 1;
}

listen()함수를 이용하여 서버소켓에서 클라이언트 연결요청을 대기 하는 상태로 만들어준다. 여기서 클라이언트 갯수를 지정가능하다.



마지막으로, 클라이언트로부터 연결요청이 오면, 그 정보를 받아들이고, 메시지를 받도록 루프를 만들어주면된다.

std::vector<SOCKET> clients; //클라이언트 소켓들
std::mutex clients_mutex; //여럿 클라이언트가 순차적으로 Clients백터에 접근하도록 보호

while (true){
    client_socket = accept(server_socket, (sockaddr*)&client_addr, &client_addr_size);
    if (client_socket == INVALID_SOCKET){
        return 1;
    }

    std::lock_guard<std::mutex> lock(clients_mutex); //clients_mutex를 잠그어 다른 클라이언트에서의 접근을 대기시킴.(보호)
    clients.push_back(client_socket); //관리 목록에 추가

    std::thread t(handle_client, client_socket);
    t.detach(); //생성한 기능(스레드)를 독립적으로 실행(실행종료후 자동 소멸)
}

여기서 std::mutex객체를 이용하여, 서버가 동시에 여러 클라이언트로부터의 접근을 받을때 일어날 수 있는 무결성을 보호해준다.
예로들면 벡터를 변경하는도중에 다른 클라이언트가 벡터에 같이 접근하여 벡터크기가 다르게 변경된다던가 여러 클라이언트가 동시에 같은 메모리에 접근하는등의 문제를 막아준다.



위에서 클라이언트로부터 받은 메시지를 관리하는 handle_client 함수는 아래와 같다.

const int BUFFER_SIZE = 1024; //최대 메시지 크기
void handle_client(SOCKET client_socket){
    char buffer[BUFFER_SIZE];
    int recv_size;
    std::string client_ip(inet_ntoa(*(in_addr*)&client_socket)); //IP주소 저장

    while (true){
        recv_size = recv(client_socket, buffer, BUFFER_SIZE, 0);
        //클라이언트와의 연결이 끝나거나 소켓오류 발생시
        if (recv_size == 0){
            printf("[%s] 연결종료\n", client_ip.c_str());
            break;
        }
        else if(recv_size == SOCKET_ERROR) {
            printf("[%s] 오류발생, 종료\n", client_ip.c_str());
            break;
        }

        buffer[recv_size] = '\0';
        printf("[%s] %s\n", client_ip.c_str(), buffer);

        std::lock_guard<std::mutex> lock(clients_mutex); 

        for (auto client : clients)
            send(client, buffer, recv_size, 0);

    }

    closesocket(client_socket); 

    std::lock_guard<std::mutex> lock(clients_mutex);
    clients.erase(std::remove(clients.begin(), clients.end(), client_socket), clients.end());
}

클라이언트소켓에서 ipv4주소를 문자열로 변환해서 client_ip에 저장하고, while(true)문으로 데이터를 수신하는 모습이다.
이 handle함수는 좀 전의 코드에서 독립적인 스레드를 만들어 실행시켰으므로, 무한루프를 돌다가 클라이언트와의 연결이 끊기는순간 무한루프가 끝나며
소켓을 닫고, 지금까지 보관했던 클라이언트 소켓정보를 지우도록 코드를 짰다.

위의 일련의 과정을 정리하면 아래의 소스코드가된다.
다음에는 좀더 간단한 클라이언트 프로그램을 만들면 되겠다.

#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
#include <winsock2.h>

//윈도우소켓 라이브러리를 사용하기 위해 필요한 라이브러리 링크를 지정
#pragma comment(lib, "ws2_32.lib")

//윈도우소켓 통신 과정: 
//윈속초기화 -> 소켓생성 -> 통신 -> 소켓닫기 -> 윈속종료

const int MAX_CLIENTS = 10;
const int BUFFER_SIZE = 1024; //최대 메시지 크기

std::vector<SOCKET> clients; //클라이언트 소켓들
std::mutex clients_mutex; //여러 스레드에서 clients개체에 동시접근을 막기위해 사용

//클라이언트에서 메시지를 받을시 실행
void handle_client(SOCKET client_socket){
    char buffer[BUFFER_SIZE];
    int recv_size;
    std::string client_ip(inet_ntoa(*(in_addr*)&client_socket)); //IP주소 저장

    while (true){
        recv_size = recv(client_socket, buffer, BUFFER_SIZE, 0);
        //클라이언트와의 연결이 끝나거나 소켓오류 발생시
        if (recv_size == 0){
            printf("[%s] 연결종료\n", client_ip.c_str());
            break;
        }
        else if(recv_size == SOCKET_ERROR) {
            printf("[%s] 오류발생, 종료\n", client_ip.c_str());
            break;
        }

        buffer[recv_size] = '\0';
        printf("[%s] %s\n", client_ip.c_str(), buffer);

        std::lock_guard<std::mutex> lock(clients_mutex); 

        //모든 연결된 클라이언트에게 메시지를 전송(브로드캐스트)
        for (auto client : clients)
            send(client, buffer, recv_size, 0);

    }

    closesocket(client_socket);

    //클라이언트 연결 종료시 clients에서 제거하여
    //클라이언트소켓의 중복 추가를 방지
    std::lock_guard<std::mutex> lock(clients_mutex);
    clients.erase(std::remove(clients.begin(), clients.end(), client_socket), clients.end());
}

int main(){
    //사용할 객체들
    WSADATA wsa;
    SOCKET server_socket, client_socket;
    sockaddr_in server_addr, client_addr;
    int client_addr_size = sizeof(client_addr);

    //소켓을 사용하기위한 윈속 라이브러리초기화
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0){
        printf("윈속 초기화 실패\n");
        return 1; //오류표시
    }printf("기본 설정 완료\n");

    //클라이언트들과 연결할 소켓을 만듬
    server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (server_socket == INVALID_SOCKET){
        printf("소켓생성 실패\n");
        return 1;
    } printf("소켓 생성 성공\n");

    //주소 구조체 초기화
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY; //서버는 모든 로컬 인터페이스의 연결을 허용함
    server_addr.sin_port = htons(8888); //서버 포트

    //소켓과 주소 구조체를 바인드
    if (bind(server_socket, (sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR){
        printf("소켓과 주소 구조체 바인드 실패\n");
        return 1;
    }printf("소켓과 주소 구조체 바인드 성공\n");

    //클라이언트와의 연결 요청 대기 시도
    if (listen(server_socket, MAX_CLIENTS) == SOCKET_ERROR){
        printf("클라이언트 소켓 수신대기 실패\n");
        return 1;
    }printf("서버 통신 준비 완료: 클라이언트 연결요청 대기중...\n");

    //클라이언트로부터 메시지를 받음
    while (true){
        client_socket = accept(server_socket, (sockaddr*)&client_addr, &client_addr_size);
        if (client_socket == INVALID_SOCKET){
            printf("클라이언트 연결 실패\n");
            return 1;
        }printf("클라이언트 연결 성공: %s\n", inet_ntoa(client_addr.sin_addr));

        //위에서 했듯이 클라이언트 소켓을 관리하도록 설정
        std::lock_guard<std::mutex> lock(clients_mutex);
        clients.push_back(client_socket); //관리 목록에 추가

        //클라이언트로부터 메시지를 받는 함수를 스레드로 생성
        std::thread t(handle_client, client_socket);
        t.detach(); //생성한 기능(스레드)를 독립적으로 실행(실행종료후 자동 소멸)
    }

    //서버종료시
    closesocket(server_socket); //소켓 닫기
    WSACleanup(); //윈속 종료

    return 0; //정상종료
}
profile
I will be a socially developer

0개의 댓글