前言1:想要实现一个简单的Tcp通信不难,对于初学者而言,难点在于使用了大量未曾接触过的函数调用,所以本篇重点在于详解每部分代码中相关函数的功能。
TCP通信协议是面向字节流的、可靠的、有连接的传输,在实现TCP协议通信时:
①:必须先建立客户端和服务端间的连接,这部分由系统函数实现,
②:其次因为TCP是面向字节流的,所以它的很多操作和文件操作是一致的
• 编写一个服务端:TcpServer;
①. 初始化服务端,设置 IP 、 端口号 and 执行方法
②. 运行服务端,接收客户端发来的数据,交由其他线程来处理
• 编写一个客户端:TcpClient;
①. 确定目标 ip and 端口号
②. 客户端发送数据到服务端,同时接收服务端的消息
步骤1:网络也是文件,一种特殊的文件,因此一开始需要打开文件(创建套接字)
相关函数:
int socket(int domain, int type, int protocol);
• 功能:
创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器):
• 参数:
domain:用于标识是网络通信还是本地通信
AF_INET:网络通信 ☆☆☆
AF_UNIX:本地通信
type:用于标识不同的套接字类型
SOCK_DGRAM:基于UDP协议的通信
SOCK_STREAM:基于TCP协议的通信
protocol:设置为0时;
对于 AF_INET 和 SOCK_STREAM,操作系统会选择 TCP 协议
对于 AF_INET 和 SOCK_DGRAM,操作系统会选择 UDP 协议
• 返回值:int sockfd = socket(AF_INET, SOCK_STREAM, 0);
sockfd(套接字网络文件描述符),用于表述对应描述符表的唯一性。
步骤2:套接字创建完毕时,sockfd需要和套接字描述符表进行绑定,就像打开文件时,文件描述符fd会和文件描述符表结构体相关联。
相关函数:
int bind(int sockfd, const struct sockaddr *address, socklen_t address_len);
• 功能:
绑定端口号 (TCP/UDP, 服务器),将当前套接字与ip和端口号绑定
• 参数:
sockfd:
套接字
address:
对于AF_INET通信,strcut sockaddr_in* 结构体指针内部保存的ip
len:
上述结构体对应的大小
• 注1☆☆☆:
对于服务端而言,将当前服务端所创建的套接字和服务端本地的ip以及指定端口绑定!
在次过程之前,需要将主机字节序转为网络字节序!
这里能够通过面向对象的方式,来完成一步骤。相关代码如下:
class InetAddr { public: //主机转网络 InetAddr(u_int16_t port) :_port(port) { memset(&_addr, 0, sizeof(_addr)); _addr.sin_addr.s_addr = INADDR_ANY; _addr.sin_family = AF_INET; _addr.sin_port = htons(_port); } const struct sockaddr* NetAddrptr() { return CONV(_addr); } //获取sockaddr* 地址 socklen_t NetAddrLen(){ return sizeof(_addr); } private: struct sockaddr_in _addr; std::string _ip; uint16_t _port; }; #define CONV(addr) ((struct sockaddr*)&addr) //类外可以通过相应函数进行构造 // InetAddr local(_port); // int n = bind(sockfd, local.NetAddrptr(), local.NetAddrLen());
• 注2:
对于服务端,需要显式调用bind,bind的过程中,ip为当前服务器下所有可用ip
对于客户端,不需显式调用bind,本地服务器在第一次进行网络通信时,操作系统会将本地IP and 随机端口号 分配给 sockfd。
原因:因为用户不知道哪个端口号现在被占用了,所以需要等待系统去给你分配
一个端口号只能被一个进程占用!
步骤3 :服务端显式调用bind后,需将当前sockfd设置为监听状态
相关函数:
int listen(int sockfd, int backlog);
sockfd:
当前套接字
backlog:
最大等待连接数
注:之所以将当前套接字设置为listen的目的是:可能会有多个客户端向服务端进行通信。
举个简单例子:这里的socket被称为监听套接字,他充当一个在外部揽客的角色。每当有一个客户端向服务端进行通信时,都会被 监听套接字纳入到连接队列。下面会介绍 accpet 函数,该函数会从连接队列中取出一个,和客户端进行通信,这部分由多线程完成。
补充1:线程更安全的 字符串 and 网络字节序 转换
相关函数:
int inet_pton(int af, const char *src, void *dst);
功能: 点分字符串 转为 网络字节序 p → n presentation → network
af:地址族,通常为(AF_INET、AF_INET6)
src: 字符串风格的ip地址
dst:转换后的 网络字节序保存的位置
相关函数:
uint16_t htons(uint16_t hostshort);
功能: 将主机字节序转为网络字节序
hostshort:主机字节序
步骤1 :多个客户端向服务端发起通信时,此时需从连接队列取出一个客户端(报文)建立通信
相关函数:
int accept(int listen_sockfd, struct sockaddr *addr, socklen_t *addrlen);
• 功能:
从连接队列取出一个 连接 进行通信
• 参数:
listen_sockfd:
从监听套接字中取出,该套接字是唯一的
addr:
用于保存客户端地址信息!!!!
addrlen:
地址结构体大小的指针
注:accpet取出的 连接,是客户端的地址信息,因此在accpet前,需要创建一个 struct sockaddr_in 结构体来保存该信息,该信息中包含客服端的ip and 端口号
步骤2:因为接收到的是网络字节序,所以需要将 网络字节序 转为 主机字节序
相关代码如下:
class InetAddr { public: //网络转主机 InetAddr(struct sockaddr_in &addr) : _addr(addr) { _port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列 char ipbuffer[64]; inet_ntop(AF_INET,&_addr.sin_addr, ipbuffer,sizeof(_addr));// 4字节网络风格的IP -> 点分十进制的字符串风格的IP _ip = ipbuffer; } private: struct sockaddr_in _addr; std::string _ip; uint16_t _port; }; //InetAddr addr(peer); // peer 为accept接收到的客户端的 struct sockaddr_in 结构体
补充: 线程更安全的 字符串 and 网络字节序 转换
相关函数:
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
功能: 网络字节序 转为 字符串 p → n presentation → network
af:地址族,通常为(AF_INET、AF_INET6)
src: 二进制格式的ip地址
dst:输出的字符串缓冲区
size:缓冲区的大小
相关函数:
uint16_t ntohs(uint16_t Netshort);
功能: 网络字节序 转 主机字节序
Netshort:网络字节序
步骤1:客户端需要提供服务端的 ip and 端口号
步骤2:客户端需要通过socket创建套接字,这部分和客户端是一样的
问:客户端是否需要bind?
答:需要!但是不是显式的bind,客户端在和服务端通过TCP建立连接 or UDP直接发送报文时,操作系会自动给当前进程绑定一个端口号,将客户端的 ip and 端口号 发给 服务端,因此客户端不需要显式的bind
步骤3:客户端向服务端发送连接请求
相关函数:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
• 功能:
用于发起客户端到服务端的连接请求
• 参数:
sockfd:通过socket创建的文件描述符
addr:目标客服端的结构体
addrlen:addr的大小
注:需将主机字节序转为网络字节序才能发送
假设:主机A是客户端、主机B是服务端
服务端B通过上述函数初始化后,并调用accept接口阻塞等待客户端A发送数据。
客户端A初始化并通过send/write接口发送数据时,send所发送的数据并不是直接通过网络传输给客服端B,send/witre是将数据拷贝给了发送缓冲区,os会在合适的时候将数据通过网络拷贝给服务端B的接收缓冲区。
当客户端A向服务端Bconnect建立通信连接时,服务端B会将客户端丢入到连接队列中,并返回一个新的网络文件描述符sockfd来进行业务处理,同样客户端recv/read不是直接从网络中获取数据,而是在服务端B的接收缓冲区中将数据拷贝到上层。
认知:
结论1:对于Tcp协议,通信的双方在传输层都有发送缓冲区和接收缓冲区,因此是全双工通信,二者相互独立
结论2:以服务端为例,操作系统向缓冲区拷贝数据,上层向缓冲区读数据,这不就是生产消费者模型吗? Tcp协议本质就是四组生产消费者模型。
• 先前结论:协议 → 结构体
上述图解TCP传输流程是从底层来说的,对于处在应用层的用户,在将数据send/write 进发送缓冲区时,需要将结构体转成一个大的字符串,这一过程叫做序列化,目的是为了方便网络传输。
对于服务端,接收缓冲区在收到网络中对应客户端的大字符串时,应用层调用recv/read 后,需要反序列化,方便上册的那个处理/阅读
注:客户端和服务端都认识该结构体,该结构体就是被称为客户端和服务端约定好的协议。该模式被称为C(client)/S(server)模式
问:为什么不直接发送结构体对象?而是将结构体对象序列化转成大字符串?再反序列化读取
答:为了兼容性,因为不同语言结构体的对齐方式不同。
• 结论:所谓的协议定制,本质就是在定制双方都能认识的,符合通信和业务需要的结构化数据,所谓的结构化数据,其实就是struct 或者 class
• Tcp协议通信的双方会存在以下现象:
1. 收发双方发送和接收次数不对等
2.收发双方是否选择发送和接收由操作系统决定
3.读方一定得读到一个完整的报文,才能进行反序列化的操作,如果在编程过程中没有读到一个完整的报文就处理,会出BUG
注:因读取报文不完整导致的问题叫做“粘报“问题
• 因此定制一个协议需要满足:
1.结构化的字段,提供 序列化 and 反序列化 的方案
2.解决因为字节流问题,导致读取报文不完整的问题(只用处理读取)
• 对于服务端,主函数中通过分层结构实现:
① 业务层
② 协议层
③ 通信连接层
• 对于客户端,主函数主要实现:
① 初始化
② 请求建立通信连接
③ 发送数据 + 接收结果
• 主函数中,通过智能指针创建一个Cal对象
std::unique_ptr
cal = std::make_unique (); • Cal类如图如下所示:
#pragma once #include "Protocol.hpp" #include
class Cal { public: Response Excute(Request &req) { Response resp(0, 0); switch (req.Oper()) { case '+': resp.SetResult(req.X() + req.Y()); break; case '-': resp.SetResult(req.X() - req.Y()); break; case '*': resp.SetResult(req.X() * req.Y()); break; case '/': { if (req.Y() == 0) resp.SetCode(1); // 除零错误 else resp.SetResult(req.X() / req.Y()); break; } case '%': { if (req.Y() == 0) resp.SetCode(2); // 余零错误 else resp.SetResult(req.X() % req.Y()); break; } default: resp.SetCode(3); // 传参错误 break; } return resp; } };
• 主函数中,同样通过创建一个对象Protocol对象来调用接口
std::unique_ptr
p = std::make_unique ([&cal](Request& req)->Response{ return cal->Excute(req); }); 客户端发送的数据经协议处理后,需要将数据交由上层业务处理,因此需通过lamda表达式来实现回调。
规定1:用户端发送给服务端的数据称为请求 , 服务端发送给用户端的数据称为回复
规定2:大字符串以 "len + \r\n + 数据 + \r\n" 的方式通过网络传递,其中len为数据的长度
\r\n为分隔符,通过上述来解决"粘报"问题。
• Protocol类中主要需要实现的函数:
①. 用户端将请求序列化 or 服务端将回复序列化, 将结构化的数据转为字符串形式,通过json库来实现,这是一个三方库
②. 用户端 加密序列化后的请求 or 客户端 加密序列化后的回复,将数据转成 "len + \r\n + 数据 + \r\n" 的形式
③. 服务端 解密用户端的请求 or 用户端 解密服务端的回复 从网络中收到的 "len + \r\n + 数据 + \r\n" 的形式的大字符串
• 客户端和服务端间通信流程:
. 服务端创建服务器,等待客户端通信,(建立套接字、bind、设置listen、accept)
. 客户端发送通信请求,双方建立通信连接 (建立套接字、connect)
. 客户端将结构化数据转为字符串形式
. 客户端将数据加密为 "len + \r\n + 数据 + \r\n"形式的数据
. 客户端发送数据 (send/write)
. 服务端接收数据 (recv/read)
. 服务器解密收到的数据
. 服务端将有效数据反序列化
. 服务器调用回调进行业务处理
. 服务端将处理的结果序列化
. 服务端将序列化后的结果加密
. 服务端发送数据
. 客户端接收数据、解密数据、反序列化数据得到结果。
注:请求的序列化和反序列化 与 回复的序列化和反序列化是分开的。两者的参数和结果均不同,当然需要分开写。
代码如下:
#pragma once #include
#include "common.hpp" #include "Socket.hpp" using namespace SocketModule; // client -> server 客户的请求:客户需要将请求序列化到服务端,服务端需要将客户的请求反序列化 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::FastWriter Writer; std::string s = Writer.write(root); return s; } bool Deserialize(std::string& Package) { Json::Value root; Json::Reader reader; bool ok = reader.parse(Package, root); // 将Json串读到root中 if (ok) { _x = root["x"].asInt(); _y = root["y"].asInt(); _oper = root["oper"].asInt(); } return ok; } int X() { return _x; } int Y() { return _y; } char Oper() { return _oper; } ~Request() { } private: int _x; int _y; char _oper; }; // server -> client 服务端需要将业务处理的结果序列化到客户端,客户端需要反序列业务处理的结果 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::FastWriter writer; return writer.write(root); } // 用户端反序列从服务端接收到的数据 bool Deserialize(std::string &in) { Json::Value root; Json::Reader reader; bool ok = reader.parse(in, root); if (ok) { _result = root["result"].asInt(); _code = root["code"].asInt(); } return ok; } void SetResult(int result) { _result = result; } void SetCode(int code) { _code = code; } void ShowResult() { std::cout << "计算结果是:" << _result << " [" << _code << "]" << std::endl; } ~Response() {} private: int _result; int _code; }; const std::string seq = "\r\n"; using func_t = std::function ; class Protocol { public: Protocol(func_t func) : _func(func) { } Protocol() { } // 编码 Encode len/r/n json码 /r/n 约定通信的双方以这种方式(协议)进行通信,json码为有效数据 std::string Encode(std::string &jsonstr) { std::string len = std::to_string(jsonstr.size()); // return (len + seq + jsonstr + seq); } // 解码 Decode 通信的双方以该协议进行通信 bool Decode(std::string &buffer, std::string *Json_Package) { // 通信的双方通过 数据长度 + /r/n + json串 + /r/n 进行加密 // 1.先找第一个 /r/n,没找到说明数据不完整 ssize_t pos = buffer.find(seq); if (pos == std::string::npos) { return false; } // 到这说明该报文有数据长度 // 2.获得数据长度 std::string package_len_str = buffer.substr(0, pos); int len = std::stoi(package_len_str); // 3.根据加密协议 以及数据长度,得到有效报文的总长度 int size = package_len_str.size() + 2 * seq.size() + len; // 4.如果当前服务端收到的数据长度小于 size 说明收到的消息不完整 if (buffer.size() < size) { return false; } // 5.到这说明buffer内至少包含一条完整的加密后数据,对加密数据进行解密 *Json_Package = buffer.substr(pos + 2, len); // 6.将解密后的数据从buffer中删除 buffer.erase(0, size); return true; } void GetRequest(std::shared_ptr &sock, InetAddr &client) { std::string buffer_queue; while (true) { int n = sock->Recv(&buffer_queue); if (n > 0) { // 读取不完整,避免粘包问题 // 服务端从用户端读取用户请求 → 经客户端序列化的数据且加密的数据 // 1. 服务端解密 std::string Json_Package; while (Decode(buffer_queue, &Json_Package)) { // 2.服务端对客户请求的反序列化 Request req; bool ret = req.Deserialize(Json_Package); if (!ret) continue; // 3. 解密反序列化完毕后 调用上层业务 Response resp = _func(req); // 要将业务处理的结果返回给客户端,所以需要返回一个Response对象 // 将得到的结果序列化传递给 用户端 // 4.先序列化 服务端 → 用户端的序列化 std::string send_str = resp.Serialize(); // 5.序加密 std::string package = Encode(send_str); // 6.发送 sock->Send(package); } } else if (n == 0) { LOG(LogLevel::INFO) << "用户退出了"; break; } else { LOG(LogLevel::WARNING) << "client" << client.StringAddr() << ": recv error"; break; } } } bool GetResponse(std::shared_ptr client, std::string &resp_buffer, Response *resp) { while (true) { int n = client->Recv(&resp_buffer); if (n > 0) { std::string json_package; while (Decode(resp_buffer, &json_package)) { resp->Deserialize(json_package); } return true; } else if (n == 0) { return false; } else { return false; } } } std::string BuildRequestString(int x, int y, char oper) { Request req(x, y, oper); // 将客户端的请求序列化 std::string json_str = req.Serialize(); // 将序列化的json串加密 return Encode(json_str); } ~Protocol() { } private: // 因为我们用的是多进程 // Request _req; // Response _resp; func_t _func; };
• 主函数中,通过智能指针创建一个TcpServer对象,当服务端收到客户端的通信请求时,创建子进程的子进程来执行回调方法实现通信双方数据的交互
std::unique_ptr
tsvr = std::make_unique (std::stoi(argv[1]),[&p](std::shared_ptr &sock, InetAddr &client){ p->GetRequest(sock, client); }); tsvr->Start(); 通信连接层通过模板方法模式进行初始化
模板方法模式:将基类中的虚函数方法在派生类中重写,同时基类中有一套固定方法,创建派生类对象时,调用该方法来初始化派生类对象的过程
补充:套接字的创建、绑定、监听、数据的发送、接收和连接,都和网络文件描述符相关,因此我们可以创建一个专门用来调用这些接口的自定义类,该类由书上的模板方法模式实现。具体代码如下所示:
#pragma once #include "common.hpp" #include "InetAddr.hpp" #include
namespace SocketModule { using namespace LogModule; class Socket { public: virtual void SocketOrDie() = 0; virtual void BindOrDie(uint16_t port) = 0; virtual void ListenOrDie(int backlog) = 0; virtual std::shared_ptr Accept(InetAddr* Client) = 0; virtual bool Connect(std::string& ip, uint16_t port) = 0; virtual void Close() = 0; virtual int Recv(std::string* out) = 0; virtual int Send(const std::string& message) = 0; public: void BuildTcpSocketMethod(int port, int backlog = 16) { SocketOrDie(); BindOrDie(port); ListenOrDie(backlog); } void BulidClientSocketMethod() { SocketOrDie(); } }; const int DefaultNum = -1; class TcpSocket : public Socket { public: TcpSocket() : _sockfd(DefaultNum) { } TcpSocket(int fd) : _sockfd(fd) { } 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 sunccess"; } void BindOrDie(uint16_t port) override { InetAddr localAddr(port); int n = ::bind(_sockfd, localAddr.NetAddrptr(), localAddr.NetAddrLen()); if (n < 0) { LOG(LogLevel::FATAL) << "bind error"; exit(BIND_ERROR); } LOG(LogLevel::INFO) << "bind sunccess"; } void ListenOrDie(int backlog) override { int n = ::listen(_sockfd, backlog); if (n < 0) { LOG(LogLevel::FATAL) << "listen error"; exit(LISTEN_ERROR); } LOG(LogLevel::INFO) << "listen sunccess"; } std::shared_ptr Accept(InetAddr* Client) override { struct sockaddr_in peer; socklen_t len = sizeof(len); int fd = ::accept(_sockfd, CONV(peer), &len); if (fd < 0) { LOG(LogLevel::WARNING) << "accept warning ..."; return nullptr; // TODO } Client->SetAddr(peer);// 解耦,让另一个对象来管理客户端的ip and 端口号信息 return std::make_shared (fd); } bool Connect(std::string& ip, uint16_t port) override { InetAddr client(ip,port);//初始化客户端 int n = ::connect(_sockfd,client.NetAddrptr(),client.NetAddrLen()); if(n < 0) { LOG(LogLevel::FATAL) << "connnect error"; return false; } return true; } int Recv(std::string* out) override { char buffer[1024]; ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0); if(n > 0) { buffer[n] = 0; *out += buffer; } return n; } virtual int Send(const std::string& message) override { return send(_sockfd, message.c_str(), message.size(), 0); } void Close() override { if(_sockfd >= 0) ::close(_sockfd); } ~TcpSocket() { } private: int _sockfd; }; } 主函数在实例化时,默认的构造函数如图所示:
TcpServer(u_int16_t port, ioservice_t func) :_ListenSockfd(std::make_unique
()) ,_port(port) ,_isrunning(false) ,_service(func) { _ListenSockfd->BuildTcpSocketMethod(port); } 在创建过程中,会自动将服务端初始化
#include "Protocol.hpp"
#include "Socket.hpp"
#include "InetAddr.hpp"
#include "common.hpp"
void GetDataFromStdin(int* x,int* y,char* opre)
{
std::cout << "please enter x:";
std::cin >> *x;
std::cout << "please enter y:";
std::cin >> *y;
std::cout << "please enter oper:";
std::cin >> *opre;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
exit(1);
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
std::shared_ptr client = std::make_shared();
client->BulidClientSocketMethod();
if(!client->Connect(ip,port))
{
exit(2);
}
std::unique_ptr protocol = std::make_unique();
std::string resp_buffer;
while(true)
{
int x, y;
char oper;
GetDataFromStdin(&x,&y,&oper);
std::string req_str = protocol->BuildRequestString(x,y,oper);
//发送
client->Send(req_str);
//获取应答
Response resp;
bool res = protocol->GetResponse(client,resp_buffer,&resp);
resp.ShowResult();
}
client->Close();
return 0;
}
在认识守护进程之前,先认识几个其他的概念:
• 进程组(PGID):
:进程是以进程组的方式完成对应任务,相当于公司内部的一个团队,负责一个任务。
:具有多个进程的进程组而言,进程组的id与父进程id一致。
:单个进程的进程组id等于当前进程id
注:
• 进程组组长的作用:进程组组长可以创建一个进程或者创建该组中的进程
• 进程组的生命周期:从进程组船舰开始到其中最后一个进程结束为止,进程组的存在与否与组长是否存在无关。
• 会话:
:同一个进程组里的会话id是相同的,值为当前进程组的父进程
:当系统成功登录时,至少会先创建一个会话,该会话包含一个终端文件,终端文件 是内核与用户交互的接口
注:前台进程组能够收到键盘产生的信号,而后台进程组不会
:一个会话可以包含多个进程组,一个进程组只能属于一个会话
举例:登录windows,就是建立会话的过程, 然后加载图形化界面(桌面)(即外壳)对于linux而言,他的图形化界面是命令行,所有的作业/任务 都在同一个会话内部进行
• 登录是建立会话的过程
• 关闭终端,就是销毁会话的过程 → 会影响服务器的运行,但是正常服务器中,用户的登录和注销不会影响服务器
问:怎么做服务器才能避免受用户登录和注销的影响?
答:将网络服务器丢到一个新的会话,丢入到新会话的进程叫做 守护进程/精灵进程
守护进程:后台进程依旧属于当前会话中,守护进程也是后台进程,但是他具有独立会话, 如果服务器是守护进程的话,一般以d结尾
变成守护进程的函数接口:当进程调用 pit_t setsid(); 就会变成守护进程
注:前提不能是进程组的组长,可以创建子进程,关闭父进程来避免这种情况,所以守护进程本质也是一个孤儿进程
补充:/dev/null 路径
凡是写入到该文件的内容都会被丢弃,凡是从该文件读取的内容都是空。
如果不想看到任何输入、输出、错误信息,可以重定向到该路径下。
const std::string dev = "/dev/null"; int fd = open(dev.c_str(), O_RDWR) dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); close(fd);
1. 自定义函数方式
const std::string dev = "/dev/null"; // 将服务进行守护进程化的服务 void Daemon(int nochdir, int noclose) { // 1. 忽略IO,子进程退出等相关的信号 signal(SIGPIPE, SIG_IGN); signal(SIGCHLD, SIG_IGN); // SIG_DFL // 2. 父进程直接结束 if (fork() > 0) exit(0); // 3. 只能是子进程,孤儿了,父进程就是1 setsid(); // 成为一个独立的会话 if(nochdir == 0) // 更改进程的工作路径 chdir("/"); // 4. 依旧可能显示器,键盘,stdin,stdout,stderr关联的. // 守护进程,不从键盘输入,也不需要向显示器打印 // 打开/dev/null, 重定向标准输入,标准输出,标准错误到/dev/null if (noclose == 0) { int fd = ::open(dev.c_str(), O_RDWR); if (fd < 0) { //OPEN_ERR exit(1); } else { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); close(fd); } } }
2.调用系统接口
int daemon(int nochdir, int noclose);
• nochdir:
0:切换工作目录到 /
非 0:保留当前工作目录
• noclose:
0:关闭并重定向 stdin/stdout/stderr 到 /dev/null
非 0:保留原来的文件描述符