一、项目概述
本文将介绍如何基于 C 语言实现一个简易的在线商城 Web 服务器,该服务器具备用户注册、登录、商品搜索、商品详情展示等核心功能,底层采用 SQLite3 数据库存储数据,同时解决了 SQL 注入、URL 解码等常见 Web 开发问题,可作为轻量级在线商城的基础框架。
技术栈
- 编程语言:C 语言
- 网络编程:Socket(TCP/IP)
- 数据库:SQLite3
- Web 协议:HTTP/1.1
- 其他:文件 I/O、URL 解码、SQL 预处理语句
二、核心功能实现
1. 项目整体架构
服务器采用经典的 TCP Socket 监听 - 连接模型,端口绑定 80(HTTP 默认端口),主要处理 GET 请求,核心流程:
- 创建监听 Socket 并设置端口复用
- 循环接收客户端连接
- 解析 HTTP 请求行,提取请求方法和 URL
- 根据 URL 路由到不同业务逻辑(注册 / 登录 / 商品搜索 / 详情)
- 处理完成后返回 HTML / 图片等资源,关闭连接
2. 数据库基础操作
SQLite3 是轻量级嵌入式数据库,本项目使用预处理语句(sqlite3_prepare_v2 + 绑定参数)解决 SQL 注入问题,核心封装了注册和登录两个数据库操作函数。
2.1 用户注册功能
int write_register(char* name, char* pass)
{
sqlite3* db = NULL;
int ret = sqlite3_open("123.db", &db);
if (ret != SQLITE_OK) {
fprintf(stderr, "sqlite3_open %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return 0;
}
sqlite3_stmt* stmt;
// 使用占位符?防止SQL注入
const char* sql = "INSERT INTO user (name, pass) VALUES (?, ?);";
ret = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (ret != SQLITE_OK) {
fprintf(stderr, "sqlite3_prepare_v2 INSERT failed: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return 0;
}
// 绑定参数到占位符
sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, pass, -1, SQLITE_STATIC);
ret = sqlite3_step(stmt);
if (ret != SQLITE_DONE) {
fprintf(stderr, "sqlite3_step INSERT failed: %s\n", sqlite3_errmsg(db));
sqlite3_finalize(stmt);
sqlite3_close(db);
return 0;
}
printf("Register success for user: %s\n", name);
sqlite3_finalize(stmt);
sqlite3_close(db);
return 1;
}
2.2 用户登录验证
int is_denglu_right(char* name, char* pass)
{
sqlite3* db = NULL;
int ret = sqlite3_open("123.db", &db);
if (ret != SQLITE_OK) {
fprintf(stderr, "sqlite3_open %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return 0;
}
int flag = 0;
sqlite3_stmt* stmt;
const char* sql = "SELECT * FROM user WHERE name = ? AND pass = ?;";
ret = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (ret != SQLITE_OK) {
fprintf(stderr, "sqlite3_prepare_v2 SELECT failed: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return 0;
}
sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, pass, -1, SQLITE_STATIC);
ret = sqlite3_step(stmt);
if (ret == SQLITE_ROW) { // 查询到数据则登录成功
flag = 1;
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return flag;
}
3. URL 解码功能
Web 请求中 URL 参数会被编码(如空格转为 +、特殊字符转为 % XX),需解码后才能正确处理:
void url_decode(char* dest, const char* src) {
char c;
while (*src) {
if (*src == '%') {
src++;
if (*src && *(src+1)) {
// 解析十六进制编码
c = ( ( (*src >= 'A') ? (toupper(*src) - 'A' + 10) : (*src - '0') ) << 4 )
+ ( (*(src+1) >= 'A') ? (toupper(*(src+1)) - 'A' + 10) : (*(src+1) - '0') );
*dest++ = c;
src += 2;
}
} else if (*src == '+') {
*dest++ = ' '; // +号还原为空格
src++;
} else {
*dest++ = *src++;
}
}
*dest = '\0';
}
4. 商品搜索功能
解析 /search 请求中的关键词,解码后查询商品表,动态生成搜索结果 HTML:
else if (strncmp(url, "/search?", 8) == 0) {
char* name_ptr = strstr(url, "username=");
if (name_ptr) {
char encoded_name[128] = {0};
char decoded_name[128] = {0};
sscanf(name_ptr, "username=%s", encoded_name);
url_decode(decoded_name, encoded_name); // 解码搜索关键词
FILE* fp = fopen("05.html", "w");
if (NULL == fp) { perror("fopen 05.html"); close(conn); continue; }
// 生成搜索结果HTML头部
fprintf(fp, "<!DOCTYPE html>\n<html>\n<head>\n<meta charset='utf-8'>\n<title>Search Results</title>\n</head>\n<body>\n");
fprintf(fp, "<h1>Search results for: %s</h1>\n", decoded_name);
sqlite3* db = NULL;
if (sqlite3_open("123.db", &db) == SQLITE_OK) {
char* errmsg = NULL;
char sql_cmd[512] = {0};
// 模糊查询商品名称
sprintf(sql_cmd, "select goods_id,goods_name,goods_img from goods where goods_name like '%s%%';", decoded_name);
ret = sqlite3_exec(db, sql_cmd, detail3, fp, &errmsg);
if (ret != SQLITE_OK) {
fprintf(stderr, "exec search [%s] msg:%s\n", sql_cmd, errmsg);
sqlite3_free(errmsg);
}
sqlite3_close(db);
}
fprintf(fp, "</body>\n</html>");
fclose(fp);
send_file(conn, "./05.html", FILE_HTML);
} else {
send_file(conn, "./03.html", FILE_HTML);
}
}
5. HTTP 响应封装
封装了文件大小计算、响应头发送、文件发送函数,支持 HTML/PNG/JPG/ICO 等类型文件:
// 计算文件大小
long file_size(char* file)
{
int fd = open(file, O_RDONLY);
if (-1 == fd) {
perror("file size open error");
fprintf(stderr, "filename is %s\n", file);
return -1;
}
long size = lseek(fd, 0, SEEK_END);
close(fd);
return size;
}
// 发送HTTP响应头
int send_head(int conn, char* file, FILE_TYPE type)
{
char buf[256] = {0};
long size = file_size(file);
if (size < 0) return -1;
char* http_cmd[7] = {NULL};
http_cmd[0] = "HTTP/1.1 200 OK\r\n";
http_cmd[1] = "Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n";
switch (type) {
case FILE_HTML: http_cmd[2] = "Content-Type: text/html;charset=utf-8\r\n"; break;
case FILE_PNG: http_cmd[2] = "Content-Type: image/png\r\n"; break;
case FILE_JPG: http_cmd[2] = "Content-Type: image/jpeg\r\n"; break;
case FILE_ICO: http_cmd[2] = "Content-Type: image/x-icon\r\n"; break;
default: http_cmd[2] = "Content-Type: text/html;charset=utf-8\r\n";
}
http_cmd[3] = buf;
sprintf(buf, "Content-Length: %ld\r\n", size);
http_cmd[4] = "Connection: close\r\n";
http_cmd[5] = "Server: MYWEBSer\r\n";
http_cmd[6] = "Content-Language: zh-CN\r\n\r\n";
for (int i = 0; i < 7; i++) {
send(conn, http_cmd[i], strlen(http_cmd[i]), 0);
}
return 0;
}
// 发送文件内容
int send_file(int conn, char* filename, FILE_TYPE type)
{
if (send_head(conn, filename, type) != 0) {
dprintf(conn, "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
return 1;
}
int fd = open(filename, O_RDONLY);
if (-1 == fd) {
perror("open file to send");
return 1;
}
char buf[4096] = {0};
int rd_ret;
while ((rd_ret = read(fd, buf, sizeof(buf))) > 0) {
send(conn, buf, rd_ret, 0);
}
close(fd);
return 0;
}
三、服务器主流程
int main(int argc, char** argv)
{
int listfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listfd) { perror("socket"); return 1; }
// 端口复用
int opt = 1;
setsockopt(listfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in ser, cli;
bzero(&ser, sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(80);
ser.sin_addr.s_addr = INADDR_ANY;
if (bind(listfd, (SA)&ser, sizeof(ser)) == -1) { perror("bind"); return 1; }
if (listen(listfd, 128) == -1) { perror("listen"); return 1; }
printf("Server is running on port 80...\n");
socklen_t len = sizeof(cli);
while (1) {
int conn = accept(listfd, (SA)&cli, &len);
if (-1 == conn) {
if (errno == EINTR) continue;
perror("accept");
continue;
}
char buf[1024] = {0};
int ret = recv(conn, buf, sizeof(buf) - 1, 0);
if (ret <= 0) { close(conn); continue; }
char method[16], url[512], ver[16];
sscanf(buf, "%s %s %s", method, url, ver);
// 路由分发
if (strcmp(method, "GET") != 0) {
dprintf(conn, "HTTP/1.1 501 Not Implemented\r\nConnection: close\r\n\r\n");
} else if (strcmp(url, "/") == 0) {
send_file(conn, "./03.html", FILE_HTML);
} else if (strncmp(url, "/register?", 10) == 0) {
// 注册逻辑
} else if (strncmp(url, "/login?", 7) == 0) {
// 登录逻辑
} else if (strncmp(url, "/search?", 8) == 0) {
// 搜索逻辑
} else if (strncmp(url, "/detail_", 8) == 0) {
// 详情逻辑
} else if (strstr(url, ".html")) {
send_file(conn, url + 1, FILE_HTML);
} else {
dprintf(conn, "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
}
close(conn);
}
close(listfd);
return 0;
}
四、编译与运行
- 编译命令
需链接 SQLite3 库,编译命令如下:
gcc 04ser.c -o web_shop -lsqlite3 -Wall
- 运行前准备
创建 SQLite3 数据库文件 123.db,并初始化表结构:
-- 用户表
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
pass TEXT NOT NULL
);
-- 商品表
CREATE TABLE goods (
goods_id INTEGER PRIMARY KEY,
goods_name TEXT NOT NULL,
goods_img TEXT,
goods_desc TEXT
);
准备前端 HTML 文件(03.html/04.html/06.html/07.html)及商品图片资源。
-
启动服务器
./web_shop
浏览器访问 http://127.0.0.1 即可进入商城首页。
五、核心优化点
- 防 SQL 注入:全程使用 SQLite3 预处理语句(sqlite3_prepare_v2 + 绑定参数),避免拼接 SQL 字符串。
- URL 解码:处理前端传递的编码参数,保证中文 / 特殊字符正常显示。
- 资源类型适配:支持 HTML、PNG、JPG、ICO 等多种文件类型的 HTTP 响应。
- 错误处理:完善的文件操作、数据库操作错误处理,提升稳定性。
- 端口复用:设置SO_REUSEADDR选项,避免服务器重启时端口占用问题。