学习笔记,C/C++的项目学习与巩固
前言
在学习编程语言的过程中,理论知识的掌握是基础,而实践则是提升技能的关键。尽管我们已经回顾了 C语言基础 与 C++ 的基础语法,但单靠理论往往难以留下深刻的印象。因此,为了巩固我们的知识并提升编程能力,深入研究一些优秀的开源项目是一个极佳的选择。
通过阅读和分析这些项目,我们不仅可以加深对 C/C++ 语言特性的理解,还能够学习到在实际开发中常用的设计模式和编程技巧。这些开源库通常涵盖了丰富的功能,涉及多线程、进程间通信、网络编程等高级主题,让我们可以深入探索现代软件开发的实践应用。
接下来我推荐几个经典入门学习项目大家参考参考。
一、C语言的回顾与扩展
我们需要回顾的是C语言函数的定义、指针操作、内存管理、结构体、文件IO等语法,这里我推荐一个简单的项目 Tinyhttpd
Tinyhttpd 是一个由 J. David Blackstone 于 1999 年编写的超轻量级 HTTP 服务器项目,代码量仅约 500 行,是学习 C 语言网络编程和 HTTP 协议原理的经典案例。
通过这里项目我们不仅可以回顾复习到之前看过的C语言基础,还能扩展学习到的线程进程管理以及Socket API使用。
由于源码也不多,这里是加上注释之后的代码:
/* This program compiles for Sparc Solaris 2.6.
* To compile for Linux:
* 1) Comment out the #include <pthread.h> line.
* 2) Comment out the line that defines the variable newthread.
* 3) Comment out the two lines that run pthread_create().
* 4) Uncomment the line that runs accept_request().
* 5) Remove -lsocket from the Makefile.
*/
#include <stdio.h> // 引入标准输入输出库
#include <sys/socket.h> // 引入 socket 的相关函数和结构体
#include <sys/types.h> // 引入数据类型定义
#include <netinet/in.h> // 引入互联网地址族的定义
#include <arpa/inet.h> // 引入互联网相关函数
#include <unistd.h> // 提供对 POSIX 操作系统 API 的访问
#include <ctype.h> // 提供字符处理函数
#include <strings.h> // 提供字符串处理函数
#include <string.h> // 提供字符串处理函数
#include <sys/stat.h> // 提供文件状态的宏和函数
#include <pthread.h> // 提供线程相关的函数和结构
#include <sys/wait.h> // 提供对进程控制的支持
#include <stdlib.h> // 提供常用函数,包括动态内存分配
#include <stdint.h> // 提供标准整数类型定义
#define ISspace(x) isspace((int)(x)) // 定义宏,检查字符是否为空白字符
#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n" // 定义服务器字符串
#define STDIN 0 // 标准输入文件描述符
#define STDOUT 1 // 标准输出文件描述符
#define STDERR 2 // 标准错误文件描述符
// 函数声明
void accept_request(void *);
void bad_request(int);
void cat(int, FILE *);
void cannot_execute(int);
void error_die(const char *);
void execute_cgi(int, const char *, const char *, const char *);
int get_line(int, char *, int);
void headers(int, const char *);
void not_found(int);
void serve_file(int, const char *);
int startup(u_short *);
void unimplemented(int);
/**********************************************************************/
/* A request has caused a call to accept() on the server port to
* return. Process the request appropriately.
* Parameters: the socket connected to the client */
/**********************************************************************/
void accept_request(void *arg) // 接受请求的函数
{
int client = (intptr_t)arg; // 将参数转为客户端 socket 描述符
char buf[1024]; // 定义缓冲区,用于存储请求数据
size_t numchars; // 请求的字符数
char method[255]; // 存储请求方法(GET/POST等)
char url[255]; // 存储请求的URL
char path[512]; // 存储文件路径
size_t i, j; // 循环变量
struct stat st; // 用于存储文件状态信息的结构体
int cgi = 0; // 是否为CGI请求的标志
char *query_string = NULL; // 查询字符串指针
numchars = get_line(client, buf, sizeof(buf)); // 从客户端读取一行请求
i = 0;
j = 0; // 初始化循环变量
// 第一步解析请求方法
while (!ISspace(buf[i]) && (i < sizeof(method) - 1)) // 找到空白字符为止
{
method[i] = buf[i]; // 将请求方法复制到数组中
i++;
}
j = i; // 记录方法的结束位置
method[i] = '\0'; // 结束方法字符串
// 检查请求方法是否合法
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
unimplemented(client); // 返回501未实现错误
return; // 退出函数
}
if (strcasecmp(method, "POST") == 0) // 如果请求方法为POST
cgi = 1; // 设置cgi标志
// 第二步解析URL
i = 0;
while (ISspace(buf[j]) && (j < numchars)) // 跳过空白字符
j++;
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars)) // 复制URL
{
url[i] = buf[j];
i++;
j++;
}
url[i] = '\0'; // 结束URL字符串
// 第三步处理GET请求中的查询字符串
if (strcasecmp(method, "GET") == 0)
{
query_string = url; // 指向URL
while ((*query_string != '?') && (*query_string != '\0')) // 查找问号
query_string++;
if (*query_string == '?') // 如果找到问号
{
cgi = 1; // 设置cgi标志
*query_string = '\0'; // 将问号替换为结束符
query_string++; // 指向查询字符串
}
}
// 第四步构造文件路径
sprintf(path, "htdocs%s", url); // 将请求的URL拼接到路径前
if (path[strlen(path) - 1] == '/') // 如果路径以斜杠结尾
strcat(path, "index.html"); // 加载默认的index.html
if (stat(path, &st) == -1)
{ // 检查文件状态
while ((numchars > 0) && strcmp("\n", buf)) // 读取并丢弃请求的头部
numchars = get_line(client, buf, sizeof(buf));
not_found(client); // 返回404未找到错误
}
else
{
if ((st.st_mode & S_IFMT) == S_IFDIR) // 如果是目录
strcat(path, "/index.html"); // 加载index.html
if ((st.st_mode & S_IXUSR) ||
(st.st_mode & S_IXGRP) ||
(st.st_mode & S_IXOTH)) // 检查是否可执行
cgi = 1; // 设置cgi标志
if (!cgi) // 如果不是cgi请求
serve_file(client, path); // 发送文件
else
execute_cgi(client, path, method, query_string); // 执行cgi
}
close(client); // 关闭客户端连接
}
/**********************************************************************/
/* Inform the client that a request it has made has a problem.
* Parameters: client socket */
/**********************************************************************/
void bad_request(int client) // 处理错误请求
{
char buf[1024]; // 定义缓冲区
sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n"); // 返回400错误
send(client, buf, sizeof(buf), 0); // 发送响应
sprintf(buf, "Content-type: text/html\r\n"); // 设置内容类型
send(client, buf, sizeof(buf), 0);
sprintf(buf, "\r\n"); // 结束头部
send(client, buf, sizeof(buf), 0);
sprintf(buf, "<P>Your browser sent a bad request, "); // 错误信息
send(client, buf, sizeof(buf), 0);
sprintf(buf, "such as a POST without a Content-Length.\r\n");
send(client, buf, sizeof(buf), 0);
}
/**********************************************************************/
/* Put the entire contents of a file out on a socket. This function
* is named after the UNIX "cat" command, because it might have been
* easier just to do something like pipe, fork, and exec("cat").
* Parameters: the client socket descriptor
* FILE pointer for the file to cat */
/**********************************************************************/
void cat(int client, FILE *resource) // 发送文件内容
{
char buf[1024]; // 定义缓冲区
fgets(buf, sizeof(buf), resource); // 读取一行文件
while (!feof(resource)) // 直到文件结束
{
send(client, buf, strlen(buf), 0); // 发送给客户端
fgets(buf, sizeof(buf), resource); // 继续读取
}
}
/**********************************************************************/
/* Inform the client that a CGI script could not be executed.
* Parameter: the client socket descriptor. */
/**********************************************************************/
void cannot_execute(int client) // 处理CGI执行错误
{
char buf[1024]; // 定义缓冲区
sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n"); // 返回500错误
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-type: text/html\r\n"); // 设置内容类型
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n"); // 结束头部
send(client, buf, strlen(buf), 0);
sprintf(buf, "<P>Error prohibited CGI execution.\r\n"); // 错误信息
send(client, buf, strlen(buf), 0);
}
/**********************************************************************/
/* Print out an error message with perror() (for system errors; based
* on value of errno, which indicates system call errors) and exit the
* program indicating an error. */
/**********************************************************************/
void error_die(const char *sc) // 处理致命错误
{
perror(sc); // 打印错误信息
exit(1); // 退出程序
}
/**********************************************************************/
/* Execute a CGI script. Will need to set environment variables as
* appropriate.
* Parameters: client socket descriptor
* path to the CGI script */
/**********************************************************************/
void execute_cgi(int client, const char *path,
const char *method, const char *query_string) // 执行CGI脚本
{
char buf[1024]; // 定义缓冲区
int cgi_output[2]; // CGI输出管道
int cgi_input[2]; // CGI输入管道
pid_t pid; // 子进程ID
int status; // 子进程状态
int i; // 循环变量
char c; // 存储接收字符
int numchars = 1; // 读取的字符数
int content_length = -1; // 内容长度
buf[0] = 'A';
buf[1] = '\0'; // 初始化缓冲区
if (strcasecmp(method, "GET") == 0) // 如果是GET请求
while ((numchars > 0) && strcmp("\n", buf)) // 读取并丢弃头部
numchars = get_line(client, buf, sizeof(buf));
else if (strcasecmp(method, "POST") == 0) // 如果是POST请求
{
numchars = get_line(client, buf, sizeof(buf)); // 读取第一行
while ((numchars > 0) && strcmp("\n", buf)) // 读取头部
{
buf[15] = '\0'; // 确保字符串结束
if (strcasecmp(buf, "Content-Length:") == 0) // 查找内容长度
content_length = atoi(&(buf[16])); // 提取内容长度
numchars = get_line(client, buf, sizeof(buf)); // 继续读取
}
if (content_length == -1)
{
bad_request(client); // 返回400错误
return; // 退出函数
}
}
else // 对于其他方法
{
}
// 创建用于CGI的管道
if (pipe(cgi_output) < 0)
{
cannot_execute(client); // 如果创建失败,返回500错误
return;
}
if (pipe(cgi_input) < 0)
{
cannot_execute(client); // 如果创建失败,返回500错误
return;
}
if ((pid = fork()) < 0)
{ // 创建子进程
cannot_execute(client); // 如果失败,返回500错误
return;
}
sprintf(buf, "HTTP/1.0 200 OK\r\n"); // 返回200 OK
send(client, buf, strlen(buf), 0); // 发送给客户端
if (pid == 0) // 如果是子进程
{
char meth_env[255]; // 请求方法环境变量
char query_env[255]; // 查询字符串环境变量
char length_env[255]; // 内容长度环境变量
dup2(cgi_output[1], STDOUT); // 将CGI输出重定向到socket
dup2(cgi_input[0], STDIN); // 将CGI输入重定向到socket
close(cgi_output[0]); // 关闭未使用的读取端
close(cgi_input[1]); // 关闭未使用的写入端
sprintf(meth_env, "REQUEST_METHOD=%s", method); // 设置请求方法环境变量
putenv(meth_env); // 将环境变量添加到环境中
if (strcasecmp(method, "GET") == 0)
{
sprintf(query_env, "QUERY_STRING=%s", query_string); // 设置查询字符串
putenv(query_env); // 将环境变量添加到环境中
}
else
{ // 如果是POST请求
sprintf(length_env, "CONTENT_LENGTH=%d", content_length); // 设置内容长度
putenv(length_env); // 将环境变量添加到环境中
}
execl(path, NULL); // 执行CGI脚本
exit(0); // 子进程结束
}
else
{ // 父进程
close(cgi_output[1]); // 关闭写入端
close(cgi_input[0]); // 关闭读取端
if (strcasecmp(method, "POST") == 0) // 如果是POST请求
for (i = 0; i < content_length; i++)
{ // 读取POST数据
recv(client, &c, 1, 0); // 接收一个字符
write(cgi_input[1], &c, 1); // 写入到CGI输入
}
// 读取CGI脚本的输出并发送给客户端
while (read(cgi_output[0], &c, 1) > 0)
send(client, &c, 1, 0);
close(cgi_output[0]); // 关闭CGI输出
close(cgi_input[1]); // 关闭CGI输入
waitpid(pid, &status, 0); // 等待子进程结束
}
}
/**********************************************************************/
/* Get a line from a socket, whether the line ends in a newline,
* carriage return, or a CRLF combination. Terminates the string read
* with a null character. If no newline indicator is found before the
* end of the buffer, the string is terminated with a null. If any of
* the above three line terminators is read, the last character of the
* string will be a linefeed and the string will be terminated with a
* null character.
* Parameters: the socket descriptor
* the buffer to save the data in
* the size of the buffer
* Returns: the number of bytes stored (excluding null) */
/**********************************************************************/
int get_line(int sock, char *buf, int size) // 从socket读取一行
{
int i = 0; // 计数器
char c = '\0'; // 当前字符
int n; // 接收的字节数
while ((i < size - 1) && (c != '\n')) // 直到达到缓冲区大小或读取到换行符
{
n = recv(sock, &c, 1, 0); // 从socket接收一个字符
/* DEBUG printf("%02X\n", c); */
if (n > 0) // 如果接收到数据
{
if (c == '\r') // 处理回车符
{
n = recv(sock, &c, 1, MSG_PEEK); // 预读下一个字符
/* DEBUG printf("%02X\n", c); */
if ((n > 0) && (c == '\n')) // 如果下一个字符是换行符
recv(sock, &c, 1, 0); // 从socket接收换行符
else
c = '\n'; // 否则将c设置为换行符
}
buf[i] = c; // 将字符存入缓冲区
i++; // 增加计数
}
else
c = '\n'; // 如果没有接收到数据,则设置为换行符
}
buf[i] = '\0'; // 在字符串末尾添加结束符
return (i); // 返回读取的字符数(不包括结束符)
}
/**********************************************************************/
/* Return the informational HTTP headers about a file. */
/* Parameters: the socket to print the headers on
* the name of the file */
/**********************************************************************/
void headers(int client, const char *filename) // 发送HTTP头部
{
char buf[1024]; // 定义缓冲区
(void)filename; // 这里未使用filename,可以用于根据文件类型设置内容类型
strcpy(buf, "HTTP/1.0 200 OK\r\n"); // 设置状态行
send(client, buf, strlen(buf), 0); // 发送状态行
strcpy(buf, SERVER_STRING); // 发送服务器信息
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n"); // 设置内容类型
send(client, buf, strlen(buf), 0);
strcpy(buf, "\r\n"); // 结束头部
send(client, buf, strlen(buf), 0);
}
/**********************************************************************/
/* Give a client a 404 not found status message. */
/**********************************************************************/
void not_found(int client) // 处理404未找到错误
{
char buf[1024]; // 定义缓冲区
sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n"); // 返回404错误
send(client, buf, strlen(buf), 0); // 发送响应
sprintf(buf, SERVER_STRING); // 发送服务器信息
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n"); // 设置内容类型
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n"); // 结束头部
send(client, buf, strlen(buf), 0);
sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n"); // 错误页面内容
send(client, buf, strlen(buf), 0);
sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "your request because the resource specified\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "is unavailable or nonexistent.\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), 0);
}
/**********************************************************************/
/* Send a regular file to the client. Use headers, and report
* errors to client if they occur.
* Parameters: a pointer to a file structure produced from the socket
* file descriptor
* the name of the file to serve */
/**********************************************************************/
void serve_file(int client, const char *filename) // 发送文件内容
{
FILE *resource = NULL; // 文件指针
int numchars = 1; // 读取的字符数
char buf[1024]; // 定义缓冲区
buf[0] = 'A';
buf[1] = '\0'; // 初始化缓冲区
while ((numchars > 0) && strcmp("\n", buf)) // 读取并丢弃请求的头部
numchars = get_line(client, buf, sizeof(buf));
resource = fopen(filename, "r"); // 打开文件
if (resource == NULL)
not_found(client); // 如果文件不存在,返回404错误
else
{
headers(client, filename); // 发送头部信息
cat(client, resource); // 发送文件内容
}
fclose(resource); // 关闭文件
}
/**********************************************************************/
/* This function starts the process of listening for web connections
* on a specified port. If the port is 0, then dynamically allocate a
* port and modify the original port variable to reflect the actual
* port.
* Parameters: pointer to variable containing the port to connect on
* Returns: the socket */
/**********************************************************************/
int startup(u_short *port) // 启动服务器
{
int httpd = 0; // socket描述符
int on = 1; // 用于设置socket选项
struct sockaddr_in name; // 地址结构体
httpd = socket(PF_INET, SOCK_STREAM, 0); // 创建TCP socket
if (httpd == -1)
error_die("socket"); // 如果创建失败,打印错误并退出
memset(&name, 0, sizeof(name)); // 清空地址结构体
name.sin_family = AF_INET; // 设置地址族为IPv4
name.sin_port = htons(*port); // 设置端口
name.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有IP地址
if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)
{
error_die("setsockopt failed"); // 设置socket选项失败
}
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0) // 绑定socket
error_die("bind"); // 绑定失败
if (*port == 0) // 如果端口为0
{
socklen_t namelen = sizeof(name); // 地址结构体长度
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1) // 获取动态分配的端口
error_die("getsockname");
*port = ntohs(name.sin_port); // 更新端口号
}
if (listen(httpd, 5) < 0) // 监听请求
error_die("listen"); // 监听失败
return (httpd); // 返回socket描述符
}
/**********************************************************************/
/* Inform the client that the requested web method has not been
* implemented.
* Parameter: the client socket */
/**********************************************************************/
void unimplemented(int client) // 处理未实现的请求方法
{
char buf[1024]; // 定义缓冲区
sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n"); // 返回501错误
send(client, buf, strlen(buf), 0); // 发送响应
sprintf(buf, SERVER_STRING); // 发送服务器信息
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n"); // 设置内容类型
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n"); // 结束头部
send(client, buf, strlen(buf), 0);
sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n"); // 错误页面内容
send(client, buf, strlen(buf), 0);
sprintf(buf, "</TITLE></HEAD>\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), 0);
}
/**********************************************************************/
int main(void) // 主函数
{
int server_sock = -1; // 服务器socket描述符
u_short port = 4000; // 默认端口号
int client_sock = -1; // 客户端socket描述符
struct sockaddr_in client_name; // 客户端地址结构体
socklen_t client_name_len = sizeof(client_name); // 地址结构体长度
pthread_t newthread; // 新线程ID
server_sock = startup(&port); // 启动服务器并获取socket描述符
printf("httpd running on port %d\n", port); // 打印正在运行的端口
while (1) // 持续接受请求
{
client_sock = accept(server_sock, // 接受客户端连接
(struct sockaddr *)&client_name,
&client_name_len);
if (client_sock == -1)
error_die("accept"); // 接受失败
if (pthread_create(&newthread, NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0) // 创建新线程处理请求
perror("pthread_create"); // 如果创建失败,打印错误
}
close(server_sock); // 关闭服务器socket
return (0); // 返回0,表示正常结束
}
1.1 基础语法架构
代码遵循典型的C语言程序结构:包含头文件、宏定义、全局变量、函数声明和定义、main函数。使用了模块化的设计,将功能划分为多个函数(如accept_request、serve_file等),提高了代码的可读性和维护性。
#include引入了标准库和系统库,如<stdio.h>(输入输出)、<sys/socket.h>(Socket编程)、<pthread.h>(多线程支持)等。
#define用于定义宏(如ISspace、SERVER_STRING),简化代码并提高可读性。
1.2 变量与常量定义
使用标准C类型(如int、char)和POSIX类型(如size_t、pid_t、u_short)。
使用了结构体struct sockaddr_in用于存储Socket地址信息,struct stat用于文件状态。
定义了一些常量,#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n":定义HTTP服务器标识,#define STDIN 0、STDOUT 1、STDERR 2:标准输入输出文件描述符。
函数中定义了大量的局部变量,如buf、numchars,通常为栈上分配,避免全局变量滥用。
1.3 函数的使用
标准库函数:
- printf、sprintf:格式化输出。
- send、recv:Socket数据发送和接收。
- fopen、fgets、fclose:文件操作。
- fork、execl:进程创建和执行。
自定义函数:
- accept_request:处理客户端请求,解析HTTP方法和URL。
- serve_file:发送静态文件内容。
- execute_cgi:执行CGI脚本。
- error_die:打印错误并退出程序。
- get_line:从Socket读取一行数据。
自定义函数在文件顶部声明。
1.4 指针的操作
使用场景:
- void *arg在accept_request中作为参数传递客户端Socket描述符,通过(intptr_t)转为整数。
- char *query_string指向URL中的查询字符串,动态调整位置。
- FILE *resource指向文件流,用于读取文件内容。
指针操作:
- *query_string = '\0':将字符串截断。
- query_string++:移动指针到下一个字符。
- sprintf(path, "htdocs%s", url):指针指向的内存被填充格式化字符串。
注意指针操作需确保不越界(如i < sizeof(url) - 1),避免缓冲区溢出。
1.5 Socket的详细用法
C语言本身没有内置网络功能,Socket API 是 POSIX 标准的一部分,提供了与网络交互的能力。Socket 是网络通信的端点,允许不同主机之间通过网络进行数据传输。Socket 提供了标准的 API 来支持 TCP/IP 协议。
主要 API 和使用方法:
- socket():创建一个新的 Socket。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket failed");
exit(1);
}
- bind():将 Socket 绑定到特定的 IP 地址和端口号。
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有接口
server_addr.sin_port = htons(port); // 转换端口为网络字节序
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(1);
}
- listen():将 Socket 设置为被动监听状态,等待连接请求。
if (listen(sockfd, 5) < 0) {
perror("listen failed");
exit(1);
}
- accept():等待并接受客户端的连接请求,返回一个新的 Socket。
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_sock = accept(sockfd, (struct sockaddr *)&client_addr, &addr_len);
if (client_sock < 0) {
perror("accept failed");
exit(1);
}
- send() 和 recv() 用于发送和接收数据。
char buffer[1024];
int bytes_received = recv(client_sock, buffer, sizeof(buffer), 0);
send(client_sock, buffer, bytes_received, 0);
- close():关闭 Socket 连接。
close(client_sock);
close(sockfd);
对应本文代码中的 socket 代码:
- Socket创建:
socket(PF_INET, SOCK_STREAM, 0):创建TCP Socket,PF_INET表示IPv4,SOCK_STREAM表示流式传输。
- 地址绑定:
struct sockaddr_in name定义地址结构,设置sin_family(地址族)、sin_port(端口)、sin_addr(IP地址)。 bind(httpd, (struct sockaddr *)&name, sizeof(name)):将Socket绑定到指定地址和端口。
- 监听与接受连接:
listen(httpd, 5):设置监听队列长度为5。 accept(server_sock, (struct sockaddr *)&client_name, &client_name_len):接受客户端连接,返回新的Socket描述符。
- 数据收发:
send(client, buf, strlen(buf), 0):发送数据到客户端。 recv(sock, &c, 1, 0):从客户端接收数据。
- 选项设置:
setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)):设置地址重用,避免端口被占用。
- 关闭Socket:
close(client):关闭客户端连接。
1.6 多线程的详细用法
多线程允许程序在同一进程中同时执行多个任务。线程共享进程的资源,能够更好地利用计算机的多核 CPU,pthread 是 Unix 系统中的标准线程库,跨平台性好。
主要 API 和使用方法:
- pthread_create():创建新线程并执行指定的函数。
pthread_t thread;
int result = pthread_create(&thread, NULL, thread_function, NULL);
if (result != 0) {
perror("pthread_create failed");
}
- pthread_join():阻塞主线程,直到指定的线程结束。
pthread_join(thread, NULL); // 等待线程结束
- pthread_exit():退出线程。
pthread_exit(NULL);
- 互斥锁(mutex):防止多个线程同时访问共享资源。
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); // 初始化
pthread_mutex_lock(&mutex); // 加锁
// 访问共享资源
pthread_mutex_unlock(&mutex); // 解锁
pthread_mutex_destroy(&mutex); // 销毁
在上面的代码中多线程的用法如下:
while (1) // 持续接受请求
{
client_sock = accept(server_sock, // 接受客户端连接
(struct sockaddr *)&client_name,
&client_name_len);
if (client_sock == -1)
error_die("accept"); // 接受失败
// 创建新线程来处理请求
if (pthread_create(&newthread, NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
{
perror("pthread_create"); // 如果创建失败,打印错误
}
}
使用 pthread_create() 创建一个新线程。&newthread 是新线程的 ID,第三个参数为处理函数 accept_request,这个函数会处理传入的客户端请求,包括解析 HTTP 请求、读取文件、发送响应等。使用 (void *)(intptr_t)client_sock 将 client_sock 转换为 void * 类型,以传递给线程函数。
1.7 进程管理与CGI管道
进程是执行中的程序,每个进程有自己的地址空间、数据栈和其他辅助数据。操作系统通过调度算法管理进程。
进程管理:通过创建子进程运行外部程序(如CGI脚本),实现动态内容生成。
进程管理 API:
- fork():创建一个新进程(子进程)。
返回值:在父进程中返回子进程的 PID。在子进程中返回 0。在出错时返回 -1。
- exec():用于替换当前进程的镜像,执行新程序。
常用形式:execl(), execv(), execle(), execve() 等。
- wait() 和 waitpid():用于等待子进程结束。
原型:pid_t wait(int *status); 和 pid_t waitpid(pid_t pid, int *status, int options);
- exit():用于终止进程。
原型:void exit(int status);
CGI管道:使用管道(pipe)在父进程(服务器)和子进程(CGI脚本)之间传递数据。
使用管道的基本步骤:
- 创建管道:
使用 pipe(int pipefd[2]) 创建一个管道,pipefd[0] 用于读取,pipefd[1] 用于写入。
- 重定向标准输入和输出:
使用 dup2() 将管道的读写端重定向到标准输入和输出。
- 执行 CGI 程序:
使用 fork() 创建子进程,并在子进程中调用 exec() 执行 CGI 程序。
- 在父进程中通过管道与 CGI 程序进行通信。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int cgi_output[2];
pipe(cgi_output);
pid_t pid = fork();
if (pid == 0) { // 子进程
close(cgi_output[0]); // 关闭读取端
dup2(cgi_output[1], STDOUT); // 重定向输出
execl("/path/to/cgi_script", "cgi_script", NULL); // 执行 CGI 脚本
exit(0);
} else { // 父进程
close(cgi_output[1]); // 关闭写入端
char buffer[1024];
while (read(cgi_output[0], buffer, sizeof(buffer)) > 0) {
printf("%s", buffer); // 打印 CGI 输出
}
}
return 0;
}
那么在前文的C代码中,我们把作者的进程与CGI的相关逻辑简化提取出来就是:
execute_cgi 函数中管道创建与进程fork
// 创建两个管道(父子进程双向通信)
int cgi_output; // 子进程输出 -> 父进程
int cgi_input; // 父进程输入 -> 子进程
pipe(cgi_output);
pipe(cgi_input);
// 创建子进程
pid_t pid = fork();
if (pid == 0) {
/* 子进程逻辑 */
} else {
/* 父进程逻辑 */
}
子进程处理(CGI脚本执行):
// 文件描述符重定向
dup2(cgi_output, STDOUT_FILENO); // 子进程输出到管道写端
dup2(cgi_input, STDIN_FILENO); // 子进程从管道读端获取输入
// 关闭不需要的管道端
close(cgi_output); // 关闭输出管道的读端
close(cgi_input); // 关闭输入管道的写端
// 设置环境变量
putenv("REQUEST_METHOD=GET"); // 传递请求方法
putenv("QUERY_STRING=name=John"); // 传递GET参数
// 执行CGI程序
execl("/usr/bin/python", "python", "cgi_script.py", NULL);
父进程处理(数据交换):
// 关闭不需要的管道端
close(cgi_output); // 关闭输出管道的写端
close(cgi_input); // 关闭输入管道的读端
// 处理POST数据(如果有)
if (method是POST) {
for (i=0; i<content_length; i++) {
recv(client, &c, 1, 0); // 从客户端接收数据
write(cgi_input, &c, 1); // 写入输入管道
}
}
// 读取CGI输出并发送给客户端
while (read(cgi_output, &c, 1) > 0) {
send(client, &c, 1, 0);
}
// 等待子进程结束
waitpid(pid, &status, 0);
这个项目代码不多,把我们之前学习的C语言代码都回顾到了,并且扩展了线程,网络编程,进程,CGI通信等新知识点。
二、C++的回顾与扩展
接下来我们回顾一下C++的语法,这里推荐几个项目给大家阅读,ThreadPool,nlohmann/json,TinyWebServer。
从易到难大家可以阅读巩固一下,这里我抽一些解析一下。
ThreadPool,这是一个简单的C++11线程池实现,适合用来学习多线程任务调度,内部包含我们之前看过的C++语法。
包括STL容器,标准库的使用,多线程相关,异步操作,函数包装,函数模板,Lambda表达式,智能指针,异常处理。
由于 ThreadPool 的代码不多,全部在 ThreadPool.h 文件中,这里我就直接加上注释贴出来
// 防止头文件被重复包含(预处理指令)
#ifndef THREAD_POOL_H
#define THREAD_POOL_H
// 包含标准库头文件
#include <vector> // 动态数组容器
#include <queue> // 队列容器(先进先出)
#include <memory> // 智能指针相关(如std::shared_ptr)
#include <thread> // 多线程支持
#include <mutex> // 互斥锁(线程同步)
#include <condition_variable> // 条件变量(线程通信)
#include <future> // 异步操作相关(std::future, std::packaged_task)
#include <functional> // 函数对象包装器(std::function, std::bind)
#include <stdexcept> // 标准异常类(如std::runtime_error)
class ThreadPool {
public:
// 构造函数(接受线程数量参数)
ThreadPool(size_t);
// 模板方法:添加任务到线程池
// - 使用可变参数模板(template<class F, class... Args>)
// - 完美转发(F&&, Args&&...)
// - 返回std::future获取异步结果
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
// 析构函数(用于安全关闭线程池)
~ThreadPool();
private:
// 成员变量
std::vector<std::thread> workers; // 工作线程容器
std::queue<std::function<void()>> tasks; // 任务队列(存储无参void函数)
// 同步相关
std::mutex queue_mutex; // 保护任务队列的互斥锁
std::condition_variable condition; // 条件变量(用于线程等待/通知)
bool stop; // 停止标志
};
/*------------------------ 构造函数实现 ------------------------*/
inline ThreadPool::ThreadPool(size_t threads)
: stop(false) // 初始化列表:初始化stop为false
{
// 创建指定数量的工作线程
for(size_t i = 0; i < threads; ++i)
// 使用emplace_back直接在vector中构造线程(避免拷贝)
workers.emplace_back(
// Lambda表达式定义线程工作逻辑
[this] // 捕获当前对象的指针
{
// 无限循环(直到线程池停止)
for(;;)
{
std::function<void()> task; // 存储待执行的任务
// 临界区开始(通过unique_lock自动管理锁)
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
// 条件变量等待:直到有任务或线程池停止
// - 避免忙等待,释放锁并休眠
this->condition.wait(lock,
[this]{
return this->stop || !this->tasks.empty();
});
// 终止条件:线程池停止且任务队列为空
if(this->stop && this->tasks.empty())
return;
// 从任务队列获取任务
task = std::move(this->tasks.front()); // 移动语义提高效率
this->tasks.pop(); // 移除队列头部
}
// 临界区结束(锁自动释放)
// 执行任务
task();
}
}
);
}
/*------------------------ 添加任务方法实现 ------------------------*/
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
// 推导返回类型(C++11类型萃取技术)
using return_type = typename std::result_of<F(Args...)>::type;
// 创建packaged_task包装任务(可获取future)
// - 使用std::bind绑定参数(支持任意可调用对象)
// - 使用完美转发保持参数类型(std::forward)
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
// 获取任务的future(用于异步获取结果)
std::future<return_type> res = task->get_future();
// 临界区(加锁修改任务队列)
{
std::unique_lock<std::mutex> lock(queue_mutex);
// 检查线程池是否已停止
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
// 将任务封装为void()类型并加入队列
// - Lambda捕获task的shared_ptr(保证任务对象存活)
tasks.emplace([task](){ (*task)(); });
}
// 通知一个等待线程有新任务
condition.notify_one();
return res;
}
/*------------------------ 析构函数实现 ------------------------*/
inline ThreadPool::~ThreadPool()
{
// 临界区(设置停止标志)
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
// 唤醒所有线程(准备结束)
condition.notify_all();
// 等待所有线程完成(join)
for(std::thread &worker : workers)
worker.join();
}
#endif // 结束头文件保护
注意这里的逻辑是写在头文件中的,和我们正常开发的头文件和实现文件分开的不同,这种头文件库的设计理念有很多好处,无需为不同平台编译二进制,用户只需包含头文件即可使用,并且用户只需#include "ThreadPool.h",无需处理库的编译和链接,缺点就是头文件内容在每次包含时都会被重新编译,用户能直接看到所有代码,不利于代码保护。
我们的 STL容器(如std::vector)、Eigen矩阵库 也是这个设计,但是通常我们商业库或业务逻辑还是推荐分离.h和.cpp以加快编译速度。
- STL容器
在代码中使用了 std::vector 和 std::queue:
#include <vector> // 用于动态数组
#include <queue> // 用于任务队列
std::vector<std::thread> workers; // 声明一个存储工作线程的向量
std::queue<std::function<void()>> tasks; // 声明一个存储任务的队列
- 标准库的使用
使用标准库提供的线程、互斥量、条件变量和异常处理等功能:
#include <thread> // 多线程
#include <mutex> // 互斥量
#include <condition_variable> // 条件变量
#include <stdexcept> // 异常处理
if (stop)
throw std::runtime_error("enqueue on stopped ThreadPool"); // 抛出异常
- 多线程相关
创建和管理线程的基本用法:
for (size_t i = 0; i < threads; ++i) {
workers.emplace_back([this] { // 创建工作线程
// 线程工作逻辑
});
}
- 异步操作
通过 std::future 和 std::packaged_task 实现异步任务的提交和结果获取:
auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
std::future<return_type> res = task->get_future(); // 获取任务的未来值
- 函数包装
使用 std::packaged_task 封装函数和参数,便于异步调用:
auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...)); // 函数包装
- 函数模板
使用函数模板使 enqueue 方法能够接受任何可调用对象:
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type; // 函数模板
- Lambda表达式
使用Lambda表达式简化线程的工作逻辑,捕获外部变量:
workers.emplace_back([this] { // Lambda表达式定义工作线程行为
for (;;) {
std::function<void()> task; // 定义任务
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition.wait(lock, [this] { return this->stop || !this->tasks.empty(); }); // 条件变量等待
if (this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}
task(); // 执行任务
}
});
- 智能指针
使用 std::shared_ptr 管理动态分配的任务,避免内存泄漏:
auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...)); // 使用智能指针
- 异常处理
使用异常处理确保在错误情况下能够妥善处理:
if (stop)
throw std::runtime_error("enqueue on stopped ThreadPool"); // 抛出异常
整体库的流程就是
- 初始化线程池。
- 使用 enqueue 方法将新任务添加到线程池。
- 在无限循环中等待条件变量的通知,有任务就执行。
- 被唤醒的工作线程从任务队列中提取任务(使用 std::function<void()>),并调用该任务来执行其逻辑。
- 当 ThreadPool 被销毁时,析构函数中,设置停止标志 stop,并通知所有线程安全退出。
完整的调用示例:
// 创建线程池(4个工作线程)
ThreadPool pool(4);
// 提交一个Lambda任务
auto future = pool.enqueue([](int a, int b) {
return a + b;
}, 10, 20);
// 获取结果(异步)
std::cout << future.get(); // 输出30
总结
通过本次学习笔记,我们深入回顾了 C 和 C++ 的基础知识,并通过实际项目的分析与实践巩固了这些知识。我们重点关注了 Tinyhttpd 项目,这不仅让我们复习了 C 语言的核心概念,如函数、指针、内存管理和文件 I/O,还进一步扩展了我们对多线程、进程管理和网络编程的理解。
在 C++ 部分,我们探讨了 ThreadPool 这个线程池实现,深入了解了现代 C++ 的特性,包括 STL 容器、智能指针、异步编程和函数对象等。这些知识与技能的掌握,不仅提高了我们对多线程编程的认识,也为我们将来的项目开发打下了坚实的基础。
通过阅读和解析这些开源项目,我们不仅提升了编程能力,还学习到了实际开发中常用的设计模式和编程技巧。这些经验使我们能够在今后的学习和工作中更加灵活地应对复杂的编程任务。
由于我本人并不是 C/C++ 从业者,我也只是了解,能看懂逻辑的阶段,如果要我自己写一个类似功能,那我做不到。
所以如果大家想要更深入的学习(如嵌入式,驱动之类的)就需要大家自行摸索了,后期的文章我就转向编译与应用方向了,主要是常用第三方库的编译使用以及在 Android 等移动端平台的应用为主。
文章到这里也就到了尾声,如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。
如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我很重要!
Ok,那么这一期就此完结了。
