用C语言制作一个简易HTTP服务器:实现手机商城用户认证与搜索

引言

在深入学习Linux环境编程和网络协议时,动手实现一个简易的Web服务器是极好的实践。本文将以一个C语言项目为例,带你逐步剖析一个支持用户注册、登录、注销以及手机品牌搜索的HTTP服务器。我们将从网络编程基础讲起,深入到HTTP协议解析、数据库集成、前端交互,并讨论其中的安全漏洞。

项目完整代码已提供,包含:main.c(服务器核心)、head.h(头文件)、以及多个HTML页面(index.htmlsearch.htmlapple.htmlhuawei.htmlxiaomi.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分割字符串,直接修改原始缓冲区(破坏性解析)。流程如下:

  1. 定位请求体开始位置(\r\n\r\n之后),保存到pconcent
  2. strtok(precvbuff, " ")获取method。
  3. strtok(NULL, " ")获取url。
  4. 对url进行进一步解析:strtok(url, "?")分离路径和查询字符串。
  5. 若存在查询字符串,继续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,失败加&regErr=1
  • /logout :调用DeleUserinfo删除用户,成功重定向到index.html,失败加&logoutErr=1
  • /search :解析品牌名,重定向到./{brand}.html

所有POST处理均返回302状态码,通过Location头指示浏览器跳转。

3.3.3 /detail 重定向

这是一个模拟的详情页路由,实际并不存在对应的HTML文件,仅演示URL重写。服务器解析出brandpage后,构造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方式提交到/searchinputname属性设为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_detailpage_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&regErr=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>
相关推荐
Aaswk2 小时前
蓝桥杯2025年第十六届省赛真题(更新中)
c语言·数据结构·c++·算法·职场和发展·蓝桥杯
香水5只用六神2 小时前
【DMA】存储器到外设模式实验2
c语言·git·stm32·单片机·嵌入式硬件·github·visual studio
Yupureki3 小时前
《C++实战项目-高并发内存池》4.CentralCache构造
c语言·开发语言·c++·单例模式·github
papaofdoudou3 小时前
QEMU和KVMTOOL在GPA(IOVA)和HVA映射方面的异同
linux·运维·服务器
Xzq2105093 小时前
部分重要协议或技术(DNS,ICMP,NAT,代理服务器)
运维·服务器·网络
xh didida3 小时前
数据结构--实现链式结构二叉树
c语言·数据结构·算法
艾莉丝努力练剑3 小时前
文件描述符fd:跨进程共享机制
java·linux·运维·服务器·开发语言·c++
工藤新一¹4 小时前
《操作系统》第一章(1)
java·服务器·前端
勇闯逆流河4 小时前
【Linux】linux进程概念(冯洛伊曼体系、操作系统、进程详解)
linux·运维·服务器