C/C++ 编程实战 -- CMake用法
文章目录
- 前言
- 项目目录
- 源码说明
- ./CMakeLists.txt
- ./src/server
- CMakeLists.txt
- server.c
- ./src/client
- CMakeLists.txt
- client.c
- ./src/cpp
- CMakeLists.txt
- main.cpp
- 编译构建项目
- 运行
- 总结
当你迷茫的时候,请点击 目录大纲 快速查看技术文章,相信你总能找到前行的方向
前言
前面我们有一篇文章 C/C++ 变量与内存,用的是 C++ 语言演示的,还有一篇用 Java 写的Java Socket – TCP/IP 通信,本篇文章也来用C语言写一个,并与 C++ 文件放在一个项目中,代码已上传到 cpp_oops,用来学习使用 CMake,如果还想了解更多高级用法可以参考CMake教程。
项目目录
主要程序文件目录如下:
.
├── CMakeLists.txt
└── src├── server│ ├── CMakeLists.txt│ └── server.c├── client│ ├── CMakeLists.txt│ └── client.c├── cpp└── ├── CMakeLists.txt└── main.cpp
源码说明
./CMakeLists.txt
最外层的 CMakeLists.txt 规定了编译环境相关,包括 版本,gcc,g++ 等,我本地电脑是 macos 系统,我另外安装了 gcc 12 ,没有用默认的 Clang 。
cmake_minimum_required(VERSION 3.10)
project(tcp_example) # 添加C++语言支持# 在project()之后立即设置编译器
set(CMAKE_C_COMPILER "/opt/homebrew/bin/gcc-12" CACHE PATH "C compiler" FORCE)
set(CMAKE_CXX_COMPILER "/opt/homebrew/bin/g++-12" CACHE PATH "C++ compiler" FORCE)# 禁用自动检测编译器
set(CMAKE_C_COMPILER_FORCED TRUE)
set(CMAKE_CXX_COMPILER_FORCED TRUE)enable_language(C CXX)# 设置C标准
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)# 设置C++标准
set(CMAKE_CXX_STANDARD 11) # 或者11/14/17/20
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)# 显示编译器信息
message(STATUS "Using C compiler: ${CMAKE_C_COMPILER}")# 设置输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)# 添加编译警告选项
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -pedantic")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")# 添加子目录
add_subdirectory(src/server)
add_subdirectory(src/client)
add_subdirectory(src/cpp)# 可选:添加包含目录
include_directories(${CMAKE_SOURCE_DIR}/src
)
./src/server
TCP/IP 服务端
CMakeLists.txt
设置服务端可执行文件与输出目录
# 服务器可执行文件
add_executable(server server.c)# 设置可执行文件输出目录
set_target_properties(server PROPERTIESRUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
server.c
服务端主程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <errno.h>// 平台检测
#ifdef __APPLE__#define PLATFORM_MACOS 1
#elif __linux__#define PLATFORM_LINUX 1
#else#define PLATFORM_UNKNOWN 1
#endif#define PORT 8080
#define BUFFER_SIZE 1024
#define BACKLOG 5void handle_client(int client_socket) {char buffer[BUFFER_SIZE] = {0};const char *response = "Hello from server";// 读取客户端数据ssize_t bytes_read = read(client_socket, buffer, BUFFER_SIZE - 1);if (bytes_read > 0) {buffer[bytes_read] = '\0';printf("Received from client: %s\n", buffer);// 发送响应send(client_socket, response, strlen(response), 0);printf("Response sent to client\n");}close(client_socket);
}int main() {int server_fd, client_socket;struct sockaddr_in server_addr, client_addr;socklen_t client_len = sizeof(client_addr);// 创建socketif ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}// 设置socket选项 - 跨平台处理int opt = 1;// 首先尝试设置 SO_REUSEADDR(所有平台都支持)if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {perror("setsockopt SO_REUSEADDR failed");close(server_fd);exit(EXIT_FAILURE);}// 在Linux上额外设置SO_REUSEPORT#ifdef PLATFORM_LINUXif (setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt))) {perror("setsockopt SO_REUSEPORT failed");// 注意:在Linux上这可能是个错误,但在macOS上我们忽略它close(server_fd);exit(EXIT_FAILURE);}#endif// 配置服务器地址server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);// 绑定socketif (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("bind failed");close(server_fd);exit(EXIT_FAILURE);}// 监听连接if (listen(server_fd, BACKLOG) < 0) {perror("listen failed");close(server_fd);exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", PORT);printf("Running on %s platform\n",#ifdef PLATFORM_MACOS"macOS"#elif PLATFORM_LINUX"Linux"#else"Unknown"#endif);// 接受连接if ((client_socket = accept(server_fd, (struct sockaddr *)&client_addr, &client_len)) < 0) {perror("accept failed");close(server_fd);exit(EXIT_FAILURE);}printf("Client connected from %s:%d\n",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));// 处理客户端请求handle_client(client_socket);// 关闭服务器socketclose(server_fd);printf("Server shutdown\n");return 0;
}
./src/client
TCP/IP 客户端
CMakeLists.txt
设置客户端可执行文件及输出目录
# 客户端可执行文件
add_executable(client client.c)# 设置可执行文件输出目录
set_target_properties(client PROPERTIESRUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
client.c
客户端程序,用 c 语言编程,用来连接服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>#define PORT 8080
#define BUFFER_SIZE 1024int main() {int sock = 0;struct sockaddr_in serv_addr;char *hello = "Hello from client";char buffer[BUFFER_SIZE] = {0};// 创建socketif ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {printf("\n Socket creation error \n");return -1;}serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(PORT);// 将IPv4和IPv6地址从文本转换为二进制形式if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {printf("\nInvalid address/ Address not supported \n");return -1;}// 连接到服务器if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {printf("\nConnection Failed \n");return -1;}// 发送消息send(sock, hello, strlen(hello), 0);printf("Hello message sent\n");// 读取服务器响应read(sock, buffer, BUFFER_SIZE);printf("Server: %s\n", buffer);// 关闭连接close(sock);return 0;
}
./src/cpp
c++ 程序文件目录
CMakeLists.txt
设置可执行文件,主程序 main.cpp 在编译阶段会编译成名为 cpp_main 的可执行文件,名称定义就是在 CMakeLists.txt 设置
# 设置可执行文件,名称为 cpp_main
add_executable(cpp_main main.cpp)# 设置可执行文件输出目录,会放到 build/bin/ 下
set_target_properties(server PROPERTIESRUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
main.cpp
c++ 程序文件
#include <iostream>
using namespace std;void test_var();
void test_mem();int main() {test_mem();
// test_var();return 0;
}void test_mem() {// 信息int var_7 = 1;// 大家记的信息int a = var_7;int *b = &var_7;int *const c = &var_7;int &d = a;// 问答时刻a = 0;*b = 0;cout << "地址: b=" << b << " ,地址: (b + 1)=" << b + 1 << " ,数值: *(b + 1)=" << *(b + 1) << endl;// b = &a; // 变量的值可以修改*c = 0;cout << "地址: c=" << c << " ,地址: (c + 1)=" << c + 1 << " ,数值: *(c + 1)=" << *(c + 1) << endl << endl;// c=&a; // 错误, const 修饰的常量 c 不可以改变//公布答案cout << "int var_7,int a,int *b,int *const c,int &d" << endl;cout << "数值: var=" << var_7 << " ,a=" << a << " ,b=" << b << " ,c=" << c << " ,d=" << d << endl;cout << "地址: &var=" << &var_7 << " ,&a=" << &a << " ,&b=" << &b << " ,&c=" << &c << " ,&d=" << &d << endl;cout << "取数: var=" << var_7 << " ,a=" << a << " ,*b=" << *b << " ,*c=" << *c << " ,d=" << d << endl;// a==*b==*c==d, &a==b==c==&d
}void test_var() {int a = 10;int *b = &a;int &c = a;int *const d = &a;cout << "int a,int *b,int &c,int *const d" << endl;cout << "数值: a=" << a << " ,b=" << b << " ,c=" << c << " ,d=" << d << endl;cout << "地址: &a=" << &a << " ,&b=" << &b << " ,&c=" << &c << " ,&d=" << &d << endl;cout << "取数: a=" << a << " ,*b=" << *b << " ,c=" << c << " ,*d=" << *d << endl;// a==c==*b==*d, &a==&c=b=d
}
编译构建项目
# 创建构建目录
mkdir build
cd build
# 生成Makefile
cmake ..
# 编译项目
make
结果如下,`
cmake ..
执行后 会生成./build/src
目录,主要是生成对应的Makefile
文件,make
执行后会根据Makefile
文件生成可执行文件放在./build/bin
目录下。
运行
- c++ 程序运行 cpp_main,首先进入 build 目录
./bin/cpp_main
结果如下:
地址: b=0x16b56ed44 ,地址: (b + 1)=0x16b56ed48 ,数值: *(b + 1)=1800858944
地址: c=0x16b56ed44 ,地址: (c + 1)=0x16b56ed48 ,数值: *(c + 1)=1800858944int var_7,int a,int *b,int *const c,int &d
数值: var=0 ,a=0 ,b=0x16b56ed44 ,c=0x16b56ed44 ,d=0
地址: &var=0x16b56ed44 ,&a=0x16b56ed40 ,&b=0x16b56ed38 ,&c=0x16b56ed30 ,&d=0x16b56ed40
取数: var=0 ,a=0 ,*b=0 ,*c=0 ,d=0
- TCP/IP 测试,先运行服务端,后运行客户端
# 在一个终端运行服务器(先运行)
./bin/server# 在另一个终端运行客户端
./bin/client
结果如下:
服务端日志
~$ ./bin/server
Server listening on port 8080...
Running on macOS platform# 下面是客户端连接上的日志
Client connected from 127.0.0.1:62767
Received from client: Hello from client
Response sent to client
Server shutdown
客户端日志
~ $ ./bin/client
Hello message sent
总结
通过上面的例子,我们学到了使用 CMake 构建编译项目,项目里面有 C 语言与 C++ 文件混合存在,但不影响编译,因为在 CMakeLists.txt 已经定义好了对应的编译环境与输出目录,CMake 目的是组织项目生成 Makefile 文件,最后执行 make 命令时再根据Makefile 文件进行编译输出可执行文件。所以,有 CMake 真是太方便了,更高级的用法参考 CMake教程