一、项目概述
本文将介绍如何基于 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; }四、编译与运行
1. 编译命令
需链接 SQLite3 库,编译命令如下:
gcc 04ser.c -o web_shop -lsqlite3 -Wall2. 运行前准备
创建 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)及商品图片资源。
3. 启动服务器
./web_shop浏览器访问 http://127.0.0.1 即可进入商城首页。
五、核心优化点
- 防 SQL 注入:全程使用 SQLite3 预处理语句(sqlite3_prepare_v2 + 绑定参数),避免拼接 SQL 字符串。
- URL 解码:处理前端传递的编码参数,保证中文 / 特殊字符正常显示。
- 资源类型适配:支持 HTML、PNG、JPG、ICO 等多种文件类型的 HTTP 响应。
- 错误处理:完善的文件操作、数据库操作错误处理,提升稳定性。
- 端口复用:设置SO_REUSEADDR选项,避免服务器重启时端口占用问题。