目录
[一. 项目背景](#一. 项目背景)
[二. 项目流程图](#二. 项目流程图)
[三. 分模块讲解](#三. 分模块讲解)
[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 接收请求报文)
[(1) 初步解析](#(1) 初步解析)
[(2) 解析注册/登录时报文实体主体](#(2) 解析注册/登录时报文实体主体)
[(3) 解析搜索数据(商品)时的请求报文](#(3) 解析搜索数据(商品)时的请求报文)
[(4) 解析搜索某个商品的详细数据报文](#(4) 解析搜索某个商品的详细数据报文)
[(1) 发送响应报文头部](#(1) 发送响应报文头部)
[(2) 发送文件资源](#(2) 发送文件资源)
[(3) 发送响应报文](#(3) 发送响应报文)
[3.3 数据库模块](#3.3 数据库模块)
[3.3.1 模块作用](#3.3.1 模块作用)
[四. 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 通信与前后端交互逻辑,但存在单线程阻塞、安全薄弱等问题。