本筆記記錄 xinetd (Extended Internet Daemon) 超級伺服器的安裝配置流程,並深度解析一個基於 xinetd 啟動的簡易 HTTP 伺服器原始碼 (myhttpd.c),涵蓋請求解析、靜態檔案服務、目錄列表、MIME 類型判斷、URL 編解碼與 HTTP 協定頭構建等核心網路程式設計技術。
select/poll/epoll 同時監聽多埠,收到請求依 /etc/xinetd.d/ 設定 fork() + exec() 啟動對應服務程式。sudo apt-get update && sudo apt-get install xinetd
/etc/xinetd.d/myhttpdservice myhttpd
{
socket_type = stream # TCP 串流
protocol = tcp
wait = no # 多執行緒/多進程並發 (每請求 fork)
user = nobody # 權限降級
server = /home/user/HttpTest/myhttpd # 實際服務程式完整路徑
server_args = /home/user/HttpTest/dir # 傳給 server argv[1] (文檔根目錄)
disable = no # 啟用
flags = IPv4
}
關鍵參數:wait=no 表示 xinetd 接受連線後立即啟動服務處理,適用高併發;server_args 會作為 argv[1] 傳遞給 main(int argc, char** argv)。
/etc/servicesmyhttpd 2222/tcp # 自訂服務名對應埠號
sudo service xinetd restart
myhttpd.c 核心邏輯解析程式以 stdin/stdout 與 xinetd 通訊 (xinetd 將 socket fd dup2 至 0/1),單進程處理單一 HTTP 請求,處理完即退出。
if (argc != 2) send_error(500, "Internal Error", NULL, "Config error - no dir specified."); if (chdir(argv[1]) < 0) send_error(500, "Internal Error", NULL, "Config error - couldn't chdir.");
server_args 傳入的文檔根目錄並 chdir,後續路徑皆為相對路徑,簡化邏輯。fgets(line, sizeof(line), stdin); // 讀請求行
sscanf(line, "%[^ ] %[^ ] %[^ ]", method, path, protocol); // 切割 METHOD PATH VERSION
while (fgets(line, sizeof(line), stdin) != NULL) { // 讀並丟棄請求頭
if (strcmp(line, "\n") == 0 || strcmp(line, "\r\n") == 0) break;
}
GET,其他回 501 Not Implemented。/ 開頭,否則 400 Bad Request。file = &path[1]; // 去掉開頭 '/'
strdecode(file, file); // URL 解碼 (%20 → space 等)
if (file[0] == '\0') file = "./";
// 防止目錄穿越攻擊
if (file[0]=='/' || strcmp(file,"..")==0 || strncmp(file,"../",3)==0 ||
strstr(file,"/../") != NULL || strcmp(&file[len-3],"/..")==0)
send_error(400, "Bad Request", NULL, "Illegal filename.");
struct stat sb;
if (stat(file, &sb) < 0) send_error(404, "Not Found", NULL, "File not found.");
if (S_ISDIR(sb.st_mode)) { // 目錄
if (file[len-1] != '/') { // 強制尾部加 '/'
snprintf(location, sizeof(location), "Location: %s/", path);
send_error(302, "Found", location, "Directories must end with a slash.");
}
// 優先尋找 index.html
snprintf(idx, sizeof(idx), "%sindex.html", file);
if (stat(idx, &sb) >= 0) { file = idx; goto do_file; }
// 產生目錄列表
send_headers(200, "Ok", NULL, "text/html", -1, sb.st_mtime);
printf("...Index of %s
\n", file);
n = scandir(file, &dl, NULL, alphasort); // 掃描目錄
for (i=0; id_name);
printf("
...\n");
} else { // 一般檔案
do_file:
fp = fopen(file, "r");
if (!fp) send_error(403, "Forbidden", NULL, "File is protected.");
send_headers(200, "Ok", NULL, get_mime_type(file), sb.st_size, sb.st_mtime);
while ((ich = getc(fp)) != EOF) putchar(ich);
}
send_headers()void send_headers(int status, char* title, char* extra_header,
char* mime_type, off_t length, time_t mod) {
printf("%s %d %s\r\n", PROTOCOL, status, title); // HTTP/1.1 200 OK
printf("Server: %s\r\n", SERVER_NAME);
printf("Date: %s\r\n", gmt_time_str);
if (extra_header) printf("%s\r\n", extra_header); // 如 Location
if (mime_type) printf("Content-Type: %s\r\n", mime_type);
if (length >= 0) printf("Content-Length: %lld\r\n", length);
if (mod != -1) printf("Last-Modified: %s\r\n", gmt_mod_str);
printf("Connection: close\r\n\r\n"); // 結束頭部
}
get_mime_type()依副檔名映射:.html/.htm → text/html, .jpg/.jpeg → image/jpeg, .png → image/png, .css → text/css, .js → application/javascript 等,預設 text/plain。
// 解碼: %XX → 字元
void strdecode(char* to, char* from) {
for (; *from; ++to, ++from) {
if (from[0]=='%' && isxdigit(from[1]) && isxdigit(from[2])) {
*to = hexit(from[1])*16 + hexit(from[2]);
from += 2;
} else *to = *from;
}
*to = '\0';
}
// 編碼: 非字母數字/_.-~ → %XX
void strencode(char* to, size_t tosize, const char* from) {
for (int tolen=0; *from && tolen+4 < tosize; ++from) {
if (isalnum(*from) || strchr("/_.-~", *from)) { *to++ = *from; ++tolen; }
else { sprintf(to, "%%%02x", (int)*from & 0xff); to+=3; tolen+=3; }
}
*to = '\0';
}
| 技術點 | 實作細節 |
|---|---|
| I/O 模型 | xinetd + fork/exec,單進程處理單請求,stdin/stdout 直連 socket |
| 安全 | chroot 風格 chdir + 路徑穿越檢查 (.., /../ 等) |
| 目錄索引 | scandir + alphasort 自動排序,動態產生 HTML |
| MIME | 副檔名映射表,支援常見靜態資源類型 |
| URL 編碼 | RFC 3986 子集,解碼用於路徑,編碼用於目錄列表連結生成 |
| 錯誤處理 | 統一 send_error → send_headers + HTML 錯誤頁 + exit(1) |
| 時間格式 | strftime + gmtime 產生 RFC 1123 格式 (GMT) |
HEAD/POST 方法與 CGI 執行。Range 請求支援斷點續傳/視頻拖曳。epoll + 非阻塞 I/O 改為高併發單進程架構 (移除 xinetd 依賴)。這個微型伺服器雖簡單,卻完整展示了 HTTP/1.1 靜態資源服務的核心流程,是理解 Web 伺服器內部運作、網路程式設計實戰與 xinetd 部署模式的優秀教學案例。