信息查询Web服务器-->电子商城信息查询项目

目录

[一. 项目背景](#一. 项目背景)

[二. 项目流程图](#二. 项目流程图)

[三. 分模块讲解](#三. 分模块讲解)

[3.1 主程序 epoll 事件调度模块](#3.1 主程序 epoll 事件调度模块)

[3.1.1 模块作用](#3.1.1 模块作用)

[3.1.2 核心函数说明](#3.1.2 核心函数说明)

[3.1.3 模块示例代码](#3.1.3 模块示例代码)

[3.2 TCP 通信模块与HTTP路由模块](#3.2 TCP 通信模块与HTTP路由模块)

[3.2.1 模块作用](#3.2.1 模块作用)

[3.2.2 模块函数讲解](#3.2.2 模块函数讲解)

[1、init_tcp_ser 初始化套接字](#1、init_tcp_ser 初始化套接字)

[2、recv_http_request 接收请求报文](#2、recv_http_request 接收请求报文)

3、解析请求报文

[(1) 初步解析](#(1) 初步解析)

[(2) 解析注册/登录时报文实体主体](#(2) 解析注册/登录时报文实体主体)

[(3) 解析搜索数据(商品)时的请求报文](#(3) 解析搜索数据(商品)时的请求报文)

[(4) 解析搜索某个商品的详细数据报文](#(4) 解析搜索某个商品的详细数据报文)

4、发送响应报文

[(1) 发送响应报文头部](#(1) 发送响应报文头部)

[(2) 发送文件资源](#(2) 发送文件资源)

[(3) 发送响应报文](#(3) 发送响应报文)

[3.3 数据库模块](#3.3 数据库模块)

[3.3.1 模块作用](#3.3.1 模块作用)

1、用户注册的实现

2、用户登录的实现

3、模糊查找所有商品

4、查找单一商品

[四. HTML模块](#四. HTML模块)

[4.1 登录HTML](#4.1 登录HTML)

[4.2 注册HTLM](#4.2 注册HTLM)

[4.3 模糊查找HTML](#4.3 模糊查找HTML)

[4.4 展示所有商品](#4.4 展示所有商品)

[4.5 详细信息HTML](#4.5 详细信息HTML)

[五. 项目中问题总结](#五. 项目中问题总结)

[5.1 epoll_del_fd 与 close 的顺序问题](#5.1 epoll_del_fd 与 close 的顺序问题)

[5.2 epoll的高并发问题](#5.2 epoll的高并发问题)

[六. 总结](#六. 总结)


一. 项目背景

项目涉及:套接字编程+数据库编程+http编程+服务端并发

嵌入式 Linux 设备(工控板、边缘网关、小型终端)资源有限,无法部署重量级 Web 服务与后端运行环境,常常需要轻量化本地网页管理界面。SQLite 无进程、零配置、占用资源极低,搭配单进程 Web 服务,非常适合嵌入式本地商品管理、设备后台页面等小型场景。

传统简易 TCP 服务多为单连接阻塞模型,并发能力差,因此引入 epoll IO 多路复用实现多客户端同时访问,解决多用户同时浏览商品、登录的并发需求;同时浏览器传递中文搜索、表单参数时会自动进行 URL 百分号编码,原生服务无法识别中文,因此配套 URL 解码工具完成参数还原,实现中文商品检索功能。

本文基于纯 C 语言结合 Linux epoll IO 多路复用技术,从零实现一款轻量级本地商品商城 Web 服务器,不依赖第三方 Web 服务框架,搭配 SQLite3 嵌入式数据库完成商品检索、商品详情展示、用户注册登录全套网页业务。项目采用单进程 epoll 高并发 模型监听 8080 端口,手动解析原生HTTP GET/POST 请求报文,实现静态 HTML 页面、图片资源分发,同时配套 URL 解码工具处理浏览器中文编码参数,解决商品中文搜索匹配问题。服务端分层设计网络处理、URL 解码、数据库操作三大核心模块:**网络层完成 TCP 连接管理与 HTTP 路由分发;解码模块转换 % XX 格式 URL 编码字符;数据库层封装 SQLite 查询回调逻辑,实现商品模糊搜索、按 ID 查询详情、用户账号读写功能。**文章完整梳理用户登录注册、商品搜索浏览、商品详情查看的业务流程,同时深度剖析源码存在的 SQL 注入、明文密码存储、缓冲区溢出、XSS 等安全漏洞与逻辑缺陷,并给出优化思路。整套代码仅依赖 Linux 系统调用与 SQLite 基础库,无复杂第三方依赖,可直接在 Linux 环境编译运行,既能帮助读者掌握 Socket 网络通信、epoll 多路复用、HTTP 协议底层原理,也能理解 C 语言与嵌入式数据库联合开发网页服务的完整实现思路,是一套兼顾网络编程与小型 Web 业务开发的实战教学案例。

二. 项目流程图

三. 分模块讲解

3.1 主程序 epoll 事件调度模块

3.1.1 模块作用

基于Linux epoll IO 多路复用 实现单进程高并发 TCP 服务,统一管理监听套接字与客户端连接套接字,分发两类事件:新客户端连接与客户端 HTTP 请求数据,串联所有业务逻辑,是整个服务的调度核心。

3.1.2 核心函数说明

1、epoll_add_fd():封装epoll_ctl(EPOLL_CTL_ADD),将 fd 注册到 epoll,监听读事件 EPOLLIN;

2、epoll_del_fd():封装epoll_ctl(EPOLL_CTL_DEL),连接关闭前从 epoll 移除 fd,避免无效事件

3、main函数主流程:

调用init_tcp_ser()创建 TCP 监听端口 8080;创建 epoll 实例,把监听 fd 加入 epoll 监听;死循环epoll_wait阻塞等待客户端事件;监听 fd 就绪:执行accept()新建客户端连接,加入 epoll,普通连接 fd 就绪:接收、解析 HTTP 请求,执行业务响应。

主函数流程图如下:

3.1.3 模块示例代码

cpp 复制代码
#define EPOLL_MAX_FD 1024/限制服务最大同时监听 1024 个文件描述符
                        /支持最多 1024 并发连接
/增加要监听的文件描述符
int epoll_add_fd(int epfd, int fd, uint32_t events)
{
    struct epoll_event ep;
    int ret;
    ep.events = events;
    ep.data.fd = fd;
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ep);
    if(ret < 0){
        perror("epoll_ctl_add()");
        return -1;
    }
    return 0;
}
/从监听的文件描述符中移除指定的fd
int epoll_del_fd(int epfd, int fd)
{
    int ret;
    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
    if(ret < 0){
        perror("epoll_ctl_del()");
        return -1;
    }
    return 0;
}
/程序入口主函数
int main()
{
    int lisfd, connfd, epfd, ret;
    struct epoll_event eps[EPOLL_MAX_FD]; 
    int nready, i;

    lisfd = init_tcp_ser();/初始化监听套接字
    if(lisfd == -1){
        exit(1);
    }

    epfd = epoll_create(EPOLL_MAX_FD);/创建文集描述符集合
    if(epfd < -1){
        perror("epoll_create()");
        return -1;
    }

    epoll_add_fd(epfd, lisfd, EPOLLIN);/添加监听套接字

    while(1){
        nready = epoll_wait(epfd, eps, EPOLL_MAX_FD, -1);/阻塞等待
        if(nready == -1){
            perror("epoll_wait()");
            return -1;
        }
        for(i = 0; i < nready; i++){
            if(eps[i].data.fd == lisfd){
                connfd = accept(lisfd, NULL, NULL);
                if(connfd < 0){
                    perror("accept()");
                    continue;
                }
                epoll_add_fd(epfd, connfd, EPOLLIN);
            }
            else{
                int sockfd = eps[i].data.fd;
                char http_req[1024] = {0};
                HTTP_req_t req;
                memset(&req, 0, sizeof(HTTP_req_t));
                /接收请求报文 
                ret = recv_http_request(sockfd, http_req, sizeof(http_req));
                if(ret == -1){
                    epoll_del_fd(epfd, sockfd);
                    close(sockfd);
                    continue;
                }
                /解析请求报文
                parse_http_request(http_req, &req);
                /发送响应报文
                ret = send_http_response(sockfd, &req);
                if(ret == -1){
                    epoll_del_fd(epfd, sockfd);
                    close(sockfd);
                    continue;
                }
                epoll_del_fd(epfd, sockfd);
                close(sockfd);
            }
        }      
    }
    close(epfd);
    close(lisfd);
    return 0;
}

3.2 TCP 通信模块与HTTP路由模块

3.2.1 模块作用

底层 TCP 服务初始化HTTP 报文收发请求解析URL 参数提取路由分发文件发送,是连接网络层与业务数据库层的中间核心,所有 GET/POST 业务路由全部在此实现。

3.2.2 模块函数讲解

1、init_tcp_ser 初始化套接字

**作用:**创建 socket、开启端口复用 setsockopt、bind 绑定 IP 端口、listen 开启监听,返回监听 fd

示例代码:

cpp 复制代码
#define SER_IP "192.168.110.219"/服务器地址
#define SER_PORT 8080/使用HTTP的备用端口8080
/初始化监听套接字
int init_tcp_ser()
{
    int lisfd, ret;
    struct sockaddr_in seraddr;

    lisfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lisfd < 0){
        perror("socket()");
        return -1;
    }
    /设置端口复用
    int opt = 1;
    ret = setsockopt(lisfd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt,
                    sizeof(opt));
    if(ret < 0){
        perror("setsockopt()");
        return -1;
    }
    seraddr.sin_family = AF_INET; 
    seraddr.sin_port = htons(SER_PORT);
    inet_pton(AF_INET, SER_IP, &seraddr.sin_addr);
    ret = bind(lisfd, (struct sockaddr*)&seraddr,
            sizeof(seraddr));
    if(ret < 0){
        perror("bind()");
        return -1;
    }
    ret = listen(lisfd, 128);
    if(ret < 0){
        perror("listen()");
        return 0;
    }
    return lisfd;
}
2、recv_http_request 接收请求报文

**作用:**接收浏览器发来的请求报文

示例代码:

cpp 复制代码
/接收请求报文
int recv_http_request(int connfd, char *http_req, int maxlen)
{
    ssize_t size;
    size = recv(connfd, http_req, maxlen, 0);
    /接收出错
    if(size < 0){
        perror("recv()");
        return -1;
    }
    /表示对方断开连接
    else if(size == 0){
        return -1;
    }
    return 0;
}
3、解析请求报文

**作用:**由于本项目共用四个网页,并且含有登录与注册网页,故每次用户通过浏览器进行操作时所发送的请求报文都不一样,因此需要将请求报文的关键字段解析出来,根据这些字段进行相应的回复

请求报文一般结构如下:

(1) 初步解析

请求报文示例:

**方法:**都是GET-->向服务器请求资源

URL: 分别是请求主页:/

请求图片:/images/lbxx.jpg

请求网页图标:/favicon.ico

故可以通过解析这些这些内容来判断向浏览器回复哪些内容

html 复制代码
GET / HTTP/1.1
Host: 192.168.0.183:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

GET /images/lbxx.jpg HTTP/1.1
Host: 192.168.0.183:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://192.168.0.183:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

GET /favicon.ico HTTP/1.1
Host: 192.168.0.183:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://192.168.0.183:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

由于后面用户还要登录,注册等等,这里先做初步的请求报文解析,即先解析出方法,URL,实体主体

示例代码:

cpp 复制代码
/解析请求报文
int parse_http_request(char *http_req, HTTP_req_t *preq)
{
    char *tmp, *key;
    /解析正文content
    tmp = strstr(http_req, "\r\n\r\n");
    if(tmp == NULL){
        perror("strstr()");
        return -1;
    }
    tmp += 4;
    strcpy(preq->content, tmp);
    /解析请求方法method
    tmp = strtok(http_req, " ");
    if(tmp == NULL){
        return -1;
    }
    strcpy(preq->method, tmp);
    /解析url
    tmp = strtok(NULL, " ");
    if(tmp == NULL){
        return -1;
    }
    strcpy(preq->url, tmp);
    return 0;
}
(2) 解析注册/登录时报文实体主体

用户注册时的请求报文:

可以看到请求方法:POST

并且包含了实体主体:邮箱、用户名、密码

html 复制代码
POST /register.html HTTP/1.1
Host: 192.168.0.183:8080
Connection: keep-alive
Content-Length: 45
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0
Origin: http://192.168.0.183:8080
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.0.183:8080/register.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

email=999%40qq.com&user_name=000&password=000

用户登录时的请求报文:

可以看到实体主体字段并没有邮箱这一部分

html 复制代码
POST /search.html HTTP/1.1
Host: 192.168.0.183:8080
Connection: keep-alive
Content-Length: 21
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0
Origin: http://192.168.0.183:8080
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.0.183:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

name=000&password=000

为了将用户注册输入的邮箱,用户名,密码保存进数据库,这就需要将这些字段解析出来,示例带如下:

这里首先对"pemail"进行判断的理由:用户登录时还需要输入信息,服务器需要根据用户输入的信息来在数据库中进行查询,若查询成功则登录成功,查询失败则登录失败。只不过登录时不需要输入邮箱,故这里先对"pemail"进行判断,增强代码复用性

cpp 复制代码
/从url中解析出用户注册提交的邮箱,名字,密码
/char *pemail, char *puser_name,char *ppassword为传出参数,保存用户注册信息
int get_user_register_msg(char *pcontent, char *pemail, char *puser_name,char *ppassword)
{
    char *tmp;
    if(pemail){
        tmp = strtok(pcontent, "&");
        if(tmp == NULL){
            return -1;
        }
        tmp = strstr(tmp, "=");
        tmp++;
        strcpy(pemail, tmp);

        tmp = strtok(NULL, "&");
        if(tmp == NULL){
            return -1;
        }
        tmp = strstr(tmp, "=");
        tmp++;
        strcpy(puser_name, tmp);

        tmp = strtok(NULL, "&");
        if(tmp == NULL){
            return -1;
        }
        tmp = strstr(tmp, "=");
        tmp++;
        strcpy(ppassword, tmp);
    }
    else{
        tmp = strtok(pcontent, "&");
        if(tmp == NULL){
            return -1;
        }
        tmp = strstr(tmp, "=");
        tmp++;
        strcpy(puser_name, tmp);

        tmp = strtok(NULL, "&");
        if(tmp == NULL){
            return -1;
        }
        tmp = strstr(tmp, "=");
        tmp++;
        strcpy(ppassword, tmp);
    }
    return 0;
}
(3) 解析搜索数据(商品)时的请求报文

用户搜索商品时的请求报文:

可以看到请求方法是:GET

GET与POST请求方法的区别:

|----------|--------------------------|
| GET | 参数拼在 URL(首行),无请求实体主体 |
| POST | 参数放在报文末尾的实体主体,URL 可不带参数。 |

URL:/allgoods.html?key=%E8%AF%BA%E5%9F%BA%E4%BA%9A

key=%E8%AF%BA%E5%9F%BA%E4%BA%9A-->这就是用户想要查找的数据

html 复制代码
GET /allgoods.html?key=%E8%AF%BA%E5%9F%BA%E4%BA%9A HTTP/1.1
Host: 192.168.0.183:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.0.183:8080/search.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

为了获取用户想要查找的数据,就必须对URL字段中的key字段进行解析,示例代码:

cpp 复制代码
/从url中获取要模糊查找的key
/purl为商品的url传入参数,pkey为传出参数
int get_good_key_from_url(char *purl, char *pkey)
{
    char *tmp;
    tmp = strstr(purl, "?");
    if(tmp == NULL){
        perror("strstr()");
        return -1;
    }
    tmp += 1;
    strcpy(pkey, tmp);

    purl = strtok(purl, "?");
    return 0;
}
(4) 解析搜索某个商品的详细数据报文

当用户点击模糊搜索出来的某个商品时,会进入这个商品的详细网页,收到的报文如下:

html 复制代码
GET /detail.html?id=32 HTTP/1.1
Host: 192.168.0.183:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.0.183:8080/allgoods.html?key=%E8%AF%BA%E5%9F%BA%E4%BA%9A
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

解析示例代码:

cpp 复制代码
/从url中获取商品的id
int get_good_id_from_url(char *purl)
{
    char *tmp = strstr(purl, "id=");
    if(tmp == NULL){
        perror("strstr()");
        return -1;
    }
    tmp += 3;
    purl = strtok(purl, "?");
    return atoi(tmp);
}
4、发送响应报文
(1) 发送响应报文头部

发送响应报文时,要先发送响应报文的头部

示例代码:

cpp 复制代码
//发送响应报文头部
int send_http_response_head(int connfd)
{
    ssize_t size;
    char *head = "HTTP/1.1 200 OK\r\n"
                 "Content-Type: text/html;charset=utf-8\r\n"
                 "Server: pegasus\r\n"
                 "Connection: keep-alive\r\n\r\n";
    size = send(connfd, head, strlen(head), 0);
    if(size < 0){
        perror("send()");
        return -1;
    }
    return 0;
}
(2) 发送文件资源

用户通过浏览器向服务器请求资源时,这些资源包含了许多的网页与图片,这些资源都以本地文件的形式统一发送给浏览器

示例代码:

cpp 复制代码
/发送文件
int send_file(int connfd, char *filename)
{
    int fd;
    char buff[4096] = { 0 };
    ssize_t size;
    fd = open(filename, O_RDONLY);
    if(fd < 0){
        perror("open()");
        return -1;
    }
    while(1){
        memset(buff, 0, sizeof(buff));
        size = read(fd, buff, sizeof(buff));
        if(size <= 0){
            break;
        }
        size = send(connfd, buff, size, 0);
        if(size < 0){
            close(fd);
            return -1;
        }
    }    
    close(fd);
    return 0;
}
(3) 发送响应报文

根据浏览器请求方所发送的响应报文,解析后判断要回复的资源并进行回复

cpp 复制代码
//发送响应报文
int send_http_response(int connfd, HTTP_req_t *preq)
{
    char filename[512] = { 0 };
    int ret;
    ssize_t size;

    send_http_response_head(connfd);//先发送响应报文头部
    
    //处理GET请求
    if( !strcmp(preq->method, "GET") ){
        if( !strcmp(preq->url, "/") ){//发送主页面
            strcpy(filename, "../HTML/login.html");
            ret = send_file(connfd, filename);
            if(ret == -1){
                return -1;
            }
        }
        else if( strstr(preq->url, "jpg") || strstr(preq->url, "png") ){
            sprintf(filename,"../HTML/source%s",preq->url);
            ret = send_file(connfd, filename);
            if(ret == -1){
                return -1;
            }
        }
        else if( strstr(preq->url, "/allgoods.html") ){
            //查找商品
            char goods_html[8192] = { 0 };
            char key[64] = { 0 };
            int ret = get_good_key_from_url(preq->url ,key);
            if(ret == -1){
                return -1;
            }
            sqlite3_search_key(key, goods_html);
            sprintf(filename, "../HTML%s",preq->url);
            ret = send_file(connfd, filename);
            if(ret == -1){
                return -1;
            }
            size = send(connfd, goods_html, strlen(goods_html), 0);
            if(size < 0){
                return -1;
            }
        }
        else if( strstr(preq->url, "/detail.html") ){
            //查找商品
            //char goods_detail_html[8192] = { 0 };       
            char *goods_detail_html = (char*)malloc(40000);
            if(goods_detail_html == NULL){
                perror("malloc()");
                return -1;
            }
            int good_id = get_good_id_from_url(preq->url);
            sqlite3_get_goods_by_id(good_id, goods_detail_html);
            sprintf(filename, "../HTML%s",preq->url);
            ret = send_file(connfd, filename);
            if(ret == -1){
                return -1;
            }
            size = send(connfd, goods_detail_html,
                    strlen(goods_detail_html), 0);
            if(size < 0){
                return -1;
            }
            free(goods_detail_html);
        }
        else if( strstr(preq->url, ".html") ){
            sprintf(filename, "../HTML%s", preq->url);
            ret = send_file(connfd, filename);
            if(ret == -1){
                return -1;
            }
        }
        

    }
    //处理POST请求
    else if( !strcmp(preq->method, "POST") ){
        /*if( strstr(preq->url, ".html") ){
            sprintf(filename, "../HTML%s", preq->url);
            ret = send_file(connfd, filename);
            if(ret == -1){
                return -1;
            }
        }*/
        if( strstr(preq->url, "register.html") ){
            char response[512] = { 0 };
            char pemail[32] = {0};
            char puser_name[32] = {0};
            char ppassword[32] = {0};
            ret = get_user_register_msg(preq->content, pemail,
                    puser_name, ppassword);

            printf("email is %s\n",pemail);
            printf("name is %s\n",puser_name);
            printf("password is %s\n",ppassword);

            ret = sqlite3_user_register(pemail, puser_name, ppassword);
            if(ret == -1){
                printf("用户:%s 注册失败!\n",puser_name);
                sprintf(response, 
                        "<html><head><meta charset='utf-8'><meta http-equiv='refresh' content='3;url=./register.html'></head>"
                        "<body align='center'><h2 style='color:red;'>注册失败!用户名/邮箱已存在</h2><p>3秒后返回注册页...</p></body></html>");
                size = send(connfd, response, strlen(response), 0);
                if(size < 0){
                    return -1;
                }
            }
            else{
                printf("用户:%s 注册成功!\n",puser_name);
                sprintf(response, "<html><head><meta charset='utf-8'><meta http-equiv='refresh' content='2;url=./login.html'></head>"
                        "<body align='center'><h2>注册成功!2秒后跳转到登录页...</h2></body></html>");
                size = send(connfd, response, strlen(response), 0);
                if(size < 0){
                    return -1;
                }
            }
        }
        else if( strstr(preq->url, "search.html") ){
            char response[512] = { 0 };
            char ppassword[32] = {0};
            char puser_name[32] = {0};
            ssize_t size;
            ret = get_user_register_msg(preq->content, NULL,
                    puser_name, ppassword);

            printf("password is %s\n",ppassword);
            printf("name is %s\n",puser_name);

            ret = sqlite3_user_login(puser_name, ppassword);
            if(ret == 0){
                sprintf(response, "<html><head><meta charset='utf-8'><meta http-equiv='refresh' content='1;url=./search.html'></head>" 
                        "<body align='center'><h2>登录成功!1秒后跳转到搜索页...</h2></body></html>");
            }
            else{
                sprintf(response, "<html><head><meta charset='utf-8'><meta http-equiv='refresh' content='3;url=./login.html'></head>" 
                        "<body align='center'><h2 style='color:red;'>登录失败!用户名或密码错误</h2><p>3秒后返回登录页...</p></body></html>");
            }
            size = send(connfd, response, strlen(response), 0);
            if(size < 0){
                return -1;
            }
        }

    }

        else if( strstr(preq->url, ".html") ){
            sprintf(filename, "../HTML%s", preq->url);
            ret = send_file(connfd, filename);
            if(ret == -1){
                return -1;
            }
        }

   return 0;
}

3.3 数据库模块

3.3.1 模块作用

此模块包括用户的注册,登录,以及商品的模糊查找与单一商品的精准查找等功能

1、用户注册的实现

根据从请求报文中解析出来的邮箱,用户名,密码字段,进行传参,在此模块中将这些信息存入到数据库中,同一用户下次可直接登录

示例代码:

cpp 复制代码
//用户注册实现
int sqlite3_user_register(const char *email, const char *user_name, const char *password)
{
    sqlite3 *pdb;
    char sql[1024] = {0};
    int ret;

    /打开数据库
    ret = sqlite3_open("../HTML/source/123.db", &pdb);
    if(ret != SQLITE_OK){
        fprintf(stderr, "sqlite3_open error:%s\n", sqlite3_errmsg(pdb));
        return -1;
    }

    /拼接插入SQL
    sprintf(sql, "INSERT INTO my_users (user_id, email, user_name, password)\
            VALUES (NULL,\"%s\", \"%s\", \"%s\");",
            email, user_name, password);

    /执行SQL
    ret = sqlite3_exec(pdb, sql, NULL, NULL, NULL);
    if(ret != SQLITE_OK){
        fprintf(stderr, "sqlite3_exec error:%s\n", sqlite3_errmsg(pdb));
        sqlite3_close(pdb);
        return -1;
    }
    sqlite3_close(pdb);
    return 0;
}
2、用户登录的实现

根据从请求报文中解析出来的用户名,密码字段,进行传参,在此模块中,在数据库查找这些信息,如果找到了说明用户存在,并提醒用户登录成功,如果没找到说明用户不存在或者用户输入错误,并提示用户重新进行登录

这一块使用数据库中存放用户信息的id来进行判断,如果id>0,则表示此用户存在,非则此用户不存在

示例代码:

cpp 复制代码
/登录校验回调函数(统计匹配的用户数)
int login_check_callback(void *arg, int col_num, char **col_data, char **col_name)
{
    int *match_count = (int *)arg;
    *match_count = atoi(col_data[0]);/结果为user_id的值
    return 0;
}
/登陆校验函数
int sqlite3_user_login(const char *user_name, const char *password)
{
    sqlite3 *pdb;
    char sql[1024] = {0};
    int ret;
    int match_count = 0; /匹配的用户数

    /打开数据库
    ret = sqlite3_open("../HTML/source/123.db", &pdb);
    if(ret != SQLITE_OK){
        fprintf(stderr, "sqlite3_open error:%s\n", sqlite3_errmsg(pdb));
        return -1;
    }

    /拼接查询SQL:校验用户名+密码是否存在
    sprintf(sql, "SELECT user_id FROM my_users WHERE user_name='%s' AND password='%s';",
            user_name, password);

    /执行SQL并获取匹配数
    ret = sqlite3_exec(pdb, sql, login_check_callback, &match_count, NULL);
    if(ret != SQLITE_OK){
        fprintf(stderr, "sqlite3_exec error:%s\n", sqlite3_errmsg(pdb));
        sqlite3_close(pdb);
        return -1;
    }
    sqlite3_close(pdb);
    /匹配数>0则登录成功
    return (match_count > 0) ? 0 : -1;

}
3、模糊查找所有商品

此模块的功能是用户输入查找信息进行查找时,从数据库中模糊查找出所有符合要求的信息

示例代码:

cpp 复制代码
//查找商品回调函数
int goods_search(void *arg, int column, char **data, char **name)
{
    char *goods_html = (char*)arg;
    char single_card[4096] = { 0 };

    int goods_id = atoi(data[0]);
    char *goods_name = data[1];
    char *market_price = data[2];
    char *shop_price = data[3];
    char *keywords = data[4];
    char *goods_desc = data[5];
    char *goods_img = data[6];
    char goods_web_img[128] = { 0 };
    sprintf(goods_web_img, "/%s",goods_img);

    sprintf(single_card,
    "<div style='display:inline-block;border:1px solid #666;width:230px;padding:12px;margin:15px;background:#fff;vertical-align:top;'>"
        // 跳转详情携带商品id ?id=%d
        "<a href='/detail.html?id=%d'>"
            "<img src='%s' width='200' height='160' style='object-fit:cover;'>"
        "</a>"
        "<h3 style='margin:8px 0;color:#222;'>%s</h3>"
        "<p style='color:#999;text-decoration:line-through;margin:4px 0;'>市场价:%s 元</p>"
        "<p style='color:red;font-size:18px;font-weight:bold;margin:4px 0;'>售价:%s 元</p>"
        "<p style='font-size:13px;color:#555;margin:6px 0;'>关键词:%s</p>"
        "<p style='font-size:13px;color:#444;margin:8px 0;'>简介:%.80s...</p>"
        // 文字按钮同样携带商品id跳转
        "<a href='/detail.html?id=%d' style='display:block;text-align:center;margin-top:10px;padding:6px;background:#007bff;color:#fff;text-decoration:none;border-radius:4px;'>查看完整商品详情</a>"
    "</div>",
    goods_id,           // 图片跳转链接id参数
    goods_web_img,      // 图片网页访问路径
    goods_name,         // 商品名称
    market_price,       // 市场价
    shop_price,         // 售价
    keywords,           // 关键词
    goods_desc,         // 商品简介截取前80字符展示
    goods_id            // 按钮跳转链接id参数
    );

    //将当前商品卡片追加到总HTML缓冲区,多条商品自动拼接
    strcat(goods_html, single_card);
    return 0;
}



//打开数据库并查找所有商品
int sqlite3_search_key(char *key, char *goods_html)
{
    int ret;
    char sql[4096] = { 0 };
    char key_char[32] = { 0 };
    sqlite3 *pdb;
    ret = sqlite3_open("../HTML/source/123.db", &pdb);
    if(ret != SQLITE_OK){
        fprintf(stderr, "sqlite3_open error:%s\n",sqlite3_errmsg(pdb));
        return -1;
    }

    char good_msg[128] = "goods_id,goods_name,market_price,shop_price,keywords, \
                     goods_desc,goods_img";
    
    //将URL编码的字符串(%XX格式)解码为原始字符串
    urlDecode(key_char, key);

    sprintf(sql, "SELECT %s FROM goods WHERE goods_name LIKE \"%%%s%%\";",
            good_msg, key_char);

    //printf("key_char is %s\n",key_char);

    ret = sqlite3_exec(pdb, sql, goods_search, (void*)goods_html , NULL);
    if(ret != SQLITE_OK){
        fprintf(stderr, "sqlite3_exec error:%s\n",sqlite3_errmsg(pdb));
        sqlite3_close(pdb);
        return -1;
    }
    sqlite3_close(pdb);
    return 0;
}
4、查找单一商品

在模糊查找到部分商品时,用户在点击某一商品时,进入这一商品的详细信息网页

示例代码:

cpp 复制代码
/**
 * 单商品详情查询回调
 * arg:存放详情完整html的缓冲区
 * column:字段总数
 * data:查询结果数组 gid,name,market,shop,key,desc,img
*/
static int detail_callback(void *arg, int column, char **data, char **name)
{
    char *detail_html = (char *)arg;
    //char temp[8192] = {0};
    int gid = atoi(data[0]);
    char *good_name = data[1];
    char *market = data[2];
    char *shop = data[3];
    char *keys = data[4];
    char *desc = data[5]; // 完整富文本介绍,直接输出
    char *img = data[6];

    char web_img[256] = {0};
    sprintf(web_img, "/%s", img);

    // 详情页面主体HTML模板
    sprintf(detail_html,
    "<div style='width:700px;margin:0 auto;background:#fff;padding:20px;border-radius:8px;'>"
        "<img src='%s' width='500' style='margin:0 auto;display:block;'>"
        "<h2 style='text-align:center'>%s</h2>"
        "<p style='font-size:16px;color:#999;text-decoration:line-through;'>市场价:%s 元</p>"
        "<p style='font-size:20px;color:red;font-weight:bold;'>售价:%s 元</p>"
        "<p>关键词:%s</p>"
        "<hr>"
        "<h3>商品详细介绍</h3>"
        "%s" // 数据库自带p、span等html标签直接渲染
        "<br>"
    "</div>",
    web_img, good_name, market, shop, keys, desc
    );
    return 0;
}

/**
 * gid:前端传过来的商品数字id
 * out_html:输出拼接好的详情页面HTML,传出参数
*/
//打开数据库查找制定的商品
int sqlite3_get_goods_by_id(int gid, char *out_html)
{
    memset(out_html, 0, 40000);
    sqlite3 *pdb;
    int ret = sqlite3_open("../HTML/source/123.db", &pdb);
    if(ret != SQLITE_OK)
    {
        fprintf(stderr, "sqlite3_open error:%s\n",sqlite3_errmsg(pdb));
        return -1;
    }
    char sql[512] = {0};
    // 根据主键精准查询单条商品
    sprintf(sql, "SELECT goods_id,goods_name,market_price,shop_price,keywords,goods_desc,goods_img FROM goods WHERE goods_id=%d;",
            gid);
    ret = sqlite3_exec(pdb, sql, detail_callback, out_html, NULL);
    if(ret != SQLITE_OK)
    {
        fprintf(stderr, "sqlite3_exec error:%s\n",sqlite3_errmsg(pdb));
        sqlite3_close(pdb);
        return -1;
    }
    sqlite3_close(pdb);
    return 0;
}

四. HTML模块

4.1 登录HTML

HTML超链接语法:

<a> 元素最重要的属性是 href 属性,它指示链接的目标。

链接文本是读者可见的部分。

点击链接文本会将读者带到指定的 URL 地址。

html 复制代码
<a href="url">链接文本</a>

示例HTML:

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <!-- 浏览器标签页标题:商品查询系统-登录 -->
    <title>商品查询系统-登录</title>
</head>
<body align="center" background="./images/lbxx.jpg" style="background-repeat:no-repeat;background-size:cover;background-position:center;min-height:100vh;margin:0;padding-top:80px;">
    <!-- 一级大标题:页面主标题 -->
    <h1>电子商品信息查询系统</h1>
    <!-- 三级标题:提醒登录 -->
    <h3>用户登录</h3>
    <br>
    <!-- 登录表单 方法为POST提交 -->
    <form action="search.html" method="post">
        <!-- 文字标签,for="name" 关联下方输入框,点击文字自动聚焦输入框 -->
        <!-- <label> 标签的 for 属性应该等于 <input> 元素的 id 属性,以便将它们绑定在一起。-->
        <label for="name">用户名称:</label>
        <input type="text" id="name" name="name" required placeholder="请输入账号">
        <br><br>
        <label for="password">用户密码:</label>
        <input type="password" id="password" name="password" required placeholder="请输入密码">
        <br><br>
        <input type="submit" value="登录系统" style="width:120px;height:30px;font-size:16px;">
    </form>
    <br>
    <!-- 新增:跳转注册页 -->
    <a href="register.html" style="font-size:14px;">还没有账号?立即注册</a>
</body>
</html>

4.2 注册HTLM

html 复制代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>用户注册</title>
</head>
<body align="center" background="./images/lbxx.jpg" style="background-repeat:no-repeat;background-size:cover;background-position:center;min-height:100vh;margin:0;padding-top:80px;">
<h1>电子商品信息查询系统-注册</h1>
<br>
<!-- 注册表单:POST提交到/register -->
<form action="./register.html" method="post">
    <label for="email">邮箱:</label>
    <input type="email" id="email" name="email" required placeholder="请输入邮箱">
    <br><br>
    <label for="user_name">用户名:</label>
    <input type="text" id="user_name" name="user_name" required placeholder="请输入用户名">
    <br><br>
    <label for="password">密码:</label>
    <input type="password" id="password" name="password" required placeholder="请输入密码">
    <br><br>
    <input type="submit" value="完成注册" style="width:120px;height:30px;font-size:16px;">
</form>
<br>
<!-- 跳转登录页 -->
<a href="./login.html" style="font-size:14px;">已有账号?前往登录</a>
</body>
</html>

4.3 模糊查找HTML

html 复制代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>商品搜索</title>
</head>
<body align="center" background="./images/lbxx.jpg" style="background-repeat:no-repeat;background-size:cover;background-position:center;min-height:100vh;margin:0;padding:30px;">
<h1>商品模糊搜索</h1>
<!-- 返回登录页 -->
<a href="./login.html">
    <button type="button" style="font-size:15px;">返回登录</button>
</a>
<br><br><hr width="80%">

<!-- 仅搜索输入框,无查看全部按钮 -->
<form action="allgoods.html" method="GET">
    <input type="text" name="key" placeholder="输入商品名称模糊搜索" style="width:320px;height:30px;font-size:16px;" required>
    <input type="submit" value="开始搜索" style="height:32px;font-size:16px;margin-left:10px;">
</form>

<br>
<p style="font-size:14px;color:#333;">输入商品名称后点击搜索,跳转列表页面查看匹配商品</p>

</body>
</html>

4.4 展示所有商品

html 复制代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>全部商品预览</title>
</head>
<body align="center" background="./images/lbxx.jpg" style="background-repeat:no-repeat;background-size:cover;background-position:center;min-height:100vh;margin:0;padding:30px;">
<h1>全部商品预览</h1>
<!-- 返回搜索页按钮 -->
<a href="./search.html">
    <button type="button" style="font-size:16px;">返回搜索页面</button>
</a>
<br><hr width="80%">

<!-- ======================
    下方所有商品卡片由后端SQL查询全部商品后拼接生成
====================== -->
<div id="allGoodsBox">
    <!-- 后端循环输出所有商品缩略卡片 -->
</div>
</body>
</html>

4.5 详细信息HTML

这里:

用来接收服务器发来的详细商品数据

html 复制代码
<div style="width:600px;margin:0 auto;background:#fff;padding:20px;border-radius:8px;">
    <!-- 商品大图 -->
    <img id="bigImg" src="" width="450">
    <!--<h2 id="goodsName">商品名称</h2>-->
    <!--<h3 style="color:red;">售价:<span id="price">0.00</span> 元</h3>-->
    <h3>商品介绍:</h3>
    <p id="desc" style="text-align:left;line-height:1.8;font-size:16px;"></p>
</div>

示例HTML:

html 复制代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>商品详细信息</title>
</head>
<body align="center" background="./images/lbxx.jpg" style="background-repeat:no-repeat;background-size:cover;background-position:center;min-height:100vh;margin:0;padding:30px;">
<h1>商品详情</h1>
<!-- 导航返回按钮 -->
<a href="./search.html"><button>返回搜索页</button></a>

<br><hr width="70%">

<!-- ======================
    商品完整数据由后端根据url中的id查询数据库后填充
====================== -->
<div style="width:600px;margin:0 auto;background:#fff;padding:20px;border-radius:8px;">
    <!-- 商品大图 -->
    <img id="bigImg" src="" width="450">
    <!--<h2 id="goodsName">商品名称</h2>-->
    <!--<h3 style="color:red;">售价:<span id="price">0.00</span> 元</h3>-->
    <h3>商品介绍:</h3>
    <p id="desc" style="text-align:left;line-height:1.8;font-size:16px;"></p>
</div>
</body>
</html>

五. 项目中问题总结

5.1 epoll_del_fd 与 close 的顺序问题

**问题:**在主程序模块中的epoll部分,是先 epoll_del_fd (epfd, sockfd);,再 close (sockfd);,还是先 close (sockfd);,再 epoll_del_fd (epfd, sockfd)

**答:**先 epoll_del_fd (epfd, sockfd),再 close (sockfd)

原因: 当直接 close(fd) 时: 内核会自动把该 fd 从 epoll 红黑树中移除,再次使用 epoll_del_fd时会报错

5.2 epoll的高并发问题

**问题:**在 epoll 处理事件时,新发生的事件会一直等待 epoll 处理完之后才会处理,而不会立即处理,还需要等待,这个能叫并发吗?

答:连接并发 :系统同时维持几万、几十万 TCP 连接(绝大多数空闲),这是 epoll 的强项;任务并行 :同一时刻同时执行多段代码(需要多线程 / 多进程 / 多核)。epoll 单线程事件循环逻辑确实是串行分批处理事件

要实现真正的并行,就得使用多线程等

六. 总结

本项目基于 Linux C 语言搭建简易商品查询 Web 系统,采用 epoll 实现并发服务,搭配 SQLite 存储用户与商品数据,前端使用 HTML 完成交互。区分 GET、POST 处理页面跳转与表单提交,实现注册、登录、商品搜索、详情查看功能。项目理清 HTTP 通信与前后端交互逻辑,但存在单线程阻塞、安全薄弱等问题。

相关推荐
不会写DN1 小时前
MySQL 并发插入竞态问题:原子写入实践指南
数据库·mysql
纳兰青华1 小时前
在 PVE 上通过 LXC 部署 Ubuntu 24.04:为 OpenClaw 打造轻量高效的 AI 环境
linux·ai编程
霖霖总总1 小时前
[MongoDB小技巧24]MongoDB $lookup 聚合阶段深度解析:从入门到实战
数据库·mongodb
梦想的旅途22 小时前
企业微信外部群智能自动回复:基于RPA接口的关键词匹配方案
java·开发语言·数据库·机器人·自动化·企业微信
Auv开心2 小时前
Ubuntu 20.04 默认视频播放器(Totem)无法播放 MP4 文件,通常是因为缺少多媒体编解码器。以下是几种解决方法:
linux·ubuntu·音视频
得物技术2 小时前
得物 OceanBase 落地实践
数据库·分布式·架构
程序员cxuan2 小时前
OceanBase 为什么要做 AI 数据库?
数据库·人工智能·大模型·llm·oceanbase
ZhengO_Oz2 小时前
虚拟机CentOS 网络配置【实操】
运维·服务器·网络
Alan_752 小时前
GraphQL 深度解析:为什么它能替代传统 REST API
数据库·后端