文章目录
- [Linux 上手 UDP Socket 程序编写(含完整具体demo)](#Linux 上手 UDP Socket 程序编写(含完整具体demo))
-
- [1. 核心技术点](#1. 核心技术点)
- [2. UDP 通信模型速览](#2. UDP 通信模型速览)
- [3. 构建服务端:步骤与 API](#3. 构建服务端:步骤与 API)
-
- [3.1 创建套接字 socket](#3.1 创建套接字 socket)
- [3.2 填充地址并绑定 bind](#3.2 填充地址并绑定 bind)
- [3.3 循环收包 recvfrom → 处理 → 回包 sendto](#3.3 循环收包 recvfrom → 处理 → 回包 sendto)
- [3.4 注入业务回调(示例)](#3.4 注入业务回调(示例))
- [4. 构建客户端:步骤与 API](#4. 构建客户端:步骤与 API)
-
- [4.1 创建套接字 socket](#4.1 创建套接字 socket)
- [4.2 组织对端地址、发送 sendto](#4.2 组织对端地址、发送 sendto)
- [4.3 接收回复 recvfrom](#4.3 接收回复 recvfrom)
- [5. 字典 Demo 的实现(作为业务回调接入)](#5. 字典 Demo 的实现(作为业务回调接入))
-
- [5.1 字典文件与数据结构](#5.1 字典文件与数据结构)
- [5.2 加载流程:读取 → 切分 → 裁剪 → 写入 map](#5.2 加载流程:读取 → 切分 → 裁剪 → 写入 map)
- [5.3 查询接口与不命中返回](#5.3 查询接口与不命中返回)
- [5.4 将业务回调注入服务端](#5.4 将业务回调注入服务端)
- [6. 模块设计与职责划分](#6. 模块设计与职责划分)
- [7. 关键函数与细节清单](#7. 关键函数与细节清单)
- [8. 测试与运行](#8. 测试与运行)
- [9. 常见坑位速查](#9. 常见坑位速查)
- [10. 结语](#10. 结语)
- 附录:完整代码
-
-
- [文件: `test/Dict.hpp`](#文件:
test/Dict.hpp
) - [文件: `test/MyUdpServer.hpp`](#文件:
test/MyUdpServer.hpp
) - [文件: `test/MyUdpServer.cc`](#文件:
test/MyUdpServer.cc
) - [文件: `test/MyUdpClient.cc`](#文件:
test/MyUdpClient.cc
) - [文件: `test/Makefile`](#文件:
test/Makefile
) - [文件: `test/Log.hpp`](#文件:
test/Log.hpp
) - [文件: `test/Mutex.hpp`](#文件:
test/Mutex.hpp
) - [文件: `test/dictionary.txt`](#文件:
test/dictionary.txt
)
- [文件: `test/Dict.hpp`](#文件:
-
Linux 上手 UDP Socket 程序编写(含完整具体demo)
本文核心讲解"如何用 UDP socket 进行通信",包括每一步该调用哪些系统调用、参数怎么填、如何设计服务器和客户端的模块与边界、常见坑点与增强方式。字典查询只是业务载体,重点在 UDP 的通信。
1. 核心技术点
- UDP 的通信模型与适用场景
- 服务端/客户端从 0 到 1 的完整步骤
- 关键系统调用的使用细节:
socket
、bind
、sendto
、recvfrom
- 地址结构
sockaddr_in
、字节序转换、IP 转换函数 - 缓冲区处理、错误处理、阻塞与非阻塞、超时与重试
- 可扩展的架构设计与实战建议
2. UDP 通信模型速览
- 无连接(connectionless):没有三次握手,直接收发。开销低、时延小。
- 不可靠(unreliable):可能丢包、乱序、重复。应用层需要容错(超时、重试、去重)。
- 保留消息边界(message-oriented):一次
sendto
对应一次recvfrom
,天然"报文"语义,不用像 TCP 处理粘包/拆包。 - 适合"体量小、请求-应答、可容忍丢包"的场景。本示例查询一次、返回一次,正合适。
3. 构建服务端:步骤与 API
3.1 创建套接字 socket
文件:
test/MyUdpServer.hpp
cpp
void Init(){
//1.创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0){
LOG(LogLevel::FATAL) << "socket error" ;
exit(1);
}
LOG(LogLevel::INFO) << "socket create success, sockfd: " << _sockfd ;
-
重要说明:
- domain :
AF_INET
(IPv4);如需 IPv6 用AF_INET6
。 - type :
SOCK_DGRAM
(UDP 数据报);TCP 则是SOCK_STREAM
。 - protocol: 一般填 0,由内核根据 type 推断。
- 返回值 : 成功为非负 fd;失败
-1
并设置errno
(如EMFILE/ENFILE
资源耗尽)。 - UDP 默认阻塞模式;如需非阻塞,后续用
fcntl
设置O_NONBLOCK
。
- domain :
-
可选优化(快速重启端口):
cpp
int yes = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
- SO_REUSEADDR :允许在 TIME_WAIT 等场景快速复用端口,对 UDP 也常用;须在
bind
前设置。
3.2 填充地址并绑定 bind
文件:
test/MyUdpServer.hpp
cpp
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
文件:
test/MyUdpServer.hpp
cpp
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if( n < 0 ){
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
LOG(LogLevel::INFO) << "bind success, sockfd:" << _sockfd;
- 重要说明:
sockaddr_in
字段:sin_family=AF_INET
、sin_port
必须htons
、sin_addr.s_addr
可设INADDR_ANY
(所有本地网卡)。htons/ntohs
端口字节序转换;IP 推荐用inet_pton
(现代、安全),此处演示INADDR_ANY
。bind
将本地地址:端口与套接字关联,服务端必须显式bind
固定端口。- 常见错误:
EADDRINUSE
端口占用、EACCES
低号端口权限不足。
3.3 循环收包 recvfrom → 处理 → 回包 sendto
文件:
test/MyUdpServer.hpp
cpp
while(_isrunning){
char buffer[1024];
//1.不断收(读)消息
ssize_t rec_msg = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr* )&peer, &len);
if(rec_msg > 0){
int peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
buffer[rec_msg] = 0;
std::string result = _func(buffer);
LOG(LogLevel::DEBUG) << "buffer:" << buffer << " from " << peer_ip << ":" << peer_port;
// 2.发消息
ssize_t snd_sz = sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
}
}
-
重要说明(
recvfrom
):- 原型:
ssize_t recvfrom(int fd, void* buf, size_t len, int flags, struct sockaddr* src, socklen_t* addrlen)
- 阻塞读:无数据会阻塞(可设置
SO_RCVTIMEO
或改非阻塞)。 - 返回值:本次数据报长度;若
len
小于数据报,超出部分被丢弃(UDP 保持报文边界,不会"分多次收")。 sizeof(buffer)-1
:为字符串结尾'\0'
预留一字节。peer
:回填对端地址,后续回包直接使用。inet_ntoa
返回静态缓冲区的字符串,非线程安全;多线程推荐inet_ntop
。
- 原型:
-
重要说明(
sendto
):- 原型:
ssize_t sendto(int fd, const void* buf, size_t len, int flags, const struct sockaddr* dst, socklen_t addrlen)
- UDP 要么整报成功,要么失败(不会部分发送)。
- 典型错误:
EAGAIN
(非阻塞且缓冲区满)、EMSGSIZE
(报文过大)、ENETUNREACH/EHOSTUNREACH
(网络不可达)。 flags
常为 0;除非需要 MSG_DONTWAIT 等特殊行为。
- 原型:
-
业务解耦:网络层只负责"收/发",实际处理交给回调
_func
。
3.4 注入业务回调(示例)
文件:
test/MyUdpServer.cc
cpp
std::unique_ptr<MyUdpServer> usvr = std::make_unique<MyUdpServer>(port, [&dict](const std::string &message){
return dict.Translate(message);
});
usvr->Init();
usvr->Start();
- 重要说明:
- 以函数对象注入业务逻辑(字典翻译/回显/计算器均可),不侵入网络层。
- 更换业务=替换回调,网络收发与生命周期管理不变。
4. 构建客户端:步骤与 API
4.1 创建套接字 socket
文件:
test/MyUdpClient.cc
cpp
int sockfd = socket (AF_INET,SOCK_DGRAM,0);
if(sockfd < 0){
std::cerr << "socket error" << std::endl;
return 2;
}
- 重要说明:
- 客户端通常无需显式
bind
;首次sendto
时内核自动分配临时端口(ephemeral port)。 - 如需固定本地端口/网卡(多播/防火墙策略等),可主动
bind
到指定本地地址:端口。
- 客户端通常无需显式
4.2 组织对端地址、发送 sendto
文件:
test/MyUdpClient.cc
cpp
std::string input;
std::cout << "Please Enter# ";
std::getline(std::cin,input);
sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(atoi(argv[2]));
dest_addr.sin_addr.s_addr = inet_addr(argv[1]);
int send_size = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
- 重要说明:
dest_addr
必须是"服务端"的地址;客户端不需要connect
也能直接sendto
。- 发送长度必须用实际数据长度:
input.size()
,绝不要用sizeof(std::string)
(那是类型大小)。 inet_addr
将点分十进制 IP 转为网络序整数;更推荐inet_pton(AF_INET, argv[1], &dest_addr.sin_addr)
(更健壮)。
4.3 接收回复 recvfrom
文件:
test/MyUdpClient.cc
cpp
char recv_buf[1024];
sockaddr_in peer_addr;
socklen_t peer_addr_len = sizeof(peer_addr);
int recv_size = recvfrom(sockfd,recv_buf,sizeof(recv_buf)-1,0,(struct sockaddr*)&peer_addr,&peer_addr_len);
if(recv_size > 0){
recv_buf[recv_size] = '\0';
std::cout << "server# " << recv_buf << std::endl;
}
- 重要说明:
- 仍然要用返回值补
'\0'
,避免打印脏数据。 peer_addr
给出实际回应者(在多播/任播/多服务端测试时很有用)。- 想要接收超时,可
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, ...)
;想非阻塞,用fcntl
+select/epoll
。
- 仍然要用返回值补
5. 字典 Demo 的实现(作为业务回调接入)
本节只聚焦"字典查询"如何作为业务层接入到 UDP 通信中,帮助你理解网络层与应用层的协作关系。
5.1 字典文件与数据结构
- 文件:
test/dictionary.txt
,每行key:value
。 - 结构:
unordered_map<string,string>
,平均 O(1) 查询。 - 默认路径:
文件:test/Dict.hpp
cpp
const std::string default_dict_path = "./dictionary.txt";
5.2 加载流程:读取 → 切分 → 裁剪 → 写入 map
文件:
test/Dict.hpp
cpp
bool Load(){
std::ifstream ifs(_path);
if(!ifs.is_open()){
return false;
}
std::string line;
while(std::getline(ifs, line)){
// 忽略空行
if(line.empty()) continue;
// 查找分隔符
std::size_t pos = line.find(':');
if(pos == std::string::npos) continue;
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + 1);
// 去除首尾空白
auto trim = [](std::string &s){
auto not_space = [](int ch){ return !std::isspace(ch); };
s.erase(s.begin(), std::find_if(s.begin(), s.end(), not_space));
s.erase(std::find_if(s.rbegin(), s.rend(), not_space).base(), s.end());
};
trim(key);
trim(value);
if(!key.empty() && !value.empty()){
_dict[key] = value;
}
}
return true;
}
- 重要说明:
std::getline
每次读取一行(不含换行符),适合按行解析。find(':')
只取第一个冒号分隔:支持定义:内容:附注
这类行时会把后缀全部并入value
。substr(pos + 1)
从冒号右侧取值(见当前文件第 31 行),避免把冒号包含进value
。trim
使用std::isspace
去除首尾空白,容忍文件中多余空格/Tab;如编译器报未声明,请#include <cctype>
。unordered_map
提供均摊 O(1) 插入/查询;若需有序遍历可换std::map
。
5.3 查询接口与不命中返回
文件:
test/Dict.hpp
cpp
std::string Translate(const std::string &word){
auto it = _dict.find(word);
if(it != _dict.end()){
return it->second;
}
return "not found";
}
- 重要说明:
- 完全匹配、大小写敏感;可在装载或查询时统一
tolower
实现不敏感匹配。 - 未命中返回
"not found"
,服务端原样返回给客户端。
- 完全匹配、大小写敏感;可在装载或查询时统一
5.4 将业务回调注入服务端
文件:
test/MyUdpServer.cc
cpp
std::unique_ptr<MyUdpServer> usvr = std::make_unique<MyUdpServer>(port, [&dict](const std::string &message){
return dict.Translate(message);
});
usvr->Init();
usvr->Start();
- 重要说明:
- 这体现"网络 I/O"与"业务处理"分层:更换业务无需触碰 UDP 细节。
- 回调签名
std::function<std::string(const std::string&)>
,输入/输出均为文本,便于替换其他业务。
6. 模块设计与职责划分
- 网络层(UDP 收发):
- 负责
socket/bind/recvfrom/sendto
,不涉及业务。 - 把"收到的消息"和"对端地址"传给业务层处理。
- 负责
- 业务层(翻译/计算/路由):
- 输入消息字符串,输出要回的字符串。
- 在本示例中以回调形式注入网络层,解耦清晰。
文件:
test/MyUdpServer.cc
cpp
std::unique_ptr<MyUdpServer> usvr = std::make_unique<MyUdpServer>(port, [&dict](const std::string &message){
return dict.Translate(message);
});
usvr->Init();
usvr->Start();
7. 关键函数与细节清单
- socket :
socket(AF_INET, SOCK_DGRAM, 0)
;失败返回 -1,查errno
。 - bind: 绑定本地地址:端口;失败多为端口占用或权限问题。
- sendto/recvfrom: 保留报文边界;UDP 不会"部分发送",要么整报成功要么失败。
- htons/ntohs、htonl/ntohl: 端口/地址的主机序 ↔ 网络序。
- inet_addr/inet_ntoa (或
inet_pton/inet_ntop
): 文本 ↔ 网络序地址;inet_ntoa
非线程安全。 - bzero/memset: 清零结构体,避免未初始化字段带来未定义行为。
- 超时 :
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, ...)
设置接收超时(阻塞模式下生效)。 - 非阻塞 :
fcntl(fd, F_SETFL, O_NONBLOCK)
配合select/poll/epoll
。
示例:接收超时 2s(阻塞模式)
cpp
timeval tv{.tv_sec = 2, .tv_usec = 0};
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
8. 测试与运行
- 编译:
bash
cd /root/linux/test
make
- 启动服务端(如 8080):
bash
./myudpserver 8080
- 启动客户端并交互:
bash
./myudpclient 127.0.0.1 8080
- 输入
apple
,观察请求-应答链路(验证 UDP 收发、长度、字节序、地址填充)。
9. 常见坑位速查
- 发送长度错用
sizeof(std::string)
→ 正确是input.size()
- 忘记
htons/ntohs
,导致端口错乱 recvfrom
后未补\0
,输出脏字符- 客户端不
bind
正常;首发sendto
自动绑定临时端口 - 字典只是业务层;用回显也能完整验证 UDP 收发
10. 结语
本文把 UDP 的核心系统调用与工程实践一网打尽,示例中的"字典查询"只是把回调接上,让你更直观看见"收到一条请求 → 处理 → 回一条响应"的完整链路。掌握这些 API 的使用细节与常见坑,你就可以自信地实现自己的 UDP 小服务,并按需扩展到更复杂的场景。
- 核心流程:
socket
→(服务端bind
)→sendto/recvfrom
- 关键细节:正确填
sockaddr_in
、字节序转换、长度与缓冲区处理 - 工程建议:业务与网络解耦、非阻塞与超时机制、应用层容错与可观测性
附录:完整代码
文件: test/Dict.hpp
cpp
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<map>
#include<unordered_map>
#include<algorithm>
#include<fstream>
const std::string default_dict_path = "./dictionary.txt";
class Dict
{
public:
Dict(const std::string &path = default_dict_path):_path(path)
{}
bool Load(){
std::ifstream ifs(_path);
if(!ifs.is_open()){
return false;
}
std::string line;
while(std::getline(ifs, line)){
// 忽略空行
if(line.empty()) continue;
// 查找分隔符
std::size_t pos = line.find(':');
if(pos == std::string::npos) continue;
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + 1);
// 去除首尾空白
auto trim = [](std::string &s){
auto not_space = [](int ch){ return !std::isspace(ch); };
s.erase(s.begin(), std::find_if(s.begin(), s.end(), not_space));
s.erase(std::find_if(s.rbegin(), s.rend(), not_space).base(), s.end());
};
trim(key);
trim(value);
if(!key.empty() && !value.empty()){
_dict[key] = value;
}
}
return true;
}
std::string Translate(const std::string &word){
auto it = _dict.find(word);
if(it != _dict.end()){
return it->second;
}
return "not found";
}
private:
std::string _path;
std::unordered_map<std::string, std::string> _dict;
};
文件: test/MyUdpServer.hpp
cpp
#pragma once
#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string>
#include <strings.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include <functional>
using namespace LogModule;
using func_t = std::function<std::string(const std::string&)>;
const int defaultfd = -1;
class MyUdpServer{
public:
MyUdpServer(uint16_t port, func_t func)
:_sockfd(defaultfd),
// _ip(ip),
_port(port),
_func(func)
{}
void Init(){
//1.创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0){
LOG(LogLevel::FATAL) << "socket error" ;
exit(1);
}
LOG(LogLevel::INFO) << "socket create success, sockfd: " << _sockfd ;
//绑定socket信息,ip和端口
//填充sockaddr_in结构体
struct sockaddr_in local;
bzero(&local,sizeof(local)); //一个用于内存初始化的函数,主要功能是将指定内存区域的所有字节设置为 0。
local.sin_family = AF_INET;
//将端口从本地格式转换成网络序列
local.sin_port = htons(_port);
//将IP从string类型转化为4个字节存储,再转化为网络序列:
//转成in_addr_t类型 inet_addr (const char *cp)
// local.sin_addr.s_addr = inet_addr(_ip.c_str());
// 不用上面的绑定具体ip,使用INADDR_ANY,表示绑定所有网卡
local.sin_addr.s_addr = INADDR_ANY;
//绑定套接字
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if( n < 0 ){
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
//绑定成功,输出日志
LOG(LogLevel::INFO) << "bind success, sockfd:" << _sockfd;
}
void Start(){
//因为udp不用管链接,一直管收发就好了
_isrunning = true;
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
while(_isrunning){
char buffer[1024];
//1.不断收(读)消息
ssize_t rec_msg = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr* )&peer, &len);
//sizeof(buffer)-1 是为了留一个位置将来添加 \n
if(rec_msg > 0){
//说明收到了消息
int peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
buffer[rec_msg] = 0;
std::string result = _func(buffer);
LOG(LogLevel::DEBUG) << "buffer:" << buffer << " from " << peer_ip << ":" << peer_port;
// 2.发消息
// std::string snd_msg = "receive message from server:";
// snd_msg += buffer;
// ssize_t snd_sz = sendto(_sockfd, snd_msg.c_str(), snd_msg.size(), 0, (struct sockaddr *)&peer, len);
ssize_t snd_sz = sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
private:
int _sockfd;
uint16_t _port; // 就是unsigned short int类型重定义了
// std::string _ip; // 这里使用字符串风格存储点分十进制的ip(比如1234.1.2.3)后需要转换成网络字节序
bool _isrunning;
func_t _func; // 服务器回调函数,用来对数据进行处理
};
文件: test/MyUdpServer.cc
cpp
#include "MyUdpServer.hpp"
#include "Dict.hpp"
#include <memory>
#include <iostream>
// ./myudpserver ip port
int main(int argc, char* argv[]){
if(argc != 2){
std::cerr << "Usage:" << argv[0] << " + port " << std::endl;
return 1;
}
// std::string ip = argv[1];
uint16_t port = std::stoi(argv[1]);
Enable_Console_Log_Strategy();
//字典对象提供翻译功能
Dict dict;
dict.Load();
std::unique_ptr<MyUdpServer> usvr = std::make_unique<MyUdpServer>(port, [&dict](const std::string &message){
return dict.Translate(message);
});
usvr->Init();
usvr->Start();
return 0;
}
文件: test/MyUdpClient.cc
cpp
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
// ./myudpclient server_ip server_port
int main(int argc, char* argv[]){
if(argc != 3){
std::cerr << "Usage:" << argv[0] << " + server_ip + server_port + " << std::endl;
return 1;
}
//1.创建套接字
int sockfd = socket (AF_INET,SOCK_DGRAM,0);
if(sockfd < 0){
std::cerr << "socket error" << std::endl;
return 2;
}
//2.绑定。
//客户端需要进行绑定,但是不需要我们手动显式地进行bind绑定
//当首次发送消息的时候,os会自动给客户端进行绑定。
//端口号采用随机端口号
while(1){
std::string input;
std::cout << "Please Enter# ";
std::getline(std::cin,input);
sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(atoi(argv[2]));
dest_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(dest_addr);
int send_size = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&dest_addr, addrlen);
char recv_buf[1024];
sockaddr_in peer_addr;
socklen_t peer_addr_len = sizeof(peer_addr);
int recv_size = recvfrom(sockfd,recv_buf,sizeof(recv_buf)-1,0,(struct sockaddr*)&peer_addr,&peer_addr_len);
if(recv_size > 0){
recv_buf[recv_size] = '\0';
std::cout << "server# " << recv_buf << std::endl;
}
}
return 0;
}
文件: test/Makefile
cpp
.PHONY: all clean
# 定义生成的可执行文件目标
all: myudpserver myudpclient
# 编译服务端
myudpserver: MyUdpServer.cc
g++ -o $@ $^ -std=c++17
# 编译客户端
myudpclient: MyUdpClient.cc
g++ -o $@ $^ -std=c++17
# 清理生成的可执行文件
clean:
rm -f myudpserver myudpclient
文件: test/Log.hpp
cpp
#pragma once
#include<iostream>
#include<sstream>
#include<string>
#include<ctime>
#include<unistd.h>
#include<pthread.h>
#include"Mutex.hpp"
namespace LogModule
{
enum class LogLevel
{
DEBUG = 0,
INFO,
WARNING,
ERROR,
FATAL
};
const char* LogLevelToString(LogLevel level)
{
switch(level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
class Log
{
public:
Log(LogLevel level, const char* file_name, int line)
: _level(level), _file_name(file_name), _line(line)
{}
~Log()
{
std::string log_str = __Stream.str();
time_t timestamp;
time(×tamp);
struct tm* tm_time = localtime(×tamp);
char time_buffer[128] = {0};
snprintf(time_buffer, sizeof(time_buffer), "%04d-%02d-%02d %02d:%02d:%02d",
tm_time->tm_year + 1900, tm_time->tm_mon + 1, tm_time->tm_mday,
tm_time->tm_hour, tm_time->tm_min, tm_time->tm_sec);
//拼接
std::stringstream _log_stream;
_log_stream << "[" << time_buffer << "] ";
_log_stream << "[" << LogLevelToString(_level) << "] ";
_log_stream << "[" << getpid() << "] ";
_log_stream << "[" << _file_name << "] ";
_log_stream << "[" << _line << "] - ";
_log_stream << log_str;
std::string log_string = _log_stream.str();
//输出
if(_consoleswitch)
std::cout << log_string << std::endl;
else if(_fileswitch)
{
_mutex.Lock();
FileWriteLog(log_string);
_mutex.Unlock();
}
}
public:
std::ostream& Stream(){ return __Stream; }
private:
void FileWriteLog(const std::string& log_string, bool create_new_file = false)
{
const std::string default_log_file = "./log.txt";
//打开文件
FILE* fp = nullptr;
if(create_new_file){
fp = fopen(default_log_file.c_str(), "w");
}else{
fp = fopen(default_log_file.c_str(), "a");
}
if(fp == nullptr){
std::cerr << "FileWriteLog::open " << default_log_file << " error" << std::endl;
return;
}
//写入,换行
log_string;
fprintf(fp, "%s\n", log_string.c_str());
//关闭文件
fclose(fp);
}
private:
LogLevel _level;
const char* _file_name;
int _line;
std::stringstream __Stream;
private:
//控制输出方式
static bool _consoleswitch;
static bool _fileswitch;
//文件锁
static Mutex _mutex;
};
bool Log::_consoleswitch = true;
bool Log::_fileswitch = false;
Mutex Log::_mutex;
Log LogMessage(LogLevel level, const char* file_name, int line)
{
return Log(level, file_name, line);
}
#define LOG(level) LogMessage(level, __FILE__, __LINE__).Stream()
//5种日志输出策略
void Enable_Console_Log_Strategy()
{
Log::_consoleswitch = true;
Log::_fileswitch = false;
}
void Enable_File_Log_Strategy()
{
Log::_consoleswitch = false;
Log::_fileswitch = true;
}
void Enable_Dual_Log_Strategy()
{
Log::_consoleswitch = true;
Log::_fileswitch = true;
}
void Disable_Log_Strategy()
{
Log::_consoleswitch = false;
Log::_fileswitch = false;
}
// #ifdef DEBUG
// #define LOG(level) DebugLog(level, __FILE__, __LINE__)
// #else
// #define LOG(level) 1 ? (void) 0 : LogMessage(level, __FILE__, __LINE__).Stream()
// #endif
}
文件: test/Mutex.hpp
cpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
~Mutex()
{
pthread_mutex_destroy(&_mutex);
}
void Lock()
{
pthread_mutex_lock(&_mutex);
}
void Unlock()
{
pthread_mutex_unlock(&_mutex);
}
private:
pthread_mutex_t _mutex;
};
class LockGuard
{
public:
LockGuard(Mutex& mutex)
:_mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex& _mutex;
};
文件: test/dictionary.txt
apple:苹果
banana:香蕉
orange:橙子
grape:葡萄
watermelon:西瓜
strawberry:草莓
peach:桃子
pear:梨
pineapple:菠萝
mango:芒果
lemon:柠檬
cherry:樱桃
blueberry:蓝莓
kiwi:奇异果
tomato:西红柿
potato:土豆
carrot:胡萝卜
cucumber:黄瓜
onion:洋葱
garlic:大蒜
ginger:生姜
rice:大米
noodles:面条
bread:面包
milk:牛奶
egg:鸡蛋
meat:肉
beef:牛肉
pork:猪肉
chicken:鸡肉
fish:鱼
shrimp:虾
tofu:豆腐
vegetable:蔬菜
fruit:水果
water:水
tea:茶
coffee:咖啡
juice:果汁
sugar:糖
salt:盐
oil:油
pepper:胡椒
computer:电脑
phone:手机
keyboard:键盘
mouse:鼠标
screen:屏幕
internet:互联网
book:书
pen:笔
paper:纸
table:桌子
chair:椅子
window:窗户
door:门
house:房子
car:汽车
bus:公交车
bicycle:自行车
thread_mutex_t _mutex;
};
class LockGuard
{
public:
LockGuard(Mutex& mutex)
:_mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex& _mutex;
};
#### 文件: `test/dictionary.txt`
apple:苹果
banana:香蕉
orange:橙子
grape:葡萄
watermelon:西瓜
strawberry:草莓
peach:桃子
pear:梨
pineapple:菠萝
mango:芒果
lemon:柠檬
cherry:樱桃
blueberry:蓝莓
kiwi:奇异果
tomato:西红柿
potato:土豆
carrot:胡萝卜
cucumber:黄瓜
onion:洋葱
garlic:大蒜
ginger:生姜
rice:大米
noodles:面条
bread:面包
milk:牛奶
egg:鸡蛋
meat:肉
beef:牛肉
pork:猪肉
chicken:鸡肉
fish:鱼
shrimp:虾
tofu:豆腐
vegetable:蔬菜
fruit:水果
water:水
tea:茶
coffee:咖啡
juice:果汁
sugar:糖
salt:盐
oil:油
pepper:胡椒
computer:电脑
phone:手机
keyboard:键盘
mouse:鼠标
screen:屏幕
internet:互联网
book:书
pen:笔
paper:纸
table:桌子
chair:椅子
window:窗户
door:门
house:房子
car:汽车
bus:公交车
bicycle:自行车
> 本附录覆盖项目目录下与本示例相关的全部源码与资源,便于直接编译运行与对照阅读。