---
# System prepended metadata

title: 計算機網路概論 Final Project

---

# 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--;
    }
}
```

---