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

cpp 复制代码
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

cpp 复制代码
int sockfd = socket(AF_INET,SOCK_DGRAM,0); // UDP默认固定写法
cpp 复制代码
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函数也会失败。
cpp 复制代码
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变量来获取具体的错误信息。

cpp 复制代码
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、服务端代码

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

cpp 复制代码
int socket(int domain, int type, int protocol);
// 和UDP中介绍的socket一样
// 在选择tcp协议时,参数选项上有所差异,如下
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // tcp 默认固定写法
cpp 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

用法和上面的UDP一样

cpp 复制代码
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",等待客户端的连接请求。

cpp 复制代码
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函数会阻塞当前线程,直到有连接请求到来或者发生错误。

cpp 复制代码
   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、服务端代码

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

如何创建守护进程?

  1. 屏蔽信号:根据具体情况,屏蔽一些信号,防止影响守护进程的执行
  2. 创建子进程并退出父进程:因为setsid()需要当前进程不能是进程组的组长,而我们启动的服务器进程就一个,所以它必然是进程组组长,所以我们需要通过fork()函数创建子进程,让子进程来执行setsid()函数
  3. 在子进程中创建新会话:在子进程中,调用setsid()函数来创建一个新的会话,并使子进程成为该会话的会话首领。这样,子进程就完全独立出来,脱离了对控制终端的依赖。
  4. 改变当前工作目录:使用chdir()函数将当前工作目录更改为根目录。当然,也可以根据需要更改到其他路径。
  5. 关闭文件描述符:关闭所有从父进程继承而来的打开文件描述符。这些描述符在守护进程中可能不会被用到,关闭它们可以节省系统资源,并防止可能的文件锁定问题。
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;
};
相关推荐
a26637896几秒前
解决yum命令报错“Could not resolve host: mirrorlist.centos.org
linux·运维·centos
粤海科技君26 分钟前
如何使用腾讯云GPU云服务器自建一个简单的类似ChatGPT、Kimi的会话机器人
服务器·chatgpt·机器人·腾讯云
傲骄鹿先生36 分钟前
阿里云centos7.9服务器磁盘挂载,切换服务路径
服务器·阿里云·磁盘
不爱学习的YY酱1 小时前
【计网不挂科】计算机网络期末考试——【选择题&填空题&判断题&简述题】试卷(4)
网络·计算机网络
装睡的小5郎1 小时前
家庭宽带如何开启公网ipv4和ipv6
网络
yfs10241 小时前
压缩Minio桶中的文件为ZIP,并通过 HTTP 响应输出
网络·网络协议·http
有谁看见我的剑了?1 小时前
Ubuntu 22.04.5 配置vlan子接口和网桥
服务器·网络·ubuntu
hgdlip1 小时前
有什么办法换网络ip动态
网络·tcp/ip·智能路由器
2739920291 小时前
Ubuntu20.04 安装build-essential问题
linux
超栈1 小时前
HCIP(11)-期中综合实验(BGP、Peer、OSPF、VLAN、IP、Route-Policy)
运维·网络·网络协议·计算机网络·web安全·网络安全·信息与通信