#C语言 #Socket #HTTP #Linux网络编程 #cJSON #SQLite3
前言
本篇博客带来两个 C 语言 Socket 实战项目,全部基于 Linux 原生 Socket API,不使用 curl、nginx 等第三方网络库。
- HTTP 客户端:TCP Socket 手动组装 HTTP GET 请求,调用天气 API 获取数据,cJSON 解析天气 JSON;
- HTTP 服务端:手写简易 Web 服务器,结合 SQLite3 实现网页登录注册、元器件模糊搜索、静态页面与图片返回、302 重定向。
适合 Linux C 网络编程课程设计,学习 Socket、HTTP 协议、URL 解码、JSON 解析、SQLite 数据库 C 接口。
运行环境:Linux (Ubuntu)、gcc 编译器,依赖库:
cJSON、sqlite3
项目总览
项目 1:HTTP 客户端‑天气抓取
功能:
- socket 创建 TCP 客户端,连接天气 API 服务器
- 手动构造 HTTP/1.1 GET 请求报文发送
- 循环 recv 接收响应,保存完整 HTTP 报文至本地
weather.json - 跳过 HTTP 响应头,cJSON 解析 JSON,控制台打印天气信息
项目 2:HTTP 服务端‑元器件查询 Web 系统
功能:
- Socket 实现 HTTP 服务器,监听 8080 端口,处理浏览器 GET/POST 请求
- 返回静态 HTML 页面、读取图片资源回显浏览器,404 错误处理
- URL 解码,解析 POST 表单数据
- SQLite 数据库:用户登录注册、元器件模糊检索,动态拼接 HTML 返回页面
- 支持 302 页面重定向
注意:服务端为单进程模型,同一时间只能处理一个客户端请求,用于学习演示,不可直接用于生产。
环境准备
# 安装cJSON
git clone https://github.com/DaveGamble/cJSON.git
cd cJSON && mkdir build && cd build
cmake ..
make && sudo make install
# sqlite3开发库
sudo apt install libsqlite3-dev
项目一:C 语言 Socket HTTP 客户端,抓取解析天气数据
头文件 head.h
#ifndef __HEAD_H__
#define __HEAD_H__
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <cJSON.h>
#endif
weather_client.c 完整源码
#include "head.h"
#define SER_PORT 80
#define SER_IP "103.205.5.206"
//初始化tcp客户端,建立连接
int init_tcp_cli()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0)
{
perror("socket error");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SER_PORT);
seraddr.sin_addr.s_addr = inet_addr(SER_IP);
int ret = connect(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
if(ret <0)
{
perror("connect error");
return -1;
}
return sockfd;
}
//发送http get请求
int send_http_request(int sockfd,char *city_id)
{
char buf[1024]={0};
snprintf(buf,sizeof(buf),
"GET /?app=weather.today&weaId=%s&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json HTTP/1.1\r\n"
"Host: api.k780.com\r\n"
"Connection: close\r\n"
"\r\n",city_id);
ssize_t size =send(sockfd,buf,strlen(buf),0);
if(size<0)
{
perror("send error");
return -1;
}
return 0;
}
//接收响应保存文件
int recv_save_file(int sockfd,char *filename)
{
FILE *fp = fopen(filename,"w");
if(NULL == fp)
{
perror("open error");
return -1;
}
char buff[1024]={0};
ssize_t n;
while((n=recv(sockfd,buff,sizeof(buff),0))>0)
{
if(n<0)
{
perror("recv error");
break;
}
fwrite(buff,1,n,fp);
memset(buff,0,sizeof(buff));
}
fclose(fp);
return 0;
}
//解析json提取天气数据
int parse_weather_json(char *filename)
{
FILE *fp = fopen(filename,"r");
if(NULL == fp)
{
perror("open json");
return -1;
}
fseek(fp,0,SEEK_END);
long filesize = ftell(fp);
fseek(fp,0,SEEK_SET);
char *json_buf = (char*)malloc(filesize+1);
fread(json_buf,1,filesize,fp);
json_buf[filesize] = '\0';
fclose(fp);
//跳过http响应头,找到json的{起始位置
char *json_start = strstr(json_buf,"{");
if(json_start == NULL)
{
printf("未找到json数据\n");
free(json_buf);
return -1;
}
cJSON *root = cJSON_Parse(json_start);
if(root == NULL)
{
printf("json解析失败\n");
free(json_buf);
return -1;
}
cJSON *result = cJSON_GetObjectItem(root,"result");
if(NULL == result)
{
printf("result不存在\n");
cJSON_Delete(root);
free(json_buf);
return -1;
}
printf("=======天气解析结果=======\n");
printf("城市:%s\n", cJSON_GetStringValue(cJSON_GetObjectItem(result,"citynm")));
printf("时间:%s\n", cJSON_GetStringValue(cJSON_GetObjectItem(result,"days")));
printf("星期:%s\n", cJSON_GetStringValue(cJSON_GetObjectItem(result,"week")));
printf("温度:%s\n", cJSON_GetStringValue(cJSON_GetObjectItem(result,"temp_curr")));
printf("湿度:%s\n", cJSON_GetStringValue(cJSON_GetObjectItem(result,"humidity")));
printf("风向:%s\n", cJSON_GetStringValue(cJSON_GetObjectItem(result,"wind")));
printf("风力:%s\n", cJSON_GetStringValue(cJSON_GetObjectItem(result,"winp")));
cJSON_Delete(root);
free(json_buf);
return 0;
}
int main(void)
{
char city_buf[128]={0};
printf("请输入城市ID:");
fgets(city_buf,sizeof(city_buf),stdin);
//去除fgets读取的换行符
for(int i = 0;city_buf[i]!='\0';++i)
{
if(city_buf[i]=='\n')
{
city_buf[i]='\0';
break;
}
}
int sockfd = init_tcp_cli();
if(sockfd<0)
{
return -1;
}
send_http_request(sockfd,city_buf);
recv_save_file(sockfd,"weather.json");
close(sockfd);
parse_weather_json("weather.json");
return 0;
}
编译运行:
gcc weather_client.c -o weather -lcjson
./weather
请输入城市ID:101010100
核心知识点
- HTTP 本质是 TCP 之上规定格式的字符串,
\r\n作为行结束标记,空行代表请求头结束; - 服务器返回内容包含 HTTP 响应头 + JSON,必须过滤头部,否则 cJSON 解析报错;
- cJSON 使用完必须调用
cJSON_Delete()释放,malloc 内存要 free,防止内存泄漏; - fgets 读取输入自带换行符,需要手动截断,避免参数错误。
待优化点
- 硬编码 IP,没有 DNS 域名解析;
- 直接写入本地磁盘文件,可以改成内存直接处理接收数据;
- 不支持 HTTPS;
- 缺少输入合法性校验。
项目二:手写 HTTP 服务器,元器件 Web 管理查询系统
web_server.c 完整源码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sqlite3.h>
#define PORT 8080
#define BUF_SIZE 4096
int url_decode(char *dest, const char *src)
{
char *d = dest;
const char *s = src;
while (*s)
{
if (*s == '%')
{
if (*(s+1) && *(s+2))
{
char hex[3] = {s[1], s[2], 0};
*d++ = (char)strtol(hex, NULL, 16);
s += 3;
}
else
*d++ = *s++;
}
else if (*s == '+')
{
*d++ = ' ';
s++;
}
else
{
*d++ = *s++;
}
}
*d = '\0';
return d - dest;
}
void send_html_file(int cfd, const char *filename)
{
FILE *fp = fopen(filename, "r");
if(!fp)
{
const char *resp = "HTTP/1.1 404 Not Found\r\nContent-Type:text/html;charset=utf-8\r\n\r\n<h1>404 页面不存在</h1>";
send(cfd, resp, strlen(resp), 0);
return;
}
fseek(fp,0,SEEK_END);
long fsize = ftell(fp);
rewind(fp);
char *content = malloc(fsize + 1);
fread(content, 1, fsize, fp);
content[fsize] = 0;
fclose(fp);
char header[256];
sprintf(header, "HTTP/1.1 200 OK\r\nContent-Type:text/html;charset=utf-8\r\nContent-Length:%ld\r\n\r\n",fsize);
send(cfd, header, strlen(header),0);
send(cfd, content, fsize,0);
free(content);
}
void send_img_file(int cfd, char *path)
{
FILE *fp = fopen(path, "rb");
if(!fp)
{
const char *resp = "HTTP/1.1 404 Not Found\r\nContent-Type:text/html\r\n\r\n404 img not found";
send(cfd, resp, strlen(resp),0);
return;
}
fseek(fp,0,SEEK_END);
long fsize = ftell(fp);
rewind(fp);
unsigned char *buf = malloc(fsize);
fread(buf,1,fsize,fp);
fclose(fp);
char header[256];
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type:image/jpeg\r\nContent-Length:%ld\r\n\r\n",fsize);
send(cfd, header, strlen(header),0);
send(cfd, buf, fsize,0);
free(buf);
}
void parse_post_data(char *req_buf, char *post_data)
{
char *p = strstr(req_buf, "\r\n\r\n");
if(p)
{
p +=4;
strcpy(post_data, p);
}
}
void handle_login(int cfd, char *post_data)
{
char user[64]={0},pwd[64]={0};
sscanf(post_data,"username=%[^&]&password=%s",user,pwd);
sqlite3 *db;
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open("component.db",&db);
if(rc != SQLITE_OK)
{
const char *resp = "HTTP/1.1 200 OK\r\nContent-Type:text/html;charset=utf-8\r\n\r\n数据库打开失败";
send(cfd,resp,strlen(resp),0);
sqlite3_close(db);
return;
}
char sql[256];
sprintf(sql,"SELECT * FROM user WHERE username='%s' AND password='%s';",user,pwd);
rc = sqlite3_prepare_v2(db,sql,-1,&stmt,NULL);
int find = 0;
if(rc == SQLITE_OK)
{
if(sqlite3_step(stmt) == SQLITE_ROW)
{
find = 1;
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
if(find)
{
const char *resp = "HTTP/1.1 302 Redirect\r\nLocation:/search.html\r\n\r\n";
send(cfd,resp,strlen(resp),0);
}
else
{
const char *resp = "HTTP/1.1 200 OK\r\nContent-Type:text/html;charset=utf-8\r\n\r\n账号或密码错误!<a href='/login'>返回登录</a>";
send(cfd,resp,strlen(resp),0);
}
}
void handle_do_register(int cfd, char *post_data)
{
char user[64]={0},pwd[64]={0};
sscanf(post_data,"username=%[^&]&password=%s",user,pwd);
sqlite3 *db;
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open("component.db",&db);
if(rc != SQLITE_OK)
{
const char *resp = "HTTP/1.1 200 OK\r\nContent-Type:text/html;charset=utf-8\r\n\r\n数据库打开失败";
send(cfd,resp,strlen(resp),0);
sqlite3_close(db);
return;
}
char sql[256];
sprintf(sql,"INSERT INTO user(username,password) VALUES ('%s','%s');",user,pwd);
rc = sqlite3_prepare_v2(db,sql,-1,&stmt,NULL);
if(rc == SQLITE_OK)
{
rc = sqlite3_step(stmt);
if(rc == SQLITE_DONE)
{
const char *resp = "HTTP/1.1 302 Redirect\r\nLocation:/login\r\n\r\n";
send(cfd,resp,strlen(resp),0);
}
else
{
const char *resp = "HTTP/1.1 200 OK\r\nContent-Type:text/html;charset=utf-8\r\n\r\n注册失败,用户名已存在!<a href='/register'>返回注册</a>";
send(cfd,resp,strlen(resp),0);
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
}
void handle_search(int cfd, char *post_data)
{
char key_encoded[64]={0};
char key[64]={0};
sscanf(post_data,"key=%s",key_encoded);
url_decode(key, key_encoded);
sqlite3 *db;
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open("component.db",&db);
char html_buf[8192];
strcpy(html_buf,"<html><head><meta charset='utf-8'><title>器件搜索结果</title></head><body><h2>元器件搜索结果</h2>");
if(rc == SQLITE_OK)
{
const char *sql = "SELECT goods_name, params, price, goods_img FROM goods WHERE goods_name LIKE ?;";
rc = sqlite3_prepare_v2(db,sql,-1,&stmt,NULL);
if(rc == SQLITE_OK)
{
char like_str[128];
sprintf(like_str,"%%%s%%", key);
sqlite3_bind_text(stmt, 1, like_str, -1, SQLITE_STATIC);
int has_result = 0;
while(sqlite3_step(stmt) == SQLITE_ROW)
{
has_result = 1;
const char *name = (const char*)sqlite3_column_text(stmt,0);
const char *param = (const char*)sqlite3_column_text(stmt,1);
const char *price = (const char*)sqlite3_column_text(stmt,2);
const char *img = (const char*)sqlite3_column_text(stmt,3);
char tmp[1024];
sprintf(tmp,
"<div style='margin:20px; border:1px solid #ccc; padding:10px'>"
"<p><b>器件名称:</b>%s</p>"
"<p><b>器件参数:</b>%s</p>"
"<p><b>参考价格:</b>%s 元</p>"
"<img src='img/%s' width='300'></div>",
name, param, price, img);
strcat(html_buf,tmp);
}
if(!has_result)
{
strcat(html_buf,"<p>未查询到该元器件</p>");
}
}
sqlite3_finalize(stmt);
}
strcat(html_buf,"<p><a href='/search.html'>返回搜索页</a></p></body></html>");
sqlite3_close(db);
char header[256];
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type:text/html;charset=utf-8\r\nContent-Length:%ld\r\n\r\n",strlen(html_buf));
send(cfd,header,strlen(header),0);
send(cfd,html_buf,strlen(html_buf),0);
}
void handle_all_goods(int cfd)
{
sqlite3 *db;
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open("component.db",&db);
char html_buf[8192];
strcpy(html_buf,"<html><head><meta charset='utf-8'><title>全部元器件</title></head><body><h2>全部元器件列表</h2>");
if(rc == SQLITE_OK)
{
const char *sql = "SELECT goods_name, params, price, goods_img FROM goods;";
rc = sqlite3_prepare_v2(db,sql,-1,&stmt,NULL);
if(rc == SQLITE_OK)
{
while(sqlite3_step(stmt) == SQLITE_ROW)
{
const char *name = (const char*)sqlite3_column_text(stmt,0);
const char *param = (const char*)sqlite3_column_text(stmt,1);
const char *price = (const char*)sqlite3_column_text(stmt,2);
const char *img = (const char*)sqlite3_column_text(stmt,3);
char tmp[1024];
sprintf(tmp,
"<div style='margin:20px; border:1px solid #ccc; padding:10px'>"
"<p><b>器件名称:</b>%s</p>"
"<p><b>器件参数:</b>%s</p>"
"<p><b>参考价格:</b>%s 元</p>"
"<img src='img/%s' width='300'></div>",
name, param, price, img);
strcat(html_buf,tmp);
}
}
sqlite3_finalize(stmt);
}
strcat(html_buf,"<p><a href='/search.html'>返回搜索页</a></p></body></html>");
sqlite3_close(db);
char header[256];
sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type:text/html;charset=utf-8\r\nContent-Length:%ld\r\n\r\n",strlen(html_buf));
send(cfd,header,strlen(header),0);
send(cfd,html_buf,strlen(html_buf),0);
}
int main()
{
int lfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(PORT);
saddr.sin_addr.s_addr = INADDR_ANY;
bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));
listen(lfd,5);
printf("服务器启动:http://127.0.0.1:%d\n",PORT);
while(1)
{
int cfd = accept(lfd,NULL,NULL);
char buf[BUF_SIZE]={0};
recv(cfd,buf,BUF_SIZE,0);
char post_data[512]={0};
parse_post_data(buf,post_data);
if(strstr(buf,"GET /login "))
{
send_html_file(cfd,"login.html");
}
else if(strstr(buf,"GET /register "))
{
send_html_file(cfd,"register.html");
}
else if(strstr(buf,"POST /do_register "))
{
handle_do_register(cfd,post_data);
}
else if(strstr(buf,"POST /login "))
{
handle_login(cfd,post_data);
}
else if(strstr(buf,"POST /search "))
{
handle_search(cfd,post_data);
}
else if(strstr(buf,"GET /all_goods "))
{
handle_all_goods(cfd);
}
else if(strstr(buf,"GET /img/"))
{
char img_path[128] = "./img/";
char *p = strstr(buf,"GET /img/") + 9;
char *end = strstr(p," ");
*end = '\0';
strcat(img_path,p);
send_img_file(cfd,img_path);
}
else if(strstr(buf,"GET /search.html"))
{
send_html_file(cfd,"search.html");
}
else
{
const char *resp = "HTTP/1.1 404 Not Found\r\nContent-Type:text/html;charset=utf-8\r\n\r\n<h1>404</h1>";
send(cfd,resp,strlen(resp),0);
}
close(cfd);
}
close(lfd);
return 0;
}
编译命令:
gcc web_server.c -o web_server -lsqlite3
./web_server
数据库建表 SQL (component.db)
CREATE TABLE user(
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE,
password TEXT
);
CREATE TABLE goods(
id INTEGER PRIMARY KEY AUTOINCREMENT,
goods_name TEXT,
params TEXT,
price TEXT,
goods_img TEXT
);
项目目录结构
├── web_server
├── login.html
├── register.html
├── search.html
├── img/ #存放元器件图片
└── component.db
核心知识点
- 请求报文
\r\n\r\n分割 HTTP 头部和 POST 表单 body; - URL 解码:浏览器中文参数会编码为 % XX 格式,需要解码还原中文;
- SQLite3 预编译语句
sqlite3_prepare_v2、sqlite3_bind_text防止简单 SQL 注入; - 302 重定向响应头
Location:实现页面跳转; - 动态拼接 HTML 字符串返回浏览器,模拟简易 CGI;
- 图片需要二进制
rb模式读取,设置对应Content-Type:image/jpeg。
现存缺陷
- 单进程阻塞模型,无法并发处理多个浏览器;
- 没有会话 Cookie,登录状态不保存;
- SQL 仍存在部分注入风险;
- recv 一次读取缓冲区,大请求会读取不全。
两个项目对比总结
表格
| 项目 | 角色 | 主要技术点 |
|---|---|---|
| 天气抓取 | HTTP 客户端 | socket connect、手动 GET 报文、recv 接收、cJSON 解析 JSON |
| 元器件 Web 系统 | HTTP 服务端 | socket bind‑listen‑accept、GET/POST 路由、URL 解码、SQLite、动态 HTML、302 重定向 |
两个项目完整覆盖了HTTP 客户端与服务端基础原理,没有使用高级封装库,可以直观理解 HTTP 协议就是 TCP 之上格式化字符串,非常适合课程设计与网络编程入门学习。