目录
[2.1 大致思路](#2.1 大致思路)
[2.2 Socket.hpp](#2.2 Socket.hpp)
[2.3 Protocol.hpp](#2.3 Protocol.hpp)
[2.3.1 Jsoncpp](#2.3.1 Jsoncpp)
[2.3.2 代码](#2.3.2 代码)
[2.4 Tcpserver.hpp](#2.4 Tcpserver.hpp)
[2.5 TcpServer.cc](#2.5 TcpServer.cc)
[2.6 TcpClient.cc](#2.6 TcpClient.cc)
[2.7 守护进程化](#2.7 守护进程化)
[2.7.1 前台进程与后台进程](#2.7.1 前台进程与后台进程)
[2.7.2 进程组&&会话&&终端](#2.7.2 进程组&&会话&&终端)
[2.7.3 守护进程](#2.7.3 守护进程)
[2.8 示例及完整代码](#2.8 示例及完整代码)
1、应用层协议
- 前面,我们写了一些Socket编程,都是按"字符串 "进行传输。但是,当我们想传输一些"结构化的数据"怎么办?
- 可以将结构化数据 序列化 为字节序列进行传输,后面再反序列化。如:
- 文本 格式(如Json)序列化后,是人类可读的字符串 ,再转成字节(每个字符对应 ASCII/UTF-8 编码的字节)。
- 二进制 格式(如Protobuf)序列化后,是人类不可读但更紧凑的字节序列。
- 之前,我们说,协议 是一种约定 ,方便快速形成共识 ,减少通信成本。
- 现在,一方对一个结构序列化并发送 和另一方收到并反序列化为同一个结构 ,这种约定,就是应用层协议。
注意:
- 不序列化,直接传结构化数据的内存字节不行吗 ?可以,但是不推荐,因为不兼容。
- 为什么要说字节呢?不是二进制吗?因为字节 是计算机硬件和网络协议能直接处理的 "最小实际单元"。
2、NetCal
2.1 大致思路
- 实现一个NetCal (网络计算器),TcpClient 给TcpServer发结构化的数据 (操作数+运算符),TcpServer 给TcpClient回结构化的数据(执行结果)。

- 以TCP为例,TCP比UDP复杂一点,UDP类似。
- write/send,将数据拷贝到TCP的发送缓存区 ,由TCP决定怎么发送;对于网络中的数据,由TCP决定怎么接收,再read/recv,拷贝TCP的接收缓冲区的数据。
- 主机之间的通信 ,本质 是:把发送方的发送缓冲区的数据拷贝到对端的接收缓冲区。
- 对于缓冲区 ,则是为内核和用户之间 的生产者和消费者模型。
- 因为TCP有独立的发送和接收缓冲区 ,所以是全双工 (双方 能同时接收和发送 消息),tcpsockfd可以边读边写。
2.2 Socket.hpp
- 对socket****相关的接口 进行封装。
- 多态 :统一接口 (父类),屏蔽实现差异(子类);便于扩展新协议(子类继承父类,进行具体实现。"开闭原则":对扩展开放(能加 UdpSocket),对修改关闭(不用改已有代码))。
- 像TcpScoket,这样的代码,这个类可以用于listen_sockfd,也可以是sockfd,但是,对于其中一个,也用不全,里面的函数,会不会冗余?函数不占空间?逻辑上存在 "用不上的函数" ,但物理上 (内存层面)几乎没有额外开销 ,工程中 "代码复用优先于极致精简" 。
cpp
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include <memory>
using namespace LogModule;
const static int default_sockfd = -1;
const static int default_backlog = 16;
class Socket
{
public:
virtual void SocketOrDie() = 0; // = 0, 不需要实现
virtual void BindOrDie(uint16_t port) = 0;
virtual void ListenOrDie(int backlog) = 0;
virtual void ConnectOrDie(std::string &server_ip, uint16_t server_port) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr* client) = 0;
virtual int Recv(std::string* out) = 0;
virtual int Send(const std::string& in) = 0;
virtual void Close() = 0;
public:
void BuildTcpServer(uint16_t port)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(default_backlog);
}
void BuildTcpClient(std::string &server_ip, uint16_t server_port)
{
SocketOrDie();
ConnectOrDie(server_ip,server_port);
}
};
class TcpSocket : public Socket
{
public:
TcpSocket(int sockfd = default_sockfd)
: _sockfd(sockfd)
{
}
virtual void Close() override
{
if (_sockfd != default_sockfd)
::close(_sockfd); // ::表示调用 全局作用域 中的 close 函数
}
virtual void SocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(SOCKET_ERROR);
}
LOG(LogLevel::INFO) << "socket success, socket: " << _sockfd;
}
virtual void BindOrDie(uint16_t port) override
{
InetAddr local(port);
int n = ::bind(_sockfd, CONST_CONV(local.Addr()), local.AddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error!";
exit(BIND_ERROR);
}
LOG(LogLevel::INFO) << "bind success, socket: " << _sockfd;
}
virtual void ListenOrDie(int backlog) override
{
int n = ::listen(_sockfd, default_backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error!";
exit(LISTEN_ERROR);
}
LOG(LogLevel::INFO) << "listen success, sockfd: " << _sockfd;
}
virtual void ConnectOrDie(std::string &server_ip, uint16_t server_port) override
{
InetAddr server(server_ip, server_port);
int n = ::connect(_sockfd, CONST_CONV(server.Addr()), server.AddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "connect error!";
exit(CONNECT_ERROR);
}
LOG(LogLevel::INFO) << "connect success, sockfd: " << _sockfd;
}
virtual std::shared_ptr<Socket> Accept(InetAddr* client) override
{
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int fd = ::accept(_sockfd, CONV(addr), &len);
if (fd < 0)
{
LOG(LogLevel::WARNING) << "accept failed";
return nullptr;
}
client->SetAddr(addr);
LOG(LogLevel::INFO) << "accept success, client: " << client->StringAddr();
return std::make_shared<TcpSocket>(fd); // 这个server的sockfd就可以调用Recv和Send方法。
}
virtual int Recv(std::string* out) override
{
char buf[1024];
ssize_t n = ::recv(_sockfd,buf,sizeof(buf)-1,0);
if(n > 0)
{
buf[n] = 0;
*out += buf; // += 可能要不断的读
}
return n;
}
virtual int Send(const std::string& in) override
{
return ::send(_sockfd,in.c_str(),in.size(),0);
}
private:
int _sockfd; // 既可以是listen_sockfd,也可以是sockfd,复用代码。
};
2.3 Protocol.hpp
2.3.1 Jsoncpp
- Jsoncpp是一个用于处理JSON数据 的C++库 。它提供了将JSON数据序列化为字符串 以及从字符串反序列化为C++数据结构的功能。Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。
- 特性 :简单 ;高性能 ;支持JSON标准中的所有数据类型 ,包括对象、数组、字符串、数 字、布尔值和null;方便错误处理(日志清晰)。
- 安装 : ubuntu : sudo apt install -y libjsoncpp-dev。
- 使用实例:
- <jsoncpp/json/json.h> 的路径设计是 Jsoncpp 为了避免头文件命名冲突、保持内部结构清晰、遵循行业惯例而采用的方案。它通过 "目录前缀" 的方式,让编译器和开发者都能明确区分这是 Jsoncpp 库的核心头文件,同时兼容库的安装和引用逻辑。
cpp
#include <iostream>
#include <json/json.h>
#include <string>
int main() {
// -------------------------- 序列化(生成字符串) --------------------------
Json::Value root;
root["name"] = "David";
root["score"] = 95.5;
root["passed"] = true;
Json::StreamWriterBuilder writer_builder;
std::string json_str = Json::writeString(writer_builder, root); // 简洁写法
std::cout << "序列化结果:\n" << json_str << std::endl;
// json_str
//{
// "name": "David",
// "passed": true,
// "score": 95.5
//}
// -------------------------- 反序列化(解析字符串) --------------------------
Json::Value parsed_root;
Json::CharReaderBuilder reader_builder;
std::string err_msg; // 用于存储解析错误信息
// 创建CharReader并解析字符串
std::unique_ptr<Json::CharReader> reader(reader_builder.newCharReader());
bool success = reader->parse(
json_str.c_str(), // 字符串起始地址
json_str.c_str() + json_str.length(), // 字符串结束地址
&parsed_root, // 解析结果存储到parsed_root
&err_msg // 错误信息存储到err_msg
);
if (success) {
std::cout << "\n反序列化结果:" << std::endl;
std::cout << "姓名:" << parsed_root["name"].asString() << std::endl;
std::cout << "分数:" << parsed_root["score"].asDouble() << std::endl;
} else {
std::cerr << "\n解析失败!错误信息:" << err_msg << std::endl;
}
return 0;
}
2.3.2 代码
- TCP是面向字节流 的,双方的收发次数可能不一致 ,要收到完整的报文 ,需要对json串加一些标记 (最主要的是在前面加上json串的长度)。
- UDP是面向数据报 的,双方收发次数一致 ,收到的报文是完整的。
- 这里先处理读取方面,先弱化写的方面,后面再讲解。
cpp
#pragma once
#include "Socket.hpp"
#include "Common.hpp"
#include <jsoncpp/json/json.h>
#include <functional>
#include <memory>
class Request
{
public:
Request()
{
}
Request(int x, int y, char oper)
: _x(x), _y(y), _oper(oper)
{
}
std::string Serialize()
{
// -------------------------- 序列化(生成字符串) --------------------------
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::StreamWriterBuilder writer_builder;
std::string json_str = Json::writeString(writer_builder, root); // 简洁写法
return json_str;
}
bool Deserialize(std::string &json_str)
{
// -------------------------- 反序列化(解析字符串) --------------------------
Json::Value parsed_root;
Json::CharReaderBuilder reader_builder;
std::string err_msg; // 用于存储解析错误信息
// 创建CharReader并解析字符串
std::unique_ptr<Json::CharReader> reader(reader_builder.newCharReader());
bool success = reader->parse(
json_str.c_str(), // 字符串起始地址
json_str.c_str() + json_str.length(), // 字符串结束地址
&parsed_root, // 解析结果存储到parsed_root
&err_msg // 错误信息存储到err_msg
);
if (success)
{
_x = parsed_root["x"].asInt();
_y = parsed_root["y"].asInt();
_oper = parsed_root["oper"].asInt(); // char通过ASCII映射为整数
}
else
{
LOG(LogLevel::ERROR) << "\n解析失败! 错误信息: " << err_msg;
}
return success;
}
int GetX()
{
return _x;
}
int GetY()
{
return _y;
}
char GetOper()
{
return _oper;
}
private:
int _x;
int _y;
char _oper;
};
class Response
{
public:
Response()
{
}
Response(int result, int code)
: _result(result), _code(code)
{
}
std::string Serialize()
{
// -------------------------- 序列化(生成字符串) --------------------------
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::StreamWriterBuilder writer_builder;
std::string json_str = Json::writeString(writer_builder, root); // 简洁写法
return json_str;
}
bool Deserialize(std::string &json_str)
{
// -------------------------- 反序列化(解析字符串) --------------------------
Json::Value parsed_root;
Json::CharReaderBuilder reader_builder;
std::string err_msg; // 用于存储解析错误信息
// 创建CharReader并解析字符串
std::unique_ptr<Json::CharReader> reader(reader_builder.newCharReader());
bool success = reader->parse(
json_str.c_str(), // 字符串起始地址
json_str.c_str() + json_str.length(), // 字符串结束地址
&parsed_root, // 解析结果存储到parsed_root
&err_msg // 错误信息存储到err_msg
);
if (success)
{
_result = parsed_root["result"].asInt();
_code = parsed_root["code"].asInt();
}
else
{
LOG(LogLevel::ERROR) << "\n解析失败! 错误信息: " << err_msg;
}
return success;
}
void ShowResult()
{
std::cout << "result is: " << _result << '[' << _code << ']' << std::endl;
}
void SetResult(int result)
{
_result = result;
}
void SetCode(int code)
{
_code = code;
}
private:
int _result;
int _code; // 0为正确,非0为各种错误
};
const static std::string sep = "\r\n";
using func_t = std::function<Response(Request &req)>;
class Protocol
{
public:
Protocol()
{
}
Protocol(func_t func)
: _func(func)
{
}
std::string Encode(const std::string &json_str)
{
// TCP是面向字节流的,要读到一个完整的json串,需要做些标记
// 这里使用格式:json串长度+\r\n+json串+\r\n,即"json串长度\r\njson串\r\n"
int len = json_str.size();
return std::to_string(len) + sep + json_str + sep;
}
bool Decode(std::string &buffer, std::string *package)
{
int pos = buffer.find("\r\n");
if (pos == std::string::npos)
return false;
int str_len = std::stoi(buffer.substr(0, pos));
int json_str_len = pos + sep.size() + str_len + sep.size();
if (buffer.size() < json_str_len)
return false;
*package = buffer.substr(pos + sep.size(), str_len);
buffer.erase(0, json_str_len);
return true;
}
void GetRequest(std::shared_ptr<Socket> &sockfd, const InetAddr &client)
{
std::string buffer;
while (true)
{
int n = sockfd->Recv(&buffer);
if (n > 0)
{
std::string json_package;
// 1. 解析报文,不完整,继续读(所以在Recv里面是+=)
while (Decode(buffer, &json_package))
{
// 2. 反序列化
Request req;
bool OK = req.Deserialize(json_package);
if (!OK)
continue; // 跳过当前无效的报文,继续处理后续可能有效的报文,防止buffer累积
// 3. 执行函数
Response resp = _func(req);
// 4. 序列化
std::string json_str = resp.Serialize();
// 5. Encode,加报头
std::string send_str = Encode(json_str);
// 6. 发送
sockfd->Send(send_str);
}
}
else if (n == 0)
{
LOG(LogLevel::INFO) << client.StringAddr() << " 退出了!";
sockfd->Close();
break;
}
else // n < 0
{
LOG(LogLevel::WARNING) << client.StringAddr() << " 异常";
sockfd->Close();
break;
}
}
}
bool GetResponse(std::shared_ptr<Socket> &client, Response *resp)
{
std::string buffer;
while (true)
{
int n = client->Recv(&buffer);
if (n > 0)
{
std::string json_package;
// 1. 解析报文,不完整,继续读(所以在Recv里面是+=)
if (Decode(buffer, &json_package))
{
// 2. 反序列化
return resp->Deserialize(json_package); // 只读取一次完整的报文
}
}
else if (n == 0)
{
std::cout << "server quit" << std::endl;
return false;
}
else // n < 0
{
std::cout << "server error" << std::endl;
return false;
}
}
}
std::string BuildRequestString(int x, int y, char oper)
{
Request req(x, y, oper);
// 1. 序列化
std::string json_str = req.Serialize();
// 2. Encode.加报头
return Encode(json_str);
}
private:
func_t _func;
};
2.4 Tcpserver.hpp
cpp
#pragma once
#include "Socket.hpp"
#include <functional>
#include <sys/wait.h>
using ioservice_t = std::function<void(std::shared_ptr<Socket>&, const InetAddr& )>;
class TcpServer : public NoCopy
{
public:
TcpServer(uint16_t port,ioservice_t service)
: _listen_socket(std::make_unique<TcpSocket>())
, _running(false)
,_service(service)
{
// 1. 创建套接字
// 2. bind套接字
// 3. 设置监听套接字
_listen_socket->BuildTcpServer(port);
}
// version-多进程
void Start()
{
_running = true;
while (_running)
{
// 4. 创建已连接套接字
InetAddr client;
std::shared_ptr<Socket> sockfd = _listen_socket->Accept(&client);
if(!sockfd)
continue;
pid_t pid = fork();
if(pid < 0)
{
LOG(LogLevel::WARNING) << "fork failed";
continue;
}
else if(pid == 0)
{
_listen_socket->Close(); // 关闭listen_sockfd
// 子进程
if(fork() > 0)
exit(OK);
// 孙子进程
_service(sockfd,client);
exit(OK);
}
else
{
sockfd->Close(); // 关闭sockfd
// 父进程
waitpid(pid,nullptr,0);
}
}
}
private:
std::unique_ptr<Socket> _listen_socket;
bool _running;
ioservice_t _service;
};
2.5 TcpServer.cc

- 在 TCP/IP 五层协议 中,OSI 七层协议 的应用层 、表示层 、会话层 ,全部由应用程序自行实现 ,内核(操作系统)不介入 ------ 因为用户需求 (如数据格式、会话逻辑)千变万化,内核无法预先定义通用的应用层,表示层、会话层逻辑。
- 但是OSI 七层协议 非常好 ,因为在应用层设计 的时候,这样分层,逻辑清晰,便于理解和模块化设。
cpp
#include "NetCal.hpp"
#include "Protocol.hpp"
#include "TcpServer.hpp"
#include "Daemon.hpp"
#include <memory>
// ./tcpserver server_port
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " server_port" << std::endl;
exit(USAGE_ERROR);
}
std::cout << "服务器已经启动,已经是一个守护进程了" << std::endl;
Daemon(0,0); // daemon(0,0);
// Enable_Console_Log_Strategy();
Enable_File_Log_Strategy(); // 向文件里打印日志
uint16_t server_port = std::stoi(argv[1]);
// 应用层 具体功能的执行
std::unique_ptr<NetCal> net_cal = std::make_unique<NetCal>();
// 表示层 数据格式的转化
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&net_cal](Request& req){
return net_cal->Execute(req);
});
// 会话层 通信连接的管理
std::unique_ptr<TcpServer> tcp_server = std::make_unique<TcpServer>(server_port,
[&protocol](std::shared_ptr<Socket>& sockfd, const InetAddr &client){
protocol->GetRequest(sockfd,client);
});
tcp_server->Start();
return 0;
}
2.6 TcpClient.cc
cpp
#include "Socket.hpp"
#include "Common.hpp"
#include "Protocol.hpp"
void GetDataFromStdin(int *x, int *y, char *oper)
{
std::cout << "Please Enter x: ";
std::cin >> *x;
std::cout << "Please Enter y: ";
std::cin >> *y;
std::cout << "Please Enter oper: ";
std::cin >> *oper;
}
// ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
exit(USAGE_ERROR);
}
Enable_Console_Log_Strategy();
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 创建套接字
// 2. 设置连接套接字
std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();
client->BuildTcpClient(server_ip,server_port);
auto protocol = std::make_unique<Protocol>();
while (true)
{
int x, y;
char oper;
GetDataFromStdin(&x, &y, &oper);
std::string request_string = protocol->BuildRequestString(x,y,oper);
client->Send(request_string);
Response resp;
bool OK = protocol->GetResponse(client,&resp);
if(!OK)
break;
resp.ShowResult();
}
client->Close();
return 0;
}
2.7 守护进程化
2.7.1 前台进程与后台进程
- 路径/可执行程序 是 前台进程 ,从 标准输入(键盘 )中获取内容 。只能有一个,因为标准输入的内容要给到一个确定的进程。
- 路径/可执行程序 & 是 后台进程 ,无法 从标准输入(键盘 )中获取内容 ,可有多个。
- 前台进程和后台进程,都可以向标准输出(显示器)打印内容。
- 一个现象,运行前台程序,父进程创建子进程,Ctrl+C父进程终止,父进程退出,子进程被一号进程"领养",变成后台进程,Ctrl+C无法杀死子进程。
- jobs。查看所有后台进程。
- fg 任务号 。指定进程 ,提到前台。
- Ctrl+Z。暂停前台进程 ,提到后台。
- bg 任务号 。让后台进程恢复运行。
2.7.2 进程组&&会话&&终端
- 前面的任务 ,也称为作业。


- PGID,即进程组id ,为组长的pid,即使组长退了,也不变。作业 以进程组 (一个或多个进程)的形式运行。
- SID,即session(会话) id。进程组属于某一个会话。
- TTY,即终端 。终端是会话的 "载体" 和 "控制界面"。
2.7.3 守护进程
- 守护进程 也称精灵进程。
- 当关闭终端 时,在该会话里的进程可能会受影响 (如:还可能向该终端打印等),守护进程 就是让进程完全脱离终端 、在后台稳定运行。
cpp
#pragma once
#include "Log.hpp"
#include "Common.hpp"
#include <string>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace LogModule;
const static std::string dev = "/dev/null";
void Daemon(int nochdir, int noclose)
{
// 1. 设置信号
signal(SIGPIPE, SIG_IGN);
signal(SIGCLD, SIG_IGN);
// 2. 孤儿进程
if (fork())
exit(0);
// 3. 创建新会话
setsid();
// 4. 更改目录
if (nochdir == 0)
chdir("/");
// 5. 重定向标准输入,标准输出,标准错误
if (noclose == 0)
{
int fd = open(dev.c_str(), O_RDWR); // 以读写的方式打开
if (fd < 0)
{
LOG(LogLevel::FATAL) << "open " << dev << " error";
exit(OPEN_ERROR);
}
else
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
}
- SIGPIPE:当一个进程向已经关闭的管道(pipe)或网络连接(如 TCP socket)写入数据时,默认处理方式是终止进程。守护进程通常作为服务端长期运行,可能会遇到 "客户端意外断开连接后,服务端仍尝试向其写数据" 的情况(比如客户端崩溃、网络中断)。此时若不处理 SIGPIPE,守护进程会被直接杀死,导致服务中断。signal(SIGPIPE, SIG_IGN) 确保服务端不会因 "向已关闭连接写数据" 而被终止。
- SIGCHLD 信号的触发场景:当子进程退出时,操作系统会向其父进程发送 SIGCHLD 信号,通知父进程 "子进程已终止",默认处理方式是忽略信号(什么都不做)。守护进程可能会创建子进程处理任务,如果不处理 SIGCHLD,大量子进程退出后会积累僵尸进程,耗尽系统的进程 ID 资源(PID 是有限的),最终导致无法创建新进程。signal(SIGCHLD, SIG_IGN),自动回收退出的子进程。
- 父进程创建子进程(子进程天生非进程组领头,满足setsid()条件 )→ 父进程退出 (子进程成孤儿被 init 收养,脱离原父进程控制 ,就变成后台进程了)→ 子进程调用 setsid()(成功创建新会话,脱离原终端 )→ 最终成为独立的守护进程。
- 通常将工作目录切换到根目录(/ ),避免守护进程占用某个挂载的文件系统 (如 U 盘),导致该文件系统无法卸载。
- 守护进程不需要与终端交互 ,因此关闭 标准输入,标准输出,标准错误 。/dev/null就是一个"黑洞",读不到数据,写入数据也被直接丢弃。重定向后所,确保所有标准流操作都合法 。守护进程可以通过向文件写日志 (注意守护进程的路径已经改变了)。
- 设置守护进程也有对应的接口:
cpp
#include <unistd.h>
int daemon(int nochdir, int noclose);
If nochdir is zero, daemon() changes the process's current working directory to the root directory ("/");
otherwise, the current working directory is left unchanged.
If noclose is zero, daemon() redirects standard input, standard output and standard error to /dev/null;
otherwise, no changes are made to these file descriptors.
RETURN VALUE
(This function forks, and if the fork(2) succeeds,
the parent calls _exit(2), so that further errors are seen by the child only.)
Onsuccess daemon() returns zero.
If an error occurs, daemon() returns -1
and sets errno to any of the errors specified for the fork(2) and setsid(2).
2.8 示例及完整代码
- 示例:
- 需要sudo,才能在**/var/log/创建my.log文件,并且需要sudo kill server_netcal**。
- 完整代码:NetCal。