這是我在計算機網路課程的期末專題
此專案是一個基於 socket programming 的簡單伺服器-客戶端應用程式
本程式目前支援用戶註冊、登入等服務
未來將開發成一個實時線上聊天室軟體
2024_CN_finalproject
├── /code/
│ ├── /data/
│ │ ├── /client/ 用戶資料存放區,用於檔案傳輸
│ │ └── /server/
│ │ └── account.csv server-side 儲存用戶資訊,用於登入檢查
│ ├── /src/ 其他主程式會用到的函式庫
│ │ ├── UI.cpp 有關TUI設計的函式庫
│ │ ├── crypt.cpp 有關加密的函式庫
│ │ ├── file.cpp 有關檔案傳輸的函式庫
│ │ └── audio.cpp 有關 audio streaming 的函式庫
│ ├── client.cpp client-side 的主要函式庫
│ ├── client.hpp
│ ├── clientmain.cpp client-side 的主程式
│ ├── server.cpp server-side 的主要函式庫
│ ├── server.hpp
│ ├── servermain.cpp server-side 的主程式
│ └── Makefile
├── .gitignore
├── LICENSE
└── README.md
若要順利的執行程式,有以下幾點是需要注意的
本程式可在 Unix-based 系統(如 Linux 或 macOS)上順利執行
如果使用 Windows,可以使用 WSL 來模擬環境。
本程式全都由 C++ 完成
因此請先確保你的環境是否有安裝 g++
g++ --version
若無安裝,可透過下方方式安裝
sudo apt install g++
請先透過下方指令下載 OpenSSL 與 SDL2 函式庫:
sudo apt-get update
sudo apt-get install libsdl2-dev
sudo apt install openssl
另外,確認環境支援下列 header file:
<bits/stdc++.h>
(C++ 中大多基本函式庫)。<fstream>
、<sstream>
(用於資料的讀出讀入)。<sys/socket.h>
、<netinet/in.h>
、<arpa/inet.h>
(用於 socket 程式設計和網路通訊)。
在 code
目錄下執行 make
命令即可編譯
./server.o <server port>
可以在 server port 開啟 server
./client.o <server ip or server domain> <server port>
可開啟 client,並將其與 <server ip or server domain>:<server port>
上的 server 連接
(可直接輸入 server ip 或輸入 server 所在的網域,程式會把 server domain 轉換成 server ip)
啟動 server 後 server 便會進入 listen 模式
等待 client 端與其連接
連接完成後,server 僅負責回應 client 的要求(如查找現在使用帳號名稱、用戶登入與登出……等)
不會主動向 client 傳訊息
啟動 client 前,請先確認 server 在運作狀態中,之後再啟動 client
啟動 client 後,會先與 server 連接成功後,才會進入主頁面。
註冊帳號與密碼時,需要符合以下條件才可註冊:
- 不可出現冒號 :
- 不可出現空格
若出現不合法的帳戶名與密碼,將會出現錯誤訊息
若註冊成功後,使用者帳號與密碼將會保存至 server 端的 ./code/data/server/account.csv
,用來作為註冊與登入檢查
在主畫面按下 1
之後,即可進入登入帳號介面
欲登入帳號,必須先確認該帳號已註冊,才可登入帳號
否則將跳出錯誤訊息要求重新輸入
登入完成帳號之後,client 端便會建立資料夾 ./code/data/client/[Your username]/
若要傳輸檔案,可將檔案放入這個資料夾內
此外,server 端程式會將用戶名名稱存放至 static unordered_map<string, int> name_to_fd
,用來記錄該帳戶所分配到的 socket
登入完成帳號後,回到主畫面
按下 2
之後,即可登出帳號
登入完成後,可依照主畫面提示進入 chatroom
輸入你想聊天的對象,等待對方回應你的邀請
對方須同樣在 chatroom 中輸入你的名字
邀請成功需要滿足以下條件:
- 雙方都必須在線上
- 雙方都需要在 chatroom 中輸入對方名字
- 雙方必須在都空閒著,沒有在跟其他人對話
邀請成功後會出現下方圖片,按下 Enter
後即可開始聊天
當對方不在線上,程式會出現下方畫面,按下 Enter
後可退回主頁面
當對方在線上但尚未在 chatroom、尚未輸入你的名字或尚未空閒時,會出現下方畫面
按下 'Enter' 後會詢問你要等待還是離開
若選擇離開,即回到主畫面
若選擇等待,則進入等待畫面。
當對方輸入你的名字後,就可以開始聊天了
本程式除了 Audio streaming 外,皆使用 OpenSSL 中的 AES 加密,內容大致如下:
- 加密一般訊息用
vector<unsigned char> encrypt(const string &plain_text, const unsigned char *key, const unsigned char *iv)
{
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx)
{
cerr << "EVP_CIPHER_CTX_new failed" << endl;
return {};
}
if (EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv) != 1)
{
cerr << "EVP_EncryptInit_ex failed" << endl;
EVP_CIPHER_CTX_free(ctx);
return {};
}
int len = static_cast<int>(plain_text.size());
int block_size = EVP_CIPHER_block_size(EVP_aes_256_cbc());
int max_len = len + block_size;
vector<unsigned char> cipher_text(max_len);
int update_len = 0, final_len = 0;
if (EVP_EncryptUpdate(ctx, cipher_text.data(), &update_len, reinterpret_cast<const unsigned char *>(plain_text.data()), len) != 1)
{
cerr << "EVP_EncryptUpdate failed" << endl;
EVP_CIPHER_CTX_free(ctx);
return {};
}
if (EVP_EncryptFinal_ex(ctx, cipher_text.data() + update_len, &final_len) != 1)
{
cerr << "EVP_EncryptFinal_ex failed" << endl;
EVP_CIPHER_CTX_free(ctx);
return {};
}
EVP_CIPHER_CTX_free(ctx);
// 修正 vector 長度為加密後的實際長度
cipher_text.resize(update_len + final_len);
// cout << "Encrypted size: " << cipher_text.size() << endl;
return cipher_text;
}
- 解密一般訊息用
string decrypt(const vector<unsigned char> &origin_cipher_text, const unsigned char *key, const unsigned char *iv)
{
if (origin_cipher_text.size() % 16 != 0)
{
cerr << "Cipher length is not a multiple of block size, decryption aborted." << endl;
return {};
}
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx)
{
cerr << "Failed to create EVP_CIPHER_CTX" << endl;
return {};
}
if (EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv) != 1)
{
cerr << "EVP_DecryptInit_ex failed" << endl;
EVP_CIPHER_CTX_free(ctx);
return {};
}
int len = static_cast<int>(origin_cipher_text.size());
int block_size = EVP_CIPHER_block_size(EVP_aes_256_cbc());
int max_len = len + block_size;
vector<unsigned char> plain_text(max_len, 0);
int update_len = 0, final_len = 0;
if (EVP_DecryptUpdate(ctx, plain_text.data(), &update_len, origin_cipher_text.data(), len) != 1)
{
cerr << "EVP_DecryptUpdate failed" << endl;
EVP_CIPHER_CTX_free(ctx);
return {};
}
if (EVP_DecryptFinal_ex(ctx, plain_text.data() + update_len, &final_len) != 1)
{
cerr << "EVP_DecryptFinal_ex failed" << endl;
EVP_CIPHER_CTX_free(ctx);
return {};
}
EVP_CIPHER_CTX_free(ctx);
plain_text.resize(update_len + final_len);
return string(plain_text.begin(), plain_text.end());
}
- 加密檔案用
vector<unsigned char> encrypt_file(const vector<unsigned char> &plain_text, const unsigned char *key, const unsigned char *iv)
{
// 建立 Context
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx)
{
cerr << "EVP_CIPHER_CTX_new failed" << endl;
return {};
}
// 初始化加密:AES-256-CBC
if (EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, key, iv) != 1)
{
cerr << "EVP_EncryptInit_ex failed" << endl;
EVP_CIPHER_CTX_free(ctx);
return {};
}
// 準備足夠的輸出空間:明文長度 + 區塊大小
int len = static_cast<int>(plain_text.size());
int block_size = EVP_CIPHER_block_size(EVP_aes_256_cbc());
int max_len = len + block_size;
vector<unsigned char> cipher_text(max_len);
int update_len = 0, final_len = 0;
// 加密 Update
if (EVP_EncryptUpdate(ctx, cipher_text.data(), &update_len, plain_text.data(), len) != 1)
{
cerr << "EVP_EncryptUpdate failed" << endl;
EVP_CIPHER_CTX_free(ctx);
return {};
}
// 加密 Final
if (EVP_EncryptFinal_ex(ctx, cipher_text.data() + update_len, &final_len) != 1)
{
cerr << "EVP_EncryptFinal_ex failed" << endl;
EVP_CIPHER_CTX_free(ctx);
return {};
}
// 釋放 Context
EVP_CIPHER_CTX_free(ctx);
// 根據實際加密長度縮小 cipher_text
cipher_text.resize(update_len + final_len);
return cipher_text;
}
- 解密檔案用
string decrypt(const vector<unsigned char> &origin_cipher_text, const unsigned char *key, const unsigned char *iv)
{
if (origin_cipher_text.size() % 16 != 0)
{
cerr << "Cipher length is not a multiple of block size, decryption aborted." << endl;
return {};
}
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx)
{
cerr << "Failed to create EVP_CIPHER_CTX" << endl;
return {};
}
if (EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv) != 1)
{
cerr << "EVP_DecryptInit_ex failed" << endl;
EVP_CIPHER_CTX_free(ctx);
return {};
}
int len = static_cast<int>(origin_cipher_text.size());
int block_size = EVP_CIPHER_block_size(EVP_aes_256_cbc());
int max_len = len + block_size;
vector<unsigned char> plain_text(max_len, 0);
int update_len = 0, final_len = 0;
if (EVP_DecryptUpdate(ctx, plain_text.data(), &update_len, origin_cipher_text.data(), len) != 1)
{
cerr << "EVP_DecryptUpdate failed" << endl;
EVP_CIPHER_CTX_free(ctx);
return {};
}
if (EVP_DecryptFinal_ex(ctx, plain_text.data() + update_len, &final_len) != 1)
{
cerr << "EVP_DecryptFinal_ex failed" << endl;
EVP_CIPHER_CTX_free(ctx);
return {};
}
EVP_CIPHER_CTX_free(ctx);
plain_text.resize(update_len + final_len);
return string(plain_text.begin(), plain_text.end());
}
- 生成 AES key 與 AES iv
void generateAESKeyAndIV(unsigned char *key, unsigned char *iv)
{
if (RAND_bytes(key, 32) != 1)
{
cerr << "Failed to generate AES key" << endl;
exit(EXIT_FAILURE);
}
if (RAND_bytes(iv, 16) != 1)
{
cerr << "Failed to generate AES IV" << endl;
exit(EXIT_FAILURE);
}
}
傳輸訊息的流程大致如下:
- 先包裝寄件人的訊息,附上相關資訊
- 經過 AES 加密
- 加密訊息傳至 server
- 加密訊息在 server 中解密,取得寄件人與收件人帳戶名
- 利用帳戶名在
static unordered_map<string, int> name_to_fd
中找到對應 socket - 訊息加密後傳輸到收件人
- 收件人解密訊息,解開包裝得到訊息
訊息包裝大致如下:
[Chatting][Message] sender:receiver:message
請先把檔案放到 ./code/data/client/[Your username]/
中
然後輸入 <file> [Your filename]
按下 Enter 後,等待片刻對方就會在對方的 ./code/data/client/[Receiver's username]/
中收到檔案
請在輸入區輸入 <audio streaming>
即可與對方開始 audio streaming
想結束請依照提示按下 Enter 就可終止 audio streaming
只有發送方可以終止 audio streaming
於輸入區輸入 <exit>
即可離開
輸入完成後,對方聊天室也會顯示你已離開
對方也會同時退出
MIT © Hank Chen