본문 바로가기

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

개인 프로젝트 공부

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

Seongjun_You 2024. 6. 14. 19:56

작업환경의 디렉터리를 압축하여 전송하는 로직을 만들었다.

개발 환경이나 버전에 따라 안 되는 기능들이 너무 많아 힘들었다.

결국 우분투 22.04버전에서 다시 코드를 구현하기로 했다.

 

여러 압축 라이브러리가 있었지만 g++ 버전 및 작업 환경에 따라 오류가 발생하고

정보량이 많지가 않아서 zip.h를 이용하기로 했다.

 

 

 

인자값이 많이 줄었다.

zip.h을 위한 -lzip을 추가해 준다.

 

 

#include <iostream>
#include <fstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstdlib>
#include <string>
#include <cstring>
#include <algorithm>
#include <sstream>
#include <arpa/inet.h>
#include <filesystem>

#include <iomanip>
#include <zip.h>


#define PORT 8080
#define BUFFER_SIZE 1024
using namespace std;
namespace fs = std::filesystem;




void compressDirectory() {
    string directory = "/home/sj/share_project/";
    string zipFile = "/home/sj/test/output.zip";
    zip_t* zip = zip_open(zipFile.c_str(), ZIP_CREATE | ZIP_TRUNCATE, NULL);
    if (!zip) {
        std::cerr << "압축 파일을 열 수 없습니다." << std::endl;

    }

    // 디렉터리 내의 파일 목록 가져오기
    for (const auto& entry : fs::directory_iterator(directory)) {
        if (entry.is_regular_file()) {
            string filePath = entry.path().string();
            zip_source_t* source = zip_source_file(zip, filePath.c_str(), 0, 0);
            zip_file_add(zip, fs::path(filePath).filename().c_str(), source, ZIP_FL_OVERWRITE);
        }
    }

    // 압축 파일 닫기
    zip_close(zip);
}


void *clientHandler(void *newsocket)
{
    int new_socket = *((int *)newsocket);
    char buffer[BUFFER_SIZE];
    string str_buffer;
    int valread = recv(new_socket, buffer, BUFFER_SIZE, 0);
    str_buffer = string(buffer, valread);

    if(str_buffer == "1234")
    {
        string send_filename = "/home/sj/test/output.zip";
        ifstream file(send_filename, ios::binary | ios::ate);
        file.seekg(0);
        std::stringstream file_stream;
        file_stream << file.rdbuf();
        std::string file_contents = file_stream.str();
        if(send(new_socket, file_contents.c_str(), file_contents.length(), 0) < 0){
            cerr << "Failed to send file" << endl;
        }     
    }

    else
    {
        str_buffer = "Failed";
        send(new_socket, str_buffer.c_str(), str_buffer.length(), 0);
    }

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

string inToip(uint32_t ip){
    struct in_addr ip_addr;
    ip_addr.s_addr = ip;
    return string(inet_ntoa(ip_addr));
}

int main() {
    int server_fd, new_socket;
    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;
    }

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

    // 소켓을 주소와 바인드
    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;
    }

    // 연결 수락
    compressDirectory();
    while(true)
    {
        struct sockaddr_in client_addr;
        int client_addrlen = sizeof(client_addr);
        if ((new_socket = accept(server_fd, (struct sockaddr *)&client_addr, (socklen_t*)&client_addrlen)) < 0) {
            std::cerr << "Accept failed" << std::endl;
            return -1;
        }
        cout << "Client IP : " << inToip(client_addr.sin_addr.s_addr) << endl;
        pthread_t thread;
        int result = pthread_create(&thread, NULL, clientHandler, &new_socket);
        pthread_detach(thread);
    }  
    close(server_fd);

    return 0;
}

서버의 코드이다.

 

 

압축 함수부터 살펴본다.

 

oid compressDirectory() {
    string directory = "/home/sj/share_project/";
    string zipFile = "/home/sj/test/output.zip";
    zip_t* zip = zip_open(zipFile.c_str(), ZIP_CREATE | ZIP_TRUNCATE, NULL);
    if (!zip) {
        std::cerr << "압축 파일을 열 수 없습니다." << std::endl;

    }

    // 디렉터리 내의 파일 목록 가져오기
    for (const auto& entry : fs::directory_iterator(directory)) {
        if (entry.is_regular_file()) {
            string filePath = entry.path().string();
            zip_source_t* source = zip_source_file(zip, filePath.c_str(), 0, 0);
            zip_file_add(zip, fs::path(filePath).filename().c_str(), source, ZIP_FL_OVERWRITE);
        }
    }

    // 압축 파일 닫기
    zip_close(zip);
}

zip_open() 함수를 이용해

새로운 zip파일을 만들어낸다.

 

zipFile.c_str()을 통해 경로를 넣어준다.

ZIP_CREATE는 새로운 zip파일을 생성한다는 것을 의미한다. 해당 파일이 존재하이 않을 경우 새로 만들어준다.

ZIP_TRUNCATE는 이미 파일이 존재할 경우 해당 파일의 내용을 비우고 재사용한다.

 

반복문에서는 

#include <filesystem>의 헤더에서

std::filesystem의 directory_iterator()을 이용하여

디렉터리를 순회하여 경로를 얻어낸다.

 

entry.is_regular_file() 조건문으로 파일이 아닌지 판단하고

파일일 경우에만 압축 로직을 실행한다.

 

zip_source_t* source = zip_source_file(zip, filePath.c_str(), 0, 0);

압축파일에 추가할 파일의 소스를 생성한다.

즉 주어진 파일의 경로로부터 zip 소스를 생성하고, 이를 압축파일에 추가할 준비를 한다.

 

 

zip_file_add(zip, fs::path(filePath).filename().c_str(), source, ZIP_FL_OVERWRITE);

실제로 압축 파일에 파일을 추가하는 로직이다.

fs::path(filePath).filename().c_str() 이 부분은 path에서 파일의 이름만 추출하는 과정이다.

세 번째 매개변수는 아까 파일들의 zip 소스들을 넣는 부분이다.

ZIP_FL_OVERWRITE 이 플래그는 추가된 파일이 이미 존재하는 경우, 이플래그를 사용하여

기존 파일을 덮어쓸 것인지 여부를 지정한다.

 

이렇게 작업공간에 대한 하나의 압축파일을 만들었다.

 

 

 

if(str_buffer == "1234")
    {
        string send_filename = "/home/sj/test/output.zip";
        ifstream file(send_filename, ios::binary | ios::ate);
        file.seekg(0);
        std::stringstream file_stream;
        file_stream << file.rdbuf();
        std::string file_contents = file_stream.str();
        if(send(new_socket, file_contents.c_str(), file_contents.length(), 0) < 0){
            cerr << "Failed to send file" << endl;
        }     
    }

압축 파일을 클라이언트에게 전송하는 로직이다.

 

ifstream : 파일 입력 스트림을 나타내는 클래스이다. send_filename의 경로에 있는 파일을 가져온다.

ios::binary    : 파일을 바이너리로 읽는다.

ios::ate    : 파일을 끝에서부터 읽는다. 이 플래그는 파일의 크기를 쉽게 알 수 있다.

 

file.seekg(0) : 이 함수를 통해 파일 포인터를 시작 부분으로 옮긴다.

이 친구가 없으면 파일이 제대로 읽어지지 않았었다.

 

std::stringstream file_stream : 문자열을 스트림으로 다루는 클래스이다. 추후 file 버퍼의 데이터를 가져오기 위해 사용했다.

 

file_stream << file.rdbuf()     :  파일 스트림의 버퍼를 file_stream으로 읽어드린다. rdbuf()를 통해 file변수의 버퍼를 읽는다.

 

file_contents에 file_stream의 내용을 문자열로 바꿔 저장한다.

그 후 socket으로 전송해 준다.

 

 

 

 

 

 

int valread;
    ofstream output_file("/home/sj/test/recived_file.zip", ios::binary);

     while ((valread = recv(sock, buffer, BUFFER_SIZE, 0)) > 0) {
        
        output_file.write(buffer, valread);
     }
    
    output_file.close();
    if(valread < 0)
        cerr << "Error receving data from server" << endl;

 

 클라이언트 코드의 데이터 받는 부분을 확인한다.

test디렉터리 경로에 압축파일을 만들어낼 것이다.

파일을 쓰기 위해 ofstream객체를 이용해서 바이너리 파일을 만들어낸다.

이제 이곳에 파일을 써 내려갈 것이다.

recv를 통해 받은 데이터를 순차적으로 써 내려간다.

 

 

 

 

 

 

recived_file.zip에 클라이언트에 의해 전송받은 압축파일이며

unzip으로 압축해제 했을 때 정상적으로 파일들이 생긴다.

 

버전오류로 정말 고생했지만

파일 스트림을 어떻게 읽어야 하고 쓰는지에 대해 알 수 있었고

압축파일을 전송하기 위해 stringstream객체를 이용해 데이터를 가공하는 스킬에 대해 공부할 수 있었다.

 

또한 파일을 안전하게 가공하고 보내기 위해 seekg함수를 이용해서 파일 포인터의 시작 부분으로 옮겨야 하는

중요한 스킬도 공부할 수 있었다.

 

 

 

이로써 현재는 로컬에서만 통신이 가능하지만

원한다면 포트포워딩으로 카페에서 실시간으로 작업코드들을 전송받아 개발을 진행할 수 있다.

다음은 전송되는 데이터가 중간에 탈취되지 않도록 암호학을 적용해 보고 해당 프로젝트를 끝내기로 했다.

 

Comments