引言
在深入学习Linux环境编程和网络协议时,动手实现一个简易的Web服务器是极好的实践。本文将以一个C语言项目为例,带你逐步剖析一个支持用户注册、登录、注销以及手机品牌搜索的HTTP服务器。我们将从网络编程基础讲起,深入到HTTP协议解析、数据库集成、前端交互,并讨论其中的安全漏洞。
项目完整代码已提供,包含:main.c(服务器核心)、head.h(头文件)、以及多个HTML页面(index.html、search.html、apple.html、huawei.html、xiaomi.html)。所有代码均可在Linux环境下编译运行。
演示视频:
在线商城
一、项目总体架构
1.1 功能需求
- 用户认证:支持新用户注册、已有用户登录、账号注销。用户信息持久化存储。
- 品牌搜索:登录成功后进入搜索页面,用户输入手机品牌名(如"apple"、"huawei"),服务器根据品牌名重定向到对应的品牌机型总览页面。
- 机型展示:每个品牌页面以卡片形式展示该品牌的热门机型,点击卡片跳转到模拟的详情页(重定向到不存在的页面,仅演示路由)。
1.2 技术选型
- 编程语言:C语言,确保高性能和底层控制。
- 网络I/O:Socket + epoll(边缘触发模式),支持高并发连接。
- 数据库:SQLite3,轻量级嵌入式数据库,无需独立服务进程。
- HTTP协议:手动解析HTTP/1.0/1.1请求,构造响应报文。
- 前端:原生HTML/CSS/JavaScript,无任何框架,便于理解前后端交互。
1.3 整体流程
浏览器 → HTTP请求 → 服务器监听socket → epoll通知 → 接收数据 → 解析请求
↓
根据方法和URL分发:
- GET / → 返回 index.html(登录页)
- GET /xxx.html → 返回静态文件
- GET /detail?brand=xx&page=1 → 302重定向到模拟详情页
- POST /login → 验证用户,重定向(成功→search.html,失败→index.html?loginErr=1)
- POST /register → 插入用户,重定向到index.html?form=register(成功/失败标志)
- POST /logout → 删除用户,重定向到index.html(成功/失败标志)
- POST /search → 解析品牌名,重定向到对应品牌页面(如/huawei.html)
二、环境准备与依赖
- 操作系统:Linux(Ubuntu 20.04+ 或 CentOS 7+)
- 编译器:GCC
- 数据库:libsqlite3-dev
- 头文件:标准C库、系统网络库,以及与epoll有关的C库
安装依赖:
sudo apt-get install gcc libsqlite3-dev # Ubuntu
sudo yum install gcc sqlite-devel # CentOS
编译:
gcc main.c -o server -lsqlite3 -lpthread
三、核心模块详解
3.1 网络模块:基于epoll的并发模型
3.1.1 创建监听Socket
CreateListenSocket函数封装了socket、bind、listen的标准步骤:
int CreateListenSocket(char *pip, int port) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 设置地址复用(可选)
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(port);
seraddr.sin_addr.s_addr = inet_addr(pip);
bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
listen(sockfd, 10); // 监听队列长度10
return sockfd;
}
3.1.2 epoll事件循环
epoll是Linux下高效的I/O多路复用机制。代码中采用水平触发 (默认)模式,注册EPOLLIN事件。
int epollfd = epoll_create(1024);
struct epoll_event ev, events[1024];
ev.data.fd = sockfd;
ev.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);
while (1) {
int nready = epoll_wait(epollfd, events, 1024, -1);
for (int i = 0; i < nready; i++) {
if (events[i].data.fd == sockfd) {
// 新连接
int confd = accept(sockfd, NULL, NULL);
ev.data.fd = confd;
ev.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &ev);
} else {
// 处理已连接socket的请求
handle_client(events[i].data.fd);
epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
close(events[i].data.fd);
}
}
}
注意 :每次处理完一个客户端请求后立即关闭连接,并未真正实现HTTP keep-alive,因为响应头中的
Connection: keep-alive并未被利用。
3.2 HTTP请求解析模块
3.2.1 接收数据
RecvHttpRequest简单调用recv,未处理粘包和分包,假设一次recv能收到完整请求。这对于小型演示足够,但生产环境需循环接收直到遇到\r\n\r\n。
3.2.2 解析请求行和参数
PraseHttpRequest使用strtok分割字符串,直接修改原始缓冲区(破坏性解析)。流程如下:
- 定位请求体开始位置(
\r\n\r\n之后),保存到pconcent。 - 用
strtok(precvbuff, " ")获取method。 - 用
strtok(NULL, " ")获取url。 - 对url进行进一步解析:
strtok(url, "?")分离路径和查询字符串。 - 若存在查询字符串,继续
strtok(NULL, "=")获取品牌参数,再用strtok(NULL, "&")获取品牌值,最后strtok(NULL, "=")获取页码(注意:代码中+5操作有硬编码风险,后面分析)。
例如请求行:GET /detail?brand=huawei&page=1 HTTP/1.1,解析后:
method= "GET"url= "/detail?brand=huawei&page=1"url_min= "/detail"band_detail= "huawei"page_detail= "1"
问题 :
strtok会修改原字符串,且多线程不安全。代码中假设查询字符串格式固定(?brand=xxx&page=1),若用户构造其他参数将导致解析错误甚至崩溃。
3.3 业务处理模块
3.3.1 GET静态文件服务
对于普通GET请求(非/detail),服务器构造200 OK响应头,然后打开对应文件并发送内容。
sprintf(tmpbuff, "HTTP/1.1 200 ok\r\n");
sprintf(tmpbuff, "%sConnection: keep-alive\r\n", tmpbuff);
sprintf(tmpbuff, "%s\r\n", tmpbuff);
send(confd, tmpbuff, strlen(tmpbuff), 0);
int fd = open(resourcename, O_RDONLY);
while ((n = read(fd, tmpbuff, sizeof(tmpbuff))) > 0) {
send(confd, tmpbuff, n, 0);
}
close(fd);
未处理文件不存在的情况,若open失败,服务器将不会发送任何响应,客户端会一直等待。改进应返回404状态码。
3.3.2 POST请求处理
根据URL分发到不同处理函数:
- /login :从POST正文解析账号密码,调用
JudgeUserInfo验证。若成功,重定向到search.html;失败则重定向到index.html?form=login&loginErr=1。 - /register :调用
AddUserInfo插入新用户,成功重定向到index.html?form=register,失败加®Err=1。 - /logout :调用
DeleUserinfo删除用户,成功重定向到index.html,失败加&logoutErr=1。 - /search :解析品牌名,重定向到
./{brand}.html。
所有POST处理均返回302状态码,通过Location头指示浏览器跳转。
3.3.3 /detail 重定向
这是一个模拟的详情页路由,实际并不存在对应的HTML文件,仅演示URL重写。服务器解析出brand和page后,构造Location: ./{brand}/{brand}_detail_{page}.html,客户端会尝试访问该路径,但会得到404(因为未实现)。可以扩展为从数据库读取机型详情动态生成页面。
3.4 数据库模块
3.4.1 表结构
数据库文件为userinfo.db,表info定义:
CREATE TABLE IF NOT EXISTS info (
用户名 TEXT PRIMARY KEY,
密码 TEXT
);
3.4.2 查询回调函数
sqlite3_exec执行查询时,每找到一条记录都会调用回调函数。在JudgeUserInfo中,回调比较密码并设置flag:
int callback(void *arg, int column, char **pcontent, char **pheaders) {
user_t *pperson = arg;
if (strcmp(pcontent[0], pperson->password) == 0) {
pperson->flag = 1;
}
return 0;
}
3.4.3 安全问题:SQL注入
所有SQL语句均使用sprintf拼接字符串,例如:
sprintf(command, "select 密码 from info where 用户名 = \"%s\";", pperson->name);
如果用户输入的用户名包含双引号,例如admin" --,将导致SQL注入,绕过密码验证。应该使用sqlite3_prepare_v2和参数化查询。
3.4.4 密码明文存储
代码中密码以明文形式存储,存在严重安全风险。实际应用必须使用哈希算法(如bcrypt、scrypt)存储密码哈希值,并在验证时重新计算对比。
3.5 前端页面交互
3.5.1 统一登录页 index.html
该页面通过CSS类切换显示登录、注册、注销表单。URL查询参数(如?form=login&loginErr=1)用于在页面加载时显示对应表单和错误提示。
关键JS逻辑:
toRegister等按钮切换active类。window.onload解析location.search,设置表单状态和错误信息。- 表单提交前进行非空校验,防止空账号密码提交。
3.5.2 搜索页 search.html
表单以POST方式提交到/search,input的name属性设为band,与服务器解析代码匹配。
<form action="/search" method="POST">
<input type="text" name="band" placeholder="请输入手机品牌名">
<button type="submit">搜索商品</button>
</form>
3.5.3 品牌展示页
每个品牌页面(如apple.html)包含四个机型卡片,点击卡片时执行:
<div class="phone-card" onclick="window.location.href='/detail?brand=apple&page=1'">
这是一个简单的GET跳转,触发服务器的/detail处理逻辑。
四、代码深度剖析
4.1 主函数流程
int main() {
sockfd = CreateListenSocket("192.168.0.130", 8080);
epollfd = epoll_create(1024);
epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);
while (1) {
nready = epoll_wait(epollfd, events, 1024, -1);
for (...) {
if (fd == sockfd) {
confd = accept(...);
epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &ev);
} else {
RecvHttpRequest(fd, recvbuff, sizeof(recvbuff));
PraseHttpRequest(recvbuff, &req);
SendHttpRespone(fd, &req);
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
}
}
}
这种串行处理方式意味着同时只能处理一个请求,因为处理过程中会阻塞在send和数据库操作上。虽然epoll可以同时监听多个fd,但处理仍是串行的,高并发下性能不佳。真正的并发需要配合多线程或异步I/O。
4.2 SendHttpRespone 函数详解
该函数约200行,负责所有响应的构造。我们将其拆解:
4.2.1 处理 /detail GET 请求
if (strcmp("GET", req->method) == 0) {
if (strncmp("/detail", req->url, 7) == 0) {
// 构造302响应
sprintf(tmpbuff, "HTTP/1.1 302 Found\r\n");
sprintf(tmpbuff, "%sLocation: ./%s/%s_detail_%s.html\r\n",
tmpbuff, req->band_detail, req->band_detail, req->page_detail);
sprintf(tmpbuff, "%sContent-Type: text/html; charset=utf-8\r\n", tmpbuff);
sprintf(tmpbuff, "%sConnection: keep-alive\r\n", tmpbuff);
sprintf(tmpbuff, "%s\r\n", tmpbuff);
send(confd, tmpbuff, strlen(tmpbuff), 0);
return 0;
}
// 其他GET...
}
这里直接使用从URL解析出的band_detail和page_detail构造路径,未做任何校验,若包含../可能导致路径穿越漏洞。
4.2.2 处理 POST /search
else if (strcmp("POST", req->method) == 0) {
if (strcmp("/search", req->url) == 0) {
char *band = strstr(req->pconcent, "=") + 1;
// 构造302跳转到 ./{band}.html
sprintf(tmpbuff, "HTTP/1.1 302 Found\r\n");
sprintf(tmpbuff, "%sLocation: ./%s.html\r\n", tmpbuff, band);
send(...);
return 0;
}
}
strstr(req->pconcent, "=") + 1直接提取等号后面的内容,假设POST数据格式为band=xxx,若用户提交band=apple&extra=1,则band会指向apple&extra=1,导致跳转到./apple&extra=1.html,这是错误的。应使用strtok正确分割。
4.2.3 处理登录/注册/注销
代码中多次出现类似片段:
if (strcmp("/login", req->url) == 0) {
JudgeUserInfo(&person);
if (person.flag == 1) {
sprintf(tmpbuff, "HTTP/1.1 302 Found\r\n");
sprintf(tmpbuff, "%sLocation: ./search.html\r\n", tmpbuff);
} else {
sprintf(tmpbuff, "%sLocation: ./index.html?form=login&loginErr=1\r\n", tmpbuff);
}
send(...);
return 0;
}
重定向到index.html并附带错误参数,前端JavaScript解析这些参数并显示错误信息,这是一种简单的前后端交互方式。
4.3 数据库操作的安全性分析
SQL注入演示
假设恶意用户在登录时输入用户名:"admin" or "1"="1" --,密码任意。构造的SQL为:
select 密码 from info where 用户名 = "admin" or "1"="1" --";
由于--将后续注释,条件永远为真,将返回表中第一个用户的密码,导致登录绕过。必须使用参数化查询防御。
密码明文存储
若数据库泄露,所有用户密码直接暴露。正确做法是存储bcrypt(密码),验证时计算输入密码的哈希并比较。
五、存在的问题与安全风险
5.1 功能缺陷
- 无会话管理 :登录成功后无Cookie/Session,后续请求无法识别用户身份,任何用户均可直接访问
search.html。 - 静态文件服务不完整:未实现MIME类型,未处理404、403等状态码。
- HTTP协议支持简陋:未解析请求头(如Content-Length),无法正确处理分块传输或长请求体。
- 并发处理能力弱:单线程串行处理,epoll仅用于监听,实际处理仍阻塞。
5.2 安全漏洞
- SQL注入:所有数据库操作均存在。
- 路径遍历 :
/detail重定向中直接拼接用户输入构造路径,攻击者可构造../../../etc/passwd尝试读取敏感文件。 - 明文密码:密码数据库泄露即灾难。
- XSS可能:未对输出进行编码,但本项目未动态生成HTML,风险较低。
- CSRF:无防跨站请求伪造措施,但功能简单影响不大。
5.3 代码健壮性
- 缓冲区溢出风险:
recv使用固定大小4096,若请求大于此值会截断,可能导致解析错误。 - 指针操作不安全:
strtok破坏性解析,多次调用可能导致混乱。 - 错误处理不完善:文件打开失败无响应,客户端挂起。
六、总结
通过这个项目,亲手实现了一个包含网络通信、HTTP协议解析、数据库操作和前端交互的简易Web服务器。虽然最终的成果存在诸多安全漏洞和功能缺陷,或者玩笑话的讲又花了点时间做了个玩具,但正是这些不足让我更深刻地理解了生产级Web服务器需要关注的诸多问题,以及认识到了未来的路还有很长。
从中学到的核心知识点:
- Socket编程与epoll事件驱动模型
- HTTP报文结构及手动解析方法
- SQLite3嵌入式数据库的基本使用
- 前后端分离的简单重定向交互
建议读者在此基础上,逐步完善,打造一个更安全、更完整的HTTP服务器。实践是掌握技术的最佳途径,希望这篇文章能激发你的动手热情,在代码的世界里不断探索。
附录:需自己建一个images的文件夹,添加一些图片,然后将图片的相对路径添加进HTML文件中
main.c
#include "head.h"
typedef struct httprequest
{
char *method;
char *url;
char *pconcent;
char *url_min;
char *band_detail;
char *page_detail;
}httprequest_t;
typedef struct userinfo
{
char *name;
char *password;
int flag;
}user_t;
int callback(void *arg, int column, char **pconent, char **pheaders)
{
user_t *pperson;
pperson = arg;
if(0 == strcmp(pconent[0], pperson->password))
{
pperson->flag = 1;
}
return 0;
}
int JudgeUserInfo(user_t *pperson)
{
int i = 0;
int ret = 0;
char command[1024] = {0};
sqlite3 *pdb = NULL;
char *perrmsg = NULL;
pperson->flag = 0;
ret = sqlite3_open("userinfo.db", &pdb);
if(SQLITE_OK != ret)
{
fprintf(stderr, "fail to sqlite3_open:%s", sqlite3_errmsg(pdb));
return -1;
}
sprintf(command,"create table if not exists info (用户名 text primary key, 密码 text);");
ret = sqlite3_exec(pdb, command, NULL, NULL, &perrmsg);
if(SQLITE_OK != ret)
{
fprintf(stderr, "fail to sqlite3_exec:%s", perrmsg);
sqlite3_free(perrmsg);
sqlite3_close(pdb);
return -1;
}
sprintf(command,"select 密码 from info where 用户名 = \"%s\";", pperson->name);
ret = sqlite3_exec(pdb, command, callback, pperson, &perrmsg);
if(SQLITE_OK != ret)
{
fprintf(stderr, "fail to sqlite3_exec:%s", perrmsg);
sqlite3_free(perrmsg);
sqlite3_close(pdb);
return -1;
}
sqlite3_close(pdb);
return 0;
}
int AddUserInfo(user_t *pperson)
{
int i = 0;
int ret = 0;
char command[1024] = {0};
sqlite3 *pdb = NULL;
char *perrmsg = NULL;
ret = sqlite3_open("userinfo.db", &pdb);
if(SQLITE_OK != ret)
{
fprintf(stderr, "fail to sqlite3_open:%s", sqlite3_errmsg(pdb));
return 0;
}
sprintf(command,"create table if not exists info (用户名 text primary key, 密码 text);");
ret = sqlite3_exec(pdb, command, NULL, NULL, &perrmsg);
if(SQLITE_OK != ret)
{
fprintf(stderr, "fail to sqlite3_exec:%s", perrmsg);
sqlite3_free(perrmsg);
sqlite3_close(pdb);
return 0;
}
sprintf(command,"insert into info values (\"%s\", \"%s\");", pperson->name, pperson->password);
ret = sqlite3_exec(pdb, command, NULL, NULL, &perrmsg);
if(SQLITE_OK != ret)
{
fprintf(stderr, "fail to sqlite3_exec:%s", perrmsg);
sqlite3_free(perrmsg);
sqlite3_close(pdb);
return 0;
}
sqlite3_close(pdb);
return 1;
}
int DeleUserinfo(user_t *pperson)
{
int i = 0;
int ret = 0;
char command[1024] = {0};
sqlite3 *pdb = NULL;
char *perrmsg = NULL;
JudgeUserInfo(pperson);
if(1 == pperson->flag)
{
ret = sqlite3_open("userinfo.db", &pdb);
if(SQLITE_OK != ret)
{
fprintf(stderr, "fail to sqlite3_open:%s", sqlite3_errmsg(pdb));
return 0;
}
sprintf(command,"create table if not exists info (用户名 text primary key, 密码 text);");
ret = sqlite3_exec(pdb, command, NULL, NULL, &perrmsg);
if(SQLITE_OK != ret)
{
fprintf(stderr, "fail to sqlite3_exec:%s", perrmsg);
sqlite3_free(perrmsg);
sqlite3_close(pdb);
return 0;
}
sprintf(command,"delete from info where 用户名 = \"%s\";", pperson->name);
ret = sqlite3_exec(pdb, command, NULL, NULL, &perrmsg);
if(SQLITE_OK != ret)
{
fprintf(stderr, "fail to sqlite3_exec:%s", perrmsg);
sqlite3_free(perrmsg);
sqlite3_close(pdb);
return 0;
}
sqlite3_close(pdb);
return 1;
}
return 0;
}
int CreateListenSocket(char *pip, int port)
{
int sockfd = 0;
int ret = 0;
struct sockaddr_in seraddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(port);
seraddr.sin_addr.s_addr = inet_addr(pip);
ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(-1 == ret)
{
perror("fail to bind");
return -1;
}
ret = listen(sockfd, 10);
if(-1 == ret)
{
perror("fail to listen");
return -1;
}
return sockfd;
}
int RecvHttpRequest(int confd, char *precvbuff, int maxlen)
{
ssize_t nret = 0;
nret = recv(confd, precvbuff, maxlen, 0);
if(-1 == nret)
{
perror("fail to recv");
return -1;
}
return 0;
}
int PraseHttpRequest(char *precvbuff, httprequest_t *ptmphttprequest)
{
ptmphttprequest->pconcent = strstr(precvbuff, "\r\n\r\n");
ptmphttprequest->pconcent += 4;
ptmphttprequest->method = strtok(precvbuff, " ");
ptmphttprequest->url = strtok(NULL, " ");
ptmphttprequest->url_min = strtok(ptmphttprequest->url, "?");
strtok(NULL, "=");
ptmphttprequest->band_detail = strtok(NULL, "&");
ptmphttprequest->page_detail = strtok(NULL, "=") + 5;
return 0;
}
int SendHttpRespone(int confd, httprequest_t *ptmphttprequest)
{
ssize_t nret = 0;
int fd = 0;
int is_redirect = 0;
char tmpbuff[4096] = {0};
char resourcename[4096] = {0};
user_t person;
int error_flag = 0;
char *band = NULL;
char *band_detail = NULL;
char *page_detail = NULL;
// GET /detail?brand=huawei&page=1 HTTP/1.1
// Host: 192.168.0.130:8080
// User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0
// Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
// Accept-Language: en-US,en;q=0.5
// Accept-Encoding: gzip, deflate
// Connection: keep-alive
// Referer: http://192.168.0.130:8080/huawei.html
// Upgrade-Insecure-Requests: 1
if(0 == strcmp("GET", ptmphttprequest->method))
{
if(0 == strncmp("/detail", ptmphttprequest->url, 7))
{
memset(tmpbuff, 0, sizeof(tmpbuff));
sprintf(tmpbuff, "HTTP/1.1 302 Found\r\n");
sprintf(tmpbuff, "%sLocation: ./%s/%s_detail_%s.html\r\n", tmpbuff, ptmphttprequest->band_detail, ptmphttprequest->band_detail, ptmphttprequest->page_detail);
sprintf(tmpbuff, "%sContent-Type: text/html; charset=utf-8\r\n",tmpbuff);
sprintf(tmpbuff, "%sConnection: keep-alive\r\n",tmpbuff);
sprintf(tmpbuff, "%s\r\n",tmpbuff);
nret = send(confd, tmpbuff, strlen(tmpbuff), 0);
if(-1 == nret)
{
perror("fail to send");
return -1;
}
return 0;
}
if(0 == strcmp("/", ptmphttprequest->url))
{
memset(resourcename, 0, sizeof(resourcename));
sprintf(resourcename, "./index.html");
}
else
{
memset(resourcename, 0, sizeof(resourcename));
sprintf(resourcename, "./%s", ptmphttprequest->url);
}
printf("url:%s\n", ptmphttprequest->url);
printf("------------------------------------------------------\n");
}
else if(0 == strcmp("POST", ptmphttprequest->method))
{
if(0 == strcmp("/login", ptmphttprequest->url) || 0 == strcmp("/logout", ptmphttprequest->url) || 0 == strcmp("/register", ptmphttprequest->url))
{
memset(&person, 0, sizeof(person));
strtok(ptmphttprequest->pconcent, "=");
person.name = strtok(NULL, "&");
strtok(NULL, "=");
person.password = strtok(NULL, "\r");
person.flag = 0;
}
else if(0 == strcmp("/search", ptmphttprequest->url))
{
band = strstr(ptmphttprequest->pconcent, "=") + 1;
printf("band:%s\n", band);
memset(tmpbuff, 0, sizeof(tmpbuff));
sprintf(tmpbuff, "HTTP/1.1 302 Found\r\n");
sprintf(tmpbuff, "%sLocation: ./%s.html\r\n",tmpbuff, band);
sprintf(tmpbuff, "%sContent-Type: text/html; charset=utf-8\r\n",tmpbuff);
sprintf(tmpbuff, "%sConnection: keep-alive\r\n",tmpbuff);
sprintf(tmpbuff, "%s\r\n",tmpbuff);
nret = send(confd, tmpbuff, strlen(tmpbuff), 0);
if(-1 == nret)
{
perror("fail to send");
return -1;
}
return 0;
}
if(0 == strcmp("/login", ptmphttprequest->url))
{
JudgeUserInfo(&person);
if(1 == person.flag)
{
memset(resourcename, 0, sizeof(resourcename));
sprintf(resourcename, "./%s", "./index.html");
memset(tmpbuff, 0, sizeof(tmpbuff));
sprintf(tmpbuff, "HTTP/1.1 302 Found\r\n");
sprintf(tmpbuff, "%sLocation: ./search.html\r\n",tmpbuff);
sprintf(tmpbuff, "%sContent-Type: text/html; charset=utf-8\r\n",tmpbuff);
sprintf(tmpbuff, "%sConnection: keep-alive\r\n",tmpbuff);
sprintf(tmpbuff, "%s\r\n",tmpbuff);
}
else
{
memset(resourcename, 0, sizeof(resourcename));
sprintf(resourcename, "./%s", "./index.html");
memset(tmpbuff, 0, sizeof(tmpbuff));
sprintf(tmpbuff, "HTTP/1.1 302 Found\r\n");
sprintf(tmpbuff, "%sLocation: ./index.html?form=login&loginErr=1\r\n",tmpbuff);
sprintf(tmpbuff, "%sContent-Type: text/html; charset=utf-8\r\n",tmpbuff);
sprintf(tmpbuff, "%sConnection: keep-alive\r\n",tmpbuff);
sprintf(tmpbuff, "%s\r\n",tmpbuff);
}
nret = send(confd, tmpbuff, strlen(tmpbuff), 0);
if(-1 == nret)
{
perror("fail to send");
return -1;
}
return 0;
}
else if(0 == strcmp("/logout", ptmphttprequest->url))
{
if(DeleUserinfo(&person))
{
memset(resourcename, 0, sizeof(resourcename));
sprintf(resourcename, "./%s", "./index.html");
memset(tmpbuff, 0, sizeof(tmpbuff));
sprintf(tmpbuff, "HTTP/1.1 302 Found\r\n");
sprintf(tmpbuff, "%sLocation: ./index.html\r\n",tmpbuff);
sprintf(tmpbuff, "%sContent-Type: text/html; charset=utf-8\r\n",tmpbuff);
sprintf(tmpbuff, "%sConnection: keep-alive\r\n",tmpbuff);
sprintf(tmpbuff, "%s\r\n",tmpbuff);
}
else
{
memset(resourcename, 0, sizeof(resourcename));
sprintf(resourcename, "./%s", "./index.html");
memset(tmpbuff, 0, sizeof(tmpbuff));
sprintf(tmpbuff, "HTTP/1.1 302 Found\r\n");
sprintf(tmpbuff, "%sLocation: ./index.html?form=logout&logoutErr=1\r\n",tmpbuff);
sprintf(tmpbuff, "%sContent-Type: text/html; charset=utf-8\r\n",tmpbuff);
sprintf(tmpbuff, "%sConnection: keep-alive\r\n",tmpbuff);
sprintf(tmpbuff, "%s\r\n",tmpbuff);
}
nret = send(confd, tmpbuff, strlen(tmpbuff), 0);
if(-1 == nret)
{
perror("fail to send");
return -1;
}
return 0;
}
else if(0 == strcmp("/register", ptmphttprequest->url))
{
if(AddUserInfo(&person))
{
memset(resourcename, 0, sizeof(resourcename));
sprintf(resourcename, "./%s", "./index.html");
memset(tmpbuff, 0, sizeof(tmpbuff));
sprintf(tmpbuff, "HTTP/1.1 302 Found\r\n");
sprintf(tmpbuff, "%sLocation: ./index.html?form=register\r\n",tmpbuff);
sprintf(tmpbuff, "%sContent-Type: text/html; charset=utf-8\r\n",tmpbuff);
sprintf(tmpbuff, "%sConnection: keep-alive\r\n",tmpbuff);
sprintf(tmpbuff, "%s\r\n",tmpbuff);
}
else
{
memset(resourcename, 0, sizeof(resourcename));
sprintf(resourcename, "./%s", "./index.html");
memset(tmpbuff, 0, sizeof(tmpbuff));
sprintf(tmpbuff, "HTTP/1.1 302 Found\r\n");
sprintf(tmpbuff, "%sLocation: ./index.html?form=register®Err=1\r\n",tmpbuff);
sprintf(tmpbuff, "%sContent-Type: text/html; charset=utf-8\r\n",tmpbuff);
sprintf(tmpbuff, "%sConnection: keep-alive\r\n",tmpbuff);
sprintf(tmpbuff, "%s\r\n",tmpbuff);
}
nret = send(confd, tmpbuff, strlen(tmpbuff), 0);
if(-1 == nret)
{
perror("fail to send");
return -1;
}
return 0;
}
}
memset(tmpbuff, 0, sizeof(tmpbuff));
sprintf(tmpbuff, "HTTP/1.1 200 ok\r\n");
sprintf(tmpbuff, "%sConnection: keep-alive\r\n", tmpbuff);
sprintf(tmpbuff, "%s\r\n", tmpbuff);
nret = send(confd, tmpbuff, strlen(tmpbuff), 0);
if(-1 == nret)
{
perror("fail to send");
return -1;
}
fd = open(resourcename, O_RDONLY);
if(-1 == fd)
{
perror("fail to open");
return -1;
}
while(1)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = read(fd, tmpbuff, sizeof(tmpbuff));
if(nret <= 0)
{
break;
}
nret = send(confd, tmpbuff, nret, 0);
if(-1 == nret)
{
perror("fail to send");
return -1;
}
}
nret = send(confd, "\r\n\r\n", 4, 0);
if(-1 == nret)
{
perror("fail to send");
return -1;
}
printf("============================================================发送成功!\n");
close(fd);
return 0;
}
int main(void)
{
int sockfd = 0;
int confd = 0;
int epollfd = 0;
int nready = 0;
char recvbuff[4096] = {0};
char sendbuff[4096] = {0};
struct epoll_event env;
struct epoll_event reevn[1024];
int ret = 0;
httprequest_t tmphttprequest;
sockfd = CreateListenSocket("192.168.0.130", 8080);
epollfd = epoll_create(1024);
if(-1 == epollfd)
{
perror("fail to epoll_create");
return-1;
}
env.data.fd = sockfd;
env.events = EPOLLIN;
ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &env);
if(-1 == ret)
{
perror("fail to epoll_ctl");
return -1;
}
while(1)
{
nready = epoll_wait(epollfd, reevn, 1024, -1);
if(-1 == nready)
{
perror("fail to epoll_wait");
return -1;
}
for(int i = 0; i < nready; i++)
{
if(reevn[i].data.fd == sockfd)
{
confd = accept(sockfd, NULL, NULL);
if(-1 == confd)
{
perror("fail to accept");
epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, reevn);
close(sockfd);
continue;
}
env.data.fd = confd;
env.events = EPOLLIN;
ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &env);
if(-1 == ret)
{
perror("fail to epoll_ctl");
epoll_ctl(epollfd, EPOLL_CTL_DEL, confd, reevn);
close(confd);
return -1;
}
}
else
{
memset(recvbuff, 0, sizeof(recvbuff));
memset(&tmphttprequest, 0, sizeof(tmphttprequest));
RecvHttpRequest(reevn[i].data.fd, recvbuff, 4096);
printf("------------------------- RECV ---------------------------\n");
printf("%s\n", recvbuff);
PraseHttpRequest(recvbuff, &tmphttprequest);
SendHttpRespone(reevn[i].data.fd, &tmphttprequest);
epoll_ctl(epollfd, EPOLL_CTL_DEL, reevn[i].data.fd, reevn);
close(reevn[i].data.fd);
}
}
}
close(sockfd);
return 0;
}
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商城登录</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Inter", "Microsoft Yahei", sans-serif;
}
body {
background: linear-gradient(135deg, #e0f7fa 0%, #f3e5f5 100%);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.container {
width: 400px;
background: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
padding: 50px 40px;
position: relative;
overflow: hidden;
}
.container::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 5px;
background: linear-gradient(90deg, #00bcd4, #9c27b0);
}
.title {
text-align: center;
font-size: 28px;
font-weight: 600;
color: #2d3748;
margin: 0 0 35px 0;
letter-spacing: 0.5px;
}
.form-box {
display:none;
}
.form-box.active {
display:block;
}
.input-group {
margin-bottom:20px;
}
.input-group label {
display:block;
margin-bottom:8px;
color: #4a5568;
font-size:14px;
}
.input-wrap {
position:relative;
}
.input-wrap input {
width:100%;
height:48px;
border:none;
outline:none;
padding:0 20px;
box-sizing:border-box;
font-size:16px;
border: 1px solid #e2e8f0;
border-radius: 10px;
background: #f8fafc;
}
.input-wrap input:focus {
border-color: #00bcd4;
box-shadow: 0 0 0 3px rgba(0, 188, 212, 0.1);
background: white;
}
.input-wrap input:focus + .input-line {
width:100%;
}
.input-line {
position:absolute;
bottom:0;
left:0;
width:0;
height:2px;
background:#00bcd4;
transition:width 0.3s;
border-radius: 1px;
}
.btn {
width:100%;
height:48px;
border:none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
color: white;
cursor: pointer;
letter-spacing: 0.5px;
transition: all 0.2s ease;
background: linear-gradient(90deg, #00bcd4, #9c27b0);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 188, 212, 0.2);
}
.btn:active {
transform: translateY(0);
}
.btn-danger {
background: #fee2e2;
color: #b91c1c;
}
.btn-danger:hover {
background: #fecaca;
box-shadow: 0 5px 15px rgba(185, 28, 28, 0.1);
}
.switch {
text-align:center;
margin-top:25px;
font-size:14px;
color:#4a5568;
}
.switch a {
color: #9c27b0;
text-decoration:none;
font-weight: 500;
}
.switch a:hover {
color: #801f96;
}
.success-box {
display:none;
text-align:center;
padding:20px 0;
}
.success-box.active {
display:block;
}
.success-icon {
font-size:60px;
color: #0f766e;
margin-bottom:20px;
}
.success-tip {
font-size:18px;
color:#2d3748;
margin-bottom:30px;
}
.reg-error {
color: #b91c1c;
font-size: 13px;
margin-top: 8px;
display: block;
padding: 8px 12px;
border-radius: 8px;
background: #fee2e2;
}
</style>
</head>
<body>
<div class="container">
<h2 class="title">商城登录</h2>
<div class="form-box active" id="loginForm">
<form action="/login" method="POST">
<div class="input-group">
<label for="loginAccount">账号</label>
<div class="input-wrap">
<input type="text" name="account" id="loginAccount" placeholder="请输入账号">
<div class="input-line"></div>
</div>
</div>
<div class="input-group">
<label for="loginPwd">密码</label>
<div class="input-wrap">
<input type="password" name="pwd" id="loginPwd" placeholder="请输入密码">
<div class="input-line"></div>
</div>
<span id="loginErrTip"></span>
</div>
<button type="submit" class="btn" id="loginBtn">登录</button>
</form>
<div class="switch">
还没有账号?<a href="javascript:;" id="toRegister">立即注册</a> |
<a href="javascript:;" id="toLogout">账号注销</a>
</div>
</div>
<div class="form-box" id="registerForm">
<form action="/register" method="POST" id="regForm">
<div class="input-group">
<label for="regAccount">账号</label>
<div class="input-wrap">
<input type="text" name="account" id="regAccount" placeholder="请设置账号">
<div class="input-line"></div>
</div>
<span id="regErrTip"></span>
</div>
<div class="input-group">
<label for="regPwd">密码</label>
<div class="input-wrap">
<input type="password" name="pwd" id="regPwd" placeholder="请设置密码">
<div class="input-line"></div>
</div>
</div>
<button type="submit" class="btn" id="registerBtn">注册</button>
</form>
<div class="switch">
已有账号?<a href="javascript:;" id="toLogin">立即登录</a> |
<a href="javascript:;" id="toLogoutFromReg">账号注销</a>
</div>
</div>
<div class="success-box" id="successBox">
<div class="success-icon">✓</div>
<div class="success-tip" id="successTip">登录成功!</div>
<button class="btn btn-danger" id="logoutBtn">注销</button>
</div>
<div class="form-box" id="logoutForm">
<form action="/logout" method="POST" id="logoutFormEle">
<div class="input-group">
<label for="logoutAccount">注销账号</label>
<div class="input-wrap">
<input type="text" name="account" id="logoutAccount" placeholder="请输入需要注销的账号">
<div class="input-line"></div>
</div>
<span id="logoutErrTip"></span>
</div>
<div class="input-group">
<label for="logoutPwd">注销密码</label>
<div class="input-wrap">
<input type="password" name="pwd" id="logoutPwd" placeholder="请输入账号密码">
<div class="input-line"></div>
</div>
</div>
<button type="submit" class="btn btn-danger" id="doLogoutBtn">确认注销</button>
</form>
<div class="switch">
返回登录?<a href="javascript:;" id="backToLogin">立即登录</a>
</div>
</div>
</div>
<script>
const loginForm = document.getElementById('loginForm');
const registerForm = document.getElementById('registerForm');
const toRegister = document.getElementById('toRegister');
const toLogin = document.getElementById('toLogin');
const logoutBtn = document.getElementById('logoutBtn');
const regErrTip = document.getElementById('regErrTip');
const regForm = document.getElementById('regForm');
const loginErrTip = document.getElementById('loginErrTip');
const logoutForm = document.getElementById('logoutForm');
const logoutFormEle = document.getElementById('logoutFormEle');
const logoutErrTip = document.getElementById('logoutErrTip');
const backToLogin = document.getElementById('backToLogin');
const toLogout = document.getElementById('toLogout');
const toLogoutFromReg = document.getElementById('toLogoutFromReg');
const logoutAccount = document.getElementById('logoutAccount');
const logoutPwd = document.getElementById('logoutPwd');
toRegister.onclick = () => {
loginForm.classList.remove('active');
registerForm.classList.add('active');
document.querySelector('.title').innerText = '商城注册';
regErrTip.innerText = '';
};
toLogin.onclick = () => {
registerForm.classList.remove('active');
loginForm.classList.add('active');
document.querySelector('.title').innerText = '商城登录';
};
toLogout.onclick = () => {
loginForm.classList.remove('active');
logoutForm.classList.add('active');
document.querySelector('.title').innerText = '账号注销';
logoutErrTip.innerText = '';
};
toLogoutFromReg.onclick = () => {
registerForm.classList.remove('active');
logoutForm.classList.add('active');
document.querySelector('.title').innerText = '账号注销';
logoutErrTip.innerText = '';
};
logoutBtn.onclick = async () => {
const account = logoutAccount ? logoutAccount.value.trim() : '';
const pwd = logoutPwd ? logoutPwd.value.trim() : '';
if (!account || !pwd) {
alert('请输入注销的账号和密码!');
return;
}
const requestBody = `account=${encodeURIComponent(account)}&pwd=${encodeURIComponent(pwd)}`;
try {
const response = await fetch('/logout', {method: 'POST', body: requestBody});
const redirectUrl = response.headers.get('Location');
if (redirectUrl && redirectUrl.includes('logoutErr=1')) {
alert('注销失败!请检查账号密码');
} else {
alert('注销成功');
window.location.href = 'index.html';
}
} catch (err) {
alert('注销失败!网络异常');
}
};
backToLogin.onclick = () => {
logoutForm.classList.remove('active');
loginForm.classList.add('active');
document.querySelector('.title').innerText = '商城登录';
};
// 仅修改这部分:恢复表单默认提交逻辑,和登录/注册保持一致
logoutFormEle.onsubmit = function(e) {
const account = logoutAccount.value.trim();
const pwd = logoutPwd.value.trim();
if (!account || !pwd) {
e.preventDefault();
logoutErrTip.innerText = '账号和密码不能为空!';
logoutErrTip.className = 'reg-error';
return false;
}
// 清空错误提示,让表单默认提交(后端重定向处理)
logoutErrTip.innerText = '';
return true;
};
regForm.onsubmit = function(e) {
const account = document.getElementById('regAccount').value.trim();
const pwd = document.getElementById('regPwd').value.trim();
if (!account || !pwd) {
e.preventDefault();
regErrTip.innerText = '账号和密码不能为空!';
regErrTip.className = 'reg-error';
return false;
}
regErrTip.innerText = '';
return true;
};
document.querySelector('#loginForm form').onsubmit = function(e) {
const account = document.getElementById('loginAccount').value.trim();
const pwd = document.getElementById('loginPwd').value.trim();
if (!account || !pwd) {
e.preventDefault();
loginErrTip.innerText = '账号和密码不能为空!';
loginErrTip.className = 'reg-error';
return false;
}
loginErrTip.innerText = '';
return true;
};
window.onload = function() {
const urlParams = new URLSearchParams(window.location.search);
const targetForm = urlParams.get('form');
const isRegFail = urlParams.get('regErr');
const isLoginFail = urlParams.get('loginErr');
const isLogoutFail = urlParams.get('logoutErr');
loginErrTip.innerText = '';
regErrTip.innerText = '';
logoutErrTip.innerText = '';
if (targetForm === 'logout' || isLogoutFail) {
loginForm.classList.remove('active');
registerForm.classList.remove('active');
logoutForm.classList.add('active');
document.querySelector('.title').innerText = '账号注销';
if (isLogoutFail) {
logoutErrTip.innerText = '注销失败!请检查账号密码';
logoutErrTip.className = 'reg-error';
}
}
else if (targetForm === 'register' || isRegFail) {
loginForm.classList.remove('active');
registerForm.classList.add('active');
document.querySelector('.title').innerText = '商城注册';
if (isRegFail) {
regErrTip.innerText = '注册失败!请检查后重新注册';
regErrTip.className = 'reg-error';
}
}
else if (targetForm === 'login' || isLoginFail) {
registerForm.classList.remove('active');
logoutForm.classList.remove('active');
loginForm.classList.add('active');
document.querySelector('.title').innerText = '商城登录';
if (isLoginFail) {
loginErrTip.innerText = '登录失败!请检查账号密码';
loginErrTip.className = 'reg-error';
}
}
else {
registerForm.classList.remove('active');
logoutForm.classList.remove('active');
loginForm.classList.add('active');
document.querySelector('.title').innerText = '商城登录';
regErrTip.innerText = '';
}
};
</script>
</body>
</html>
search.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商城搜索 - 我的商城</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Inter", "Microsoft Yahei", sans-serif;
}
body {
background: linear-gradient(135deg, #e0f7fa 0%, #f3e5f5 100%);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.container {
width: 520px;
background: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
padding: 50px 40px;
position: relative;
overflow: hidden;
}
.container::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 5px;
background: linear-gradient(90deg, #00bcd4, #9c27b0);
}
.title {
text-align: center;
font-size: 28px;
font-weight: 600;
color: #2d3748;
margin-bottom: 35px;
letter-spacing: 0.5px;
}
.search-form {
position: relative;
display: flex;
gap: 12px;
align-items: center;
}
#searchInput {
flex: 1;
padding: 16px 20px;
border: 1px solid #e2e8f0;
border-radius: 10px;
font-size: 16px;
outline: none;
transition: all 0.2s ease;
background: #f8fafc;
}
#searchInput:focus {
border-color: #00bcd4;
box-shadow: 0 0 0 3px rgba(0, 188, 212, 0.1);
background: white;
}
#searchInput::placeholder {
color: #94a3b8;
}
.search-btn {
padding: 16px 28px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
color: white;
cursor: pointer;
background: linear-gradient(90deg, #00bcd4, #9c27b0);
transition: all 0.2s ease;
letter-spacing: 0.5px;
white-space: nowrap;
}
.search-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 188, 212, 0.2);
}
.search-btn:active {
transform: translateY(0);
}
.back-btn {
display: block;
width: 100%;
margin-top: 25px;
padding: 14px;
border: none;
border-radius: 10px;
font-size: 15px;
font-weight: 500;
color: #4a5568;
cursor: pointer;
background: #f8fafc;
transition: all 0.2s ease;
text-align: center;
text-decoration: none;
}
.back-btn:hover {
background: #f1f5f9;
transform: translateY(-1px);
}
.result-tip {
margin-top: 20px;
padding: 15px;
border-radius: 8px;
background: #f0fdfa;
color: #0f766e;
font-size: 14px;
text-align: center;
display: none;
animation: fadeIn 0.3s ease forwards;
}
.result-tip.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<div class="container">
<h2 class="title">商城商品搜索</h2>
<!-- 核心修改:替换为form表单,指定POST提交到/search -->
<form class="search-form" action="/search" method="POST">
<!-- 关键:给input添加name="band",适配服务端参数解析 -->
<input type="text" name="band" id="searchInput" placeholder="请输入手机品牌名(如:苹果、华为、小米)">
<button type="submit" class="search-btn" id="searchBtn">搜索商品</button>
</form>
<div class="result-tip" id="resultTip"></div>
<a href="index.html" class="back-btn">返回登录页面</a>
</div>
<script>
const searchInput = document.getElementById('searchInput');
const searchForm = document.querySelector('.search-form');
// 表单提交前的校验逻辑
searchForm.onsubmit = function(e) {
const keyword = searchInput.value.trim();
// 校验空值
if (!keyword) {
e.preventDefault(); // 阻止表单提交
alert('请输入要搜索的手机品牌名!');
searchInput.focus(); // 聚焦输入框
return false;
}
// 校验通过,表单自动提交(触发浏览器页面跳转)
return true;
};
// 保留回车触发搜索的逻辑
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
// 阻止默认回车行为(避免重复提交)
e.preventDefault();
// 触发表单提交
searchForm.submit();
}
});
</script>
</body>
</html>
hauwei.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>华为手机官方商城 - 热门机型总览</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
}
body {
background: #f0f2f5;
color: #333;
}
/* 新增返回按钮样式 */
.back-to-search {
position: fixed;
top: 20px;
left: 20px;
z-index: 999;
padding: 10px 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
color: #e00000;
font-size: 14px;
font-weight: 500;
text-decoration: none;
transition: all 0.3s ease;
}
.back-to-search:hover {
background: #f8f8f8;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
/* 顶部品牌头图 */
.brand-header {
width: 100%;
height: 300px;
background: linear-gradient(135deg, #e00000 0%, #b80000 100%);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.brand-header::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url([华为品牌背景图URL]) center/cover no-repeat;
opacity: 0.15;
}
.brand-title-wrap {
position: relative;
z-index: 1;
text-align: center;
}
.brand-main-title {
font-size: 48px;
color: #fff;
font-weight: 700;
margin-bottom: 12px;
letter-spacing: 2px;
}
.brand-sub-title {
font-size: 18px;
color: #fff;
opacity: 0.9;
}
/* 导航栏 */
.nav-bar {
width: 1200px;
margin: 0 auto;
height: 70px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
margin-top: -35px;
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
}
.nav-item {
padding: 0 25px;
font-size: 16px;
color: #333;
font-weight: 500;
cursor: pointer;
transition: color 0.3s;
}
.nav-item.active {
color: #e00000;
position: relative;
}
.nav-item.active::after {
content: "";
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 3px;
background: #e00000;
border-radius: 3px;
}
.nav-item:hover {
color: #e00000;
}
/* 容器 */
.container {
width: 1200px;
margin: 50px auto;
}
/* 机型标题区 */
.phone-section-title {
font-size: 28px;
font-weight: 600;
color: #333;
margin-bottom: 30px;
display: flex;
align-items: center;
}
.phone-section-title::after {
content: "";
flex: 1;
height: 1px;
background: #eee;
margin-left: 20px;
}
/* 机型卡片容器 */
.phone-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 25px;
}
/* 机型卡片 - 精美升级 */
.phone-card {
background: #fff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0,0,0,0.03);
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
cursor: pointer;
position: relative;
}
/* 卡片悬浮动效 */
.phone-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 25px rgba(0,0,0,0.1);
}
/* 新品/旗舰标签 */
.phone-tag {
position: absolute;
top: 20px;
left: 20px;
background: #e00000;
color: #fff;
font-size: 12px;
padding: 4px 12px;
border-radius: 20px;
z-index: 1;
}
/* 机型图片容器 */
.phone-img {
width: 100%;
height: 260px;
background: #f9f9f9 url([华为机型图片URL]) center/cover no-repeat;
transition: all 0.3s;
}
.phone-card:hover .phone-img {
transform: scale(1.05);
}
/* 机型图片包裹层(限制缩放范围) */
.phone-img-wrap {
width: 100%;
height: 260px;
overflow: hidden;
}
/* 机型信息区 */
.phone-info {
padding: 25px 20px;
text-align: center;
}
.phone-name {
font-size: 19px;
color: #333;
font-weight: 600;
margin-bottom: 10px;
}
.phone-desc {
font-size: 14px;
color: #666;
margin-bottom: 15px;
line-height: 1.5;
}
.phone-price-wrap {
display: flex;
align-items: baseline;
justify-content: center;
}
.phone-price-symbol {
font-size: 16px;
color: #e00000;
margin-right: 4px;
}
.phone-price {
font-size: 22px;
color: #e00000;
font-weight: bold;
}
.phone-price-unit {
font-size: 14px;
color: #999;
margin-left: 4px;
}
/* 响应式适配 */
@media (max-width: 1200px) {
.container, .nav-bar {
width: 90%;
}
.phone-list {
grid-template-columns: repeat(2, 1fr);
}
.brand-main-title {
font-size: 36px;
}
}
@media (max-width: 768px) {
.phone-list {
grid-template-columns: 1fr;
}
.brand-header {
height: 200px;
}
.brand-main-title {
font-size: 28px;
}
.nav-item {
padding: 0 15px;
font-size: 14px;
}
}
</style>
</head>
<body>
<!-- 新增返回搜索页面按钮 -->
<a href="search.html" class="back-to-search">返回搜索页面</a>
<!-- 品牌头图 -->
<div class="brand-header">
<div class="brand-title-wrap">
<h1 class="brand-main-title">华为 HUAWEI</h1>
<p class="brand-sub-title">突破边界,重构体验</p>
</div>
</div>
<!-- 导航栏 -->
<div class="nav-bar">
<div class="nav-item active">全部机型</div>
<div class="nav-item">Mate系列</div>
<div class="nav-item">Pura系列</div>
<div class="nav-item">Nova系列</div>
<div class="nav-item">畅享系列</div>
</div>
<!-- 核心容器 -->
<div class="container">
<h2 class="phone-section-title">热门旗舰机型</h2>
<div class="phone-list">
<!-- 机型1:Mate 70 Pro -->
<div class="phone-card" onclick="window.location.href='/detail?brand=huawei&page=1'">
<div class="phone-tag">旗舰新品</div>
<div class="phone-img-wrap">
<div class="phone-img" style="background-image: url(/images/huaweiMate70.jpg);"></div>
</div>
<div class="phone-info">
<div class="phone-name">华为 Mate 70 Pro</div>
<div class="phone-desc">鸿蒙OS 4.2 | 四曲昆仑玻璃屏 | 麒麟9010</div>
<div class="phone-price-wrap">
<span class="phone-price-symbol">¥</span>
<span class="phone-price">5699</span>
<span class="phone-price-unit">起</span>
</div>
</div>
</div>
<!-- 机型2:Pura 80 -->
<div class="phone-card" onclick="window.location.href='/detail?brand=huawei&page=2'">
<div class="phone-tag">轻薄旗舰</div>
<div class="phone-img-wrap">
<div class="phone-img" style="background-image: url(/images/huaweiPura80.jpg);"></div>
</div>
<div class="phone-info">
<div class="phone-name">华为 Pura 80</div>
<div class="phone-desc">超感臻彩屏 | 骁龙7+ Gen3 | 66W快充</div>
<div class="phone-price-wrap">
<span class="phone-price-symbol">¥</span>
<span class="phone-price">3999</span>
<span class="phone-price-unit">起</span>
</div>
</div>
</div>
<!-- 机型3:Nova 13 -->
<div class="phone-card" onclick="window.location.href='/detail?brand=huawei&page=3'">
<div class="phone-tag">潮流影像</div>
<div class="phone-img-wrap">
<div class="phone-img" style="background-image: url(/images/huaweiNove13.jpg);"></div>
</div>
<div class="phone-info">
<div class="phone-name">华为 Nova 13</div>
<div class="phone-desc">柔光自拍 | 5000mAh长续航 | 鸿蒙3.0</div>
<div class="phone-price-wrap">
<span class="phone-price-symbol">¥</span>
<span class="phone-price">2699</span>
<span class="phone-price-unit">起</span>
</div>
</div>
</div>
<!-- 机型4:畅享 70X -->
<div class="phone-card" onclick="window.location.href='/detail?brand=huawei&page=4'">
<div class="phone-tag">超长续航</div>
<div class="phone-img-wrap">
<div class="phone-img" style="background-image: url(/images/huawei70x.jpg);"></div>
</div>
<div class="phone-info">
<div class="phone-name">华为 畅享 70X</div>
<div class="phone-desc">7.0英寸巨幕 | 6000mAh电池 | 反向充电</div>
<div class="phone-price-wrap">
<span class="phone-price-symbol">¥</span>
<span class="phone-price">1599</span>
<span class="phone-price-unit">起</span>
</div>
</div>
</div>
</div>
</div>
</body>
</html>