サーバーを介したWindows Socketプログラミングによるファイル転送システム

システム概要

このシステムは、中央サーバーをハブとするクライアント/サーバー型ファイル共有プラットフォームです。複数のクライアントがサーバーを経由してファイルのアップロード、ダウンロード、管理を行うことができます。社内ネットワークでのドキュメント共有やチームプロジェクトでのファイル配布などの用途を想定しています。

システム設計

graph LR SubA[クライアントA] --> B[中央サーバー] SubC[クライアントB] --> B SubD[クライアントC] --> B B --> E[(ファイルシステム)] B --> F[(メタデータDB)]

システムは以下の主要コンポーネントで構成されています:

  • 中央サーバー: クライアント接続管理、コマンド処理、ファイル転送制御
  • ファイル管理システム: アップロードされたファイルの物理的保存
  • メタデータ管理: ユーザー情報、ファイル情報、アクセス権限の管理
  • クライアントアプリケーション: ユーザーインターフェースとファイル操作機能の提供

サーバー実装 (サーバープログラム)

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

#define DEFAULT_PORT 5555
#define TRANSFER_BLOCK 8192
#define MAX_CONCURRENT_USERS 50

using std::string;
using std::unordered_map;

std::mutex sessionLock;

struct SessionData {
    SOCKET connection;
    string loginName;
    bool isAuthenticated;
};

struct FileRecord {
    string name;
    string uploader;
    size_t fileSize;
    std::time_t timestamp;
};

unordered_map<SOCKET, SessionData> activeSessions;
unordered_map<string, FileRecord> fileRegistry;
const string STORAGE_DIR = "ServerFiles/";

void manageClientConnection(SOCKET clientSock);
void interpretClientRequest(SOCKET sock, const string& request);
void transmitFileToClient(SOCKET sock, const string& fileName);
void saveFileFromClient(SOCKET sock, const string& fileName, size_t totalSize);
void notifyAllUsers(SOCKET excludedSocket = INVALID_SOCKET);
void recordEvent(const string& logMsg);

int main() {
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2, 2), &wsa)) {
        std::cerr << "Winsock初期化エラー" << std::endl;
        return 1;
    }

    _mkdir(STORAGE_DIR.c_str());

    SOCKET mainSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (mainSocket == INVALID_SOCKET) {
        std::cerr << "ソケット生成失敗" << std::endl;
        WSACleanup();
        return 1;
    }

    sockaddr_in addrConfig;
    addrConfig.sin_family = AF_INET;
    addrConfig.sin_addr.s_addr = INADDR_ANY;
    addrConfig.sin_port = htons(DEFAULT_PORT);

    if (bind(mainSocket, (sockaddr*)&addrConfig, sizeof(addrConfig)) == SOCKET_ERROR) {
        std::cerr << "バインド失敗" << std::endl;
        closesocket(mainSocket);
        WSACleanup();
        return 1;
    }

    if (listen(mainSocket, SOMAXCONN) == SOCKET_ERROR) {
        std::cerr << "リスニング失敗" << std::endl;
        closesocket(mainSocket);
        WSACleanup();
        return 1;
    }

    std::cout << "サーバーがポート " << DEFAULT_PORT << " で起動しました" << std::endl;
    recordEvent("サーバー起動");

    while (true) {
        sockaddr_in clientAddr;
        int addrLen = sizeof(clientAddr);
        SOCKET clientSocket = accept(mainSocket, (sockaddr*)&clientAddr, &addrLen);
        
        if (clientSocket == INVALID_SOCKET) {
            std::cerr << "接続受付エラー" << std::endl;
            continue;
        }

        char clientIP[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &clientAddr.sin_addr, clientIP, INET_ADDRSTRLEN);
        std::cout << "新規接続: " << clientIP << ":" << ntohs(clientAddr.sin_port) << std::endl;

        std::thread clientHandler(manageClientConnection, clientSocket);
        clientHandler.detach();
    }

    closesocket(mainSocket);
    WSACleanup();
    return 0;
}

void manageClientConnection(SOCKET clientSock) {
    char inputBuffer[TRANSFER_BLOCK];
    int bytesRead;

    {
        std::lock_guard<std::mutex> guard(sessionLock);
        activeSessions[clientSock] = {clientSock, "", false};
    }

    string greeting = "ファイル転送サーバーへようこそ\n";
    send(clientSock, greeting.c_str(), greeting.length(), 0);

    while (true) {
        bytesRead = recv(clientSock, inputBuffer, TRANSFER_BLOCK, 0);
        if (bytesRead <= 0) break;

        inputBuffer[bytesRead] = '\0';
        string clientRequest(inputBuffer);
        interpretClientRequest(clientSock, clientRequest);
    }

    {
        std::lock_guard<std::mutex> guard(sessionLock);
        string userName = activeSessions[clientSock].loginName;
        activeSessions.erase(clientSock);
        std::cout << "接続切断: " << userName << std::endl;
        recordEvent(userName + " が切断しました");
    }

    closesocket(clientSock);
}

void interpretClientRequest(SOCKET sock, const string& request) {
    std::lock_guard<std::mutex> guard(sessionLock);
    string command = request.substr(0, request.find(' '));
    string parameters = request.substr(request.find(' ') + 1);

    if (command == "AUTH") {
        size_t sep = parameters.find(' ');
        string userId = parameters.substr(0, sep);
        string passcode = parameters.substr(sep + 1);
        
        activeSessions[sock].loginName = userId;
        activeSessions[sock].isAuthenticated = true;
        
        string reply = "認証成功 " + userId + "\n";
        send(sock, reply.c_str(), reply.length(), 0);
        std::cout << "ユーザー認証: " << userId << std::endl;
        recordEvent(userId + " がログインしました");
        
        notifyAllUsers(sock);
    }
    else if (command == "DIR") {
        if (!activeSessions[sock].isAuthenticated) {
            send(sock, "認証が必要です\n", 18, 0);
            return;
        }
        
        string listing = "サーバーファイル一覧:\n";
        for (const auto& entry : fileRegistry) {
            listing += entry.first + " (" + std::to_string(entry.second.fileSize) + 
                      " bytes) - " + entry.second.uploader + "\n";
        }
        send(sock, listing.c_str(), listing.length(), 0);
    }
    else if (command == "PUT") {
        if (!activeSessions[sock].isAuthenticated) {
            send(sock, "認証が必要です\n", 18, 0);
            return;
        }
        
        size_t spacePos = parameters.find(' ');
        string fileName = parameters.substr(0, spacePos);
        size_t fileBytes = std::stoul(parameters.substr(spacePos + 1));
        
        FileRecord newFile;
        newFile.name = fileName;
        newFile.uploader = activeSessions[sock].loginName;
        newFile.fileSize = fileBytes;
        newFile.timestamp = std::time(nullptr);
        fileRegistry[fileName] = newFile;
        
        string ack = "受信準備完了 " + fileName + "\n";
        send(sock, ack.c_str(), ack.length(), 0);
        
        saveFileFromClient(sock, fileName, fileBytes);
        
        notifyAllUsers();
        recordEvent(activeSessions[sock].loginName + " が " + fileName + " をアップロード");
    }
    else if (command == "GET") {
        if (!activeSessions[sock].isAuthenticated) {
            send(sock, "認証が必要です\n", 18, 0);
            return;
        }
        
        string targetFile = parameters;
        if (fileRegistry.find(targetFile) != fileRegistry.end()) {
            string fileHeader = "SENDING " + targetFile + " " + 
                               std::to_string(fileRegistry[targetFile].fileSize) + "\n";
            send(sock, fileHeader.c_str(), fileHeader.length(), 0);
            
            transmitFileToClient(sock, targetFile);
            recordEvent(activeSessions[sock].loginName + " が " + targetFile + " をダウンロード");
        } else {
            string errMsg = "ファイルが見つかりません\n";
            send(sock, errMsg.c_str(), errMsg.length(), 0);
        }
    }
    else if (command == "REMOVE") {
        if (!activeSessions[sock].isAuthenticated) {
            send(sock, "認証が必要です\n", 18, 0);
            return;
        }
        
        string targetFile = parameters;
        if (fileRegistry.find(targetFile) != fileRegistry.end() && 
            fileRegistry[targetFile].uploader == activeSessions[sock].loginName) {
            
            fileRegistry.erase(targetFile);
            string filePath = STORAGE_DIR + targetFile;
            std::remove(filePath.c_str());
            
            string confirm = "削除完了\n";
            send(sock, confirm.c_str(), confirm.length(), 0);
            
            notifyAllUsers();
            recordEvent(activeSessions[sock].loginName + " が " + targetFile + " を削除");
        } else {
            string deny = "権限がありません\n";
            send(sock, deny.c_str(), deny.length(), 0);
        }
    }
    else {
        string unknown = "未定義のコマンドです\n";
        send(sock, unknown.c_str(), unknown.length(), 0);
    }
}

void transmitFileToClient(SOCKET sock, const string& fileName) {
    string fullPath = STORAGE_DIR + fileName;
    std::ifstream sourceFile(fullPath, std::ios::binary | std::ios::ate);
    if (!sourceFile) {
        std::cerr << "ファイルオープン失敗: " << fullPath << std::endl;
        return;
    }

    std::streamsize remaining = sourceFile.tellg();
    sourceFile.seekg(0, std::ios::beg);

    char dataBlock[TRANSFER_BLOCK];
    while (remaining > 0) {
        std::streamsize blockSize = (remaining < TRANSFER_BLOCK) ? remaining : TRANSFER_BLOCK;
        sourceFile.read(dataBlock, blockSize);
        send(sock, dataBlock, static_cast<int>(blockSize), 0);
        remaining -= blockSize;
    }

    sourceFile.close();
}

void saveFileFromClient(SOCKET sock, const string& fileName, size_t totalSize) {
    string destinationPath = STORAGE_DIR + fileName;
    std::ofstream destFile(destinationPath, std::ios::binary);
    if (!destFile) {
        std::cerr << "ファイル作成失敗: " << destinationPath << std::endl;
        return;
    }

    size_t bytesRemaining = totalSize;
    char dataBlock[TRANSFER_BLOCK];

    while (bytesRemaining > 0) {
        int blockSize = (bytesRemaining < TRANSFER_BLOCK) ? static_cast<int>(bytesRemaining) : TRANSFER_BLOCK;
        int received = recv(sock, dataBlock, blockSize, 0);
        if (received <= 0) break;
        
        destFile.write(dataBlock, received);
        bytesRemaining -= received;
    }

    destFile.close();
}

void notifyAllUsers(SOCKET excludedSocket) {
    string updateMsg = "LIST_UPDATE\n";
    for (const auto& entry : fileRegistry) {
        updateMsg += entry.first + "|" + entry.second.uploader + "|" + 
                    std::to_string(entry.second.fileSize) + "|" + 
                    std::to_string(entry.second.timestamp) + "\n";
    }
    
    for (const auto& session : activeSessions) {
        if (session.first != excludedSocket && session.second.isAuthenticated) {
            send(session.first, updateMsg.c_str(), updateMsg.length(), 0);
        }
    }
}

void recordEvent(const string& logMsg) {
    std::ofstream logStream("operation.log", std::ios::app);
    if (logStream) {
        std::time_t now = std::time(nullptr);
        char* timeStr = std::ctime(&now);
        logStream << "[" << timeStr << "] " << logMsg << std::endl;
        logStream.close();
    }
}

クライアント実装 (クライアントプログラム)

#include <winsock2.h>
#include <windows.h>
#include <commctrl.h>
#include <string>
#include <thread>

#define SERVER_PORT 5555
#define DATA_CHUNK 8192

class FileTransferClient : public CDialog {
public:
    FileTransferClient(CWnd* parent = nullptr);
    enum { IDD = IDD_CLIENT_INTERFACE };

protected:
    virtual BOOL OnInitDialog();
    virtual void DoDataExchange(CDataExchange* pDX);
    DECLARE_MESSAGE_MAP()

private:
    CEdit m_txtServerAddr;
    CEdit m_txtUserID;
    CEdit m_txtPasscode;
    CButton m_btnConnect;
    CButton m_btnDisconnect;
    CListBox m_lstFiles;
    CEdit m_txtLocalFile;
    CButton m_btnSelectFile;
    CButton m_btnSendFile;
    CButton m_btnFetchFile;
    CButton m_btnRemoveFile;
    CProgressCtrl m_prgTransfer;
    CStatic m_lblStatus;

    SOCKET m_connectionSocket;
    bool m_isConnected;
    string m_currentUser;
    std::thread m_receiverThread;

    void establishConnection();
    void terminateConnection();
    void sendServerCommand(const string& cmd);
    void requestFileListing();
    void initiateFileUpload(const string& filePath);
    void initiateFileDownload(const string& fileName);
    void initiateFileRemoval(const string& fileName);
    void refreshInterface(bool connected);
    void updateFileDisplay(const string& fileData);
    void updateTransferPercentage(int percent);
    void showStatusMessage(const string& msg);
    static DWORD WINAPI backgroundReceiver(LPVOID context);
    void launchReceiver();
    void haltReceiver();
};

BEGIN_MESSAGE_MAP(FileTransferClient, CDialog)
    ON_BN_CLICKED(IDC_CONNECT, &FileTransferClient::OnConnectClicked)
    ON_BN_CLICKED(IDC_DISCONNECT, &FileTransferClient::OnDisconnectClicked)
    ON_BN_CLICKED(IDC_SELECT_FILE, &FileTransferClient::OnFileSelectClicked)
    ON_BN_CLICKED(IDC_UPLOAD_FILE, &FileTransferClient::OnUploadClicked)
    ON_BN_CLICKED(IDC_DOWNLOAD_FILE, &FileTransferClient::OnDownloadClicked)
    ON_BN_CLICKED(IDC_DELETE_FILE, &FileTransferClient::OnDeleteClicked)
    ON_LBN_DBLCLK(IDC_FILE_LIST, &FileTransferClient::OnFileDoubleClick)
END_MESSAGE_MAP()

FileTransferClient::FileTransferClient(CWnd* parent) 
    : CDialog(IDD_CLIENT_INTERFACE, parent), m_connectionSocket(INVALID_SOCKET), m_isConnected(false) {
}

BOOL FileTransferClient::OnInitDialog() {
    CDialog::OnInitDialog();

    m_txtServerAddr.SetWindowText(_T("localhost"));
    m_txtUserID.SetWindowText(_T("testuser"));
    m_txtPasscode.SetWindowText(_T("testpass"));
    m_txtLocalFile.SetWindowText(_T("C:\\example.txt"));
    m_btnDisconnect.EnableWindow(FALSE);
    m_btnSendFile.EnableWindow(FALSE);
    m_btnFetchFile.EnableWindow(FALSE);
    m_btnRemoveFile.EnableWindow(FALSE);
    m_prgTransfer.SetRange(0, 100);
    m_prgTransfer.SetPos(0);

    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2, 2), &wsa)) {
        MessageBox(_T("ネットワーク初期化エラー"), _T("エラー"), MB_ICONERROR);
        return FALSE;
    }

    return TRUE;
}

void FileTransferClient::establishConnection() {
    CString serverAddr, userID, passcode;
    m_txtServerAddr.GetWindowText(serverAddr);
    m_txtUserID.GetWindowText(userID);
    m_txtPasscode.GetWindowText(passcode);

    m_connectionSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (m_connectionSocket == INVALID_SOCKET) {
        MessageBox(_T("ソケット作成失敗"), _T("エラー"), MB_ICONERROR);
        return;
    }

    sockaddr_in serverInfo;
    serverInfo.sin_family = AF_INET;
    serverInfo.sin_port = htons(SERVER_PORT);
    InetPton(AF_INET, CT2A(serverAddr), &serverInfo.sin_addr);

    if (connect(m_connectionSocket, (sockaddr*)&serverInfo, sizeof(serverInfo)) == SOCKET_ERROR) {
        MessageBox(_T("サーバー接続失敗"), _T("エラー"), MB_ICONERROR);
        closesocket(m_connectionSocket);
        m_connectionSocket = INVALID_SOCKET;
        return;
    }

    string authCmd = "AUTH " + string(CT2A(userID)) + " " + string(CT2A(passcode)) + "\n";
    send(m_connectionSocket, authCmd.c_str(), authCmd.length(), 0);

    launchReceiver();

    m_isConnected = true;
    m_currentUser = CT2A(userID);
    refreshInterface(true);
    showStatusMessage("サーバーに接続しました");
}

void FileTransferClient::sendServerCommand(const string& cmd) {
    if (m_isConnected) {
        send(m_connectionSocket, cmd.c_str(), cmd.length(), 0);
    }
}

void FileTransferClient::initiateFileUpload(const string& filePath) {
    std::ifstream localFile(filePath, std::ios::binary | std::ios::ate);
    if (!localFile) {
        MessageBox(_T("ファイルを開けません"), _T("エラー"), MB_ICONERROR);
        return;
    }

    std::streamsize fileLength = localFile.tellg();
    localFile.seekg(0, std::ios::beg);

    string fileName = filePath.substr(filePath.find_last_of("\\/") + 1);
    string uploadCmd = "PUT " + fileName + " " + std::to_string(fileLength) + "\n";
    sendServerCommand(uploadCmd);

    char buffer[DATA_CHUNK];
    while (fileLength > 0) {
        std::streamsize chunk = (fileLength < DATA_CHUNK) ? fileLength : DATA_CHUNK;
        localFile.read(buffer, chunk);
        send(m_connectionSocket, buffer, static_cast<int>(chunk), 0);
        fileLength -= chunk;

        int progressPercent = static_cast<int>(100 - (100.0 * fileLength / localFile.tellg()));
        updateTransferPercentage(progressPercent);
    }

    localFile.close();
    showStatusMessage("アップロード完了: " + fileName);
}

DWORD WINAPI FileTransferClient::backgroundReceiver(LPVOID context) {
    FileTransferClient* client = (FileTransferClient*)context;
    char receiveBuffer[DATA_CHUNK];

    while (client->m_isConnected) {
        int received = recv(client->m_connectionSocket, receiveBuffer, DATA_CHUNK, 0);
        if (received <= 0) {
            client->terminateConnection();
            break;
        }

        receiveBuffer[received] = '\0';
        string response(receiveBuffer);

        if (response.find("SENDING ") == 0) {
            size_t firstSpace = response.find(' ');
            size_t secondSpace = response.find(' ', firstSpace + 1);
            string incomingFile = response.substr(firstSpace + 1, secondSpace - firstSpace - 1);
            long fileBytes = std::stol(response.substr(secondSpace + 1));

            CFileDialog saveDialog(FALSE, NULL, CA2T(incomingFile.c_str()),
                                  OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
                                  _T("すべてのファイル (*.*)|*.*||"), client);
            if (saveDialog.DoModal() == IDOK) {
                CString saveLocation = saveDialog.GetPathName();
                std::ofstream outputFile(CT2A(saveLocation), std::ios::binary);
                if (outputFile) {
                    long remaining = fileBytes;
                    while (remaining > 0) {
                        int chunkSize = (remaining < DATA_CHUNK) ? remaining : DATA_CHUNK;
                        int bytesRead = recv(client->m_connectionSocket, receiveBuffer, chunkSize, 0);
                        if (bytesRead <= 0) break;

                        outputFile.write(receiveBuffer, bytesRead);
                        remaining -= bytesRead;

                        int progress = static_cast<int>(100 - (100.0 * remaining / fileBytes));
                        client->updateTransferPercentage(progress);
                    }
                    outputFile.close();
                    client->showStatusMessage("ダウンロード完了: " + CT2A(saveLocation));
                }
            }
        } else {
            client->updateFileDisplay(response);
        }
    }
    return 0;
}

void FileTransferClient::launchReceiver() {
    m_receiverThread = std::thread(backgroundReceiver, this);
}

void FileTransferClient::OnConnectClicked() {
    establishConnection();
    requestFileListing();
}

void FileTransferClient::OnUploadClicked() {
    CString localPath;
    m_txtLocalFile.GetWindowText(localPath);
    initiateFileUpload(CT2A(localPath));
}

void FileTransferClient::OnDownloadClicked() {
    int selectedIndex = m_lstFiles.GetCurSel();
    if (selectedIndex != LB_ERR) {
        CString fileName;
        m_lstFiles.GetText(selectedIndex, fileName);
        initiateFileDownload(CT2A(fileName));
    }
}

タグ: WinSock C++/CLI MFC クライアントサーバー ファイル転送

5月13日 03:33 投稿