网络套接字(socket)
网络通信仅仅是为了让两台主机间传送数据吗?数据是被谁需要的呢?--- 进程,所以网络通信的本质是两个进程间的通信。那么如何找到两台主机上的两个进程呢?
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语言的时候就应该了解过电脑有大小端之分(大端:高位放在低地址处,小端:低位放在低地址处,是两种不同的数据存储方式)。本来在一台主机上没什么问题,但是网络通信发生在大端机和小端机之间,那么我们解释数据就会出问题,如下
为了方便我们进行数据的大小端转化,系统还提供了一些接口
(直接用就行,不用我们再去关心机器是大端还是小端)
socket编程接口
接口的具体使用,会在后面介绍
一、UDP协议网络编程
(这里暂且不去关心UDP的底层原理,先来熟悉一下UDP的相关接口)
1、接口介绍
cppint 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
cppint sockfd = socket(AF_INET,SOCK_DGRAM,0); // UDP默认固定写法
cppint 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
函数也会失败。
cppssize_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
变量来获取具体的错误信息。
cppssize_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实现的将客户端发来的消息再给它发回去的代码
2、服务端代码
cpp
// Udp_Server.hpp
#include <string>
#include <unistd.h>
#include <string.h>
// 网络相关的头文件
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 自定义的头文件 --- 在文章的结尾 - 附录部分 - 有具体的代码实现,有兴趣可以看看
// (这些头文件对我们理解代码的核心逻辑没有太大影响,只是为了让编码更加有条理)
#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 <memory>
#include <string>
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<UdpServer> s(new UdpServer(port));
s->Init();
s->Start();
return 0;
}
3、客户端代码
cpp
#include <cerrno>
#include <iostream>
// 网络相关的头文件
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 自定义的头文件
#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;
}
当我们熟悉了这些接口,我们就可以将代码进行改造,实现一些其他的有趣的功能,比如多人聊天会议室(需要结合多线程),远程控制自己的服务器等等。
二、TCP协议网络编程
1、接口介绍
cppint socket(int domain, int type, int protocol); // 和UDP中介绍的socket一样 // 在选择tcp协议时,参数选项上有所差异,如下 int sockfd = socket(AF_INET, SOCK_STREAM, 0); // tcp 默认固定写法
cppint bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
用法和上面的UDP一样
cppint 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",等待客户端的连接请求。
cppint 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函数会阻塞当前线程,直到有连接请求到来或者发生错误。
cppint 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套接字的读写工作
2、服务端代码
cpp
// TcpServer.hpp
#pragma once
#include <string>
#include <sys/wait.h>
#include <pthread.h>
#include <functional>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#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 <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
#include "Comm.hpp"
#include <cerrno>
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;
}
3、客户端代码
cpp
// TcpClient.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
#include "Comm.hpp"
#include <cerrno>
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的后台,以守护进程(精灵进程)的方式进行运行。
什么是守护进程?
在解释这个之前,我们先来了解一下其他的相关概念
(父进程和进程大家都很了解了,这里就不做介绍了,这里引入两个新概念---进程组和会话)
- 进程组是一个或多个进程的集合,每个进程都属于一个进程组。进程组主要目的是为了简化对多个进程的管理。每个进程组都有一个唯一的进程组ID,这个ID通常是进程组中的组长进程的进程ID。组长进程可以创建一个进程组,并在其中创建新的进程。只要进程组中有一个进程存在,进程组就会持续存在,与组长进程是否终止无关。此外,进程组ID在诸如waitpid函数和kill函数的参数中都有使用,使得我们可以对整个进程组内的进程进行统一的管理和操作,比如使用kill命令通过进程组ID来结束整个进程组的进程。
- 会话则是操作系统中用于管理进程的抽象概念,它提供了一个运行环境和资源共享的上下文。会话由一个或多个进程组组成,每个会话都有一个唯一的会话ID,这个ID与会话的领头进程(即创建会话的进程)的PID相同。会话的主要功能包括进程间通信、控制终端以及作业控制。会话中的进程可以通过进程间通信机制相互通信和共享信息。会话通常与一个控制终端相关联,用于输入和输出的交互。会话首领可以通过控制终端与用户进行交互,并控制终端的行为。此外,会话还提供了作业控制的机制,可以将进程从前台切换到后台,或者从后台切换到前台。
这里首先强调:不要将这两个概念下的进程间关系往父子上去靠,这是两个不同层面的概念。
相信大家看完上面这段话还是不太理解这两个概念,下面我们结合bash再来具体的理解一下会话和进程组。
如何创建守护进程?
- 屏蔽信号:根据具体情况,屏蔽一些信号,防止影响守护进程的执行
- 创建子进程并退出父进程:因为setsid()需要当前进程不能是进程组的组长,而我们启动的服务器进程就一个,所以它必然是进程组组长,所以我们需要通过fork()函数创建子进程,让子进程来执行setsid()函数
- 在子进程中创建新会话:在子进程中,调用setsid()函数来创建一个新的会话,并使子进程成为该会话的会话首领。这样,子进程就完全独立出来,脱离了对控制终端的依赖。
- 改变当前工作目录:使用chdir()函数将当前工作目录更改为根目录。当然,也可以根据需要更改到其他路径。
- 关闭文件描述符:关闭所有从父进程继承而来的打开文件描述符。这些描述符在守护进程中可能不会被用到,关闭它们可以节省系统资源,并防止可能的文件锁定问题。
cpp
#pragma once
#include <sys/signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
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);
}
}
cpp
#include "dameon.hpp"
int main()
{
daemon(true,false); // 测试
while(1)
{
sleep(1);
}
return 0;
}
从上图中,我们得知testd这个进程是孤儿进程,不和任何终端相连,是一个独立的会话, 同时它的当前工作路径被我们改为了/,标准输入/输出/错误也被重定向到了/dev/null这个字符设备文件,要想结束该进程,直接kill -9就行,当然我们也可以通过设置相关的信号用来关闭该守护进程。
系统其实也帮我们写个daemon这个函数用来创建守护进程
(具体的函数内部实现和我们写的不太一样,可以看看文档)
这里推荐自己写,因为不同系统的daeman实现可能不同,不容易把控,还是自己写比较稳妥,可以根据具体需求自己定制设计
四、附录
上面客户端和服务端中包含的头文件内容
cpp
//InetAddr.hpp
#pragma once
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
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 <iostream>
#include <cstdio>
#include <cstring>
#include <cstdarg>
#include <fstream>
#include <ios>
#include <sys/stat.h>
#include <pthread.h>
#include <unistd.h>
#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<<info;
ifs.close();
// std::cout<<info;
}
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;
};