Linux---Socket

网络套接字(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、接口介绍

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,表示选择默认的协议。在大多数情况下,内核可以根据domaintype参数自动确定所使用的协议。

函数成功返回套接字的文件描述符,失败返回-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标志则使函数以非阻塞方式工作,如果没有数据可读,它将立即返回。默认设置为0
  • src_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实现的将客户端发来的消息再给它发回去的代码

2、服务端代码

// 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、客户端代码

#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、接口介绍

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套接字的读写工作

2、服务端代码

// 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、客户端代码

// 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再来具体的理解一下会话和进程组。

如何创建守护进程?

  1. 屏蔽信号:根据具体情况,屏蔽一些信号,防止影响守护进程的执行
  2. 创建子进程并退出父进程:因为setsid()需要当前进程不能是进程组的组长,而我们启动的服务器进程就一个,所以它必然是进程组组长,所以我们需要通过fork()函数创建子进程,让子进程来执行setsid()函数
  3. 在子进程中创建新会话:在子进程中,调用setsid()函数来创建一个新的会话,并使子进程成为该会话的会话首领。这样,子进程就完全独立出来,脱离了对控制终端的依赖。
  4. 改变当前工作目录:使用chdir()函数将当前工作目录更改为根目录。当然,也可以根据需要更改到其他路径。
  5. 关闭文件描述符:关闭所有从父进程继承而来的打开文件描述符。这些描述符在守护进程中可能不会被用到,关闭它们可以节省系统资源,并防止可能的文件锁定问题。
#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);
    }
}
#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 <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;
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/572924.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux关闭swap分区操作[适用于CDH报警等]

1.查看swap分区挂载路径(没卵用) swapon -s 2.设置配置文件的swap配置 echo “vm.swappiness 0” > /etc/sysctl.conf 3.设置内存中的swap状态。有时候配置文件为0&#xff0c;但集群或服务仍然使用了swap分区&#xff0c;可能原因就是内存没有同步配置 echo “0” > …

C++入门----内联函数auto范围fornullptr指针

1.内联函数 顾名思义&#xff0c;内联函数也是函数的一种&#xff0c;我们在C语言的学习过程里面知道了函数和宏之间的区别和各自的优缺点&#xff1b; 函数的使用需要建立栈帧&#xff0c;宏的使用需要考虑各种符号的优先级问题&#xff0c;很容易出错&#xff0c;因为宏在使…

jmeter之跨线程关联

1&#xff09;_setproperty函数&#xff1a;将值保存成jmeter属性 2&#xff09;_property函数&#xff1a;在其他线程组中使用property函数读取属性 一、跨线程接口引用变量 1. 法一&#xff1a;jmeter自带函数_setProperty和_property 1. 1线程组 01 创建登录的【HTTP请求】…

Java知识总结---并发篇

线程 线程的状态 Java中线程可以有如下6中状态&#xff1a; NEW 新创建 RUNNABLE 可运行 BLOCKED 阻塞 WAITING 等待 TIMED WAITING 计时等待 TERMINATED 终止 线程的创建 &#xff08;1&#xff09;继承Thread类 public class ExtendsThread extends Thread { O…

后端工程师——C++工程师招聘需求

相比 Java 语言方向&#xff0c;C 入门简单&#xff0c;精通难&#xff0c;找工作竞争压力更小&#xff0c;但 C 依然是近年来招聘的热门岗位之一。本文将从以下三个方面进行详细讲解&#xff0c;帮助你对 C 相关岗位的就业前景、岗位要求、学习路线等有更充分的了解。 C工程师…

48-70V降12V/33V 5A高效同步降压DC-DC——AH1007

AH1007是一款高效率、高压外置MOSFET管降压芯片TEL&#xff1a;186*4884*3702*&#xff0c;芯片典型输入是8V~100V,输出 电压可调&#xff0c;AH1007最大输出电流可支持6A以上&#xff0c;需要注意板子的散热和温升。 AH1007典型开关频率为150KHz。轻载时会自动降低开关频率以…

SSRF—服务器请求伪造 漏洞详解

漏洞简述 SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造&#xff0c;由服务端发起请求的一个网络攻击&#xff0c;一般用来在外网探测或攻击内网服务&#xff0c;其影响效果根据服务器用的函数不同&#xff0c;从而造成不同的影响。 SSRF 形成的原因…

H5点击复制功能 兼容安卓、IOS

效果图 HTML代码 <div>链接&#xff1a;<span style"color: #FF8A21" click"CopyUrl" id"copyId"> https://blog.csdn.net/qq_51463650?spm1000.2115.3001.5343</span> </div>复制方法 const CopyUrl () > {let …

水库大坝安全白蚁监测系统解决方案

一、系统背景 白蚁作为河岸生态系统中的重要病害&#xff0c;不仅会导致水库大坝外部环境发生改变&#xff0c;甚至会引发水库大坝破坏&#xff0c;进而导致自身结构失去稳定性&#xff0c;严重影响水库大坝的正常运行。因此&#xff0c;治理水库大坝白蚁是确保水库大坝工程顺利…

chrome 浏览器 f12 如何查看 websocket 消息?

1. 打开目标页面 2. f12--》网络--》WS&#xff0c;然后刷新页面( 如果不刷页面&#xff0c;就会看不到 websocket 请求&#xff0c;因为 websocket 是长连接&#xff0c;页面加载后只发出一次连接请求&#xff0c;不像 http 接口&#xff0c;不用刷新页面&#xff0c;待会儿也…

Linux Docker下载镜像更改默认存储位置/usr/lib/docker

用于解决docker默认存储位置磁盘空间不足&#xff0c;切换存储位置 1、执行下面命令查看 现在docker的存储位置 docker info | grep "Docker Root Dir" 1.2、如果之前已经下载过镜像可以用mv命令把原来的镜像复制到新的地址 mv /var/lib/docker /data/docker 2、…

十堰着力建设城市级供应链安全检测平台,全力打造数字政府安全示范区

4月23日&#xff0c;十堰市互联网信息办公室、十堰政务服务和大数据管理局、十堰数安技术有限公司共同签订了《城市供应链安全检测平台战略合作框架协议》&#xff0c;通过政企深度协作&#xff0c;加强十堰市数字安全体系建设&#xff0c;为推动十堰市乃至湖北省数字经济产业发…

RS1G08XF5规格详情

RS1G08XF5 是一款由润石&#xff08;RUNIC&#xff09;公司生产的电子元器件。根据所提供的信息&#xff0c;这是一款SOT-23-5封装的器件&#xff0c;其主要参数包括最小电源电压为5V&#xff0c;最大电源电压为6V&#xff0c;最小工作温度为-40C。 为了更准确地了解这款器件的…

【开源】I.Mx6uLL的QT项目合集

文章目录 前言仓库11.下载仓库代码2.使用qmake交叉编译3.效果展示 前言 不定期的更新基于I.Mx6uLL的arm架构下的QT项目合集&#xff1a; 项目移植前的准备 【环境安装】 仓库1 1.下载仓库代码 https://github.com/cocowts/QtCustomcomponent git clone https://github.com/…

ssm框架的网上招聘系统的设计与实现,ssm框架,java编程,mysql数据库 myeclipse开发平台

网上招聘是一个功能很复杂的系统&#xff0c;各个部门之间要有一定的协调能力。 要建立一个高效的管理系统的关键问题就是系统内部的各个模块的相互作用&#xff0c;简单的编写一个网站 只用html &#xff0c;css &#xff0c;javascript &#xff0c;xml &#xff0c;xsl技术…

XUbuntu18.04 源码编译Qt4.5.3的过程

由于新公司很多旧的软件都是基于这个版本做的嵌入式开发。 所以想要自己搭一套基于Linux的非嵌入式开发环境&#xff0c;方便用来调试和编译代码。 这样就可以完成在linux下开发&#xff0c;然后直接嵌入式打包&#xff0c;涉及到界面的部分就不需要上机调试看问题了。 所以…

java 当中的流

初识io 代码演示 private final static String URL "E:\\";private final static String READURL "H:\\kj\\202402\\LS0205.00P";Testpublic void testOutputStream() {long start System.currentTimeMillis();try (InputStream in new FileInputStrea…

RC电路延时时间常数在线计算器

RC电路延时时间常数在线计算器: https://www.838dz.com/calculator/1888.html 急用时&#xff0c;找不到。

古籍数字化平台中的OCR:这个平台更精准

在浩瀚的历史长河中&#xff0c;古籍作为中华民族的文化瑰宝&#xff0c;承载着无数先人的智慧与心血。然而&#xff0c;由于岁月侵蚀、保存不当等多种原因&#xff0c;许多珍贵的古籍面临损坏、失传的危机。为了守护这些无价之宝&#xff0c;云聪研发团队倾力打造了一款尖端的…

portaudio 怎么调用获取输出流

PortAudio是一个跨平台的音频I/O库&#xff0c;它允许你访问计算机的音频硬件进行录音和播放。要使用PortAudio获取输出流&#xff08;播放流&#xff09;&#xff0c;你需要遵循以下步骤&#xff1a; 官方下载地址&#xff1a;PortAudio - an Open-Source Cross-Platform Audi…
最新文章