# Computer Networks Final Project Report **Student ID:** 113062330 **Name:** 林柏崴 --- ## 1. Project Overview 使用 **socket C++ programming** 實作了一個簡單的 **TCP client-server application**。 Server 端可依照 client 傳送的指令,提供以下三種服務: - **DNS**:將 domain name 解析成對應的 IPv4 address - **QUERY**:輸入 student ID,回傳對應的 email - **QUIT**:結束 client 與 server 之間的連線 --- ## 2. System Architecture 採用 **client-server model**,架構如下: - **Server** - 綁定固定的 IP address 與 port - 使用 `listen()` 等待 client 連線 - 可在同一條 TCP connection 中處理多個 request - 依據 client 傳送的 command 回傳對應結果 - **Client** - 透過 TCP 與 server 建立連線 - 提供選單式的操作介面 - 將使用者輸入轉換成 command 傳送給 server - 接收並顯示 server 回傳的結果 --- ## 3. Implementation Details ### 3.1 TCP Socket Setup ```cpp= const char * SERVER_IP = "127.0.0.1"; const int SERVER_PORT = 1234; loadQueryFile("query.txt"); // IPv4 TCP int listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd < 0) { std::cerr << "socket() failed: " << strerror(errno) << "\n"; return 1; } // allows to "reuse" the port you just used int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); sockaddr_in addr{}; addr.sin_family = AF_INET; // IPv4 addr.sin_port = htons(SERVER_PORT); if (inet_pton(AF_INET, SERVER_IP, &addr.sin_addr) != 1) { std::cerr << "inet_pton failed\n"; close(listen_fd); return 1; } // this socket needs to be bound to this address if (bind(listen_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { std::cerr << "bind() failed: " << strerror(errno) << "\n"; close(listen_fd); return 1; } // backlog : 5 (queue limit) if (listen(listen_fd, 5) < 0) { std::cerr << "listen() failed: " << strerror(errno) << "\n"; close(listen_fd); return 1; } ``` Server 端建立 TCP socket 的流程如下: 1. 使用 `socket()` 建立 IPv4 TCP socket 2. 使用 `setsockopt()` 設定 `SO_REUSEADDR`,避免重啟 server 時出現 port 被占用的問題 3. 使用 `bind()` 將 socket 綁定至指定的 IP 與 port 4. 使用 `listen()` 進入監聽狀態 5. 使用 `accept()` 接收 client 的連線請求 Client 端則使用 `socket()` 建立 socket,並透過 `connect()` 與 server 建立 TCP connection。 --- ### 3.2 Message Transmission ```cpp= static bool recvLine(int fd, std::string &out){ out.clear(); char ch; // from socket 'fd', each time 1 byte until '\n' => completed msg while(true){ ssize_t n = recv(fd, &ch, 1, 0); if (n == 0) return false; // no connect if (n < 0) { // wrong if (errno == EINTR) continue; // interrupt, recv again return false; } if (ch == '\n') break; out.push_back(ch); } return true; } ``` ```cpp= static bool sendAll(int fd, const std::string &msg) { size_t sent = 0; while (sent < msg.size()) { // TCP : send(fd, msg.data(), msg.size(), 0); may be interrupted // So using send the all the remaining msg ssize_t n = send(fd, msg.data() + sent, msg.size() - sent, 0); if (n < 0) { if (errno == EINTR) continue; return false; } sent += (size_t)n; } return true; } ``` 由於 **TCP 是 byte-stream protocol** `send()` 與 `recv()` 也可能發生 partial send / partial receive 的情況。 - Client 端在每一個 request 結尾加上 **newline character(`\n`)** - Server 端透過 `recv()` 每次讀取 1 byte,直到收到 `\n` 為止,視為一個完整的 request - Server 回傳的 response 同樣以 `\n` 作為結尾 `sendAll()` 則透過迴圈確保所有資料都能成功送出,避免資料只送出部分 bytes。 --- ### 3.3 DNS Function ```cpp= static std::string DNS(const std::string& host){ struct addrinfo hints; struct addrinfo *result = NULL; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; // IPv4 hints.ai_socktype = SOCK_STREAM; // TCP int ret = getaddrinfo(host.c_str(), NULL, &hints, &result); if (ret != 0 || result == NULL) { return "URL Not Found"; } struct sockaddr_in *addr_v4; addr_v4 = (struct sockaddr_in *)result->ai_addr; char ipbuf[INET_ADDRSTRLEN]; if (inet_ntop(AF_INET, &(addr_v4->sin_addr), ipbuf, sizeof(ipbuf)) == NULL) { freeaddrinfo(result); return "URL Not Found"; } freeaddrinfo(result); return std::string(ipbuf); } ``` DNS 功能是透過 **`getaddrinfo()`** system call 實作 - Server 接收 client 傳送的 domain name - 使用 `getaddrinfo()` 將 domain name 解析成 IPv4 address - 透過 `inet_ntop()` 將 binary address 轉換成可讀的字串格式 - 若失敗,則回傳 `"URL Not Found"` --- ### 3.4 QUERY Function ```cpp= static std::unordered_map<std::string, std::string> query_map; static void loadQueryFile(const std::string& filename) { query_map.clear(); std::ifstream fin(filename); std::string id, email; while (fin >> id >> email) { query_map[id] = email; } } static std::string queryEmail(const std::string& id) { auto it = query_map.find(id); if (it == query_map.end()) return "ID Not Found"; return it->second; } ``` QUERY 功能的實作方式如下: - Server 啟動時讀取 `query.txt` 檔案 - 檔案中每一行包含一組 `student ID` 與 `email` - Server 使用 `unordered_map` 儲存對應關係 `<student ID> <email>` - 當收到 QUERY request 時,根據 student ID 查詢並回傳 email - 若查無資料,則回傳 `"ID Not Found"` --- ## 4. Personal Experience and Reflection(心得與反思) 一開始在撰寫程式時,我原本以為可以在 client 端直接呼叫類似 ```cpp send(fd, msg.data(), msg.size(), 0) ``` 就可以一次把整個字串送到 server,server 端再用 `recv()` 直接讀取即可。 但這樣的寫法並不可靠,因為 **TCP 並不保證一次 `send()` 就能送出所有資料**,server 端的 `recv()` 也可能只接收到部分內容,導致指令被切成多段或黏在一起,進而造成解析錯誤。 為了解決這個問題,我在程式中額外實作了 `sendAll()` 函式,透過迴圈反覆呼叫 `send()`,直到整個訊息的所有 bytes 都成功送出,避免發生 partial send 的情況。同時,在 server 端也設計了 `recvLine()` 函式,每次只讀取 1 byte,並將資料累積成一行字串,直到接收到 newline character(`\n`)為止,才視為一個完整的 request。 此外,由於 TCP 是 byte-stream protocol,本身沒有「一筆指令」的概念,我選擇使用 `\n` 作為每個 request 的 delimiter 在 parsing 指令時,server 端也會忽略多餘的空白與換行字元,使系統在面對不完全整齊的輸入時仍能正確運作。 ```cpp= static void parseCommand(const char *line, char *cmd, size_t cmd_sz, char *arg, size_t arg_sz){ size_t i = 0; while (line[i] == ' ' || line[i] == '\t') i++; size_t j = 0; while (line[i] != '\0' && line[i] != ' ' && line[i] != '\t' && line[i] != '\r' && line[i] != '\n') { if (j + 1 < cmd_sz) cmd[j++] = line[i]; i++; } cmd[j] = '\0'; while (line[i] == ' ' || line[i] == '\t') i++; j = 0; while (line[i] != '\0' && line[i] != '\r' && line[i] != '\n') { if (j + 1 < arg_sz) arg[j++] = line[i]; i++; } arg[j] = '\0'; while (j > 0 && (arg[j - 1] == ' ' || arg[j - 1] == '\t')) { arg[j - 1] = '\0'; j--; } } ``` ---