欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Linux] TCP 网络套接字编程 + 守护进程TCP 网络套接字编程 + 守护进程

最编程 2024-04-03 17:02:11
...

文章目录

  • 日志类(完成TCP/UDP套接字常见连接过程中的日志打印)
  • 单进程版本的服务器客户端通信
  • 多进程版本和多线程版本
  • 守护进程化的多线程服务器


日志类(完成TCP/UDP套接字常见连接过程中的日志打印)

为了让我们的代码更规范化,所以搞出了日志等级分类,常见的日志输出等级有 Info Debug Warning Error Fatal 等,再配合上程序运行的时间,输出的内容等,公司中就是使用日志分类的方式来记录程序的输出,方便程序员找bug。 实际上在系统目录/var/log/messages文件中也记录了Linux系统自己的日志输出,可以看到我的Linux系统中之前在使用时产生了很多的error和warning,我们的代码也可以搞出来这样的输出日志信息到文件或者显示器的功能。

#pragma once
#include <iostream>
#include <string>
#include <stdio.h>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

#define SIZE 1024
#define Screen 1    // 向屏幕打印
#define oneFile 2   // 向一个文件中打印
#define classFile 3 // 分类打印
#define LogFileName "log.txt"
enum
{
    Info = 0, // 信息
    Debug,    // 调试
    Warning,
    Error,
    Fatal // 严重错误
};

class Log
{
private:
    int _printMethod;

public:
    Log()
    {
        _printMethod = Screen;
    }
    ~Log()
    {
    }

    // 设置打印方式
    void Enable(int method)
    {
        _printMethod = method;
    }

    // 将日志等级转化为string
    std::string LevelToSting(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    // 向一个文件中打印
    void PrintfOneFile(const std::string &filename, const std::string &logtxt) // log.txt
    {
        int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }

    // 分类打印
    void PrintfClassFile(int level, const std::string &logtxt) // log.txt.Info/Debug/Error等等
    {
        std::string filename = LogFileName;
        filename += '.';
        filename += LevelToSting(level);
        PrintfOneFile(filename, logtxt);
    }

    void printlog(int level, std::string logtxt)
    {
        switch (_printMethod)
        {
        case Screen:
        {
            std::cout << logtxt << std::endl;
            break;
        }
        case oneFile:
        {
            PrintfOneFile(LogFileName, logtxt);
            break;
        }
        case classFile:
        {
            PrintfClassFile(level, logtxt);
            break;
        }
        default:
            break;
        }
    }

    // 将日志信息写入到screen \ file
    void LogMessage(int level, const char *format, ...)
    {
        char LeftBuffer[SIZE];
        time_t t = time(NULL);
        struct tm *ctime = localtime(&t);
        snprintf(LeftBuffer, sizeof(LeftBuffer), "[%s]:[%d-%d-%d %d:%d:%d]", LevelToSting(level).c_str(), ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        char RightBuffer[SIZE];
        va_list list;
        va_start(list, format);                                    // 将list指向可变参数的第一个参数
        vsnprintf(RightBuffer, sizeof(RightBuffer), format, list); // 这个函数按照调用者传过来的format格式执行list的可变参数部分
        va_end(list); //将list置NUll

        char logtxt[2 * SIZE];
        snprintf(logtxt, sizeof(logtxt), "%s %s", LeftBuffer, RightBuffer);
        // 现在将Log打印到stdout
        // printf("%s", logtxt);
        printlog(level, logtxt);
    }
};
  1. 上面的localtime()是Linux中将时间戳转化本地时间的API,函数会返回一个结构struct tm *这个结构里面的成员就是年月日-时分秒,这个API的参数是本机的时间戳使用time(NULL)在这里插入图片描述
  2. snprintf是按照格式将指定内容和长度写入到指定缓冲区
  3. va_list 是 C 语言中用于处理可变参数列表的数据类型。在使用可变参数函数(如 printf、vprintf、fprintf、vfprintf 等)时,需要使用 va_list 类型的变量来访问这些参数。
    通常,你会在函数中声明一个 va_list 类型的变量,然后使用一系列宏来访问可变参数列表中的参数。在使用完之后,需要调用相应的宏来清理 va_list 变量。
    在这里插入图片描述4. vsnprintf是一个 C 标准库函数,用于格式化字符串并将结果输出到字符数组中。它类似于 snprintf,但是接受一个 va_list 类型的参数,允许处理可变参数列表。通过 vsnprintf,你可以将格式化后的字符串输出到指定的字符数组中,而不需要提前知道可变参数的数量。在这里插入图片描述

单进程版本的服务器客户端通信

TCP套接字的创建和UDP一样,先使用socket创建套接字,在结构中设置IP和port,其次就是将IP 和 端口的bind

  1. 不同点是bind之后需要将套接字设置为监听状态,因为TCP协议是面向连接的 在这里插入图片描述
    监听函数success的返回0,错误则返回-1,错误码被设置
  2. 在UDPbind完成套接字之后,就是recvfrom接受客户端发过来的数据,其次就是sendto
    将消息处理后发回客户端。但是在TCP将套接字设置为监听状态之后,需要accept接收客户端连接请求,并且返回一个新的sockfd文件描述符这个新的套接字用于与客户端进行通信,而原始的监听套接字仍然可以继续接受其他客户端的连接请求。,那么我们使用socketAPI创建套接字的时候,这个API返回的sockfd和我们使用accept返回的sockfd有什么区别呢?
    在这里插入图片描述
    使用socketAPI创建的套接字属于监听套接字,也就是说listenAPI需要使用它,它不能进行网络通信,使用accept接收的套接字这才是我们进行网络通信的套接字,如果是多线程或者多进程版本的服务器,我们就会使用监听套接字来进行另一个客户端的accept
  3. 在TCP套接字编程中使用read 和 write 进行读写数据
//TcpSever.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include "Log.hpp"
#include <arpa/inet.h> //struct sockaddr_in 结构在这个头文件里面
#include <unistd.h>
#include <signal.h>
#include <pthread.h>

const uint16_t default_port = 8080;
const std::string default_ip = "0.0.0.0";
Log lg;

class TcpSever
{
private:
    int _listen_sockfd;
    uint16_t _port;
    std::string _ip;
public:
    TcpSever(uint16_t port = default_port, std::string ip = default_ip) : _port(port), _ip(ip)
    {
    }

    ~TcpSever()
    {
    }

    void Init()
    {
        // 创建tcp套接字
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            lg.LogMessage(Fatal, "socket Error: %s", strerror(errno));
            exit(-1);
        }
        lg.LogMessage(Info, "socket success: %d", _listen_sockfd);

        // 设置端口的IP
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));

        // 绑定套接字
        if (bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg.LogMessage(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(-1);
        }

        lg.LogMessage(Info, "bind socket success, listensock_: %d", _listen_sockfd);

        // 将套接字设置为监听状态
        if (listen(_listen_sockfd, 10) < 0)
        {
            lg.LogMessage(Fatal, "listen Error: %s", strerror(errno));
            exit(-1);
        }
        lg.LogMessage(Info, "listen success");
    }

    void Service(int sockfd, uint16_t clientport, const std::string &clientip)
    {
        char buffer[4096];
        while (true)
        {
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if (n > 0)
            {
                buffer[n] = 0;
                std::cout << "client say# " << buffer << std::endl;
                std::string echo_string = "server echo# ";
                echo_string += buffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            // 如果客户端提前退出,服务端会读取到0
            else if (n == 0)
            {
                lg.LogMessage(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else
            {
                lg.LogMessage(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }
    void Run()
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        while (true)
        {
            // 接收客户端连接  返回通信套接字!!!
            struct sockaddr_in client;
            socklen_t len = sizeof(client);

            int sockfd = accept(_listen_sockfd, (struct sockaddr *)&client, &len);

            if (sockfd < 0)
            {
                lg.LogMessage(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue;
            }

            // 接收数据
            // 拿到客户端的 IP地址 和 端口
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            // version 1 单进程版本  只能有一个用户进程进行读写
            Service(sockfd, clientport, clientip);
            close(sockfd);
        }
    }
};

我们在Main.cc中创建一个服务器对象,然后进行初始化 和 运行服务器端
使用命令行参数告诉服务器端的port

//Main.cc
#include "TcpSever.hpp"
#include<iostream>
#include<memory>

void Useage(const std::string& argv)
{
    std::cout << argv << " -> Should Enter port 1024+" << std::endl;
}

// ./tcpsever 8080
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Useage(argv[0]);
        return -1;
    }
    uint port = atoi(argv[1]);
    std::unique_ptr<TcpSever> tcp_sever(new TcpSever(port));
    tcp_sever->Init();
    tcp_sever->Run();
    return 0;
}

接下来就是编写客户端代码了:在TCP套接字编程中,connect 函数用于向服务器发起连接请求。当客户端创建一个套接字后,需要调用 connect 函数来连接到服务器的指定地址和端口。

这里是引用
同样客户端也是需要bind的,但是不需要用户显式bind:在TCP套接字编程中,客户端不需要显式调用 bind 函数来绑定地址的原因主要有两点:

  1. 动态选择本地端口: 在客户端调用 connect 函数时,系统会自动为客户端选择一个合适的本地端口,并将其绑定到客户端的套接字上。这样可以确保客户端套接字与服务器端建立连接时不会与其他套接字冲突。
  2. 客户端套接字的行为: 客户端通常不需要在网络上提供服务,而是主动连接到服务器端,因此不需要像服务器端那样在特定地址上监听连接请求。客户端的套接字行为是发起连接,而不是等待连接,因此不需要显式绑定地址。
//TcpClient.cc
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include "Log.hpp"
#include <arpa/inet.h> //struct sockaddr_in 结构在这个头文件里面
Log lg;
using namespace std;

void Useage(const std::string &argv)
{
    std::cout << argv << " -> Should Enter port 1024+" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Useage(argv[0]);
        return -1;
    }
    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        lg.LogMessage(Fatal, "socket Error: %s", strerror(errno));
        exit(-1);
    }
    lg.LogMessage(Info, "socket success: %d", sockfd);
    // 建立连接
    struct sockaddr_in sever;
    socklen_t len = sizeof(sever);
    memset(&sever, 0, sizeof(sever));
    uint port = atoi(argv[2]);
    std::string ip = argv[1];
    sever.sin_family = AF_INET;
    sever.sin_port = htons(port);
    inet_aton(ip.c_str(), &(sever.sin_addr));
    if (connect(sockfd, (sockaddr *)&sever, len) < 0)
    {
        lg.LogMessage(Fatal, "connect Error: %s", strerror(errno));
        exit(-1);
    }
    std::string message;
    while(true)
    {
        cout << "client please Enter@ " << endl;
        getline(cin, message);
        write(sockfd, message.c_str(), message.size());
        char inbuffer[4096];
        int n = read(sockfd, inbuffer, sizeof(inbuffer));
        if(n > 0)
        {
            inbuffer[n] = 0;
            cout <<  inbuffer << endl;
        }
    }
    close(sockfd);
    return 0;
}

客户端开始死循环运行时,第一件事就是向服务器发起连接请求,这个连接的工作也不难做,因为客户端知道目的ip和目的port,所以直接填充server结构体中的各个字段,然后直接发起连接请求即可。连接成功后就可以开始通信,同样的客户端也是使用read和write等接口来进行数据包的发送和接收。如果服务器读到0,则说明客户端已经不写了,那么如果客户端继续向服务器发消息,就相当于写端向已经关闭的读端继续写入,此时OS会终止掉客户端进程。
由于UDP和TCP分别是无连接和面向连接的,所以两者有些许不同,TCP的服务器

推荐阅读