본문 바로가기

리눅스 멀티스레드 파일 공유 프로그램 - 2 본문

개인 프로젝트 공부

리눅스 멀티스레드 파일 공유 프로그램 - 2

Seongjun_You 2024. 6. 7. 19:17

이번에는 연습으로 c++로 소켓 프로그램을 구현해보려 한다.

c++로는 처음 구현해 보아 gpt의 힘을 빌려

공부를 진행하기로 했다.

 

결과부터 확인을 해보면

 

 

이게 서버의 결과

 

이건 클라이언트의 결과이다.

 

 

서버의 코드부터 분석을 진행해본다.

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>
#include <cstdlib>

#define PORT 8080
#define BUFFER_SIZE 1024
using namespace std;
int main() {
    int server_fd, new_socket;
    
    struct sockaddr_in address;
    
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    int opt = 1;
    
    // 소켓 파일 디스크립터 생성
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        std::cerr << "Socket creation error" << std::endl;
        return -1;
    }


    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        std::cerr << "setsockopt failed" << std::endl;
        close(server_fd);
        return -1;
    }
    // 주소 구조체 설정
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    //cout << sizeof(address) << endl;
    //cout << sizeof((struct sockaddr *)&address) << endl;
    // 소켓을 주소와 바인드
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        std::cerr << "Bind failed" << std::endl;
        return -1;
    }

    // 클라이언트의 연결을 대기
    if (listen(server_fd, 3) < 0) {
        std::cerr << "Listen failed" << std::endl;
        return -1;
    }

    // 연결 수락
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        std::cerr << "Accept failed" << std::endl;
        return -1;
    }

    // 클라이언트로부터 메시지 읽기
    int valread = read(new_socket, buffer, BUFFER_SIZE);
    std::cout << "Message received: " << buffer << std::endl;

    // 클라이언트에 메시지 보내기
    const char *message = "Hello from server";
    send(new_socket, message, strlen(message), 0);
    std::cout << "Hello message sent" << std::endl;

    // 소켓 종료
    close(new_socket);
    close(server_fd);

    return 0;
}

 

 

 

 

struct sockaddr_in {
    sa_family_t    sin_family;  // 주소 체계(AF_INET)
    in_port_t      sin_port;    // 16-bit 포트 번호
    struct in_addr sin_addr;    // 32-bit IP 주소
    char           sin_zero[8]; // 패딩 (사용되지 않음)
};

main함수부터 쭉 보면

sockaddr_in이라는 구조체가 존재한다.

이는 <netinet/in.h> 헤더파일에 존재하는 친구이다.

위와 같은 구조를 가지고 있으며 목적은 IPv4의 정보를 관리하기 위해서이다.

 

 

 

// 소켓 파일 디스크립터 생성
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        std::cerr << "Socket creation error" << std::endl;
        return -1;
    }

다음 소켓 디스크립터를 생성한다.

AF_INET과 SOCK_STREAM은 변수가 아닌 상수로서 존재한다.

AF_INET은 IPv4라는 것을 알리기 위함이며

SOCK_STREAM은 TCP통신을 위함이다.

세 번째 매개변수는 소켓의 프로토콜을 지정하는데 보통 0이 디폴트이다.

TCP 소켓일 경우 'IPPROTO_TCP' 상수를 전달하기도 하지만 0을 사용하기도 한다.

UDP 소켓일 경우 'IPPROTO_UDP' 상수를 전달한다.

 

 

 

if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        std::cerr << "setsockopt failed" << std::endl;
        close(server_fd);
        return -1;
    }

이 조건문은 없어도 실행은 되나

빠르게 주소를 재사용하기 위해서 넣어 놓았다.

서버 소켓이 종료된 후에도 빠르게 같은 주소로 소켓을 다시 열 수 있게 해 준다.

즉 소켓 재사용 및 타임아웃을 설정한데 쓴다.

 

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

함수의 원형이다.

 

  • sockfd: 옵션을 설정할 소켓의 파일 디스크립터
  • level: 옵션의 레벨을 지정합니다. 일반적으로 SOL_SOCKET을 사용하여 소켓 레벨 옵션을 설정
  • optname: 설정할 옵션의 이름을 지정합니다. 예를 들어, 소켓의 재사용을 위해 SO_REUSEADDR을 사용
  • optval: 설정할 옵션의 값을 담고 있는 포인터
  • optlen: 설정할 옵션의 크기를 나타내는 변수

 

주요 옵션

 

  • SO_REUSEADDR: 소켓이 사용하는 주소를 다시 사용할 수 있도록 허용, 주로 서버 소켓을 종료한 후 즉시 같은 포트 번호로 소켓을 다시 열 때 사용
  • SO_REUSEPORT: 같은 포트를 여러 소켓이 공유하여 사용할 수 있도록 허용, 다중 프로세스 또는 스레드에서 서버 소켓을 공유하여 부하 분산을 위해 사용.
  • SO_KEEPALIVE: TCP 소켓이 유휴 상태일 때 TCP keep-alive 메시지를 보내도록 설정. 이를 통해 연결이 유효한지 확인 가능.
  • SO_SNDBUF: 소켓의 송신 버퍼 크기를 설정 이는 송신 측에서 버퍼링 되는 데이터의 양을 조절
  • SO_RCVBUF: 소켓의 수신 버퍼 크기를 설정 이는 수신 측에서 버퍼링되는 데이터의 양을 조절
  • SO_RCVTIMEO: 소켓의 수신 타임아웃을 설정 이는 소켓이 데이터를 수신하기 위해 대기하는 시간을 제어

 

전부 알아둘 필요는 없겠지만

빠른 개발을 위해 SO_REUSEADDR과 SO_REUSEPORT를 이용하였다.

 

 

 

// 주소 구조체 설정
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

 

 

sockaddr_in 구조체에 데이터를 담는다.

htons는 네트워 바이트 순서로 변환 즉 빅 엔디안 형식으로 바꾸어준다.

 

 

 

// 소켓을 주소와 바인드
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        std::cerr << "Bind failed" << std::endl;
        return -1;
    }

아까 struct sockaddr_in address에 데이터를 넣어두었다.

bind()를 통해 소켓에 주소를 할당해 준다.

많은 소켓 함수들은 범용 주소 구조체인 sockaddr을 사용한다.

address를 타입캐스트 해준다.

 

 

// 클라이언트의 연결을 대기
    if (listen(server_fd, 3) < 0) {
        std::cerr << "Listen failed" << std::endl;
        return -1;
    }

listen을 통해 클라이언트의 연결을 대기한다.

두 번째 매개변수는 대기열의 크기이다.

동시에 대기할 수 있는 연결 요청의 최대 수이다.

 

 

// 연결 수락
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        std::cerr << "Accept failed" << std::endl;
        return -1;
    }

첫 번째 매개변수는 연결을 수락할 서버 소켓의 파일 디스크립터이며

두 번째는 클라이언트의 주소 정보를 저장할 구조체

세 번째는 구조체의 크기

 

원래 address에 서버의 정보가 담겨있었다면

해당 함수를 통해 클라이언트의 정보로 바뀐다.

new_socket에 새로운 소켓 디스크립터가 만들어진다.

 

 

// 클라이언트로부터 메시지 읽기
    int valread = read(new_socket, buffer, BUFFER_SIZE);
    std::cout << "Message received: " << buffer << std::endl;

    // 클라이언트에 메시지 보내기
    const char *message = "Hello from server";
    send(new_socket, message, strlen(message), 0);
    std::cout << "Hello message sent" << std::endl;

    // 소켓 종료
    close(new_socket);
    close(server_fd);

read() 함수는 파일디스크립터를 통해 데이터를 읽는데

소켓 프로그래밍에서는 소켓을 매개변수로 데이터를 읽을 수 있다.

 

즉 read(), send()에 소켓을 이용해서 데이터를 주고받을 수 있다.

 

마지막 서버 디스크립터와 만들어진 소켓을 종료한다.

 

 

다음 클라이언트 코드에 대해 분석을 진행한다.

Comments