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