This commit is contained in:
2026-07-18 16:39:01 +08:00
parent 25f7ef1967
commit ce5e55a2c1
24 changed files with 5015 additions and 2 deletions

311
server-exp4/cache.c Normal file
View File

@@ -0,0 +1,311 @@
/**
* cache.c — LRU 缓存实现
*
* 实现细节:
* 1. 使用 DJB2 哈希算法将 URL 映射到桶索引
* 2. 双向链表 (head↔...↔tail) 维护访问顺序:
* - head 方向是最近使用 (MRU)
* - tail 方向是最久未使用 (LRU)
* 3. 查找命中时,将节点移动到链表头部
* 4. 插入满时,淘汰链表尾部的 LRU 节点
* 5. 线程安全: 所有操作使用 pthread_mutex 保护
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include "cache.h"
/* ======================== 内部常量 ======================== */
#define DEFAULT_NUM_BUCKETS 101 /* 哈希桶数量 (质数,减少冲突) */
#define MAX_CACHE_SIZE 1024 /* 默认最大条目数 */
/* ======================== 内部辅助函数 ======================== */
/**
* hash_url — DJB2 哈希算法
* 该算法简单高效,分布均匀,适合字符串键
*/
static unsigned long hash_url(const char *url)
{
unsigned long hash = 5381;
int c;
while ((c = *url++))
hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
return hash;
}
/**
* list_remove — 从双向链表中移除节点 (不释放内存)
*/
static void list_remove(cache_t *cache, cache_entry_t *entry)
{
if (entry->prev)
entry->prev->next = entry->next;
else
cache->head = entry->next; /* 移除的是头节点 */
if (entry->next)
entry->next->prev = entry->prev;
else
cache->tail = entry->prev; /* 移除的是尾节点 */
}
/**
* list_add_head — 将节点添加到链表头部 (MRU 位置)
*/
static void list_add_head(cache_t *cache, cache_entry_t *entry)
{
entry->prev = NULL;
entry->next = cache->head;
if (cache->head)
cache->head->prev = entry;
else
cache->tail = entry; /* 链表为空,也是尾节点 */
cache->head = entry;
}
/**
* evict_lru — 淘汰 LRU 节点 (链表尾部)
* 从哈希表和链表中移除,释放内存
*/
static void evict_lru(cache_t *cache)
{
if (cache->tail == NULL)
return;
cache_entry_t *victim = cache->tail;
/* 从链表中移除 */
list_remove(cache, victim);
/* 从哈希桶中移除 */
unsigned long h = hash_url(victim->url) % cache->num_buckets;
cache_entry_t **pp = &cache->buckets[h];
while (*pp) {
if (*pp == victim) {
*pp = victim->hnext;
break;
}
pp = &(*pp)->hnext;
}
/* 释放内存 */
free(victim->url);
free(victim->data);
free(victim);
cache->cur_size--;
cache->evictions++;
}
/* ======================== 公开 API 实现 ======================== */
/**
* cache_init — 初始化 LRU 缓存
*/
cache_t *cache_init(int max_size)
{
cache_t *cache = (cache_t *)calloc(1, sizeof(cache_t));
if (cache == NULL) {
perror("cache_init: calloc failed");
return NULL;
}
cache->num_buckets = DEFAULT_NUM_BUCKETS;
cache->max_size = (max_size > 0 && max_size <= MAX_CACHE_SIZE)
? max_size : MAX_CACHE_SIZE;
cache->cur_size = 0;
cache->head = NULL;
cache->tail = NULL;
cache->hits = 0;
cache->misses = 0;
cache->evictions = 0;
cache->bytes_served = 0;
cache->buckets = (cache_entry_t **)calloc(cache->num_buckets,
sizeof(cache_entry_t *));
if (cache->buckets == NULL) {
perror("cache_init: calloc buckets failed");
free(cache);
return NULL;
}
if (pthread_mutex_init(&cache->lock, NULL) != 0) {
perror("cache_init: mutex_init failed");
free(cache->buckets);
free(cache);
return NULL;
}
printf("[Cache] 初始化完成: 最大条目=%d, 桶数量=%d\n",
cache->max_size, cache->num_buckets);
return cache;
}
/**
* cache_lookup — 在缓存中查找 URL (O(1) 平均)
*
* 流程:
* 1. 计算哈希值,定位桶
* 2. 遍历桶内冲突链查找匹配 URL
* 3. 命中: 将条目移到链表头部 (标记为最近使用),拷贝数据
* 4. 未命中: 返回 0
*/
int cache_lookup(cache_t *cache, const char *url,
char **data, long *size, char *filetype)
{
if (cache == NULL || url == NULL)
return 0;
pthread_mutex_lock(&cache->lock);
unsigned long h = hash_url(url) % cache->num_buckets;
cache_entry_t *entry = cache->buckets[h];
while (entry) {
if (strcmp(entry->url, url) == 0) {
/* 缓存命中: 移动到链表头部 */
list_remove(cache, entry);
list_add_head(cache, entry);
/* 更新访问时间 */
clock_gettime(CLOCK_MONOTONIC, &entry->timestamp);
/* 拷贝数据给调用者 */
*size = entry->size;
strcpy(filetype, entry->filetype);
*data = (char *)malloc(entry->size);
if (*data)
memcpy(*data, entry->data, entry->size);
cache->hits++;
cache->bytes_served += entry->size;
pthread_mutex_unlock(&cache->lock);
return 1;
}
entry = entry->hnext;
}
/* 缓存未命中 */
cache->misses++;
pthread_mutex_unlock(&cache->lock);
return 0;
}
/**
* cache_insert — 向缓存插入新条目
*
* 流程:
* 1. 若缓存已满,先淘汰 LRU 条目
* 2. 创建新条目,拷贝数据
* 3. 添加到哈希表和链表头部
*/
int cache_insert(cache_t *cache, const char *url,
const char *data, long size, const char *filetype)
{
if (cache == NULL || url == NULL || data == NULL || size <= 0)
return -1;
pthread_mutex_lock(&cache->lock);
/* 检查是否已存在 (更新) */
unsigned long h = hash_url(url) % cache->num_buckets;
cache_entry_t *entry = cache->buckets[h];
while (entry) {
if (strcmp(entry->url, url) == 0) {
/* 已存在: 更新数据和位置 */
free(entry->data);
entry->data = (char *)malloc(size);
if (entry->data == NULL) {
pthread_mutex_unlock(&cache->lock);
return -1;
}
memcpy(entry->data, data, size);
entry->size = size;
strcpy(entry->filetype, filetype);
clock_gettime(CLOCK_MONOTONIC, &entry->timestamp);
/* 移动到头部 */
list_remove(cache, entry);
list_add_head(cache, entry);
pthread_mutex_unlock(&cache->lock);
return 0;
}
entry = entry->hnext;
}
/* 缓存已满,淘汰 LRU */
while (cache->cur_size >= cache->max_size)
evict_lru(cache);
/* 创建新条目 */
cache_entry_t *new_entry = (cache_entry_t *)calloc(1, sizeof(cache_entry_t));
if (new_entry == NULL) {
pthread_mutex_unlock(&cache->lock);
return -1;
}
new_entry->url = strdup(url);
new_entry->data = (char *)malloc(size);
if (new_entry->url == NULL || new_entry->data == NULL) {
free(new_entry->url);
free(new_entry->data);
free(new_entry);
pthread_mutex_unlock(&cache->lock);
return -1;
}
memcpy(new_entry->data, data, size);
new_entry->size = size;
strcpy(new_entry->filetype, filetype);
clock_gettime(CLOCK_MONOTONIC, &new_entry->timestamp);
/* 添加到哈希桶 */
new_entry->hnext = cache->buckets[h];
cache->buckets[h] = new_entry;
/* 添加到链表头部 */
list_add_head(cache, new_entry);
cache->cur_size++;
pthread_mutex_unlock(&cache->lock);
return 0;
}
/**
* cache_free — 释放所有缓存资源
*/
void cache_free(cache_t *cache)
{
if (cache == NULL) return;
pthread_mutex_lock(&cache->lock);
/* 遍历链表释放所有条目 */
cache_entry_t *entry = cache->head;
while (entry) {
cache_entry_t *next = entry->next;
free(entry->url);
free(entry->data);
free(entry);
entry = next;
}
free(cache->buckets);
pthread_mutex_unlock(&cache->lock);
pthread_mutex_destroy(&cache->lock);
printf("[Cache] 释放完成: 命中=%lld 未命中=%lld 淘汰=%lld "
"命中率=%.1f%% 服务字节=%lld\n",
cache->hits, cache->misses, cache->evictions,
cache->hits + cache->misses > 0
? 100.0 * cache->hits / (cache->hits + cache->misses)
: 0.0,
cache->bytes_served);
free(cache);
}

92
server-exp4/cache.h Normal file
View File

@@ -0,0 +1,92 @@
/**
* cache.h — Web服务器 LRU 缓存模块
*
* 设计思想:
* 使用 哈希表 + 双向链表 实现 O(1) 的查找、插入和淘汰操作。
* 哈希表提供快速键值查找,双向链表维护 LRU 访问顺序。
* 最近被访问的节点移动到链表头部 (MRU位置)
* 链表尾部的节点是最久未被访问的 (LRU位置),淘汰时优先移除。
*
* 数据结构:
* cache_entry: 单个缓存条目 (文件名、文件内容、大小、MIME类型、时间戳)
* cache_t: 缓存管理器 (哈希表桶数组、链表头尾、统计计数器)
*
* 主要操作:
* cache_init(max_size): 初始化缓存,设置最大条目数
* cache_lookup(url, buf, size, type): 查找缓存,命中时拷贝数据并返回 1
* cache_insert(url, data, size, type): 插入缓存,必要时淘汰 LRU 条目
* cache_free(): 释放所有缓存内存
*/
#ifndef _CACHE_H_
#define _CACHE_H_
#include <time.h>
#include <pthread.h>
/* 缓存条目 */
typedef struct cache_entry {
char *url; /* 请求 URL (作为键) */
char *data; /* 文件内容 */
long size; /* 文件大小 */
char filetype[64]; /* MIME 类型 */
struct timespec timestamp; /* 最后访问时间 */
struct cache_entry *prev; /* 双向链表前驱 */
struct cache_entry *next; /* 双向链表后继 */
struct cache_entry *hnext; /* 哈希桶内链表 (冲突链) */
} cache_entry_t;
/* 缓存管理器 */
typedef struct {
cache_entry_t **buckets; /* 哈希桶数组 */
int num_buckets; /* 哈希桶数量 */
int max_size; /* 最大条目数 */
int cur_size; /* 当前条目数 */
cache_entry_t *head; /* LRU 链表头 (最近使用) */
cache_entry_t *tail; /* LRU 链表尾 (最久未使用) */
pthread_mutex_t lock; /* 线程安全锁 */
/* 统计信息 */
long long hits; /* 缓存命中次数 */
long long misses; /* 缓存未命中次数 */
long long evictions; /* 淘汰次数 */
long long bytes_served; /* 从缓存服务的字节数 */
} cache_t;
/**
* cache_init — 初始化 LRU 缓存
* @max_size: 最大缓存条目数
* @return: 指向 cache_t 的指针,失败返回 NULL
*/
cache_t *cache_init(int max_size);
/**
* cache_lookup — 在缓存中查找 URL
* @cache: 缓存管理器指针
* @url: 请求的 URL 键
* @data: 输出参数,命中时指向缓存数据的拷贝 (调用者需 free)
* @size: 输出参数,数据大小
* @filetype: 输出参数MIME 类型
* @return: 1=命中, 0=未命中
*/
int cache_lookup(cache_t *cache, const char *url,
char **data, long *size, char *filetype);
/**
* cache_insert — 向缓存插入新条目
* @cache: 缓存管理器指针
* @url: URL 键
* @data: 文件内容 (缓存会拷贝一份)
* @size: 文件大小
* @filetype: MIME 类型
* @return: 0=成功, -1=失败
*/
int cache_insert(cache_t *cache, const char *url,
const char *data, long size, const char *filetype);
/**
* cache_free — 释放所有缓存资源
*/
void cache_free(cache_t *cache);
#endif /* _CACHE_H_ */

BIN
server-exp4/cgi-bin/add Executable file

Binary file not shown.

63
server-exp4/cgi-bin/add.c Normal file
View File

@@ -0,0 +1,63 @@
/*
* cgi-bin/add.c — 简单CGI加法计算器
*
* 用于测试Web服务器的CGI功能
* GET: /cgi-bin/add?a=10&b=20
* POST: 提交表单到 /cgi-bin/add
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
char *query = getenv("QUERY_STRING");
char *method = getenv("REQUEST_METHOD");
printf("Content-Type: text/html; charset=UTF-8\r\n\r\n");
printf("<!DOCTYPE html>\n<html><head>\n");
printf("<meta charset=\"UTF-8\">\n");
printf("<title>CGI Calculator</title>\n");
printf("<style>body{font-family:Arial;margin:40px;}"
"h1{color:#2a7;}.result{background:#f5f5f5;padding:20px;"
"border-radius:10px;font-size:1.5em;text-align:center;}"
"a{color:#667eea;}</style>\n");
printf("</head><body>\n");
printf("<h1>🔧 CGI 动态计算器 (实验4)</h1>\n");
/* 从 stdin 读取 POST 数据 */
char post_data[1024] = "";
if (method && strcmp(method, "POST") == 0) {
char *len_str = getenv("CONTENT_LENGTH");
if (len_str) {
int len = atoi(len_str);
if (len > 0 && len < (int)sizeof(post_data) - 1) {
fread(post_data, 1, len, stdin);
post_data[len] = '\0';
}
}
query = post_data;
}
/* 解析参数 a 和 b */
int a = 0, b = 0;
if (query) {
char *pa = strstr(query, "a=");
char *pb = strstr(query, "b=");
if (pa) a = atoi(pa + 2);
if (pb) b = atoi(pb + 2);
}
printf("<div class=\"result\">\n");
printf("<p>%d + %d = <strong>%d</strong></p>\n", a, b, a + b);
printf("<p>Method: %s | PID: %d</p>\n",
method ? method : "UNKNOWN", getpid());
printf("</div>\n");
printf("<p><a href=\"/\">← 返回首页</a></p>\n");
printf("<hr><em>Experiment4 CGI v1.0</em>\n");
printf("</body></html>\n");
return 0;
}

170
server-exp4/common.c Normal file
View File

@@ -0,0 +1,170 @@
/* $begin common.c */
#include "common.h"
/*********************************************************************
* The Rio package - robust I/O functions
**********************************************************************/
ssize_t rio_readn(int fd, void *usrbuf, size_t n)
{
size_t nleft = n;
ssize_t nread;
char *bufp = usrbuf;
while (nleft > 0) {
if ((nread = read(fd, bufp, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return -1;
}
else if (nread == 0)
break;
nleft -= nread;
bufp += nread;
}
return (n - nleft);
}
ssize_t rio_writen(int fd, void *usrbuf, size_t n)
{
size_t nleft = n;
ssize_t nwritten;
char *bufp = usrbuf;
while (nleft > 0) {
if ((nwritten = write(fd, bufp, nleft)) <= 0) {
if (errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
bufp += nwritten;
}
return n;
}
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)
{
int cnt;
while (rp->rio_cnt <= 0) {
rp->rio_cnt = read(rp->rio_fd, rp->rio_buf,
sizeof(rp->rio_buf));
if (rp->rio_cnt < 0) {
if (errno != EINTR)
return -1;
}
else if (rp->rio_cnt == 0)
return 0;
else
rp->rio_bufptr = rp->rio_buf;
}
cnt = n;
if (rp->rio_cnt < n)
cnt = rp->rio_cnt;
memcpy(usrbuf, rp->rio_bufptr, cnt);
rp->rio_bufptr += cnt;
rp->rio_cnt -= cnt;
return cnt;
}
void rio_readinitb(rio_t *rp, int fd)
{
rp->rio_fd = fd;
rp->rio_cnt = 0;
rp->rio_bufptr = rp->rio_buf;
}
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n)
{
size_t nleft = n;
ssize_t nread;
char *bufp = usrbuf;
while (nleft > 0) {
if ((nread = rio_read(rp, bufp, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return -1;
}
else if (nread == 0)
break;
nleft -= nread;
bufp += nread;
}
return (n - nleft);
}
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)
{
int n, rc;
char c, *bufp = usrbuf;
for (n = 1; n < maxlen; n++) {
if ((rc = rio_read(rp, &c, 1)) == 1) {
*bufp++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
if (n == 1)
return 0;
else
break;
} else
return -1;
}
*bufp = 0;
return n;
}
/********************************
* Client/server helper functions
********************************/
int open_client_sock(char *hostname, int port)
{
int client_sock;
struct hostent *hp;
struct sockaddr_in serveraddr;
if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return -1;
if ((hp = gethostbyname(hostname)) == NULL)
return -2;
bzero((char *) &serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
bcopy((char *)hp->h_addr_list[0],
(char *)&serveraddr.sin_addr.s_addr, hp->h_length);
serveraddr.sin_port = htons(port);
if (connect(client_sock, (SA *) &serveraddr, sizeof(serveraddr)) < 0)
return -1;
return client_sock;
}
int open_listen_sock(int port)
{
int listen_sock, optval=1;
struct sockaddr_in serveraddr;
if ((listen_sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return -1;
if (setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR,
(const void *)&optval , sizeof(int)) < 0)
return -1;
bzero((char *) &serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons((unsigned short)port);
if (bind(listen_sock, (SA *)&serveraddr, sizeof(serveraddr)) < 0)
return -1;
if (listen(listen_sock, LISTENQ) < 0)
return -1;
return listen_sock;
}

66
server-exp4/common.h Normal file
View File

@@ -0,0 +1,66 @@
/* $begin common.h */
#ifndef __COMMON_H__
#define __COMMON_H__
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <setjmp.h>
#include <signal.h>
#include <ctype.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <math.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <limits.h>
#include <sys/epoll.h>
#include <sys/timerfd.h>
/* Default file permissions */
#define DEF_MODE S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
#define DEF_UMASK S_IWGRP|S_IWOTH
/* Simplifies calls to bind(), connect(), and accept() */
typedef struct sockaddr SA;
/* Persistent state for the robust I/O (Rio) package */
#define RIO_BUFSIZE 8192
typedef struct {
int rio_fd;
int rio_cnt;
char *rio_bufptr;
char rio_buf[RIO_BUFSIZE];
} rio_t;
/* External variables */
extern int h_errno;
extern char **environ;
/* Misc constants */
#define MAXLINE 8192
#define MAXBUF 8192
#define LISTENQ 1024
/* Rio (Robust I/O) package */
ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);
void rio_readinitb(rio_t *rp, int fd);
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
/* Client/server helper functions */
int open_client_sock(char *hostname, int portno);
int open_listen_sock(int portno);
#endif /* __COMMON_H__ */

BIN
server-exp4/example.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

BIN
server-exp4/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

191
server-exp4/index.html Normal file
View File

@@ -0,0 +1,191 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>实验4 高性能Web服务器 - 测试页面</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 20px;
padding: 40px;
max-width: 800px;
width: 90%;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 10px;
font-size: 2em;
}
h2 {
color: #555;
text-align: center;
margin-bottom: 30px;
font-weight: normal;
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.feature-card {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
text-align: center;
border: 2px solid #e9ecef;
transition: transform 0.2s, border-color 0.2s;
}
.feature-card:hover {
transform: translateY(-3px);
border-color: #667eea;
}
.feature-icon { font-size: 2em; margin-bottom: 10px; }
.feature-title { font-weight: bold; color: #333; margin-bottom: 5px; }
.feature-desc { font-size: 0.9em; color: #666; }
.test-section {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
}
.test-section h3 {
color: #333;
margin-bottom: 15px;
border-bottom: 2px solid #667eea;
padding-bottom: 8px;
}
form {
display: flex;
flex-direction: column;
gap: 10px;
}
input[type="text"], input[type="email"], textarea {
padding: 10px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 1em;
transition: border-color 0.2s;
}
input:focus, textarea:focus {
border-color: #667eea;
outline: none;
}
button, .btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1em;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
text-decoration: none;
display: inline-block;
text-align: center;
}
button:hover, .btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102,126,234,0.4);
}
.link-list { list-style: none; padding: 0; }
.link-list li { margin: 8px 0; }
.link-list a {
color: #667eea;
text-decoration: none;
font-weight: 500;
}
.link-list a:hover { text-decoration: underline; }
.server-info {
text-align: center;
color: #999;
font-size: 0.85em;
margin-top: 20px;
}
pre { background: white; padding: 15px; border-radius: 8px; overflow-x: auto; }
</style>
</head>
<body>
<div class="container">
<h1>🚀 实验4 高性能Web服务器</h1>
<h2>epoll + 线程池 + LRU缓存 + GET/POST</h2>
<div class="features">
<div class="feature-card">
<div class="feature-icon"></div>
<div class="feature-title">epoll I/O复用</div>
<div class="feature-desc">基于epoll的边缘触发模式高效处理海量并发连接</div>
</div>
<div class="feature-card">
<div class="feature-icon">🧵</div>
<div class="feature-title">线程池</div>
<div class="feature-desc">预创建工作线程,避免频繁创建销毁开销</div>
</div>
<div class="feature-card">
<div class="feature-icon">💾</div>
<div class="feature-title">LRU缓存</div>
<div class="feature-desc">哈希表+双向链表O(1)查找与淘汰</div>
</div>
<div class="feature-card">
<div class="feature-icon">📡</div>
<div class="feature-title">GET/POST</div>
<div class="feature-desc">完整支持HTTP GET和POST方法</div>
</div>
</div>
<div class="test-section">
<h3>📋 功能测试</h3>
<ul class="link-list">
<li><a href="/test.html">📄 静态页面测试 (GET)</a></li>
<li><a href="/example.jpg">🖼️ 图片访问测试 (JPEG)</a></li>
<li><a href="/nonexistent.html">❌ 404错误页面测试</a></li>
<li><a href="/cgi-bin/add?a=10&b=20">🔧 CGI动态内容测试</a></li>
</ul>
</div>
<div class="test-section">
<h3>📝 POST 方法测试</h3>
<form action="/submit" method="POST">
<label for="name">姓名:</label>
<input type="text" id="name" name="name" placeholder="输入您的姓名" required>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" placeholder="输入您的邮箱" required>
<label for="message">留言:</label>
<textarea id="message" name="message" rows="3" placeholder="输入您的留言..."></textarea>
<button type="submit">✉️ 提交 POST 请求</button>
</form>
</div>
<div class="test-section">
<h3>📊 缓存测试</h3>
<p style="color:#666;margin-bottom:10px;">
多次访问同一资源观察 X-Cache 响应头: 首次 MISS后续 HIT
</p>
<pre># 测试缓存命中
curl -I http://localhost:8088/index.html
# 第一次: X-Cache: MISS
# 第二次: X-Cache: HIT (从缓存返回)
# 压力测试
ab -n 10000 -c 100 http://localhost:8088/index.html
wrk -t4 -c100 -d30s http://localhost:8088/index.html</pre>
</div>
<div class="server-info">
<p>Powered by Experiment4-WebServer v40.0 | epoll + Thread Pool + LRU Cache</p>
</div>
</div>
</body>
</html>

48
server-exp4/test.html Normal file
View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>静态页面测试 - 实验4</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; background: #f0f4f8; color: #333; }
h1 { color: #2a7; }
.box { background: white; padding: 20px; border-radius: 10px; margin: 15px 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
th { background: #667eea; color: white; }
a { color: #667eea; }
</style>
</head>
<body>
<h1>📄 静态页面测试</h1>
<p>本页面用于验证Web服务器的静态资源服务能力包括缓存功能</p>
<div class="box">
<h2>服务器特性对比</h2>
<table>
<tr><th>版本</th><th>I/O模型</th><th>并发模型</th><th>缓存</th><th>方法</th></tr>
<tr><td>实验1</td><td>阻塞I/O</td><td>单进程</td><td></td><td>GET</td></tr>
<tr><td>实验2</td><td>阻塞I/O</td><td>多线程/预线程</td><td></td><td>GET</td></tr>
<tr><td>实验3</td><td>阻塞I/O</td><td>线程池/流水线</td><td></td><td>GET</td></tr>
<tr><td style="color:#2a7;font-weight:bold;">实验4</td>
<td style="color:#2a7;">epoll</td>
<td style="color:#2a7;">线程池</td>
<td style="color:#2a7;">✅ LRU</td>
<td style="color:#2a7;">GET+POST</td></tr>
</table>
</div>
<div class="box">
<h2>实验4 新增功能</h2>
<ul>
<li><strong>epoll I/O多路复用:</strong> 主线程使用epoll监控所有连接边缘触发模式</li>
<li><strong>LRU缓存:</strong> 哈希表 + 双向链表O(1)查找和淘汰</li>
<li><strong>POST方法:</strong> 支持表单提交和CGI转发</li>
<li><strong>缓存命中指示:</strong> 响应头 X-Cache: HIT/MISS</li>
</ul>
</div>
<p><a href="/">← 返回首页</a></p>
</body>
</html>

478
server-exp4/thpool.c Normal file
View File

@@ -0,0 +1,478 @@
/* ********************************
* Author: Johan Hanssen Seferidis
* License: MIT
* Description: Library providing a threading pool where you can add
* work. For usage, check the thpool.h file or README.md
*
*/
/** @file thpool.h */ /*
*
********************************/
#if defined(__APPLE__)
#include <AvailabilityMacros.h>
#else
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE 500
#endif
#endif
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#if defined(__linux__)
#include <sys/prctl.h>
#endif
#if defined(__FreeBSD__) || defined(__OpenBSD__)
#include <pthread_np.h>
#endif
#include "thpool.h"
#ifdef THPOOL_DEBUG
#define THPOOL_DEBUG 1
#else
#define THPOOL_DEBUG 0
#endif
#if !defined(DISABLE_PRINT) || defined(THPOOL_DEBUG)
#define err(str) fprintf(stderr, str)
#else
#define err(str)
#endif
#ifndef THPOOL_THREAD_NAME
#define THPOOL_THREAD_NAME thpool
#endif
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
static volatile int threads_keepalive;
static volatile int threads_on_hold;
/* Thread-local worker ID供外部模块统计每个线程的请求处理数 */
__thread int thpool_worker_id = -1;
/* ========================== STRUCTURES ============================ */
/* ========================== PROTOTYPES ============================ */
static int thread_init(thpool_* thpool_p, struct thread** thread_p, int id);
static void* thread_do(struct thread* thread_p);
static void thread_hold(int sig_id);
static void thread_destroy(struct thread* thread_p);
static int jobqueue_init(jobqueue* jobqueue_p);
static void jobqueue_clear(jobqueue* jobqueue_p);
static void jobqueue_push(jobqueue* jobqueue_p, struct job* newjob_p);
static struct job* jobqueue_pull(jobqueue* jobqueue_p);
static void jobqueue_destroy(jobqueue* jobqueue_p);
static void bsem_init(struct bsem* bsem_p, int value);
static void bsem_reset(struct bsem* bsem_p);
static void bsem_post(struct bsem* bsem_p);
static void bsem_post_all(struct bsem* bsem_p);
static void bsem_wait(struct bsem* bsem_p);
/* ========================== THREADPOOL ============================ */
/* Initialise thread pool */
struct thpool_* thpool_init(int num_threads) {
threads_on_hold = 0;
threads_keepalive = 1;
if (num_threads < 0) {
num_threads = 0;
}
/* Make new thread pool */
thpool_* thpool_p;
thpool_p = (struct thpool_*)malloc(sizeof(struct thpool_));
if (thpool_p == NULL) {
err("thpool_init(): Could not allocate memory for thread pool\n");
return NULL;
}
thpool_p->num_threads_alive = 0;
thpool_p->num_threads_working = 0;
/* Initialise the job queue */
if (jobqueue_init(&thpool_p->jobqueue) == -1) {
err("thpool_init(): Could not allocate memory for job queue\n");
free(thpool_p);
return NULL;
}
/* Make threads in pool */
thpool_p->threads = (struct thread**)malloc(num_threads * sizeof(struct thread*));
if (thpool_p->threads == NULL) {
err("thpool_init(): Could not allocate memory for threads\n");
jobqueue_destroy(&thpool_p->jobqueue);
free(thpool_p);
return NULL;
}
pthread_mutex_init(&(thpool_p->thcount_lock), NULL);
pthread_cond_init(&thpool_p->threads_all_idle, NULL);
/* Thread init */
int n;
for (n = 0; n < num_threads; n++) {
thread_init(thpool_p, &thpool_p->threads[n], n);
#if THPOOL_DEBUG
printf("THPOOL_DEBUG: Created thread %d in pool \n", n);
#endif
}
/* Wait for threads to initialize */
while (thpool_p->num_threads_alive != num_threads) {
}
return thpool_p;
}
/* Add work to the thread pool */
int thpool_add_work(thpool_* thpool_p, void (*function_p)(void*), void* arg_p) {
job* newjob;
newjob = (struct job*)malloc(sizeof(struct job));
if (newjob == NULL) {
err("thpool_add_work(): Could not allocate memory for new job\n");
return -1;
}
/* add function and argument */
newjob->function = function_p;
newjob->arg = arg_p;
/* add job to queue */
jobqueue_push(&thpool_p->jobqueue, newjob);
return 0;
}
/* Wait until all jobs have finished */
void thpool_wait(thpool_* thpool_p) {
pthread_mutex_lock(&thpool_p->thcount_lock);
while (thpool_p->jobqueue.len || thpool_p->num_threads_working) {
pthread_cond_wait(&thpool_p->threads_all_idle, &thpool_p->thcount_lock);
}
pthread_mutex_unlock(&thpool_p->thcount_lock);
}
/* Destroy the threadpool */
void thpool_destroy(thpool_* thpool_p) {
/* No need to destroy if it's NULL */
if (thpool_p == NULL) return;
volatile int threads_total = thpool_p->num_threads_alive;
/* End each thread 's infinite loop */
threads_keepalive = 0;
/* Give one second to kill idle threads */
double TIMEOUT = 1.0;
time_t start, end;
double tpassed = 0.0;
time(&start);
while (tpassed < TIMEOUT && thpool_p->num_threads_alive) {
bsem_post_all(thpool_p->jobqueue.has_jobs);
time(&end);
tpassed = difftime(end, start);
}
/* Poll remaining threads */
while (thpool_p->num_threads_alive) {
bsem_post_all(thpool_p->jobqueue.has_jobs);
sleep(1);
}
/* Job queue cleanup */
jobqueue_destroy(&thpool_p->jobqueue);
/* Deallocs */
int n;
for (n = 0; n < threads_total; n++) {
thread_destroy(thpool_p->threads[n]);
}
free(thpool_p->threads);
free(thpool_p);
}
/* Pause all threads in threadpool */
void thpool_pause(thpool_* thpool_p) {
int n;
for (n = 0; n < thpool_p->num_threads_alive; n++) {
pthread_kill(thpool_p->threads[n]->pthread, SIGUSR1);
}
}
/* Resume all threads in threadpool */
void thpool_resume(thpool_* thpool_p) {
// resuming a single threadpool hasn't been
// implemented yet, meanwhile this suppresses
// the warnings
(void)thpool_p;
threads_on_hold = 0;
}
int thpool_num_threads_working(thpool_* thpool_p) { return thpool_p->num_threads_working; }
/* ============================ THREAD ============================== */
/* Initialize a thread in the thread pool
*
* @param thread address to the pointer of the thread to be created
* @param id id to be given to the thread
* @return 0 on success, -1 otherwise.
*/
static int thread_init(thpool_* thpool_p, struct thread** thread_p, int id) {
*thread_p = (struct thread*)malloc(sizeof(struct thread));
if (*thread_p == NULL) {
err("thread_init(): Could not allocate memory for thread\n");
return -1;
}
(*thread_p)->thpool_p = thpool_p;
(*thread_p)->id = id;
pthread_create(&(*thread_p)->pthread, NULL, (void* (*)(void*))thread_do, (*thread_p));
pthread_detach((*thread_p)->pthread);
return 0;
}
/* Sets the calling thread on hold */
static void thread_hold(int sig_id) {
(void)sig_id;
threads_on_hold = 1;
while (threads_on_hold) {
sleep(1);
}
}
/* What each thread is doing
*
* In principle this is an endless loop. The only time this loop gets interrupted is once
* thpool_destroy() is invoked or the program exits.
*
* @param thread thread that will run this function
* @return nothing
*/
static void* thread_do(struct thread* thread_p) {
/* Set thread name for profiling and debugging */
char thread_name[16] = {0};
snprintf(thread_name, 16, TOSTRING(THPOOL_THREAD_NAME) "-%d", thread_p->id);
#if defined(__linux__)
/* Use prctl instead to prevent using _GNU_SOURCE flag and implicit declaration */
prctl(PR_SET_NAME, thread_name);
#elif defined(__APPLE__) && defined(__MACH__)
pthread_setname_np(thread_name);
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
pthread_set_name_np(thread_p->pthread, thread_name);
#else
err("thread_do(): pthread_setname_np is not supported on this system");
#endif
/* Assure all threads have been created before starting serving */
thpool_* thpool_p = thread_p->thpool_p;
/* Register signal handler */
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_ONSTACK;
act.sa_handler = thread_hold;
if (sigaction(SIGUSR1, &act, NULL) == -1) {
err("thread_do(): cannot handle SIGUSR1");
}
/* Mark thread as alive (initialized) */
pthread_mutex_lock(&thpool_p->thcount_lock);
thpool_p->num_threads_alive += 1;
pthread_mutex_unlock(&thpool_p->thcount_lock);
while (threads_keepalive) {
bsem_wait(thpool_p->jobqueue.has_jobs);
if (threads_keepalive) {
pthread_mutex_lock(&thpool_p->thcount_lock);
thpool_p->num_threads_working++;
pthread_mutex_unlock(&thpool_p->thcount_lock);
/* Read job from queue and execute it */
void (*func_buff)(void*);
void* arg_buff;
job* job_p = jobqueue_pull(&thpool_p->jobqueue);
if (job_p) {
#ifdef DEBUG
printf("线程%d开始处理\n",thread_p->id);
#endif
func_buff = job_p->function;
arg_buff = job_p->arg;
thpool_worker_id = thread_p->id; /* 设置 thread-local ID */
func_buff(arg_buff);
#ifdef DEBUG
printf("线程%d处理完成\n",thread_p->id);
#endif
free(job_p);
}
pthread_mutex_lock(&thpool_p->thcount_lock);
thpool_p->num_threads_working--;
if (!thpool_p->num_threads_working) {
pthread_cond_signal(&thpool_p->threads_all_idle);
}
pthread_mutex_unlock(&thpool_p->thcount_lock);
}
}
pthread_mutex_lock(&thpool_p->thcount_lock);
thpool_p->num_threads_alive--;
pthread_mutex_unlock(&thpool_p->thcount_lock);
return NULL;
}
/* Frees a thread */
static void thread_destroy(thread* thread_p) { free(thread_p); }
/* ============================ JOB QUEUE =========================== */
/* Initialize queue */
static int jobqueue_init(jobqueue* jobqueue_p) {
jobqueue_p->len = 0;
jobqueue_p->front = NULL;
jobqueue_p->rear = NULL;
jobqueue_p->has_jobs = (struct bsem*)malloc(sizeof(struct bsem));
if (jobqueue_p->has_jobs == NULL) {
return -1;
}
pthread_mutex_init(&(jobqueue_p->rwmutex), NULL);
bsem_init(jobqueue_p->has_jobs, 0);
return 0;
}
/* Clear the queue */
static void jobqueue_clear(jobqueue* jobqueue_p) {
while (jobqueue_p->len) {
free(jobqueue_pull(jobqueue_p));
}
jobqueue_p->front = NULL;
jobqueue_p->rear = NULL;
bsem_reset(jobqueue_p->has_jobs);
jobqueue_p->len = 0;
}
/* Add (allocated) job to queue
*/
static void jobqueue_push(jobqueue* jobqueue_p, struct job* newjob) {
pthread_mutex_lock(&jobqueue_p->rwmutex);
newjob->prev = NULL;
switch (jobqueue_p->len) {
case 0: /* if no jobs in queue */
jobqueue_p->front = newjob;
jobqueue_p->rear = newjob;
break;
default: /* if jobs in queue */
jobqueue_p->rear->prev = newjob;
jobqueue_p->rear = newjob;
}
jobqueue_p->len++;
bsem_post(jobqueue_p->has_jobs);
pthread_mutex_unlock(&jobqueue_p->rwmutex);
}
/* Get first job from queue(removes it from queue)
* Notice: Caller MUST hold a mutex
*/
static struct job* jobqueue_pull(jobqueue* jobqueue_p) {
pthread_mutex_lock(&jobqueue_p->rwmutex);
job* job_p = jobqueue_p->front;
switch (jobqueue_p->len) {
case 0: /* if no jobs in queue */
break;
case 1: /* if one job in queue */
jobqueue_p->front = NULL;
jobqueue_p->rear = NULL;
jobqueue_p->len = 0;
break;
default: /* if >1 jobs in queue */
jobqueue_p->front = job_p->prev;
jobqueue_p->len--;
/* more than one job in queue -> post it */
bsem_post(jobqueue_p->has_jobs);
}
pthread_mutex_unlock(&jobqueue_p->rwmutex);
return job_p;
}
/* Free all queue resources back to the system */
static void jobqueue_destroy(jobqueue* jobqueue_p) {
jobqueue_clear(jobqueue_p);
free(jobqueue_p->has_jobs);
}
/* ======================== SYNCHRONISATION ========================= */
/* Init semaphore to 1 or 0 */
static void bsem_init(bsem* bsem_p, int value) {
if (value < 0 || value > 1) {
err("bsem_init(): Binary semaphore can take only values 1 or 0");
exit(1);
}
pthread_mutex_init(&(bsem_p->mutex), NULL);
pthread_cond_init(&(bsem_p->cond), NULL);
bsem_p->v = value;
}
/* Reset semaphore to 0 */
static void bsem_reset(bsem* bsem_p) {
pthread_mutex_destroy(&(bsem_p->mutex));
pthread_cond_destroy(&(bsem_p->cond));
bsem_init(bsem_p, 0);
}
/* Post to at least one thread */
static void bsem_post(bsem* bsem_p) {
pthread_mutex_lock(&bsem_p->mutex);
bsem_p->v = 1;
pthread_cond_signal(&bsem_p->cond);
pthread_mutex_unlock(&bsem_p->mutex);
}
/* Post to all threads */
static void bsem_post_all(bsem* bsem_p) {
pthread_mutex_lock(&bsem_p->mutex);
bsem_p->v = 1;
pthread_cond_broadcast(&bsem_p->cond);
pthread_mutex_unlock(&bsem_p->mutex);
}
/* Wait on semaphore until semaphore has value 0 */
static void bsem_wait(bsem* bsem_p) {
pthread_mutex_lock(&bsem_p->mutex);
while (bsem_p->v != 1) {
pthread_cond_wait(&bsem_p->cond, &bsem_p->mutex);
}
bsem_p->v = 0;
pthread_mutex_unlock(&bsem_p->mutex);
}

222
server-exp4/thpool.h Normal file
View File

@@ -0,0 +1,222 @@
/**********************************
* @author Johan Hanssen Seferidis
* License: MIT
*
**********************************/
#ifndef _THPOOL_
#define _THPOOL_
#ifdef __cplusplus
extern "C" {
#endif
/* =================================== API ======================================= */
/* ========================== STRUCTURES ============================ */
/* Binary semaphore */
typedef struct bsem {
pthread_mutex_t mutex;
pthread_cond_t cond;
int v;
} bsem;
/* Job */
typedef struct job {
struct job* prev; /* pointer to previous job */
void (*function)(void* arg); /* function pointer */
void* arg; /* function's argument */
} job;
/* Job queue */
typedef struct jobqueue {
pthread_mutex_t rwmutex; /* used for queue r/w access */
job* front; /* pointer to front of queue */
job* rear; /* pointer to rear of queue */
bsem* has_jobs; /* flag as binary semaphore */
int len; /* number of jobs in queue */
} jobqueue;
/* Thread */
typedef struct thread {
int id; /* friendly id */
pthread_t pthread; /* pointer to actual thread */
struct thpool_* thpool_p; /* access to thpool */
} thread;
/* Threadpool */
typedef struct thpool_ {
thread** threads; /* pointer to threads */
volatile int num_threads_alive; /* threads currently alive */
volatile int num_threads_working; /* threads currently working */
pthread_mutex_t thcount_lock; /* used for thread count etc */
pthread_cond_t threads_all_idle; /* signal to thpool_wait */
jobqueue jobqueue; /* job queue */
} thpool_;
typedef struct thpool_* threadpool;
/**
* @brief Initialize threadpool
*
* Initializes a threadpool. This function will not return until all
* threads have initialized successfully.
*
* @example
*
* ..
* threadpool thpool; //First we declare a threadpool
* thpool = thpool_init(4); //then we initialize it to 4 threads
* ..
*
* @param num_threads number of threads to be created in the threadpool
* @return threadpool created threadpool on success,
* NULL on error
*/
threadpool thpool_init(int num_threads);
/**
* @brief Add work to the job queue
*
* Takes an action and its argument and adds it to the threadpool's job queue.
* If you want to add to work a function with more than one arguments then
* a way to implement this is by passing a pointer to a structure.
*
* NOTICE: You have to cast both the function and argument to not get warnings.
*
* @example
*
* void print_num(int num){
* printf("%d\n", num);
* }
*
* int main() {
* ..
* int a = 10;
* thpool_add_work(thpool, (void*)print_num, (void*)a);
* ..
* }
*
* @param threadpool threadpool to which the work will be added
* @param function_p pointer to function to add as work
* @param arg_p pointer to an argument
* @return 0 on success, -1 otherwise.
*/
int thpool_add_work(threadpool, void (*function_p)(void*), void* arg_p);
/**
* @brief Wait for all queued jobs to finish
*
* Will wait for all jobs - both queued and currently running to finish.
* Once the queue is empty and all work has completed, the calling thread
* (probably the main program) will continue.
*
* Smart polling is used in wait. The polling is initially 0 - meaning that
* there is virtually no polling at all. If after 1 seconds the threads
* haven't finished, the polling interval starts growing exponentially
* until it reaches max_secs seconds. Then it jumps down to a maximum polling
* interval assuming that heavy processing is being used in the threadpool.
*
* @example
*
* ..
* threadpool thpool = thpool_init(4);
* ..
* // Add a bunch of work
* ..
* thpool_wait(thpool);
* puts("All added work has finished");
* ..
*
* @param threadpool the threadpool to wait for
* @return nothing
*/
void thpool_wait(threadpool);
/**
* @brief Pauses all threads immediately
*
* The threads will be paused no matter if they are idle or working.
* The threads return to their previous states once thpool_resume
* is called.
*
* While the thread is being paused, new work can be added.
*
* @example
*
* threadpool thpool = thpool_init(4);
* thpool_pause(thpool);
* ..
* // Add a bunch of work
* ..
* thpool_resume(thpool); // Let the threads start their magic
*
* @param threadpool the threadpool where the threads should be paused
* @return nothing
*/
void thpool_pause(threadpool);
/**
* @brief Unpauses all threads if they are paused
*
* @example
* ..
* thpool_pause(thpool);
* sleep(10); // Delay execution 10 seconds
* thpool_resume(thpool);
* ..
*
* @param threadpool the threadpool where the threads should be unpaused
* @return nothing
*/
void thpool_resume(threadpool);
/**
* @brief Destroy the threadpool
*
* This will wait for the currently active threads to finish and then 'kill'
* the whole threadpool to free up memory.
*
* @example
* int main() {
* threadpool thpool1 = thpool_init(2);
* threadpool thpool2 = thpool_init(2);
* ..
* thpool_destroy(thpool1);
* ..
* return 0;
* }
*
* @param threadpool the threadpool to destroy
* @return nothing
*/
void thpool_destroy(threadpool);
/**
* @brief Show currently working threads
*
* Working threads are the threads that are performing work (not idle).
*
* @example
* int main() {
* threadpool thpool1 = thpool_init(2);
* threadpool thpool2 = thpool_init(2);
* ..
* printf("Working threads: %d\n", thpool_num_threads_working(thpool1));
* ..
* return 0;
* }
*
* @param threadpool the threadpool of interest
* @return integer number of threads working
*/
int thpool_num_threads_working(threadpool);
threadpool* initThreadPool(int);
#ifdef __cplusplus
}
#endif
#endif

16
server-exp4/urls.txt Normal file
View File

@@ -0,0 +1,16 @@
http://localhost:8088/index.html
http://localhost:8088/cgi-bin/add?a=1&b=2
http://localhost:8088/cgi-bin/add?a=10&b=20
http://localhost:8088/cgi-bin/add?a=100&b=200
http://localhost:8088/cgi-bin/add?a=5&b=15
http://localhost:8088/index.html
http://localhost:8088/cgi-bin/add?a=7&b=8
http://localhost:8088/cgi-bin/add?a=99&b=1
http://localhost:8088/index.html
http://localhost:8088/cgi-bin/add?a=50&b=50
http://localhost:8088/index.html
http://localhost:8088/cgi-bin/add?a=33&b=67
http://localhost:8088/index.html
http://localhost:8088/cgi-bin/add?a=6&b=14
http://localhost:8088/index.html
http://localhost:8088/cgi-bin/add?a=42&b=58

BIN
server-exp4/webserver Executable file

Binary file not shown.

1149
server-exp4/webserver.c Normal file

File diff suppressed because it is too large Load Diff

1200
server-exp4/webserver.log Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.