网络通信仅仅是为了让两台主机间传送数据吗?数据是被谁需要的呢?--- 进程,所以网络通信的本质是两个进程间的通信。那么如何找到两台主机上的两个进程呢?
1、通过IP地址确定网络中的唯一一台主机
2、通过port(端口号)确定一台主机上的唯一一个进程(端口号是一个2字节16位的整数)
IP和port 就是 网络socket(套接字),由此可以标识互联网中的唯一一个进程
如何理解端口号(port)
(端口号和进程是对应的,通过端口号来找到进程id)
我们知道操作系统中可以通过pid来标识进程,为什么还要有端口号呢?
1、将其他模块(进程管理)和网络进行解耦
2、端口号是专门为网络服务的
一个进程可以有多个端口号,但是一个端口号只能对应一个进程。
TCP和UDP协议特点
TCP:1、传输层协议 2、有连接 3、可靠传输 4、面向字节流
UDP:1、传输层协议 2、无连接 3、不可靠传输 4、面向数据报
(注意:这里说的可靠不可靠,没有褒贬之分,是协议的特性,即TCP在传输数据时会考虑数据出错 / 丢失的问题,但是UDP不会,所以TCP会更慢,UDP会更快,各有各的特性)
网络字节序
大家在学C语言的时候就应该了解过电脑有大小端之分(大端:高位放在低地址处,小端:低位放在低地址处,是两种不同的数据存储方式)。本来在一台主机上没什么问题,但是网络通信发生在大端机和小端机之间,那么我们解释数据就会出问题,如下
为了方便我们进行数据的大小端转化,系统还提供了一些接口
(直接用就行,不用我们再去关心机器是大端还是小端)

接口的具体使用,会在后面介绍
(这里暂且不去关心UDP的底层原理,先来熟悉一下UDP的相关接口)
int socket(int domain, int type, int protocol);
domain:指定通信协议族,例如AF_INET表示IPv4协议,AF_INET6表示IPv6协议,AF_UNIX表示本地通信(Unix域套接字)等。type:指定套接字类型,常见的类型有:
SOCK_STREAM:提供流式套接字,用于TCP协议。SOCK_DGRAM:提供数据报套接字,用于UDP协议。SOCK_RAW:提供原始套接字,允许对底层协议(如IP或ICMP)进行直接访问。protocol:通常设置为0,表示选择默认的协议。在大多数情况下,内核可以根据domain和type参数自动确定所使用的协议。函数成功返回套接字的文件描述符,失败返回-1
int sockfd = socket(AF_INET,SOCK_DGRAM,0); // UDP默认固定写法
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:这是由socket函数返回的套接字文件描述符。addr:这是一个指向sockaddr结构体的指针,该结构体包含了要绑定的地址信息,包括IP地址和端口号。对于IPv4地址,通常使用sockaddr_in结构体;对于IPv6地址,使用sockaddr_in6结构体。addrlen:这是addr参数所指向的结构体的长度,通常以字节为单位。这可以用来确定结构体中包含了哪些字段,以及这些字段的长度。功能描述
bind函数将sockfd所指定的套接字与addr参数所指向的地址结构体进行绑定。这允许服务器在特定的IP地址和端口号上监听客户端的连接请求。如果addr中的地址是通配符地址(如IPv4中的INADDR_ANY),则套接字将绑定到所有可用的网络接口上。返回值
- 如果绑定成功,
bind函数返回0。- 如果绑定失败,
bind函数返回-1,并设置全局变量errno以指示错误原因。注意事项
- 在调用
bind函数之前,通常需要先调用socket函数来创建一个套接字。bind函数通常用于服务器端套接字,以指定服务器应该在哪个地址和端口上监听连接请求。客户端套接字通常不需要调用bind函数,因为它们会由操作系统自动分配一个本地端口号。- 在某些情况下,如果套接字已经与某个地址绑定,再次调用
bind函数可能会失败。此外,如果指定的地址或端口号已经被其他套接字使用,或者是不合法的,bind函数也会失败。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
sockfd:指定接收数据的套接字文件描述符。buf:指向用于存储接收数据的缓冲区。len:指定缓冲区的最大长度,即最多可以接收多少字节的数据。flags:控制接收数据的方式。例如,MSG_WAITALL标志可以确保在函数返回之前,指定长度的数据全部被接收;MSG_DONTWAIT标志则使函数以非阻塞方式工作,如果没有数据可读,它将立即返回。默认设置为0src_addr:指向一个sockaddr结构体(或其特定类型,如sockaddr_in对于IPv4)的指针,该结构体在函数返回时将被填充发送方的地址信息。addrlen:是一个指向socklen_t变量的指针,用于传入src_addr结构体的初始长度,并在函数返回时更新为实际填充的长度。
recvfrom函数的返回值是实际接收到的字节数。如果返回值为0,表示连接已关闭。如果出现错误,返回值为-1,此时可以通过errno变量来获取具体的错误信息。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd:是已经创建好的socket文件描述符,代表要发送数据的套接字。buf:指向包含待发送数据的缓冲区的指针。len:指定缓冲区中数据的长度,即要发送的字节数。flags:发送选项,用于控制发送数据的方式。通常情况下,这个参数设置为0即可。dest_addr:指向包含目的地址信息的sockaddr结构体(或其特定类型,如sockaddr_in对于IPv4)的指针。addrlen:指定dest_addr结构体的大小。返回值:
- 如果发送成功,
sendto函数返回发送的字节数。- 如果发送失败,返回-1,并设置全局变量
errno以指示错误原因。使用
sendto函数时,通常不需要事先与目的主机建立连接,因此它非常适合于实现无连接的数据传输服务,如UDP。同时,由于发送和接收数据报是独立的,所以sendto函数允许数据报以任意顺序到达,并且不保证数据报的可靠传输。
下面是一个简单用UDP实现的将客户端发来的消息再给它发回去的代码
- // Udp_Server.hpp
- #include
- #include
- #include
-
- // 网络相关的头文件
- #include
- #include
- #include
- #include
-
- // 自定义的头文件 --- 在文章的结尾 - 附录部分 - 有具体的代码实现,有兴趣可以看看
- // (这些头文件对我们理解代码的核心逻辑没有太大影响,只是为了让编码更加有条理)
- #include "nocopy.hpp" // 包含防拷贝和赋值的类
- #include "Log.hpp" // 包含日志类
- #include "Comm.hpp" // 包含一些公用的变量
- #include "InetAddr.hpp" // 包含管理网络地址的类
-
- uint16_t portdefault = 8888;
-
- class UdpServer : public nocopy // 继承防拷贝类,让自己也防拷贝
- {
- public:
- UdpServer(uint16_t port = portdefault)
- : _port(port)
- {}
-
- void Init()
- {
- // 创建套接字
- _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
- if (_sockfd == -1)
- {
- lg(Fatal, "socket faild : %d - %s", errno, strerror(errno)); // 打印日志,可以理解为cout,是我们单独封装的日志类
- exit(SocketErr);
- }
- lg(Info, "socket success, sockfd: %d", _sockfd);
-
- // 绑定 bind
- // 1、填充结构体
- struct sockaddr_in local;
- memset(&local, 0, sizeof(local));
- local.sin_family = AF_INET;
- local.sin_port = htons(_port);
- local.sin_addr.s_addr = INADDR_ANY; // 绑定本机任意ip地址,可以收到发送到该主句的该端口号上的所有请求
- // 因为一台主机可能有多个ip地址,如果绑死了,就只能接收到ipx这个地址收到请求,发送到其他的ip地址的请求就收不到了!!!
- // 2、进行绑定
- int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
- if (n != 0)
- {
- lg(Fatal, "bind faild : %d - %s", errno, strerror(errno));
- exit(BindErr);
- }
- lg(Info, "bind success");
- }
-
- void Start()
- {
- char buffer[1024];
- for (;;)
- {
- struct sockaddr_in peer;
- socklen_t len = sizeof(peer);
- ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
- if (n > 0)
- {
- InetAddr addr(peer); // 这是用来管理发送方的ip和port的类,也是我们自己封装的
- buffer[n] = 0;
- std::cout << "[" << addr.DebugPrinit() << "]#" << buffer << std::endl;
- sendto(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, len);
- }
- }
- }
-
- ~UdpServer()
- {}
-
- private:
- uint16_t _port;
- int _sockfd;
- };
-
- // UdpServer.cpp
- #include "Udp_Sercer.hpp"
- #include
- #include
-
- void Usage(std::string commond)
- {
- std::cout << "Usage:\n";
- std::cout << commond << " port" << std::endl;
- }
-
- int main(int argc, char *args[])
- {
- if (argc != 2)
- {
- Usage(args[0]);
- exit(1);
- }
- uint16_t port = std::stoi(args[1]);
- std::unique_ptr
s(new UdpServer(port)) ; - s->Init();
- s->Start();
- return 0;
- }
- #include
- #include
-
- // 网络相关的头文件
- #include
- #include
- #include
- #include
-
- // 自定义的头文件
- #include "Log.hpp"
- #include "Comm.hpp"
-
- void Usage(std::string commond)
- {
- std::cout << commond << " server_ip server_port" << std::endl;
- }
-
- int main(int argc, char *args[])
- {
- if (argc != 3)
- {
- Usage(args[0]);
- exit(1);
- }
-
- uint16_t port = std::stoi(args[2]);
- // 创建socket
- int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
- if (sockfd < 0)
- {
- lg(Fatal, "socket faild : %d - %s", errno, strerror(errno));
- exit(SocketErr);
- }
-
- // 客户端需要bind吗?需要,但是我们不用显示的bind,这样也不好
- // 因为客户端会有多个,每个人的ip地址和端口号都是动态变化的,一旦绑死,就会出问题
- // 当我们第一次发送数据时,本机OS会自动帮我们bind上随机对应的ip地址和port
-
- // 填充服务器的网络地址
- struct sockaddr_in server;
- memset(&server, 0, sizeof(server));
- server.sin_family = AF_INET;
- server.sin_port = htons(port); // 注意转换成网络字节序
- server.sin_addr.s_addr = inet_addr(args[1]); // inet_addr()将字符串转换成4字节的整形,并变成网络字节序,系统提供的函数
-
- //sockfd支持全双工通信
- while (true)
- {
- std::cout << "please enter# ";
- std::string buffer;
- std::getline(std::cin, buffer);
- ssize_t n = sendto(sockfd, buffer.c_str(), buffer.size(), 0, (struct sockaddr *)&server, sizeof(server));
- if (n > 0)
- {
- char buffer[1024];
- struct sockaddr_in tmp;
- socklen_t len = sizeof(tmp);
- ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&tmp, &len); // 虽然我们知道服务器的网络地址信息,但是后面两个参数一般都要传,传nullptr可能会出错
- if (m > 0)
- {
- buffer[m] = 0;
- std::cout << "server recv# " << buffer << std::endl;
- }
- else
- break;
- }
- else
- break;
- }
- close(sockfd);
- return 0;
- }

当我们熟悉了这些接口,我们就可以将代码进行改造,实现一些其他的有趣的功能,比如多人聊天会议室(需要结合多线程),远程控制自己的服务器等等。
- int socket(int domain, int type, int protocol);
- // 和UDP中介绍的socket一样
- // 在选择tcp协议时,参数选项上有所差异,如下
- int sockfd = socket(AF_INET, SOCK_STREAM, 0); // tcp 默认固定写法
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);用法和上面的UDP一样
int listen(int sockfd, int backlog);
sockfd:这是listen函数的操作对象,它是建立socket时的返回值,即已绑定但未连接的套接字描述符。backlog:这个参数代表了TCP连接请求的最大队列长度。具体来说,它指定了内核接受客户端SYN但未完成三次握手的TCP连接数量。如果连接请求的数量超过了backlog设置的值,后续的连接请求将会被拒绝。backlog参数的设置越大,TCP服务器可以同时处理的客户端连接就越多,但请注意,backlog的值应大于2,否则listen函数会失败。函数返回值:如果listen函数调用成功,则返回0;如果失败,则返回-1,并可以通过errno获取错误编号。
注意:listen函数通常在调用bind函数之后和调用accept函数之前被调用。当listen函数调用成功后,TCP服务端的状态就会变为“LISTEN”,等待客户端的连接请求。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:这是accept函数的操作对象,它通常是一个处于监听状态的套接字描述符,由socket函数创建并经过bind和listen函数处理后的套接字。addr:这是一个指向sockaddr结构的指针,用于存储客户端的地址信息。当accept函数成功接受一个连接后,它会把客户端的IP地址和端口号等信息写入这个结构中。addrlen:这是一个指向socklen_t类型变量的指针,用于存储addr结构体的实际长度。在调用accept函数前,通常会把这个变量的值设置为addr结构体的大小,函数返回时,它会被更新为实际写入的长度。函数返回值:
- 如果accept函数成功接受了一个连接,它会返回一个新的套接字描述符,这个描述符用于和客户端进行通信。原来的sockfd套接字则继续用于监听其他客户端的连接请求。
- 如果accept函数失败,则返回-1,并设置全局变量errno以表示错误。
值得注意的是,当sockfd套接字处于监听状态时,它会等待客户端的连接请求。一旦有客户端连接请求到来,accept函数会从已完成连接队列中取出一个连接请求,并创建一个新的套接字与该客户端通信。同时,accept函数会阻塞当前线程,直到有连接请求到来或者发生错误。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:这是标识一个套接字的文件描述符。它是由先前的socket函数调用返回的值,代表了客户端想要建立连接的套接字。
- serv_addr:这是一个指向sockaddr结构的指针,包含了客户端想要连接的主机的地址和端口号信息。这个参数指定了数据发送的目的地,即服务器的地址。
- addrlen:这是一个socklen_t类型的变量,表示serv_addr参数所指向的地址结构的长度。它帮助函数正确解析serv_addr中的地址信息。
返回值:
- 成功:当connect函数成功建立连接时,返回0。
- 失败:如果连接建立失败,connect函数返回-1,并将错误原因存储在全局变量errno中。可能的错误原因包括但不限于:
- EBADF:参数sockfd不是一个合法的socket处理代码。
- EFAULT:参数serv_addr指针指向无法存取的内存空间。
- ENOTSOCK:参数sockfd是一个文件描述词,但它不是一个socket。
- EISCONN:参数sockfd的socket已经是连线状态。
- ECONNREFUSED:连线要求被server端拒绝。
- ETIMEDOUT:企图连线的操作超过限定时间仍未有响应。
- ENETUNREACH:无法传送数据包至指定的主机。
- EAFNOSUPPORT:sockaddr结构的sa_family不正确。
- EALREADY:socket为不可阻塞且先前的连线操作还未完成。
这里可以通过read和write两个文件相关的系统调用进行对tcp套接字的读写工作
- // TcpServer.hpp
- #pragma once
- #include
- #include
- #include
- #include
- #include
- #include
-
- #include
- #include
- #include
- #include
-
- #include "nocopy.hpp"
- #include "Log.hpp"
- #include "Comm.hpp"
- #include "InetAddr.hpp"
- #include "ThreadPool.hpp"
-
- const int DefaultBacklog = 5;
-
- class TcpServer : public nocopy
- {
- public:
- TcpServer(uint16_t port) : _port(port), _isrunning(false)
- {
- }
-
- void Init()
- {
- // 创建套接字
- _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
- if (_listensockfd < 0)
- {
- lg(Fatal, "socket failed");
- exit(SocketErr);
- }
- lg(Info, "socket success, listensockfd: %d", _listensockfd);
-
- int opt = 1;
- setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 为了确保套接字_listensockfd在关闭后,其地址和端口可以立即被重新使用,而不是等待它们进入TIME_WAIT状态
-
- // bind
- struct sockaddr_in local;
- memset(&local, 0, sizeof(local));
- local.sin_family = AF_INET;
- local.sin_port = htons(_port);
- local.sin_addr.s_addr = INADDR_ANY;
- if (bind(_listensockfd, CONV(&local), sizeof(local)) != 0)
- {
- lg(Fatal, "bind failed");
- exit(BindErr);
- }
- lg(Info, "bind success, listensockfd: %d", _listensockfd);
-
- // 监听
- if (listen(_listensockfd, DefaultBacklog) != 0)
- {
- lg(Fatal, "listen failed");
- exit(ListenErr);
- }
- lg(Info, "listen success, listensockfd: %d", _listensockfd);
- }
-
- void Server(int sockfd)
- {
- char buffer[4096];
- while (true)
- {
- ssize_t n = read(sockfd, buffer, sizeof(buffer));
- if (n > 0)
- {
- buffer[n] = 0;
-
- std::cout << "client says# " << buffer << std::endl;
- std::string message = "server echo# ";
- message += buffer;
- write(sockfd, message.c_str(), message.size());
- }
- else if (n == 0)
- {
- lg(Info, "client quit");
- break;
- }
- else
- {
- lg(Info, "failed");
- break;
- }
- }
- }
-
- void Start()
- {
- _isrunning = true;
- while (_isrunning)
- {
- struct sockaddr_in t;
- socklen_t len = sizeof(t);
- int sockfd = accept(_listensockfd, CONV(&t), &len);
- if (sockfd < 0)
- {
- lg(Info, "accept failed");
- continue;
- }
- lg(Info, "accept success, new sockfd:%d", sockfd);
-
- Server(sockfd);
- close(sockfd);
- }
- }
-
- ~TcpServer()
- {
- }
-
- private:
- uint16_t _port;
- int _listensockfd;
- bool _isrunning;
- };
-
- // TcpServer.cpp
- #include
- #include
- #include
- #include
- #include
- #include "Log.hpp"
- #include "Comm.hpp"
- #include
-
- void Usage(std::string commond)
- {
- std::cout << commond << " server_ip server_port" << std::endl;
- }
-
- bool VisitServer(std::string ip, uint16_t port)
- {
- // 创建套接字
- int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- if (sockfd < 0)
- {
- lg(Fatal, "sockfd failed");
- return false;
- }
- // bind -- 交给OS
- // connect
- struct sockaddr_in server;
- memset(&server, 0, sizeof(server));
- server.sin_family = AF_INET;
- server.sin_port = htons(port);
- inet_pton(AF_INET, ip.c_str(), &server.sin_addr);
- int n = connect(sockfd, CONV(&server), sizeof(server));
- if (n < 0)
- {
- lg(Fatal, "connect failed");
- return false;
- }
- while (true)
- {
- std::cout << "Please enter# ";
- std::string buffer;
- std::getline(std::cin, buffer);
- ssize_t n = write(sockfd, buffer.c_str(), buffer.size());
- if (n > 0)
- {
- char buffer[1024];
- ssize_t m = read(sockfd, buffer, sizeof(buffer));
- if (m > 0)
- {
- buffer[m] = 0;
- std::cout << buffer << std::endl;
- }
- else if (m == 0)
- {
- close(sockfd);
- return false;
- }
- else
- {
- close(sockfd);
- return false;
- }
- }
- else
- {
- break;
- }
- }
- }
-
- int main(int argc, char *args[])
- {
- if (argc != 3)
- {
- Usage(args[0]);
- exit(1);
- }
-
- std::string ip = args[1];
- uint16_t port = std::stoi(args[2]);
- VisitServer(ip, port);
- return 0;
- }
- // TcpClient.cpp
- #include
- #include
- #include
- #include
- #include
- #include "Log.hpp"
- #include "Comm.hpp"
- #include
-
- void Usage(std::string commond)
- {
- std::cout << commond << " server_ip server_port" << std::endl;
- }
-
- bool VisitServer(std::string ip, uint16_t port)
- {
- // 创建套接字
- int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- if (sockfd < 0)
- {
- lg(Fatal, "sockfd failed");
- return false;
- }
- // bind -- 交给OS
- // connect
- struct sockaddr_in server;
- memset(&server, 0, sizeof(server));
- server.sin_family = AF_INET;
- server.sin_port = htons(port);
- inet_pton(AF_INET, ip.c_str(), &server.sin_addr);
- int n = connect(sockfd, CONV(&server), sizeof(server));
- if (n < 0)
- {
- lg(Fatal, "connect failed");
- return false;
- }
- while (true)
- {
- std::cout << "Please enter# ";
- std::string buffer;
- std::getline(std::cin, buffer);
- ssize_t n = write(sockfd, buffer.c_str(), buffer.size());
- if (n > 0)
- {
- char buffer[1024];
- ssize_t m = read(sockfd, buffer, sizeof(buffer));
- if (m > 0)
- {
- buffer[m] = 0;
- std::cout << buffer << std::endl;
- }
- else if (m == 0)
- {
- close(sockfd);
- return false;
- }
- else
- {
- close(sockfd);
- return false;
- }
- }
- else
- {
- break;
- }
- }
- }
-
- int main(int argc, char *args[])
- {
- if (argc != 3)
- {
- Usage(args[0]);
- exit(1);
- }
-
- std::string ip = args[1];
- uint16_t port = std::stoi(args[2]);
- VisitServer(ip, port);
- return 0;
- }

我们在熟悉了这些接口之后,还能实现一些其他的有趣的功能,有时间,我会单独出几篇文章来讲述一些基于网络的有趣项目的实现。
对于UDP和TCP的接口的介绍暂时就说到这里,下面我们透过上面的编码简单聊聊UDP和TCP
1、在前面,我们说过网络传输的字节序是大端,而主机分为大端机和小端机,那么为什么我们上面的代码没有对大小端进行相应的处理呢?
本质是因为我们传输的数据都是char类型的数据,只有1字节,而大小端针对的是多字节数据的存储,如整形,长整型,浮点数等,所以char类型的数据不受大小端影响。故能顺利得出,否则就需要我们手动对数据进行大小端转换的处理。
2、如何理解面向数据报和面向字节流?
这里简单说明一下,面向数据报类似我们收取快递,一个箱子就是一个快递,也就是说,数据报是一份一份的,分开的,有边界的。
而面向字节流类似我们之前在进程通信中讲过的管道,我们可以往里面多次输入数据,我们在读取时可以一次性全拿出来,也能分成多次拿出,取决于我们如何使用这些数据
从中我们也能看出,tcp协议要比udp协议更难写,相比于收取"快递数据",如何从一堆数据中拼凑出正确的数据显然难度更大(并且具体的收发是由OS实现的,也就是说客户端无法得知我们发送的数据是否全部发送了,服务端也无法得知我们的数据是否完整的到达了)。
3、我们写的服务端代码难道要像上面这样在前台运行吗,如果用户退出,我们的服务挂掉了怎么办?如何让服务在服务器上一直跑,直到服务器被关掉呢?
我们写的网络服务,不能在bash中以前台进程的方式运行,真正的服务器,必须在Linux的后台,以守护进程(精灵进程)的方式进行运行。
在解释这个之前,我们先来了解一下其他的相关概念

(父进程和进程大家都很了解了,这里就不做介绍了,这里引入两个新概念---进程组和会话)
这里首先强调:不要将这两个概念下的进程间关系往父子上去靠,这是两个不同层面的概念。
相信大家看完上面这段话还是不太理解这两个概念,下面我们结合bash再来具体的理解一下会话和进程组。

如何创建守护进程?
- #pragma once
- #include
- #include
- #include
- #include
- #include
- #include
-
- const char *root = "/";
- void daemon(bool ischdir, bool isclose)
- {
- // 忽略可能引起程序异常退出的信号
- signal(SIGCHLD, SIG_IGN);
- signal(SIGPIPE, SIG_IGN);
- // 创建子进程 --- 不让自己成为组长
- if (fork() > 0) exit(0);
- // 设置让自己成为一个新的会话
- setsid();
- // 改变cwd
- if (ischdir)
- {
- chdir(root);
- }
- // 关闭文件描述符 / 重定向
- if (isclose)
- {
- close(0);
- close(1);
- close(2);
- }
- else
- {
- int fd = open("/dev/null", O_RDWR); // /dev/null是一个字符设备文件,输入进去的数据被清空,读取出来的数据为空,推荐用这个
- dup2(fd, 0);
- dup2(fd, 1);
- dup2(fd, 2);
- close(fd);
- }
- }
- #include "dameon.hpp"
- int main()
- {
- daemon(true,false); // 测试
- while(1)
- {
- sleep(1);
- }
- return 0;
- }

从上图中,我们得知testd这个进程是孤儿进程,不和任何终端相连,是一个独立的会话, 同时它的当前工作路径被我们改为了/,标准输入/输出/错误也被重定向到了/dev/null这个字符设备文件,要想结束该进程,直接kill -9就行,当然我们也可以通过设置相关的信号用来关闭该守护进程。
系统其实也帮我们写个daemon这个函数用来创建守护进程

(具体的函数内部实现和我们写的不太一样,可以看看文档)
这里推荐自己写,因为不同系统的daeman实现可能不同,不容易把控,还是自己写比较稳妥,可以根据具体需求自己定制设计
上面客户端和服务端中包含的头文件内容
- //InetAddr.hpp
- #pragma once
- #include
- #include
- #include
- #include
- #include
- #include
- class InetAddr // 用来管理网络套接字ip,port
- {
- public:
- InetAddr(const struct sockaddr_in& addr)
- {
- _ip = inet_ntoa(addr.sin_addr);
- _port = ntohs(addr.sin_port);
- }
- std::string IP()
- {
- return _ip;
- }
- uint16_t Port()
- {
- return _port;
- }
- std::string DebugPrinit()
- {
- return _ip + ":" + std::to_string(_port);
- }
- ~InetAddr()
- {}
- private:
- std::string _ip;
- uint16_t _port;
- };
-
- // Comm.hpp
- enum{
- SocketErr=1,
- BindErr,
- ListenErr
- };
-
- #define CONV(addr) ((struct sockaddr *)addr)
-
- // Log.hpp
- #pragma once
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include "LockGuard.hpp"
-
- enum
- {
- Debug,
- Info,
- Warning,
- Error,
- Fatal
- };
-
- enum
- {
- Screem = 10,
- OneFile,
- Files
- };
-
- const int defaultstyle = Screem;
- const std::string filename = "Log";
- const std::string dir = "Log";
-
- std::string LevelToString(int level)
- {
- switch (level)
- {
- case Debug:
- return "Debug";
- case Info:
- return "Info";
- case Warning:
- return "Warning";
- case Error:
- return "Error";
- case Fatal:
- return "Fatal";
- default:
- return "unknown";
- }
- }
-
- class Log
- {
- public:
- Log():_style(defaultstyle),_filename(filename),_filepath(dir)
- {
- mkdir(_filepath.c_str(),0775); // 在当前目录下创建目录
- pthread_mutex_init(&_mutex,nullptr);
- }
-
- std::string local_time()
- {
- time_t cur = time(nullptr);
- struct tm* t = localtime(&cur);
- char buffer[128];
- // asctime_r(t, buffer);
- snprintf(buffer,sizeof(buffer),"%d-%d-%d %d:%d:%d",t->tm_year+1900,t->tm_mon+1,t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
- return buffer;
- }
-
- void Write(const std::string &info, const std::string &suffix)
- {
- // 可以加锁,保证线程安全 ...
- LockGuard lock(&_mutex);
- std::string name = _filepath + "/" + _filename + suffix;
- std::ofstream ifs(name.c_str(), std::ios_base::out | std::ios_base::app);
- if(ifs.is_open())
- ifs<
- ifs.close();
- // std::cout<
- }
-
- void WriteToFile(const std::string &info, const std::string& level)
- {
- switch(_style)
- {
- case Screem:
- std::cout << info;
- break;
- case OneFile:
- Write(info,".all");
- break;
- case Files:
- Write(info,"."+level);
- break;
- default:
- break;
- }
- }
-
- void Message(int level, const char *format, ...)
- {
- char buffer[1024];
- va_list args;
- va_start(args, format);
- vsnprintf(buffer, sizeof(buffer), format, args);
- va_end(args);
- // printf("[%s][%s][%s]\n",LevelToString(level).c_str(),local_time().c_str(),buffer);
- char info[4096];
- std::string lev = LevelToString(level);
- snprintf(info,sizeof(info),"[%s][%s][%s] %s\n",std::to_string(getpid()).c_str(),
- lev.c_str(),local_time().c_str(),buffer);
- WriteToFile(info,lev);
- }
-
- // 将Message函数转换成仿函数,方便调用
- void _Message_(int level, const char *format, va_list args)
- {
- char buffer[1024];
- vsnprintf(buffer, sizeof(buffer), format, args);
- char info[4096];
- std::string lev = LevelToString(level);
- snprintf(info,sizeof(info),"[%s][%s][%s] %s\n",std::to_string(getpid()).c_str(),
- lev.c_str(),local_time().c_str(),buffer);
- WriteToFile(info,lev);
- }
-
- void operator()(int level, const char *format, ...)
- {
- va_list args;
- va_start(args, format);
- _Message_(level,format,args);
- va_end(args);
- }
-
- ~Log()
- {
- pthread_mutex_destroy(&_mutex);
- }
-
- // 提供接口,方便我们改变日志的输出
- void Enable(int mode)
- {
- _style = mode;
- }
-
- private:
- int _style;
- const std::string _filename;
- const std::string _filepath;
- pthread_mutex_t _mutex;
- };
-
- Log lg;
-
- class Conf
- {
- public:
- Conf()
- {
- lg.Enable(Screem);
- }
- ~Conf()
- {}
- };
-
- Conf conf;
-
- // nocopy.hpp
- #pragma once
- class nocopy
- {
- public:
- nocopy()=default;
- nocopy(const nocopy&tmp)=delete;
- nocopy& operator=(const nocopy&tmp)=delete;
- };