Linux入门DAY38(手写 HTTP 客户端抓取天气 + 简易 HTTP 服务器实现元器件 Web 系统)

#C语言 #Socket #HTTP #Linux网络编程 #cJSON #SQLite3

前言

本篇博客带来两个 C 语言 Socket 实战项目,全部基于 Linux 原生 Socket API,不使用 curl、nginx 等第三方网络库

  1. HTTP 客户端:TCP Socket 手动组装 HTTP GET 请求,调用天气 API 获取数据,cJSON 解析天气 JSON;
  2. HTTP 服务端:手写简易 Web 服务器,结合 SQLite3 实现网页登录注册、元器件模糊搜索、静态页面与图片返回、302 重定向。

适合 Linux C 网络编程课程设计,学习 Socket、HTTP 协议、URL 解码、JSON 解析、SQLite 数据库 C 接口。

运行环境:Linux (Ubuntu)、gcc 编译器,依赖库:cJSONsqlite3

项目总览

项目 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

核心知识点

  1. HTTP 本质是 TCP 之上规定格式的字符串,\r\n作为行结束标记,空行代表请求头结束;
  2. 服务器返回内容包含 HTTP 响应头 + JSON,必须过滤头部,否则 cJSON 解析报错;
  3. cJSON 使用完必须调用cJSON_Delete()释放,malloc 内存要 free,防止内存泄漏;
  4. 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

核心知识点

  1. 请求报文\r\n\r\n分割 HTTP 头部和 POST 表单 body;
  2. URL 解码:浏览器中文参数会编码为 % XX 格式,需要解码还原中文;
  3. SQLite3 预编译语句sqlite3_prepare_v2sqlite3_bind_text防止简单 SQL 注入;
  4. 302 重定向响应头Location:实现页面跳转;
  5. 动态拼接 HTML 字符串返回浏览器,模拟简易 CGI;
  6. 图片需要二进制rb模式读取,设置对应Content-Type:image/jpeg

现存缺陷

  1. 单进程阻塞模型,无法并发处理多个浏览器;
  2. 没有会话 Cookie,登录状态不保存;
  3. SQL 仍存在部分注入风险;
  4. recv 一次读取缓冲区,大请求会读取不全。

两个项目对比总结

表格

项目 角色 主要技术点
天气抓取 HTTP 客户端 socket connect、手动 GET 报文、recv 接收、cJSON 解析 JSON
元器件 Web 系统 HTTP 服务端 socket bind‑listen‑accept、GET/POST 路由、URL 解码、SQLite、动态 HTML、302 重定向

两个项目完整覆盖了HTTP 客户端与服务端基础原理,没有使用高级封装库,可以直观理解 HTTP 协议就是 TCP 之上格式化字符串,非常适合课程设计与网络编程入门学习。

相关推荐
fangjianj1 小时前
windows环境反弹shell
linux·windows·web安全
明月不会写代码2 小时前
Ubuntu 下 Kitty 终端配置记录
linux·ubuntu
weipt2 小时前
利用wsl给windows电脑上安装一个ubuntu
linux·运维·ubuntu
xiaoye-duck2 小时前
《Linux 网络编程》深入理解 TCP 协议(二):序号、确认应答与流量控制机制详解
linux·网络·tcp
楚疏笃2 小时前
linux下给OpenCloudOS根目录扩容
linux·运维·服务器
dog2502 小时前
网络的时延抖动
linux·运维·网络
阿钱真强道2 小时前
01 嵌入式操作系统 | 课程导论与虚拟机安装 Ubuntu
linux·ubuntu·嵌入式·虚拟机
程序员-Benothing3 小时前
Linux 软件包管理:yum / apt / dnf 命令使用全解析
linux·运维·服务器
GeW3 小时前
拿RHCE证书只是起点:这样备考,顺便把真实运维能力也练了
linux