• 【Linux】socket网络编程



    1. 网络进程的端口号

    网络通信的本质是:两个网络进程进行通信。 上节我们提到,网络中ip地址可以标识唯一的一台主机,而主机中的网络进程是通过端口号(port)确定进程唯一性的。进一步讲,在网络通信中,通过ip+port来唯一标识某个主机上的某个进程。

    💭端口号是传输层协议的内容,它是一个2字节16bit的整数,用于标识一个进行网络通信的进程,告知OS当前数据传递给哪一个进程。一个端口号只能标识一个进程,一个进程可以绑定多个端口号。端口号可由用户指定,也可由OS自动分配。

    • 理解“端口号”和“进程PID”

      🔎既然需要标识网络通信中唯一一个进程,那么为什么不用系统中的进程PID,而是重新定义了一个端口号呢?

      1. 跨计算机通信: 进程PID是针对每台计算机上运行的进程的,不同计算机上可能存在相同的PID。在网络中,需要一种机制来标识不同计算机上运行的进程,因此需要使用全局唯一的标识符。
      2. 动态性: 进程在运行时可以创建和销毁,其PID也可能会更改。如果使用PID作为标识符,那么在进程重新启动后,其他进程无法识别它,这会导致通信中断。
      3. 端口号的多样性: 端口号是一种广泛用于网络通信的标识符,它不仅用于标识进程,还可以用于标识不同类型的服务。这种多样性使得不同类型的通信可以共存于同一台计算机上,而无需担心冲突。
      4. 网络层次: 在计算机网络中,通信涉及多个层次,从物理层到应用层。端口号位于传输层(通常是TCP或UDP协议),而进程PID是操作系统内核层的概念。因此,端口号更适合在传输层标识和管理不同进程之间的通信。

    2. 认识UDP和TCP

    此处我们先简单直观的认识一下UDP和TCP两种协议,以便更好掌握socket套接字编程。

    UDP协议 (User Datagram Protocol,用户数据报协议)

    • 传输层协议
    • 无连接
    • 不可靠传输
    • 面向数据报

    TCP协议(Transmission Control Protocol,传输控制协议)

    • 传输层协议
    • 有连接
    • 可靠传输
    • 面向字节流

    3. 网络字节序

    这里先简单复习一下系统大小端字节序的概念

    • 小端:低位在低地址,高位在高地址

    • 大端:低位在高地址,高位在低地址

    下面是4字节整数0x12345678在内存中小端与大端不同的字节序。

    在这里插入图片描述

    不同的主机可能以不同的字节序存储多字节数据,那么,一台小端机器和一台大端机器就不能直接将数据传递给对方了,双方都不认识对方的数据。

    为了解决这一问题,TPC/IP协议规定:网络数据流采用大端字节序。 即:小端机器向网络中发送数据,需要先将数据转成大端字节序,从网络中获取数据也需将数据先转成小端再使用。而大端就直接收发数据即可,无需转换。

    网络通信双方传输和接收的核心数据,一般由系统调用自动做字节序的转换,而需要用户手动转换字节序的一般是通信的端口号和ip地址。

    #include 
    
    uint32_t htonl(uint32_t hostlong);	// 主机转网络(4byte)
    
    uint16_t htons(uint16_t hostshort); // 主机转网络(2byte)
    
    uint32_t ntohl(uint32_t netlong);   // 网络转主机(4byte)
    
    uint16_t ntohs(uint16_t netshort);  // 网络转主机(2byte)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    4. socket通信

    **Linux中的socket套接字是一种用于在不同进程之间进行通信的机制,它允许在同一台或不同计算机上的进程之间进行数据交换。**Socket API是一种应用层与传输层之间的接口,它使开发者能够创建网络应用程序,如客户端-服务器应用程序。

    1️⃣

    Linux下一切皆文件,因此网络通信本质上也是进程打开一个文件,获取一个文件描述符,并向这个文件描述符中传输或获取网络数据。这是网络通信的第一步,用到的是socket这个系统接口。

    #include          
    #include 
    
    int socket(int domain, int type, int protocol); 
    
    • 1
    • 2
    • 3
    • 4

    参数:

    • domain:通信类型,IPv4通信:AF_INET, IPV6通信:AF_INET6, 本地通信:AF_UNIX
    • type:传输数据类型,UDP:SOCK_DGRAM(数据报), TCP:SOCK_STREAM(字节流)
    • protocol:协议类型,传入0可根据type自动推导

    返回值:

    ​ 一个套接字的文件描述符,后续通过该文件描述符传输或获取数据

    在这里插入图片描述

    2️⃣

    第二步要绑定网络进程的地址,以便其它进程能找到该进程,实现通信。socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及UNIX Domain Socket(本地进程通信)。然而, 各种协议的地址格式并不相同。

    在这里插入图片描述

    绑定本地地址用到的系统接口是bind,需要用户先定义并填充一个地址结构体,再用bind接口绑定到系统当中,值得注意的是,bind接口的第二个参数addr的类型是struct sockaddr *,因为addr可能指向不同的地址结构体类型,这里传入统一类型的指针,再内部判断指针指向的空间头部的地址类型(AF_INET/AF_UNIX),即可判断地址结构体的类型,此处类似cpp多态的思想。

    #include        
    #include 
    
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
    • 1
    • 2
    • 3
    • 4

    参数:

    • sockfd:接口socket创建的文件描述符

    • addr:用户定义的地址结构体指针

    • addrlen:结构体的长度

    返回值:

    ​ 成功返回0,失败返回-1并设置错误码errno。

    在这里插入图片描述

    💬struct sockaddr_in的代码结构

    在这里插入图片描述


    5. UDP服务器和客户端

    💭UDP协议规定的是无连接的网络通信,通信双方无需连接直接通过地址找到对方并通信,传输数据面向数据报。优点是代码实现简单,缺点是传输不稳定可靠。

    在这里插入图片描述

    ⭕UDP协议用于收发数据的系统接口:

    1. 接收数据recvfrom

      #include 
      #include 
         
      ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
      
      • 1
      • 2
      • 3
      • 4

      参数:

      • sockfd:套接字文件描述符,用于指定要从哪个套接字接收数据。

      • buf:一个指向用于存储接收数据的缓冲区的指针。

      • len:接收缓冲区的长度,即可接收的最大字节数。

      • flags:控制接收操作的标志位,通常设置为0。

      • src_addr:一个指向 struct sockaddr 类型的指针,用于填充发送数据方的地址信息。这个参数可以为NULL,如果不关心对方的地址信息。

      • addrlen:一个指向 socklen_t 类型的指针,用于指定 src_addr 缓冲区的长度。在调用 recvfrom 之前,你需要将 addrlen 设置为 src_addr 缓冲区的大小。

      返回值:

      ​ 成功返回值接收到的字节数。如果发生错误,返回值为 -1,并且可以使用 errno 来获取错误代码。

    2. 发送数据sendto

      #include 
      #include 
      
      ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
      
      • 1
      • 2
      • 3
      • 4

      参数:

      • sockfd:套接字文件描述符,用于指定要发送数据的套接字。
      • buf:一个指向包含要发送数据的缓冲区的指针。
      • len:要发送的数据的长度(以字节为单位)。
      • flags:控制发送操作的标志位,通常设置为0。
      • dest_addr:一个指向 struct sockaddr 类型的指针,用于指定数据的目标地址。这个参数通常用于指定接收方的地址信息。
      • addrlen:一个 socklen_t 类型的整数,用于指定 dest_addr 缓冲区的长度。

      返回值:

      ​ 成功返回发送的字节数。如果发生错误,返回值为 -1,并且可以使用 errno 来获取错误代码。

    5.1 基础UDP服务器和客户端

    • 服务器
    // server.cc
    #include 
    #include 
    #include 
    #include 
    #include "server.hpp"
    #include "err.hpp"
    
    // 该服务器完成工作:将客户端数据接收并原封不动地发挥给客户端即可
    void Usage()
    {
        // 使用手册
        std::cout << "Please enter the correct format: "
                  << "./server [port]" << std::endl;
    }
    
    std::string EchoService(const std::string& msg)
    {
        return msg;
    }
    
    // ./server [port] (port为该网络进程的端口号)
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            Usage();
            exit(USAGE_ERR);
        }
    	
        // 将服务器封装成一个类,先理清调用逻辑
        
        // 向服务器传入用户指定的端口号,以及业务处理函数(只需将消息收到并返回给即可)
        std::unique_ptr<UdpServer> us_ptr(new UdpServer(atoi(argv[1]), EchoService));
    
        us_ptr->Initial(); // 初始化服务器
        
        us_ptr->Start();   // 启动服务器
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    // server.hpp
    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "err.hpp"
    
    const int MSG_BUF_SIZE = 1024;
    using serv_func_t = std::function<std::string(const std::string &str)>;
    
    class UdpServer
    {
    public:
        // 构造函数
        UdpServer(int port, serv_func_t service) : _port(port), _service(service)
        {
        }
    	
        // 初始化服务器
        void Initial()
        {
            // 1. 创建socket套接字(协议族,声明是哪种通信、哪种协议)
            int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (sockfd < 0)
            {
                // 创建套接字失败
                std::cerr << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            _sockfd = sockfd;
    
            std::cout << "socket creat success: " << _sockfd << std::endl;
    
            // 2. 绑定IP地址和端口号(地址族,标定网络通信地址)
            
            // 2.1 填充sockaddr_in各个字段
            struct sockaddr_in sin;
            bzero(&sin, sizeof(sin));
            sin.sin_family = AF_INET;
            sin.sin_port = htons(_port); // 主机转网络字节序
    
            // sin.sin_addr.s_addr = inet_addr(_ip.c_str());
            // in_addr_t inet_addr(const char *cp);
            // 将点分字符串形式的ip地址转成四字节整数形式,并且从主机字节序转换为网络字节序
    
            // 云服务器可能有多个ip地址,不允许用户指定某一个,用INADDR_ANY表示该服务器的任意ip
            // 表示只要发到该服务器上的信息都可以接收
            sin.sin_addr.s_addr = INADDR_ANY;
    
            // 2.2 调用bind绑定到系统
            if (bind(_sockfd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
            {
                // 绑定失败
                std::cerr << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
    
            std::cout << "socket bind success: "
                      << "port->" << _port << std::endl;
        }
    
        // 启动服务器
        void Start()
        {
            // 不断地接收客户端数据,并将数据返回给客户端
            while (true)
            {
                // 1. 数据接收
                char msg_buf[MSG_BUF_SIZE];
                memset(msg_buf, 0, MSG_BUF_SIZE);
    			
                // 客户端地址信息,recv会自动填充
                struct sockaddr_in cln;
                bzero(&cln,sizeof(cln));
                socklen_t len;
    
                ssize_t rn = recvfrom(_sockfd, msg_buf, sizeof(msg_buf) - 1, 0, (struct sockaddr *)&cln, &len);
                if (rn < 0)
                {
                    // 接收失败
                    std::cerr << strerror(errno) << std::endl;
                    exit(RECV_ERR);
                }
                msg_buf[rn] = '\0';
    
                // 2. 业务处理
                std::cout << "[" << inet_ntoa(cln.sin_addr) << ":" << ntohs(cln.sin_port) << "] ";
                std::cout << "#用户输入指令# " << msg_buf << std::endl;
                std::string respond = _service(msg_buf);
    
                // 3. 数据传回客户端
                ssize_t sn = sendto(_sockfd, respond.c_str(), respond.size(), 0, (struct sockaddr *)&cln, sizeof(cln));
                if (sn < 0)
                {
                    std::cerr << errno << " " << strerror(errno) << std::endl;
                    exit(SEND_ERR);
                }
            }
        }
    
    private:
        int _sockfd; 		 // 套接字文件描述符
        uint16_t _port; 	 // 服务器端口号
        serv_func_t _service;// 业务处理接口
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 客户端
    //client.cc
    #include 
    #include 
    #include "client.hpp"
    #include "err.hpp"
    
    void Usage()
    {
        std::cout << "Please enter the correct format: "
                  << "./client [server's ip] [server's port]" << std::endl;
    }
    
    // ./client [ip] [port] (由用户指定服务器的ip和port)
    int main(int argc, char *argv[])
    {
        if (argc != 3)
        {
            Usage();
            exit(USAGE_ERR);
        }
    
        std::unique_ptr<UdpClient> uc_ptr(new UdpClient(argv[1], atoi(argv[2])));
        uc_ptr->Initial();
    
        while (true)
        {
            std::cout << "ENTER:> ";
            std::string msg;
            std::getline(std::cin, msg);
            // 用户不断发送消息并接收从服务器返回的数据
            uc_ptr->Send(msg);
            uc_ptr->Recv();
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "err.hpp"
    
    const int MSG_BUF_SIZE = 1024;
    
    static struct sockaddr_in tmp;
    static socklen_t len;
    
    class UdpClient
    {
    public:
        UdpClient(std::string svr_ip, uint16_t svr_port)
            : _svr_ip(svr_ip), _svr_port(svr_port)
        {
            // 填充服务器的地址信息
            bzero(&_svr, sizeof(_svr));
            _svr.sin_family = AF_INET;
            _svr.sin_port = htons(_svr_port);
            _svr.sin_addr.s_addr = inet_addr(_svr_ip.c_str());
        }
    
        void Initial()
        {
            // 1. 创建套接字
            int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (sockfd < 0)
            {
                // 创建套接字失败
                std::cerr << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            _sockfd = sockfd;
    
            // 2. 绑定(由操作系统自动绑定地址,端口号由OS分配)
        }
    
        // 向服务端发送消息
        void Send(std::string &msg)
        {
            ssize_t n = sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr *)&_svr, sizeof(_svr));
            if (n < 0)
            {
                std::cerr << strerror(errno) << std::endl;
                exit(SEND_ERR);
            }
        }
    
        // 接收服务端发回的消息
        void Recv()
        {
            char msg_buf[MSG_BUF_SIZE];
            memset(msg_buf, 0, MSG_BUF_SIZE);
    
            int n = recvfrom(_sockfd, msg_buf, sizeof(msg_buf) - 1, 0, (struct sockaddr *)&tmp, &len);
            if (n < 0)
            {
                std::cerr << strerror(errno) << std::endl;
                exit(RECV_ERR);
            }
            msg_buf[n] = '\0';
            std::cout << "[" << inet_ntoa(_svr.sin_addr) << ":" << ntohs(_svr.sin_port) << "] " << std::endl
                      << msg_buf;
        }
    
    private:
        int _sockfd;             // 套接字文件描述符
        std::string _svr_ip;     // 服务器ip
        uint16_t _svr_port;      // 服务器port
        struct sockaddr_in _svr; // 服务器地址信息
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    tips:

    • 服务器是稳定的,长期运行的,因此其端口号需要在启动时指定且运行时一直保持不变,才能让客户端能准确地找到服务器。

    • 客户端是动态的,随时可能退出与重连,而且可能会有多个客户端同时存在。因此客户端的端口号不应该由用户指定,而是OS动态分配,避免端口冲突,提高并发性能。OS在客户端第一次调用Socket Api完成地址的绑定工作。客户端分配的端口号是临时的,在连接关闭后释放。

    5.2 群聊服务器和客户端

    实现客户端能模拟类似微信群聊的功能。在服务器中设置一个环形队列cirQueue,并设置两个线程,一个用于接收用户消息,一个用于广播用户消息(即向每位用户发送head消息)。为了满足向用户广播消息的需求,服务器里还需储存当前在线用户的信息。

    在这里插入图片描述

    • 服务器
    // GroupChatServer.hpp
    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "err.hpp"
    #include "cirQueue.hpp"
    #include "Thread.hpp"
    
    const int BUF_SIZE = 1024;
    using serv_func_t = std::function<std::string(const std::string &str)>;
    
    // 接收客户端的信息,并传回客户端
    
    class UdpServer
    {
    public:
        UdpServer(uint16_t port, serv_func_t service)
            : _port(port), _service(service)
        {
            // 调用之前写的Thread组件,两个线程,一个负责收消息,一个负责广播数据
            _p = Thread(1, RecvThreadRoutine, this);
            _c = Thread(2, BoardcastThreadRoutine, this);
        }
        
        static void *RecvThreadRoutine(void *args)
        {
            UdpServer *ts = static_cast<UdpServer *>(args);
            while (true)
            {
                ts->Recv();
            }
            return nullptr;
        }
    
        static void *BoardcastThreadRoutine(void *args)
        {
            UdpServer *ts = static_cast<UdpServer *>(args);
            while (true)
            {
                ts->Boardcast();
            }
            return nullptr;
        }
    
        void Initial()
        {
            // 1. 创建socket套接字(协议族,声明是哪种通信、哪种协议)
            int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (sockfd < 0)
            {
                // 创建套接字失败
                std::cerr << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            _sockfd = sockfd;
    
            std::cout << "socket creat success: " << _sockfd << std::endl;
    
            // 2. 绑定IP地址和端口号(地址族,标定网络通信地址)
            struct sockaddr_in local;
            bzero(&local, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = INADDR_ANY;
    
            if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                // 绑定失败
                std::cerr << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
            std::cout << "socket bind success: "
                      << "port->" << _port << std::endl;
        }
        
        // 服务器启动,即两个线程开始运行,主线程等待即可
        void Start()
        {
            _p.run();
            _c.run();
    
            _p.join();
            _c.join();
        }
    
        void Recv()
        {
            // 1.1 创建接收数据的缓冲区
            char buf[BUF_SIZE];
            memset(buf, 0, sizeof(buf));
    
            // 1.2 创建套接字
            struct sockaddr_in client;
            bzero(&client, sizeof(client));
            socklen_t len = sizeof(client);
    
            // 1.3 从sock中接收客户端数据
            ssize_t n = recvfrom(_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&client, &len);
            if (n < 0)
            {
                std::cerr << "recv error: " << strerror(errno) << std::endl;
                exit(RECV_ERR);
            }
            buf[n] = '\0';
    
            // 1.4 打包用户信息,并添加新用户
            std::string client_info = inet_ntoa(client.sin_addr);
            client_info += '-';
            client_info += std::to_string(ntohs(client.sin_port));
            AddOnlineUser(client_info, client);
    
            // 1.5 将消息投放到公共聊天窗口(即环形缓冲区)中
            std::string message = "[" + client_info + "] " + buf;
            _messages.push(message);
        }
    
        void Boardcast()
        {
            // 1. 取出环形缓冲区的头部数据,这是我们这次要广播的消息
            std::string message;
            _messages.pop(&message);
    
            // 2. 发给每一个在线用户
    
            // 2.1 先加锁拷贝一份在线用户信息副本(公有->私有)
            list<struct sockaddr_in> sins;
            {
                std::unique_lock<std::mutex> lck(_mtx);
                for (auto &usr : _online_users)
                {
                    std::cout << "send to " << usr.first << ": " << message << std::endl;
                    sins.push_back(usr.second);
                }
            }
            // 2.2 再用线程私有的副本进行网络IO将信息传给客户端
            for (auto &sin : sins)
            {
                ssize_t n = sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&sin, sizeof(sin));
                if (n < 0)
                {
                    std::cerr << "send error: " << strerror(errno) << std::endl;
                    exit(SEND_ERR);
                }
            }
        }
    
    private:
        void AddOnlineUser(const std::string &user, struct sockaddr_in &sin)
        {
            std::unique_lock<std::mutex> lck(_mtx);
            // if user is a new client, add it into online_users, else do nothing
            size_t size = _online_users.size();
            _online_users[user] = sin;
            if (_online_users.size() > size)
                std::cout << "新用户加入: " << user << std::endl;
        }
    
    private:
        int _sockfd;                                                       // 套接字文件fd
        uint16_t _port;                                                    // 服务器端口号
        serv_func_t _service;                                              // 业务处理函数
        cirQueue<std::string> _messages;                                   // 存储用户消息的环形队列
        std::unordered_map<std::string, struct sockaddr_in> _online_users; // 在线用户信息表(用户ip和port)
        Thread _p;                                                         // recv线程(生产者)
        Thread _c;                                                         // boardcast线程(消费者)
        std::mutex _mtx;                                                   // 保护_online_users的锁
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 客户端
    //client.cc(无封装版本)
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "Thread.hpp"
    #include "err.hpp"
    
    const int BUF_SIZE = 1024;
    
    void Usage()
    {
        std::cout << "Please enter the correct format: " << std::endl
                  << "      ./client [server's ip] [server's port]" << std::endl;
    }
    
    struct ThreadData
    {
        ThreadData(int sockfd, struct sockaddr_in *psvr) : _sockfd(sockfd), _psvr(psvr)
        {
        }
        int _sockfd;
        struct sockaddr_in *_psvr;
    };
    
    void *SendThreadRountine(void *args)
    {
        ThreadData *td = static_cast<ThreadData *>(args);
        while (true)
        {
            // 1. 用户输入消息
            std::string message;
            std::cout << "Please Enter# ";
            std::getline(std::cin, message);
            // 2. 发送消息到服务端
            ssize_t n = sendto(td->_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)td->_psvr, sizeof(*td->_psvr));
            if (n < 0)
            {
                std::cerr << "send error: " << strerror(errno) << std::endl;
                exit(SEND_ERR);
            }
        }
        delete td;
        return nullptr;
    }
    
    void *RecvThreadRountine(void *args)
    {
        ThreadData *td = static_cast<ThreadData *>(args);
        while (true)
        {
            // 接收其它客户端的消息(来自服务端)
            char buf[BUF_SIZE];
            memset(buf, 0, sizeof(buf));
            struct sockaddr_in tmp;
            bzero(&tmp, sizeof(tmp));
            socklen_t len = sizeof(tmp);
    
            ssize_t n = recvfrom(td->_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&tmp, &len);
            if (n < 0)
            {
                std::cerr << "recv error: " << strerror(errno) << std::endl;
                exit(RECV_ERR);
            }
            buf[n] = '\0';
    
            // 群聊信息打印到2号文件描述符上,方便重定向观察输出结果
            std::cerr << buf << std::endl;
        }
        delete td;
        return nullptr;
    }
    
    // ./client [server's ip] [server's port]
    int main(int argc, char *argv[])
    {
        if (argc != 3)
        {
            Usage();
            exit(USAGE_ERR);
        }
    
        // 1. 创建套接字
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd < 0)
        {
            std::cerr << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
        // IP和端口号由OS自动分配绑定
    
        // 2. 获取服务端ip和port
        std::string server_ip = argv[1];
        uint16_t server_port = atoi(argv[2]);
    
        // 指明服务端的地址族(ip and port)
        struct sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    
        // 一个线程负责让用户输入并发送消息,一个线程负责接收群聊服务器的消息
        Thread send_thread(1, SendThreadRountine, new ThreadData(sockfd, &server));
        Thread recv_thread(2, RecvThreadRountine, new ThreadData(sockfd, &server));
    
        send_thread.run();
        recv_thread.run();
    
        send_thread.join();
        recv_thread.join();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115

    呈现效果如下

    在这里插入图片描述


    6. TCP服务器和客户端

    💭TCP协议规定面向连接的网络通信,传输数据面向字节流。TCP服务器除了完成socket创建套接字和bind绑定本机地址外,还需要做如下两件事:

    1. 设置套接字为监听状态,等待客户端的连接请求

      #include  
      #include 
      
      int listen(int sockfd, int backlog);
      
      • 1
      • 2
      • 3
      • 4

      参数:

      • sockfd:服务器的套接字文件描述符
      • backlog:用于指定等待连接队列的最大长度

      返回值:

      ​ 成功返回0,失败返回-1,错误码errno被设置

    2. 接收客户端的连接请求

      #include 
      #include 
      
      int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
      
      • 1
      • 2
      • 3
      • 4

      参数:

      • sockfd:服务器的套接字文件描述符
      • addr:用于存储接收到的客户端的地址信息
      • 一个指向 socklen_t 类型的指针,用于指定 src_addr 缓冲区的长度。在调用 accept 之前,你需要将 addrlen 设置为 addr 缓冲区的大小。

      返回值:

      ​ 返回一个套接字文件描述符,该描述符面向已接收到的客户端,服务器通过该描述符与此客户端进行通信。也就是说,TCP服务器为每一个已连接的客户端创建一个专属的套接字。

    🔗服务器等待客户端的连接请求,客户端调用connect函数向指定的服务器发送连接请求。

    #include 
    #include 
    
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
    • 1
    • 2
    • 3
    • 4

    参数:

    • sockfd:客户端的套接字文件描述符
    • addr:指定的服务器地址信息
    • addrlen:addr指向空间的长度

    返回值:

    ​ 成功返回0,失败返回-1,错误码errno被设置

    在这里插入图片描述

    6.1 TcpServer

    1. TcpServer的成员变量

      private:
          int _sockfd;        // 服务器的套接字文件描述符
          uint16_t _svr_port; // 服务器端口号
          func_t _service;    // 业务处理函数
      
      • 1
      • 2
      • 3
      • 4
    2. TcpServer的初始化

      void Initial()
      {
          // 1.创建套接字,TCP的传输数据类型是SOCK_STREAM
          if ((_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
          {
              exit(SOCKET_ERR);
          }
      
      	// 2.绑定服务器本地地址
          struct sockaddr_in local;
          bzero(&local, sizeof(local));
          local.sin_family = AF_INET;
          local.sin_port = htons(_svr_port);
          local.sin_addr.s_addr = INADDR_ANY;
      
          if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
          {
              exit(BIND_ERR);
          }
      
          // 3.设置服务器套接字为监听状态
          if (listen(_sockfd, backlog) < 0)
          {
              exit(LISTEN_ERR);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
    3. TcpServer的启动工作

      // 3.1 多进程版本
      void Start()
      {
          while (true)
          {
              struct sockaddr_in client;
              bzero(&client, sizeof(client));
              socklen_t len = sizeof(client);
      
              // 1.接收请求连接的客户端
              int cfd = accept(_sockfd, (struct sockaddr *)&client, &len);
              if (cfd < 0)
              {
                  // no client connect, continue try to accept
                  continue;
              }
              std::string client_info = std::string(inet_ntoa(client.sin_addr)) + "-" + std::to_string(ntohs(client.sin_port));
      
              std::cout << "新用户已接入" << client_info << " cfd: " << cfd << std::endl;
      
              // 2. 收发数据的工作交给子进程做,父进程只负责监听与接收客户端
              
              // 2.1 父进程不关心子进程的退出结果,不等待子进程
          	signal(SIGCHLD, SIG_IGN);
              
              // 2.2 创建子进程
              pid_t id = fork();
              assert(id >= 0);
              if (id == 0)
              {
                  while (true)
                  {
                      std::string respond = Recv(cfd, client_info);
                      Send(cfd, respond);
                  }
              }
              // 2.3 父进程不再需要维护当前客户端的sockfd,直接close,并继续accept其它客户端,这样做可减少文件描述符的消耗
              close(cfd);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      // 3.2 多线程版本
      void Start()
      {
          std::cout << "server start!" << std::endl;
          while (true)
          {
              struct sockaddr_in client;
              bzero(&client, sizeof(client));
              socklen_t len = sizeof(client);
      
              // 1.接受监听的客户端
              int cfd = accept(_sockfd, (struct sockaddr *)&client, &len);
              if (cfd < 0)
              {
                  // no client connect, continue try to accept
                  continue;
              }
              std::string client_info = std::string(inet_ntoa(client.sin_addr)) + "-" + std::to_string(ntohs(client.sin_port));
              
              std::cout << "### 新用户已接入" << client_info << " cfd: " << cfd << " ###" << std::endl;
      
              // 2. 收发数据的工作交给新线程做,主线程只负责监听与接收客户端
              
              // 一个客户端创建一个服务线程
              pthread_t pid;
              pthread_create(&pid, nullptr, threadRoutine, new ThreadData(cfd, client_info, this));
          }
      }
      
      struct ThreadData
      {
          ThreadData() = default;
      
          ThreadData(int cfd, std::string cinfo, TcpServer *ts)
              : _cfd(cfd), _cinfo(cinfo), _ts(ts)
          {
          }
      
          int _cfd;
          std::string _cinfo;
          TcpServer *_ts;
      };
      
      static void *threadRoutine(void *args)
      {
          ThreadData *td = static_cast<ThreadData *>(args);
          pthread_detach(pthread_self());  // 分离线程,这样主线程无需等待该线程,提高服务器效率
          while (true)
          {
              std::string respond = td->_ts->Recv(td->_cfd, td->_cinfo);
              td->_ts->Send(td->_cfd, respond);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53

      ⭕注意:多进程版本,因为子进程拷贝了父进程的文件描述符表,每个进程拥有一张独立的文件描述符表,所以父进程可以关闭已经交给子进程的客户端sockfd。而多线程版本,各线程共享同一张文件描述符表,主线程在客户端退出之前不能关闭客户端sockfd,否则会导致工作线程找不到对接的客户端

    4. TcpServer的Recv函数(接收客户端数据)

      由于TCP是面向字节流传输的,所以TcpServer的数据传输本质上就是对文件的读写,调用 readwrite ,操作的是客户端套接字文件描述符。与管道文件的同步机制类似,如果套接字对接的客户端退出,服务器read的返回值就是0。

      std::string Recv(int cfd, const std::string &ci)
      {
          // read读取客户端数据
          char buf[BUF_SIZE];
          memset(buf, 0, sizeof(buf));
          std::string respond;
          ssize_t n = read(cfd, buf, sizeof(buf) - 1);
          if (n < 0)
          {
              // 读取失败,当前子执行流退出
              std::cerr << "read from client fail: " << strerror(errno) << std::endl;
      
              // exit(READ_ERR); // 多进程版
              pthread_exit(nullptr);
          }
          else if (n == 0)
          {
              // 客户端已退出
              close(cfd);
              // 多进程版可以不close,子进程exit也就回收了,对父进程没有影响
              // 多线程版必须close,防止文件fd泄漏
      
              // exit(0); // 多进程版
              pthread_exit(nullptr);
          }
          else
          {
              // 读取成功,将数据业务处理后返回
              buf[n] = '\0';
              return _service(buf);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
    5. TcpServer的Send函数(发送数据到客户端)

      void Send(int cfd, std::string respond)
      {
          // 向客户端发回数据
          ssize_t n = write(cfd, respond.c_str(), respond.size());
          if (n < 0)
          {
              // write fail
              std::cerr << "write to client fail: " << strerror(errno) << std::endl;
              exit(WRITE_ERR);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

    6.2 TcpClient

    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "err.hpp"
    
    class TcpClient
    {
        static const size_t BUF_SIZE = 1024;
    
    public:
        TcpClient(uint16_t svr_port, std::string svr_ip)
            : _svr_port(svr_port), _svr_ip(svr_ip)
        {
            bzero(&_svr, sizeof(_svr));
        }
    
        void Initial()
        {
            // 1.创建套接字
            if ((_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
            {
                std::cerr << "socket create fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
    
            // 由OS自动绑定ip和port
    
            // 2.指定客户端地址族
            _svr.sin_family = AF_INET;
            _svr.sin_port = htons(_svr_port);
            _svr.sin_addr.s_addr = inet_addr(_svr_ip.c_str());
        }
    
        void Start()
        {
            // 1. 向服务器发送connect请求
            int times = 5;
            while (times > 0 && connect(_sockfd, (struct sockaddr *)&_svr, sizeof(_svr)) < 0)
            {
                std::cout << "正在尝试重新连接服务器..." << times-- << std::endl;
                sleep(1);
            }
            if (times == 0)
            {
                std::cout << "连接失败!" << std::endl;
                exit(CONNECT_ERR);
            }
            // connect success
            std::cout << "连接成功!" << std::endl;
    
            // 2. 开始工作
            while (true)
            {
                // 2.1 向服务端发送消息
                std::string message;
                std::cout << "ENTER:> ";
                std::getline(std::cin, message);
    
                if (message == "quit")
                {
                    close(_sockfd);
                    return;
                }
    
                ssize_t n = write(_sockfd, message.c_str(), message.size());
                if (n < 0)
                {
                    // write fail
                    std::cerr << "write to client fail: " << strerror(errno) << std::endl;
                    exit(WRITE_ERR);
                }
    
                // 2.2 接收服务端返回的消息
                char buf[BUF_SIZE];
                memset(buf, 0, sizeof(buf));
                n = read(_sockfd, buf, sizeof(buf) - 1);
                if (n < 0)
                {
                    // read fail
                    std::cerr << "read from server fail: " << strerror(errno) << std::endl;
                    exit(READ_ERR);
                }
                else if (n == 0)
                {
                    // 同样的,服务器退出,客户端read返回值为0
                    std::cout << "server quit" << std::endl;
                    close(_sockfd);
                    break;
                }
                else
                {
                    buf[n] = '\0';
                    std::cout << "server sent to: " << buf << std::endl;
                }
            }
        }
    
    private:
        int _sockfd;             // 客户端套接字文件描述符
        uint16_t _svr_port;      // 服务器端口号
        std::string _svr_ip;     // 服务器IP
        struct sockaddr_in _svr; // 服务器地址信息
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111

    6.3 TcpServer的优化

    引入线程池

    TcpServer的多进程版,频繁创建子进程,开销大,效率低。多线程版相较于多进程版提高了效率,减少创建子进程的效率损耗和资源浪费,但频繁创建线程依然开销不低。因此可以引入线程池,减少频繁创建和销毁线程的开销,提高并发效率。

    // 引入线程池的版本(仅展示与其它版本不同之处)
    
    class Task
    {
    using func_t = function<void(int cfd, const std::string &client_info)>;
    public:
        Task() = default;
    
        Task(int cfd, const std::string &client_info, func_t cb)
            : _cfd(cfd), _client_info(client_info), _cb(cb)
        {
        }
        
    	// 线程池中调用Task::operator()执行任务
        void operator()()
        {
            _cb(_cfd, _client_info);
        }
    
    private:
        int _cfd;                 // 客户端套接字文件描述符
        std::string _client_info; // 客户端信息
        func_t _cb;               // 回调函数
    };
    
    void Start()
    {
        while (true)
        {
            struct sockaddr_in client;
            bzero(&client, sizeof(client));
            socklen_t len = sizeof(client);
    
            // 1.接受监听的客户端
            int cfd = accept(_sockfd, (struct sockaddr *)&client, &len);
            if (cfd < 0)
            {
                // no client connect, continue try to accept
                LogMessage(WARNING, "accept fail: %s\n", strerror(errno));
                sleep(1);
                continue;
            }
            std::string client_info = std::string(inet_ntoa(client.sin_addr)) + "-" + std::to_string(ntohs(client.sin_port));
            
            // 2.创建一个与客户端交互的任务t,并交给线程池
            Task t(cfd, client_info,
                   std::bind(&TcpServer::ServerThreadRountine, this, std::placeholders::_1, std::placeholders::_2));
            // 回调函数需要绑定this指针,否则无法调用TcpServer::ServerThreadRountine
            threadPool<Task>::get_instance()->pushTask(t);
        }
    }
    
    void ServerThreadRountine(int cfd, const std::string &client_info)
    {
        bool quit = false;   // 判断客户端是否已退出的标志
        std::string respond; // 输出型参数
        while (true)
        {
            Recv(cfd, client_info, respond, quit); // Recv内部检查客户端是否已退出
            if (quit)
            {
                break;
            }
            Send(cfd, respond);
        }
    }
    
    void Recv(int cfd, const std::string &ci, std::string &respond, bool &quit)
    {
        char buf[BUF_SIZE];
        memset(buf, 0, sizeof(buf));
        
        ssize_t n = read(cfd, buf, sizeof(buf) - 1);
        if (n < 0)
        {
            // read fail
            quit = true;
            return;
        }
        else if (n == 0)
        {
            close(cfd);
            quit = true;
            return;
        }
        else
        {
            buf[n] = '\0';
            respond = _service(buf);
        }
    }
    
    void Send(int cfd, const std::string &respond)
    {
        ssize_t n = write(cfd, respond.c_str(), respond.size());
        if (n < 0)
        {
            // write fail
            std::cerr << "write to client fail: " << strerror(errno) << std::endl;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101

    日志系统

    服务器需要有日志系统,方便开发者对于服务器的维护工作。

    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    static const char *filename = "server.log";
    
    // 日志等级
    enum loglevel_t
    {
        TRACE,
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };
    static std::map<loglevel_t, std::string> ltos = {{TRACE, "TRACE"}, {DEBUG, "DEBUG"}, {INFO, "INFO"}, {WARNING, "WARNING"}, {ERROR, "ERROR"}, {FATAL, "FATAL"}};
    
    // 日志格式:log = title(log level, time, pid) + body
    void LogMessage(loglevel_t lv, const char *format, ...)
    {
        // 1. title
        time_t t = time(nullptr);
        struct tm *tp = localtime(&t);
        // 2. time = y-m-d h:m:s
        char timestr[64];
        memset(timestr, 0, sizeof(timestr));
        snprintf(timestr, sizeof(timestr), "%d-%d-%d %d:%d:%d", tp->tm_year + 1900, tp->tm_mon + 1, tp->tm_mday, tp->tm_hour, tp->tm_min, tp->tm_sec);
        std::string logtitle = ltos[lv] + " " + timestr + " " + std::to_string(getpid());
    
        // 3. body
        va_list ap;
        char logbody[128];
        memset(logbody, 0, sizeof(logbody));
        va_start(ap, format);
        vsnprintf(logbody, sizeof(logbody), format, ap);
        va_end(ap);
    	
        // 4. 输出
        
        // 输出到终端
        // combine and output
        // printf("[%s] %s", logtitle.c_str(), logbody);
    
        // 保存到文件
        FILE *fp = fopen(filename, "a");
    
        fprintf(fp, "[%s] %s", logtitle.c_str(), logbody);
    
        fclose(fp);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    💭about C语言函数的可变参数

    函数的传输列表中,...表示可变参数。拿上述日志系统中void LogMessage(loglevel_t lv, const char *format, ...)进行分析。

    C语言提供一组宏函数来对可变参数进行操作,包含头文件#include

    🔎参考文章:

    va_list:指向可变参数首地址的指针类型

    void va_start(va_list ap, last):以固定参数last(参数列表...前的最后一个参数)的地址为起点确定变参的内存起始地址,获取第一个参数的首地址赋值给ap

    type va_arg(va_list ap, type):获取下一个参数的地址(跳转字节数sizeof type

    void va_end(va_list ap): 将ap指针置空

    而有了格式控制字符串format,可以更好地使用可变参数

    int vsnprintf(char *str, size_t size, const char *format, va_list ap)
    
    • 1

    函数解释:ap指向可变参数列表的首地址,根据格式控制format,将长度为size的字符串拷贝到str中。

    服务器守护进程化

    🔎先介绍一个概念,Linux中的会话

    会话(Session)是一个用于管理和组织进程的概念。会话是一个抽象层级,用于将相关进程分组在一起,以便它们可以协同工作并共享某些属性。以下是有关Linux会话的详细解释。

    1. 一个会话可以与一个控制终端相关联。 我们平时用shell时启动的一个终端窗口,实际上就关联了某个独立的会话。
    2. 每个会话都有一个唯一的标识符,称为SID(Session ID),它是一个整数值。这个SID与会话中第一个创建的进程(也称话首进程)的PID相同。
    3. 一个会话中可以有一个或多个进程组。 进程组是一组相关进程的集合,它们通常用于完成同一项任务,一个进程组中的进程都在同一个会话中。进程组有一个唯一的标识符PGID,进程组的PGID=进程组组长的PID。
    4. 一个会话至多有一个前台进程,可以有多个后台进程。

    🔎我们在shell以用户身份登录时,Linux操作系统中执行了哪些动作。

    1. 建立一个专属的会话
    2. 在该会话中启动一个bash进程,这个bash进程就是该会话的话首进程,bash pid = 会话sid
    3. bash进程自成一个进程组,在bash下启动的进程都是bash的子进程

    在这里插入图片描述

    💭回到服务器的层面上。服务器一般都是一直在运行,不分昼夜,就如我们三更半夜也能刷b站、发微信。而我们刚刚写的服务器,启动在与某个终端相关联的会话中,一旦该会话关闭,服务器也随之退出,客户端将无法找到服务器。那么,我们需要将服务器与终端分离,创建一个专属于服务器的、不依赖于某个终端的会话,使之一直在系统中运行,这个过程称为服务器的守护进程化Daemon)。

    ⭕核心的系统调用接口:setsid

    #include 
    
    pid_t setsid(void);
    
    • 1
    • 2
    • 3

    功能:创建一个新会话,并将调用进程设为新会话的话首进程。新会话不与任何终端产生关联,

    参数:无

    返回值:成功返回新会话的SID,失败返回(pid_t)-1,错误码被设置。

    需要注意的是,调用setsid的进程不能是某个进程组的组长进程,否则创建新会话失败。

    // daemon.hpp
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void Daemon()
    {
        // 1.创建子进程,父进程退出(保证服务器非组长进程)
        if (fork() > 0)
            exit(0);
    
        // 2.子进程创建新会话
        pid_t id = setsid();
    
        // 3.修改工作路径(可选做)
    
        // 4. 处理文件描述符0/1/2,因为守护进程没有关联终端
    
        // 方法1:重定向文件描述符0/1/2到/dev/null(因为守护进程没有关联终端)
        int fd = open("/dev/null", O_RDWR);
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
        
        // 方法2:直接close
        // close(0);
        // close(1);
        // close(2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    tcp服务器与客户端完成代码已push到本人gitee,需要的小伙伴可以自取~

    「tcp服务器与客户端、日志系统、守护进程代码」


    Ending…

  • 相关阅读:
    Git学习
    最小生成树之Kruskal、Prim
    cdh6,使用oozie进行spark的jar任务调度
    学生宿舍管理系统(前端java+后端Vue)实现-含前端与后端程序
    Redis之事务
    汽车雨量传感器PCB板围坝填充用胶方案
    【前端面试题】【布局】【样式】
    黑猫带你学Makefile第3篇:Makefile基本语法
    计算机网络TCP篇之流量控制
    面试官:Redis 过期删除策略和内存淘汰策略有什么区别?
  • 原文地址:https://blog.csdn.net/C0631xjn_/article/details/133304370