基于TCP的网络电子词典
基于TCP的网络电子词典
网络电子词典核心代码块实现
1.1流程设计
1.2主要模块
-
数据库模块:两个数据库,用户数据库、单词数据库
单词数据库:存储四级单词词汇表
用户数据库:两个表:
1、存储用户名和用户的密码
2、用户查询历史记录表
-
服务器模块:创建服务器套接字、绑定IP和端口号、启动监听、接收连接请求、分线程管理客户端
-
处理客户端请求模块:
-
传输数据类型:用户注册类型、用户登录类型、用户退出类型、单词查询类型、查询历史记录类型
-
数据类型:MSG int type,char name[20],char text[128]
注意:type要进行数据转换:网络字节序和主机字节序的转换,除了char统一格式不需要变化
1.3主要函数功能
1.4代码实现及具体知识讲解
1.4.1server构造函数代码块
Server::Server(shared_ptr db_manager, const string ip, int port) : db_manager_(db_manager), ip_(ip), port_(port), running_(false) // 初始化列表{ // 创建用于连接的套接字文件描述符 sfd_ = socket(AF_INET, SOCK_STREAM, 0); if (sfd_ < 0) { throw runtime_error(\"socket error\"); // 抛出异常 } // 设置端口号快速重用 int opt = 1; if (setsockopt(sfd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { close(sfd_); throw runtime_error(\"setsockopt error\"); } // 绑定ip地址和端口号 sockaddr_in sin; sin.sin_family = AF_INET; // 通信域 sin.sin_port = htons(port); // 端口号 sin.sin_addr.s_addr = inet_addr(ip.c_str()); // ip地址 if (bind(sfd_, (struct sockaddr *)&sin, sizeof(sin)) < 0) { close(sfd_); throw runtime_error(\"bind error\"); }}
智能指针
智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
C++11中提供了三种智能指针,使用这些智能指针时需要引用头文件
std::shared_ptr:共享的智能指针
std::unique_ptr:独占的智能指针
std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。
TCP通信
TCP/UDP的区别
统属于传输层的相关协议
1> TCP通信
1、TCP提供了面向连接的、可靠的数据传输服务2、传输过程中,能够保障数据无误、数据无丢失、数据无重复、数据无失序 TCP通信中会给每个数据包编上编号,该编号称为序列号 每个序列号都需要应打包应答,如果没有应答,则会将上面的数据包重复传输3、TCP通信中,数据传输效率较低,耗费资源较多4、数据收发是不同步的 为了提高传输效率,tcp通信中会将多个较小的,发送时间间隔较短的数据包,沾成一个数据包进行发送,该现象称为沾包现象5、TCP通信使用场景:对于传输质量要求较高的以及传输量较大的数据通信,在需要可靠传输信息的场合,一般使用TCP通信 例如:账户和密码登录注册、大型文件的下载
2> UDP通信
1、提供面向无连接、不保证数据可靠传输、尽最大努力传输的协议2、数据传输过程中,可能会出现数据丢失、重复、失序、乱序等现象3、数据传输效率较高、实时性高4、限制每次传输的数据大小,多出部分会直接被忽略删除5、数据的收发是同步的,不会沾包6、使用场景:发送小尺寸的,在收到数据后给出应答比较困难的情况下,采用udp通信 例如:广播、组播、通信软件的音视频传输
字节序概念
字节序:计算机在存储多字节整数时,根据主机的CPU处理架构不同,我们将主机分成大端存储的主机和小端存储的主机
- 大端存储:内存地址低位存储的是数据的高位
- 小端存储:内存地址低位存储的是数据的低位
#include //检查是否是对应int main(int argc, const char *argv[]) { //定义一个整形变量 int num = 0x12345678; //定义一个字符类型的指针,指向整形变量的起始地址 char *ptr = (char *)# //对ptr所指向的字节中的内容进行判断,如果是0x12则说明是大端存储 //如果是0x78则说明是小端存储 if(*ptr == 0x12) { cout<<\"big endian\"<<endl; }else if(*ptr == 0x78) { cout << \"little endian\"<<endl; } std::cout << \"Hello, World!\" << std::endl; return 0;}
无论发送端是大端存储还是小端存储,在传输多字节整数时,一律先转换为网络字节序。经由网络传输后,到达目的主机后,在转换为主机字节序即可
系统提供了一套有关网络字节序和主机字节序之间相互转换的函数
主机:host h
网络:network n
转换:to to
#include uint32_t htonl(uint32_t hostlong);//将4字节整数主机字节序转换为网络字节序,参数是主机字节序,返回值是网络字节序 uint16_t htons(uint16_t hostshort);//将2字节整数主机字节序转换为网络字节序,参数是主机字节序,返回值是网络字节序 uint32_t ntohl(uint32_t netlong);//将4字节整数的网络字节序转换为主机字节序,参数是网络字节序,返回值是主机字节序 uint16_t ntohs(uint16_t netshort);//将2字节整数的网络字节序转换为主机字节序,参数是网络字节序,返回值是主机字节序 eg:int num = 0x12345678; //调用函数,将该数据转换为网络字节序 int res = htonl(num); printf(\"res = %#x\\n\", res); //res = 0x78563412//常见使用情况:封装消息结构体(MSG msg),端口号使用(port)
不需要使用网络字节序转换函数的情况:
1、在进行单字节整数传输时,不需要使用 eg:char类型
2、在网络中传输字符串时,也不需要使用 eg:string类型除此以外在进行多字节整数网络传输时,需要使用字节序转换函数
点分十进制
我们将ip地址的每一个字节单独计算出十进制数据(192.168.56.129即为4字节的十进制数据),并用点进行分割,这种方式,称 为点分十进制,在程序中使用的是字符串来存储的。但是,ip地址的本质是4字节无符号整数,在网络中进行传输时,需要使用的是4字节无符号整数,而不是点分十进制的字符串。此时,就需要引入关于点分十进制数据向4字节无符号整数转换的相关函数
地址:address
网络:network
转换:to
#include #include #include in_addr_t inet_addr(const char *cp);//将点分十进制的ip地址转换为4字节无符号整数的网络字节序,参数时点分十进制数据,返回值时4字节无符号整数 char *inet_ntoa(struct in_addr in);//将4字节无符号整数的网络字节序,转换为点分十进制的字符串eg://地址指针记录ip地址 const char *ip = \"172.20.10.8\"; //点分十进制 unsigned int ip_net = inet_addr(ip); //调用函数将点分十进制转换为4字节无符号整数
网络通信基础
在网络通信过程中,有两种通信方式,分别是基于BS模型的,即浏览器服务器模型,和基于CS模型,即客户端服务器模型
客户端服务器模型六件套 socket bind listen accept send/recv close
注意事项
- bind和accept函数使用的地址结构体不同
- bind和accept函数参数3不同(前者Int类型,后者为*类型)
bind
accept
socket函数
#include /* See NOTES */ #include int socket(int domain, int type, int protocol); //协议族,通信类型(专属TCP,UDP),通信协议(具体可为0) 功能:为通信创建一个端点,并返回该端点对应的文件描述符,文件描述符的使用原则是最小未分配原则 参数1:协议族,常用的协议族如下 Name Purpose Man page AF_UNIX, AF_LOCAL 本地通信,同一主机的多进程通信 具体内容查看 man 7 unix AF_INET 提供IPv4的相关通信方式 具体内容查看 man 7 ip AF_INET6 提供IPv6的相关通信方式 具体内容查看 man 7 ipv6 参数2:通信类型,指定通信语义,常用的通信类型如下 SOCK_STREAM 支持TCP面向连接的通信协议 SOCK_DGRAM 支持UDP面向无连接的通信协议 参数3:通信协议,当参数2中明确指定特定协议时,参数3可以设置为0,但是有多个协议共同使用时,需要用参数3指定当前套接字确定的协议 返回值:成功返回创建的端点对应的文件描述符,失败返回-1并置位错误码 eg: int cfd = socket(AF_INET,SOCK_STREAM,0); //cfd作为文件描述符
bind函数
#include /* See NOTES */ #include int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //文件描述符,地址信息结构体,参数2的大小(不需要&) 功能:为套接字分配名称,给套接字绑定ip地址和端口号 参数1:要被绑定的套接字文件描述符 cfd 参数2:通用地址信息结构体,对于不同的通信域而言,使用的实际结构体是不同的,该结构体的目的是为了强制类型转换,防止警告 //需要提前包装结构体 通信域为:AF_INET而言,ipv4的通信方式 struct sockaddr_in { sa_family_t sin_family; /* 地址族: AF_INET */ in_port_t sin_port; /* 端口号的网络字节序 */ struct in_addr sin_addr; /* 网络地址 */ }; /* Internet address. 嵌套结构体 */ struct in_addr { uint32_t s_addr; /* ip地址的网络字节序 */ }; 通信域为:AF_UNIX而言,本地通信 struct sockaddr_un { sa_family_t sun_family; /* 通信域:AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* 通信使用的文件 */ }; 参数3:参数2的大小 返回值:成功返回0,失败返回-1并置位错误码 eg:// 绑定ip地址和端口号 sockaddr_in sin; sin.sin_family = AF_INET; // 通信域 sin.sin_port = htons(port); // 端口号 sin.sin_addr.s_addr = inet_addr(ip.c_str()); // ip地址
listen函数
#include /* See NOTES */ #include int listen(int sockfd, int backlog);//文件描述符,挂起队列个数 功能:将套接字设置成被动监听状态 参数1:套接字文件描述符 参数2:挂起队列能够增长的最大长度,一般为128 返回值:成功返回0,失败返回-1并置位错误码 eg:// 启动监听 if (listen(sfd_, 5) < 0) { cerr << \"listen error\" << endl; return false; }
accept函数(服务器专属)
#include /* See NOTES */ #include int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);//文件套接字,地址信息结构体,参数2大小(需要&) 功能:阻塞等待客户端的连接请求,如果已连接队列中有客户端,则从连接队列中拿取第一个,并创建一个用于通信的套接字 参数1:服务器套接字文件描述符 参数2:通用地址信息结构体,用于接受已连接的客户端套接字地址信息的 参数3:接收参数2的大小 返回值:成功发那会一个新的用于通信的套接字文件描述符,失败返回-1并置位错误码eg: sockaddr_in client_addr; // 接收客户端地址信息结构体 socklen_t addr_len = sizeof(client_addr); // 接收客户端地址信息结构体的大小 // 接收客户端连接请求操作 sfd_是socket创建的 int cfd = accept(sfd_, (struct sockaddr *)&client_addr, &addr_len);
send/recv函数
#include #include ssize_t recv(int sockfd, void *buf, size_t len, int flags);//文件描述符,结构体起始地址,参数2的长度,阻塞or非阻塞 功能:从套接字中读取消息放入到buf中 参数1:通信的套接字文件描述符 参数2:要存放数据的起始地址 参数3:读取的数据的大小 参数4:读取标识位,是否阻塞读取 0:表示阻塞等待 MSG_DONTWAIT:非阻塞 返回值:可以是大于0:表示成功读取的字节个数 可以是等于0:表示对端已经下线(针对于TCP通信) -1:失败,并置位错误码 ssize_t send(int sockfd, const void *buf, size_t len, int flags);//文件描述符,结构体起始地址,参数2的长度,阻塞or非阻塞 功能:向套接字文件描述符中将buf这个容器中的内容写入 参数1:通信的套接字文件描述符 参数2:要发送的数据的起始地址 参数3:发送的数据的大小 参数4:读取标识位,是否阻塞读取 0:表示阻塞等待 MSG_DONTWAIT:非阻塞 返回值:成功返回发送字节的个数 -1:失败,并置位错误码
close函数
#include int close(int fd); 功能:关闭套接字文件描述符 参数:要关闭的套接字文件描述符 返回值:成功返回0,失败返回-1并置位错误码
connect函数(客户端专属)
#include /* See NOTES */ #include int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);//文件套接字,地址信息结构体,参数2大小 功能:将指定的套接字,连接到给定的地址上 参数1:要连接的套接字文件描述符 参数2:通用地址信息结构体 参数3:参数2的大小 返回值:成功返回0,失败返回-1并置位错误码 if(connect(sockfd_,(struct sockaddr*)&sin,sizeof(sin))<0) { perror(\"connect error\"); }
网络属性
1> 网络套接字在不同层中,有不同的设置,分别有应用层(套接字层)、传输层、网络层、以太网层
2> 对套接字属性进行设置,可以使用setsockopt和getsockopt函数完成
setsockopt()
getsockopt()
#include /* See NOTES */ #include int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t*optlen);//文件描述符,操作的套接字层, int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen); 功能:设置或者获取套接字文件描述符的属性 参数1:套接字文件描述符 参数2:表示操作的套接字层 SOL_SOCKET:表示应用层或者套接字层,通过man 7 socket进行查找 该层中常用的属性:SO_REUSEADDR 地址快速重用 SO_BROADCAST 允许广播 SO_RCVTIMEO and SO_SNDTIMEO:发送或接收超时时间 //传输层 IPPROTO_TCP:表示传输层的基于TCP的协议 IPPROTO_UDP:表示传输层中基于UDP的协议 //网络层 IPPROTO_IP:表示网络层 该层常用的属性:IP_ADD_MEMBERSHIP,加入多播组 参数3:对应层中属性的名称 //上面对应的层级的属性 参数4:参数3属性的值,一般为int类型,其他类型,会给出 //单独开辟出来int空间 参数5:参数4的大小 //get与set有不同,get需要传地址,set直接sizeof()即可 返回值:成功返回0,失败返回-1并置位错误码 //测试套接字地址默认是否能快速重用 eg:char reuse = -1; //接收获取下来的数据 socklen_t reuselen ; //接收大小 if(getsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, &reuselen)==-1) { perror(\"getscokopt error\"); return -1; }//设置端口号快速重用 int reu = 1; if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reu, sizeof(reu)) == -1) { perror(\"setsockopt error\"); return -1; }
runtime_error()异常处理
runtime_error
是 C++ 标准库中用于表示运行时逻辑错误的异常类
设计目的:报告程序运行时无法通过逻辑检查的外部错误(例如文件操作失败、网络中断等),而非代码逻辑本身的错误
logic_error
runtime_error
domain_error
bad_alloc
exception
,非 runtime_error
子类)1.4.2server开始函数代码块
bool Server::start(){ // 启动监听 if (listen(sfd_, 5) < 0) { cerr << \"listen error\" << endl; return false; } // 设置运行状态 running_ = true; // 表示可以接受客户端请求 cout << \"Server started on \" << ip_ << \" : \" << port_ << endl; // 等待客户端连接请求:循环完成 while (running_) { sockaddr_in client_addr; // 接收客户端地址信息结构体 socklen_t addr_len = sizeof(client_addr); // 接收客户端地址信息结构体的大小 // 接收客户端连接请求操作 int cfd = accept(sfd_, (struct sockaddr *)&client_addr, &addr_len); if (cfd < 0) { if (running_) { cerr << \"accept error\" << endl; } continue; } // 创建一个分支线程,在分支线程中,进行跟客户端的通信 thread([this, cfd, client_addr]() { cout<<\"Client connected :\"<<inet_ntoa(client_addr.sin_addr)<<\":\"<<ntohs(client_addr.sin_port)<<endl; //调用客户端线程处理函数 handleClient(cfd,client_addr); }) .detach(); // 将线程设置成分离态,资源由系统回收 } return true; }
thread线程体(C++11系列)
1. 全局函数作为线程体
#include void global_func(int x, const std::string& s) { std::cout << \"全局函数: \" << x << \", \" << s << std::endl; } int main() { std::thread t(global_func, 10, \"Hello\"); // 直接传递函数指针和参数 t.join(); }//参数1:函数结构体,参数2,参数3...都是传入对应的函数结构体里当成对应的参数
特点:默认按值传递(参数被复制到线程独立存储中)
std::ref
需传递引用时,使用std::ref
或std::cref
包装(避免悬空引用)**常量包装用cref **ref的详细知识
std::ref()
是 C++11
引入的标准库函数(定义在 头文件中),用于将对象包装成可拷贝的引用包装器(
std::reference_wrapper
),解决函数式编程和模板中传递引用时的值拷贝问题
- 函数式编程(
std::bind
,std::function
)
void func(int& a) { a++; }int val = 0;auto bound = std::bind(func, val); // 错误:val 被拷贝,修改不影响原值 auto bound_ref = std::bind(func, std::ref(val)); // 正确:修改 val
- 多线程(
std::thread
)
void worker(int& data) { data = 42; }int value = 0;std::thread t(worker, value); // 错误:value 被拷贝std::thread t_ref(worker, std::ref(value)); // 正确:修改 value
- 模板参数推导
template void template_func(T arg) { arg++; }int num = 1;template_func(num); // 值传递,num 不变 template_func(std::ref(num)); // 引用传递,num = 2
2. 类成员函数作为线程体
class MyClass { public: void member_func(int data) { std::cout << \"成员函数: \" << data << std::endl; } }; int main() { MyClass obj; std::thread t(&MyClass::member_func, &obj, 100); // 传递对象指针和参数 t.join(); }//替代版本 // 使用lambda绑定成员函数std::thread t([&obj] { obj.member_func(100); });
特点:需传递成员函数指针和对象实例指针(或引用)
3. 仿函数(Functor)作为线程体
4. Lambda表达式作为线程体
int main() { int local_var = 42; std::thread t([local_var] { // 按值捕获局部变量 std::cout << \"Lambda: \" << local_var << std::endl; }); t.join(); }eg: // 创建一个分支线程,在分支线程中,进行跟客户端的通信 //this用来调用handleClient函数 thread([this, cfd, client_addr]() { cout<<\"Client connected :\"<<inet_ntoa(client_addr.sin_addr)<<\":\"<<ntohs(client_addr.sin_port)<<endl; //调用客户端线程处理函数 handleClient(cfd,client_addr); }) .detach(); // 将线程设置成分离态,资源由系统回收
注意事项
-
参数传递规则
- 参数默认拷贝到线程内部存储(即使函数声明为引用)
- 强制传递引用需用
std::ref
:、
int x = 10;std::thread t([](int& y) { y *= 2; }, std::ref(x)); // x被修改为20
-
线程所有权管理
- 必须调用
join()
或detach()
避免std::terminate
- 使用
std::jthread
(C++20) 可自动合并线程
- 必须调用
-
异常安全:
std::thread t(...); try { /* 可能抛异常的操作 */ } catch (...) { t.join(); // 确保异常时线程不泄漏 throw; } t.join();
lambda表达式
Lambda
表达式是 C++11 引入的核心特性,用于创建匿名函数对象,可简化代码并增强灵活性
[capture-list] (params) mutable? noexcept? -> return-type? { body }
[]
[x]
(值捕获)、[&y]
(引用捕获)()
(int a, string b)
const
)[=]() mutable { x++; }
[ ]() noexcept { ... }
->
[ ]() -> int { return 42; }
{}
{ cout << \"Hello\"; }
捕获列表的 4 种方式
[ ]
[=]
[&]
[x, &y]
) int count = 0; thread t([&] { count++; // 直接操作外部变量 }); t.join();class MyClass { void run() { thread t([this] { this->process(); }); // 捕获 this }};
1.4.2 server处理客户端请求函数
void Server::handleClient(int cfd, sockaddr_in client_addr){ Msg msg; // 用于存储收发的数据 while (1) { // 接收客户端消息 ssize_t recv_len = recv(cfd, &msg, sizeof(msg), 0); if (recv_len <= 0) { if (recv_len == 0) // 当前客户端已下线 { cout << \"Client disconnect :\" << inet_ntoa(client_addr.sin_addr) << endl; } else { cerr << \"recv error\" <registerUser(msg.name, msg.text); strcpy(response.text, success ? \"**OK**\" : \"**EXISTS**\"); // 如果成功注册则回复OK,否则回复EXISTS break; } case L: // 登录功能 { // 执行登录操作 bool is_online = false; // 返回是否在线 bool success = db_manager_->loginUser(msg.name, msg.text, is_online); // 执行登录函数 if (success) { strcpy(response.text, is_online ? \"**EXISTS**\" : \"**OK**\"); // 根据是否已经在线,进行回复 } else { strcpy(response.text, \"**FAIL**\"); // 表示登录失败 } break; } case Q: // 退出功能 { // 执行退出操作 db_manager_->logoutUser(msg.name); close(cfd); break; } case S: // 搜查功能 { // 执行搜查操作 string meaning; // 报错单词含义 if (db_manager_->querryWord(msg.text, meaning)) { // 成功找到,将单词和含义一起回复给客户端 snprintf(response.text, sizeof(response.text), \"%s %s\", msg.text, meaning.c_str()); } else { strcpy(response.text, \"Not Found\"); // 表示没找到 } // 保存历史记录 time_t now = time(NULL); // 获取当前系统时间对应的秒数 tm *local = localtime(&now); // 将以秒数为单位的时间转变成时间的结构体 char time_str[20] = \"\"; // 保存当前系统时间的字符串 strftime(time_str, sizeof(time_str), \"%Y-%m-%d %H:%M:%S\", local); // string format time 转为字符串 // 将结构体中的年月日时分秒转变成格式串 // 调用历史查询记录 db_manager_->recordHistory(msg.name, msg.text, meaning, time_str); break; } case H: // 历史功能 { // 执行历史操作 string history; // 保存得到的历史记录 if (db_manager_->getHistory(msg.name, history)) { // 执行成功,history就有历史记录 strncpy(response.text, history.c_str(), sizeof(response.text)); // 将历史记录发送给客户端 } else { strcpy(response.text, \"No history\"); // 当前用户没有进行查单词操作 } break; } default: strcpy(response.text, \"valid Command\"); // 表示请求非法操作 break; } // 发送请求 response.networkByteOrder(); // 转变成网络字节序 if (send(cfd, &response, sizeof(response), 0) < 0) { cerr << \"send error\" << endl; break; } //判断如果是查看历史记录,那么最后还要给出一个历史查询接收的情况 if(msg.type == H) { strcpy(response.text,\"**OVER**\"); send(cfd,&response,sizeof(response),0); } } close(cfd); // 关闭客户端套接字}
strncpyvs
strcpy比对
strncpy
的核心功能是 从源字符串(src
)复制最多 n
个字符到目标缓冲区(dest
)
strcpy
strncpy
char* dest, const char* src
char* dest, const char* src, size_t n
src
的\\0
到dest
末尾(\\0
为空字符串)src
长度≥n,不会添加\\0
;若src
长度<n,填充\\0
至n字符src
,直到遇到\\0
n
个字符(可能截断)char dest[5];strncpy(dest, \"abcdef\", 5); // dest内容为\'a\',\'b\',\'c\',\'d\',\'e\'(无\\0)printf(\"%s\", dest); // 可能输出乱码或崩溃
snprintf
函数
int snprintf(char *str, size_t size, const char *format, ...); 参数说明: str:目标缓冲区指针,用于存储格式化后的字符串。 size:缓冲区总容量(单位:字节),包含终止符 \\0 所需空间。 format:格式化字符串(与 printf 语法一致)。 ...:可变参数列表,对应 format 中的占位符。 返回值: 成功时:返回预期写入的字符数(不包括 \\0),即使缓冲区不足也会计算完整长度。 错误时:返回负值(如编码错误或非法参数)。 eg:snprintf(response.text, sizeof(response.text), \"%s %s\", msg.text, meaning.c_str());
sprintf参数只在size处缺少,其余都正常处理,但尽量少使用。
C语言时间函数
// 保存历史记录 time_t now = time(NULL); // 获取当前系统时间对应的秒数 tm *local = localtime(&now); // 将以秒数为单位的时间转变成时间的结构体 char time_str[20] = \"\"; // 保存当前系统时间的字符串 strftime(time_str, sizeof(time_str), \"%Y-%m-%d %H:%M:%S\", local); // string format time 转为字符串 // 将结构体中的年月日时分秒转变成格式串 return string(time_str);
time()
是C标准库()中获取系统时间戳的核心函数
time_t time(time_t *timer); 参数 timer: 若传入 NULL(常用):直接返回当前时间戳。 若传入 time_t 变量地址:将时间戳存入该变量并返回。 返回值: 成功:返回自 Epoch(1970-01-01 00:00:00 UTC) 起的秒数(time_t 类型)。 失败:返回 (time_t)-1(如系统不支持时间获取)。 time_t now = time(NULL); // 当前时间戳存入 now printf(\"Seconds since Epoch: %ld\\n\", now);
localtime()
将UTC时间戳(time_t
)转换成本地时区时间
struct tm *localtime(const time_t *timer); 输入:指向UTC时间戳的指针(如 time(NULL) 的结果)输出:指向 tm 结构体的指针(内含分解时间字段) struct tm { int tm_year; // 年份 - 1900(2025年值为125) int tm_mon; // 月份 0-11(0=1月) int tm_mday; // 日 1-31 int tm_hour; // 时 0-23 int tm_min; // 分 0-59 int tm_sec; // 秒 0-60(60用于闰秒) };
strftime(time_str, sizeof(time_str), \"%Y-%m-%d %H:%M:%S\", local);
时间格式化:将tm
结构体转换为人类可读字符串
%Y
%m
%d
%H
%M
%S