Linux学习记录——삼십일 socket编程---TCP套接字

news2024/11/28 23:37:21

文章目录

  • TCP套接字简单通信
    • 1、服务端
      • 1、基本框架
      • 2、获取连接
    • 2、客户端
    • 3、多进程
    • 4、多线程
    • 5、线程池
    • 6、简单的日志系统
    • 7、守护进程
    • 8、其它


TCP套接字简单通信

本篇gitee

学习完udp套接字通信后,再来看TCP套接字。

四个文件tcp_server.hpp, tcp_server.cc,tcp_client.cc,makefile。

makefile

.PHONY: all
all:tcp_client tcp_server

tcp_client:tcp_client.cc
	g++ -o $@ $^ -std=c++11 -lpthread

tcp_server:tcp_server.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY: clean
clean:
	rm -f tcp_client tcp_server

1、服务端

1、基本框架

和udp的有些一样。我们有些序列需要主机转网络,但发送的消息不需要,是因为操作系统会自动转大小端,处理交互用的消息。

tcp_server.hpp

#pragma once

#include <iostream>
#include <memory>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"

namespace ns_server
{
    static const uint16_t defaultport = 8081;
    class TcpServer
    {
    public:
        TcpServer(uint16_t port = defaultport): port_(port)
        {}
        
        void initDerver()
        {
            //1. 创建socket
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if(sock < 0)
            {
                std::cerr << "create socket fail" << std::endl;
                exit(SOCKET_ERR);
            }
            //2. 绑定
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(port_);
            local.sin_addr.s_addr = htonl(INADDR_ANY);//也可以直接写INADDR_ANY
            if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
            {
                std::cerr << "bind socket fail" << std::endl;
                exit(BIND_ERR);
            }
        }

        void start()
        {}

        ~TcpServer()
        {}
    private:
        uint16_t port_;//只要是服务器,就要有端口号
        int sock;
    };
};

err.hpp

#pragma once

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};

tcp_server.cc

#include "tcp_server.hpp"
using namespace std;
using namespace ns_server;

int main()
{
    unique_ptr<TcpServer> tsvr(new TcpServer());
    tsvr->initServer();
    tsvr->start();
    return 0;
}

tcp_client.cc

#include <iostream>

int main()
{
    
    return 0;
}

接下来开始就是tcp的部分了。tcp是面向连接的,它不像udp一样可以直接接发消息,它得需要先连接再通信。

在这里插入图片描述
在这里插入图片描述

backlog先不用管,设置成一个小的数字就行。在类前设置一下

    static const uint16_t defaultport = 8081;
    static const int backlog = 32;
            //3. 监听(先让客户端连接过来,才能通信,而服务端就得一直等着连接)
            if(listen(sock, backlog) < 0)
            {
                std::cerr << "listen socket fail" << std::endl;
                exit(LISTEN_ERR);//err.hpp就得加一个LISTEN.ERR
            }

开始写start函数。

        void start()
        {
            quit_ = false;
            while(!quit_)
            {
                //4. 客户端要连接,服务端就要先获取连接
                sleep(1);
            }
        }

写到这里就可以启动试试了。./tcp_server,用netstat命令来查看是否启动成功,后面的命令选项,-nltp,n把能显示成数字的显示成数字,l就是listen,t是指tcp,p是进程,打出来的内容中就有一个处于监听状态,IP地址是0.0.0.0的一个进程,显示出了它的PID,以及还有程序名字tcp_server。

2、获取连接

服务端必须处于监听状态,客户端才能来连接它。连接用的函数是accept。

在这里插入图片描述

addr和addrlen是客户端的数据。sockfd是一个套接字。

在这里插入图片描述

它的返回值实际也是一个文件描述符。accept接口,sockfd是用来监听的套接字,也就是用来连接客户端的,而它的返回值则是用来处理数据的。前面创建的sock就是这里的sockfd,为了方便,我们把它改名为listensock_。

                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                //4. 客户端要连接,服务端就要先获取连接
                int sock = accept(listensock_, (struct sockaddr*)&client, &len);

连接有可能失败,比如客户端不连接这个服务端,但这对于服务端并没有什么,它继续连接其它客户端就好,所以即使失败也继续。

        void start()
        {
            quit_ = false;
            while(!quit_)
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                //4. 客户端要连接,服务端就要先获取连接
                int sock = accept(listensock_, (struct sockaddr*)&client, &len);
                if(sock < 0)
                {
                    std::cerr << "accept error" << std::endl;
                    continue;
                }
                std::string clientip = inet_ntoa(client.sin_addr);
                uint16_t clientport = ntohs(client.sin_port);
                //5. 获取新连接成功,开始业务处理
                std::cout << "获取新连接成功: " << sock << " from "<< listensock_ << ", " << clientip << "-" << clientport << std::endl;
                service(sock);
            }
        }

写用来处理数据的函数service。先写一个读写操作。我们用socket创建的tcp套接字是流式套接字,访问时也是用字节流来访问的,想要读取数据,就用read系统调用来读取。read可以读文件,也可以读网络,就对应了Linux一切皆文件。

        void service(int sock)
        {
            char buffer[1024];
            while(true)
            {
                ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
                if(s > 0) 
                {
                    buffer[s] = 0;
                    std::cout << buffer << std::endl;
                }
                else if(s == 0)//和管道一样,把写端关闭,如果读到文件结尾就会返回0,而网络这里读到0,说明对方将连接关闭了
                {
                    close(sock);
                    std::cout << "client quit, me too" << std::endl;
                    break;
                }
                else
                {
                    close(sock);
                    std::cerr << "read error: " << strerror(errno) << std::endl;
                    break;
                }
            }
        }

这里只写了打印语句。接下来用回调来完成对数据的处理。
引入头文件functional。

加上成员变量func_t func_。

命名空间里的类的前面加上using func_t = std::function<std::string(const std::string&)>。

在读取成功后,buffer[s] = 0下一行加上std::string res = func_(buffer)。

初始化里也得初始化TcpServer(func_t func, uint16_t port = defaultport): func_(func), port_(port), quit_(true)。

然后在tcp_server.cc中写上回调函数。

#include "tcp_server.hpp"
using namespace std;
using namespace ns_server;

static void usage(string proc)
{
    cout << "Usage:\n\t" << proc << " port\n" << endl;
}

string echo(const string& message)
{
    return message;//简单的返回
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<TcpServer> tsvr(new TcpServer(echo, port));
    tsvr->initServer();
    tsvr->start();
    return 0;
}

hpp文件中连接成功后调用回调函数,用的是res来接收,那么下面就不用打印buffer了,打印res就好了。然后再把res写给连接过来的客户端。

                if(s > 0) 
                {
                    buffer[s] = 0;
                    std::string res = func_(buffer);
                    std::cout << res << std::endl;
                    write(sock, res.c_str(), res.size());
                }

2、客户端

客户端全部都写在一个tcp_client.cc文件中。

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
using namespace std;

static void usage(string proc)
{
    cout << "Usage:\n\t" << proc << " port\n" << endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t serverport = atoi(argv[2]);
    string serverip = argv[1];
    
    //1. 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        cerr << "socket error : " << strerror(errno) << endl;
        exit(SOCKET_ERR);
    }
    return 0;
}

客户端需要绑定吗?需要绑定,但不需要自己绑定,因为客户端来源于很多处,所以靠系统来绑定,防止端口冲突。客户端需要监听吗?服务端是监听的,客户端则不需要,客户端是连接服务端的,服务端是等待被连接的,所以客户端不需要监听listen,也不需要获取连接accept。

客户端需要做的是连接。用connect接口。

在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
using namespace std;

static void usage(string proc)
{
    cout << "Usage:\n\t" << proc << " port\n" << endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t serverport = atoi(argv[2]);
    string serverip = argv[1];
    
    //1. 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        cerr << "socket error : " << strerror(errno) << endl;
        exit(SOCKET_ERR);
    }
    //2. 发起连接
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_aton(serverip.c_str(), &server.sin_addr);
    int cnt = 5;
    while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0)
    {
        cout << "正在尝试重连,重连次数还有: " << cnt-- << endl;
        if(cnt <= 0) break;
    }
    if(cnt <= 0)
    {
        cerr << "连接失败..." << endl;
        exit(CONNECT_ERR);
    }
    //3. 连接成功
    char buffer[1024];
    while(true)
    {
        string line;
        cout << "Enter>> ";
        getline(cin, line);
        write(sock, line.c_str(), line.size());
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if(s > 0)
        {
            buffer[s] = 0;
            cout << "server echo >>>" << buffer << endl;
        }
        else if(s == 0)
        {
            cerr << "server quit" << endl;
            break;
        }
        else
        {
            cerr << "read error: " << strerror(errno) << endl;
            break;
        }
    }
    close(sock);
    return 0;
}

3、多进程

为了让效果更明显,我们对代码做一些变更。

在这里插入图片描述

服务端的这部分代码,对service改一下。除了传客户端的套接字,再传进去ip和port。

        void service(int sock, const std::string &clientip, const uint16_t &clientport)
        {
            std::string who = clientip + "-" + std::to_string(clientport);
            char buffer[1024];
            while(true)
            {
                ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
                if(s > 0) 
                {
                    buffer[s] = 0;
                    std::string res = func_(buffer);
                    std::cout << who << ">>>" << res << std::endl;
                    write(sock, res.c_str(), res.size());
                }
                else if(s == 0)//和管道一样,把写端关闭,如果读到文件结尾就会返回0,而网络这里读到0,说明对方将连接关闭了
                {
                    close(sock);
                    std::cout << who << "quit, me too" << endl;
                    break;
                }
                else
                {
                    close(sock);
                    std::cerr << "read error: " << strerror(errno) << std::endl;
                    break;
                }
            }
        }

这样就能看出来连接的是哪个客户端,哪个客户端的消息,哪个客户端退出了。

当我们把现在所有的代码编译启动后,会发现有问题。同时开两个客户端,连接好后,只有一个在服务端那里出现了连接的消息,另一个没有,并且另一个发消息,服务端也没有打印出来,只有连接上的那个能打印消息;当把两个客户端都退出时,之前连接上的那个正常退出,而紧接着,没连接上的那个这时却打印了连接成功的消息,并且文件描述符和之前连接的那个一样,也就是说,它是在上一个文件取消连接后才去连接的,所以文件描述符是同一个数字,并且之前没有打印出来的消息这时也都打印出来了。

这就说明,目前的服务端无法处理多个客户端。我们得让服务端能多进程运行。

                //5. 获取新连接成功,开始业务处理
                std::cout << "获取新连接成功: " << sock << " from "<< listensock_ << ", " << clientip << "-" << clientport << std::endl;
                //service(sock, clientip, clientport); 
                pid_t id = fork();
                if(id < 0)
                {
                    close(sock);
                    continue;
                }
                //子进程会继承父进程的fd,但父子不是用同一张文件描述表的,子进程会拷贝父进程的
                //子进程一定有sock和listensock
                //分工明确一下,父进程负责获取连接,子进程处理数据,所以两个进程都要close不需要的部分
                else if(id == 0)
                {
                    close(listensock_);
                    service(sock, clientip, clientport); 
                    exit(0);
                }
                close(sock);//如果父进程不关闭,一直accept,一直往下开文件描述符,文件描述符存储在数组中,总有满的时候,就会造成文件描述符泄漏
                pid_t ret = waitpid(id, nullptr, 0);
                if(ret == id) std::cout << "wait child " << id << " succeed" << std::endl;

这里面有个明显的问题,等待默认是阻塞的,所以父进程还是在串行运行的。我们可以用非阻塞式运行,0换成WHOHANG,但是假如最后一个客户端已经连接上了,子进程在处理,父进程回去继续accept,子进程退出后,父进程还卡在那里,没办法退出了,所以不行;还可以用signal函数,子进程退出时会发出SIG_CHILD信号,那么对它捕捉并用handler处理就行,但不如直接忽略掉这个信号更方便,所以这里采用忽略。

除了忽略,还有一个办法。

                else if(id == 0)
                {
                    close(listensock_);
                    if(fork() > 0) exit(0);
                    //到这里时,子进程已经退了,孙子进程在运行
                    //子进程退,父进程就wait结束,也退了
                    //这时候孙子进程是孤儿进程,由系统管理,所以不需要担心它的回收
                    service(sock, clientip, clientport); 
                    exit(0);
                }

但fork太多,对系统要求也高,所以直接忽略就好。现在再次运行,会发现所有的客户端的文件描述符都是一个数字,这是因为有了多进程后,一个客户端连接上,子进程就会把这个客户端拿过来处理,而父进程那边给关闭了这个文件描述符,再去获取下一个连接,所以父进程给客户端分配的一直都是一个文件描述符。

4、多线程

多进程还是不够高效,把处理数据的部分换成多线程。

    class TcpServer;
    class ThreadData
    {
    public:
        ThreadData(int fd, const std::string &ip, const uint16_t &port, TcpServer* ts)
        : sock(fd), clientip(ip), clientport(port), current(ts)
        {}
    public:
        int sock;
        std::string clientip;
        uint16_t clientport;
        TcpServer *current;
    };
        void start()
        {
            //signal(SIGCHLD, SIG_IGN);
            quit_ = false;
            while(!quit_)
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                //4. 客户端要连接,服务端就要先获取连接
                int sock = accept(listensock_, (struct sockaddr*)&client, &len);
                if(sock < 0)
                {
                    std::cerr << "accept error" << std::endl; 
                    continue;
                }
                std::string clientip = inet_ntoa(client.sin_addr);
                uint16_t clientport = ntohs(client.sin_port);
                //5. 获取新连接成功,开始业务处理
                std::cout << "获取新连接成功: " << sock << " from "<< listensock_ << ", " << clientip << "-" << clientport << std::endl;
                pthread_t tid;
                ThreadData* td = new ThreadData(sock, clientip, clientport, this);
                pthread_create(&tid, nullptr, threadRoutine, td);
            }
        }

        static void* threadRoutine(void* args)
        {
            ThreadData* td = static_cast<ThreadData*>(args);
            td->current->service(td->sock, td->clientip, td->clientport);
            delete td;//service完后退出
        }

线程要不要关闭不要的套接字?不需要,因为多个线程共享文件描述符,所以不能关掉,关掉后服务端就不能正常运行了。这里要不要回收线程?肯定要,但如果create完后join后,join会阻塞,又会出现多进程里的问题。应当在threadRoutine函数里先detach,分离出当前线程,那么主线程就不需要管理这个分离出去的线程了,它运行完自己结束,而服务端可以继续做自己的工作。

5、线程池

现在的程序是客户端连接过来了,服务端才建立线程,为了更高效,我们可以用线程池来优化。

之前已经写过线程池了。ThreadPool_V4.hpp

#pragma once

#include <iostream>
#include <memory>//智能指针的头文件
#include <string>
#include <vector>
#include <queue>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"
#include "task.hpp"
#include "LockGuard.hpp"

const static int N = 5;

template <class T>
class ThreadPool
{
private:
    ThreadPool(int num = N) : _num(num)//也可以不初始化_threads,因为我们用的是库,直接push就行
    {
        pthread_mutex_init(&_lock, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    ThreadPool(const ThreadPool<T> &tp) = delete;//去掉默认生成的拷贝构造
    void operator=(const ThreadPool<T> &tp) = delete;//去掉默认生成的拷贝赋值

public:
    static ThreadPool<T> *getinstance()//这个要设置成静态的,因为如果cc文件中要调用这个静态对象的函数的话,函数也应当是静态的才行
    {
        if(nullptr == instance) //提高效率,减少加锁的次数
        {
            LockGuard lockguard(&instance_lock);//用锁类
            if (nullptr == instance)
            {
                logMessage(Debug, "线程池单例形成");
                instance = new ThreadPool<T>();
                instance->init();
                instance->start();
            }
        }
        return instance;
    }

    pthread_mutex_t *getlock() {return &_lock; }
    void threadWait() {pthread_cond_wait(&_cond, &_lock); }
    void threadWakeup() {pthread_cond_signal(&_cond); }
    bool isEmpty() {return _tasks.empty(); }

    T popTask()
    {
        T t = _tasks.front();
        _tasks.pop();
        return t;
    }

    static void threadRoutine(void *args)//加static?类内的线程函数,要记得加static,放在静态区,因为在类内会有this指针,导致函数参数类型不对
    {
        // pthread_detach(pthread_self());
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        while (true)
        {
            T t;
            {//括号里就是临界区
                //1、检测有没有任务,有就处理,无就等待,这里一定要加锁
                LockGuard lockguard(tp->getlock());
                //因为是静态函数,不能直接访问类内私有成员,所以init函数那里要传this指针就可以了
                while(tp->isEmpty())
                {
                    tp->threadWait();  
                }
                t = tp->popTask();//从公共区域拿到私有区域
            }
            //测试
            t();
        }
    }

    void init()
    {
        //插入若干个线程
        for (int i = 0; i < _num; i++)
        {
            _threads.push_back(Thread(i, threadRoutine, this));
        }
    }

    void start()
    {
        for (auto &t : _threads)
        {
            t.run();
        }
    }

    void check()
    {
        for (auto& t : _threads)
        {
            std::cout << t.threadname() << " running..." << std::endl;
        }
    }

    void pushTask(const T &t)
    {
        LockGuard lockgrard(&_lock);//V2是调用系统接口,V3就是调用我们自己写的类,初始化,函数结束时自动析构,也就是释放锁
        _tasks.push(t);
        threadWakeup();
    }

    ~ThreadPool()
    {
        for (auto &t : _threads)
        {
            t.join();
        }
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_cond);
    }
private:
    std::vector<Thread> _threads;//pthread_t是用库中的
    int _num;

    std::queue<T> _tasks; // 使用STL的自动扩容

    pthread_mutex_t _lock;
    pthread_cond_t _cond;//当没有任务,所有线程应当休息,挂起,所以用条件变量来控制

    static ThreadPool<T> *instance;//对象
    static pthread_mutex_t instance_lock;//静态锁
};

template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;

template <class T>
pthread_mutex_t ThreadPool<T>::instance_lock = PTHREAD_MUTEX_INITIALIZER;

在tcp_server.hpp里引入这个头文件。这个线程池是默认有5个线程可供使用的。task.hpp要改,不同的场景有不同的任务。

task.hpp先写一个框架

#pragma once
#include <iostream>
#include <string>
#include <unistd.h>

class Task
{
public:
    Task()
    {}

    Task(int sock): _sock(sock)
    {}

    void operator()()//仿函数,在tcp_server.hpp中用t()来调用
    {

    }

    ~Task()
    {}
private:
    int _sock;
};

接着看tcp_server.hpp文件。

                //5. 获取新连接成功,开始业务处理
                std::cout << "获取新连接成功: " << sock << " from "<< listensock_ << ", " << clientip << "-" << clientport << std::endl;
                Task t(sock, clientip, clientport, std::bind(&TcpServer::service, this, std::placeholder::_1, std::placeholder::_2, std::placeholder::_3));//绑定类内用的方法,三个是占位符,前面三个是这个方法的参数
                ThreadPool<Task>::getinstance()->pushTask(t);

task.hpp中使用回调函数。

#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>

using cb_t = std::function<void(int, const std::string&, const uint16_t&)>;

class Task
{
public:
    Task()
    {}

    Task(int sock, const std::string& ip, const std::uint16_t& port, cb_t cb)
     : _sock(sock), _ip(ip), _port(port), _cb(cb)
    {}

    void operator()()
    {
        _cb(_sock, _ip, _port);
    }

    ~Task()
    {}
private:
    int _sock;
    std::string _ip;
    std::uint16_t _port;
    cb_t _cb;
};

写好后整体运行起来,会有以下的现象。程序貌似不是很快;有的客户端会连接不上,只能重连;文件描述符依次增大,如果有客户端退出,紧接着连接上的客户端就会用上退出的客户端的文件描述符;连不上的客户端等其它客户端退出一些,它们才能连上。因为service函数是一个死循环,一个线程进去执行任务后就出不来了,没有执行任务才会break,线程池也只有5个线程,这样的设计就注定如果5个线程都用上了,其它来连接的就得等着,只能处理短任务。我们也可以使用多线程的办法,在service函数中要调用函数去处理数据时在动用线程池,这样就是多线程内带着线程池。

比较简单的做法就是service变成一次的,而不是死循环,去掉while。线程池的个数也加多一些。

        void service(int sock, const std::string &clientip, const uint16_t &clientport)
        {
            std::string who = clientip + "-" + std::to_string(clientport);
            char buffer[1024];
            ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
            if(s > 0) 
            {
                buffer[s] = 0;
                std::string res = func_(buffer);
                std::cout << who << ">>>" << res << std::endl;
                write(sock, res.c_str(), res.size());
            }
            else if(s == 0)//和管道一样,把写端关闭,如果读到文件结尾就会返回0,而网络这里读到0,说明对方将连接关闭了
            {
                close(sock);
                std::cout << who << "quit, me too" << endl;
            }
            else
            {
                close(sock);
                std::cerr << "read error: " << strerror(errno) << std::endl;
            }
            close(sock);
        }

这样的改动也只是处理简单的操作,IO数据的时候就要有更详细的做法。

6、简单的日志系统

上面的代码一直是用cout来打印消息,但实际上就写日志来记录这些信息。这里要写的日志不是完整的,而是简易版本,用来完成简单的TCP通信。创建一个log.hpp。日志中要使用v开头的几个函数。

在这里插入图片描述

日志是有等级的,编译器会给警告,会给报错,就是在打印日志消息。日志要处理多种类的信息。

#pragma once

#include <cstdio>
#include <cstring>
#include <cstdarg>

#define DEBUG 0//调试信息
#define INFO 1//正常信息
#define WARNING 2//告警,不影响运行
#define ERROR 3//一般错误
#define FATAL 4//严重错误

void logMessage(int level, char* format, ...)//...就是可变参数,format是输出格式
{
    
}

要用可变参数,需要用到几个宏

void logMessage(int level, char* format, ...)//...就是可变参数,format是输出格式
{
    //format是一个字符串,里面有格式,比如%d, %c,通过这个就可以用arg来提取参数
    va_list p;//char* 
    //下面是三个宏函数
    int a = va_arg(p, int);//根据类型提取参数
    va_start(p, format);//让p指向可变参数部分的起始地址
    va_end(p);//把p置为空, p = NULL
}

下面写出整个功能实现。

#pragma once

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdarg>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>

enum
{
    Debug = 0,//调试信息
    Info,//正常信息
    Warning,//告警,不影响运行
    Error,//一般错误
    Fatal,//严重错误
    Unknown
};

static std::string toLevelString(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";
    }
}

static std::string getTime()
{
    time_t curr = time(nullptr);//拿到当前时间
    struct tm *tmp = localtime(&curr);//这个结构体有对于时间单位的int变量
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d", tmp->tm_year + 1900, tmp->tm_mon + 1, tmp->tm_mday, \
        tmp->tm_hour, tmp->tm_min, tmp->tm_sec);//这些tm_的变量就是结构体中自带的,tm_year是从1900年开始算的,所以+1900;月份从0开始,要+1
    return buffer;
}

//日志格式: 日志等级 时间 pid 消息体
//logMessage(DEBUG, "hello: %d, %s", 12, s.c_str()); 12以%d形式打印, s.c_str()%s形式打印
void logMessage(int level, const char* format, ...)//...就是可变参数,format是输出格式
{
    //写入到两个缓冲区中
    char logLeft[1024];//用来显示日志等级,时间,pid
    std::string level_string = toLevelString(level);
    std::string curr_time = getTime();
    snprintf(logLeft, sizeof(logLeft), "[%s] [%s] [%d] ", level_string.c_str(), curr_time.c_str(), getpid());
    char logRight[1024];//用来显示消息体
    va_list p;
    va_start(p, format);
    //直接用这个接口来对format进行操作,提取信息
    vsnprintf(logRight, sizeof(logRight), format, p);
    va_end(p);
    //打印
    printf("%s%s\n", logLeft, logRight);
    //format是一个字符串,里面有格式,比如%d, %c,通过这个就可以用arg来提取参数
    //va_list p;//char*
    //下面是三个宏函数
    //int a = va_arg(p, int);//根据类型提取参数
    //va_start(p, format);//让p指向可变参数部分的起始地址
    //va_end(p);//把p置为空, p = NULL
}

tcp_server.hpp引入这个头文件,以及线程池头文件,都用日志来打印消息,这个在最后的代码链接中会看到。

先放上几句

//5. 获取新连接成功,开始业务处理
logMessage(Info, "获取新连接成功: %d from %d, who: %s - %d", sock, listensock_, clientip.c_str(), clientport);

        void service(int sock, const std::string &clientip, const uint16_t &clientport)
        {
            std::string who = clientip + "-" + std::to_string(clientport);
            char buffer[1024];
            ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
            if(s > 0) 
            {
                buffer[s] = 0;
                std::string res = func_(buffer);
                logMessage(Debug, "%s# %s", who.c_str(), res.c_str());
                write(sock, res.c_str(), res.size());
            }
            else if(s == 0)//和管道一样,把写端关闭,如果读到文件结尾就会返回0,而网络这里读到0,说明对方将连接关闭了
            {
                close(sock);
                logMessage(Info, "%s quit,me too", who.c_str());
            }
            else
            {
                close(sock);
                logMessage(Error, "read error, %d:%s", errno, strerror(errno));
            }
            close(sock);
        }

7、守护进程

如果关闭服务端,整个程序就不能继续了,但服务端应当一直存在,无论什么时候访问都行,所以我们要写守护进程。创建daemon.hpp。

通常./运行起来程序后都是前台运行,还可以在命令后加上空格和&做到后台运行,但也不能解决问题。

系统有sleep进程,我们可以sleep 10000就可以打开这个可执行文件, 然后用ps ajx | head -1 && axj | grep sleep来查看。进程有进程组,组有组号PGID。SID是会话ID,TTY是终端,有问号的就是对应的进程和终端无关,不是问号的显示的就是终端文件,这个进程打开了这个终端,并向这个终端文件放入内容。用户使用命令ls,pwd这样的时候,就是进程运行时在用户这里打开了终端文件,向这个文件输入内容。

打开的几个程序,如果以管道连接起来,那么PGID,会话id(SID)和终端文件都是一样的,都是第一个进程的,第一个进程也是组长,不过如果我们分为几个前台工作,几个后台工作,假如都是sleep进程,那么后台和前台不同的是PGID,但都是一个会话,打开一个终端文件。sleep的会话id其实就是bash。

会话包含多个线程组,一个线程组包含多个线程;会话关联一个终端文件;进程之间有组关系,组长都是多个进程中的第一个。

当用户登录云服务器时,登录成功会分配一个命令行提示符,也就是用户输入命令时前面的[…@…]这部分,这本质也是一个进程,也属于一个进程组,组内只有它自己,也属于一个会话,这个会话是由bash创建的,这个会话以它来起名,之后所有的用户建立的进程都属于这个会话,只是进程组不同。进程组在会话中,一个会话里,操作系统就给用户创建多个进程组。每次登录都会创建一个新的会话。

为什么要有进程组?jobs命令可以查看当前会话中所有的后台程序,每个后台程序最前面都有1个数字,从1开始,只要增加一个程序就数字就加1,每次创建的一个程序,自成一个进程组,所以PGID不同。前面的数字编号,叫做任务编号,用命令fg 任务编号就可以把这个程序放到前台(后台的任务编号不变),用Ctrl + Z就会让这个程序停止,就会自动回到后台,用bg 任务编号会让这个程序再次运行起来。所以进程组创建是为了完成任务的,一个任务可以由多个进程完成,也可以由一个进程完成。所以用户用命令启动的一个进程,其实就是在启动一个任务。

进程组有前台和后台任务,如果把后台任务提到前台,老的前台任务就无法运行,前台任务只能有一个在运行,比如提到前台后,输入命令就不起作用了,所以用户在用命令行启动一个进程时,bash无法运行。登录云服务器时就是在创建一个会话,会话里有bash任务,启动进程时就是在当前会话中创建新的前台任务,而退出则是销毁会话,会影响会话内部的所有任务。销毁会话就是注销,通常的网络服务器,为了不受到用户登录注销的影响,会以守护进程的方式运行。既然创建的进程都会在一开始登录时创建的会话里,注销时也会注销这个会话,那就让被守护的进程放入另外一个会话,这样注销就不会受影响了,这就是守护进程的做法。

需要用到setsid接口

在这里插入图片描述
在这里插入图片描述

创建一个会话,设置进程组ID,谁调用这个接口,谁就是组长。返回新会话的ID,也就是这个进程的ID,失败返回-1,错误码被设置。

如何创建守护进程?

核心是setsid接口,但不止这点。要想调用这个接口,不能是组长调用,这样就得保证调用者不是组长。守护进程要忽略异常信号,并对文件描述符012做特殊处理,改变工作路径。进程的工作路径默认为当前路径,但守护进程不想这样,它会放在根目录下,不属于某个用户目录。更改路径这个操作用daemon这个接口,两个参数分别表示要不要更改路径,要不要关闭012。

在这里插入图片描述

不过一般是自己来更改路径,不用这个接口。外部的调用逻辑是这样的,也就是tcp_server.cc中

    tsvr->initServer();
    //将服务器守护进程化
    Daemon();
    tsvr->start();

err.hpp中加上SETSID_ERR这个错误

#pragma once

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    SETSID_ERR
};

如果daemon.hpp这样写

#pragma once

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include "log.hpp"
#include "err.hpp" 

void Daemon()
{
    pid_t ret = setsid();
    if((int)ret == -1)
    {
        lodMessage(Fatal, "daemon error, code: %d, string: %s", errno, strerror(errno));
        exit(SETSID_ERR);
    }
}

./运行起服务端肯定出错,因为新创建的这个进程自成一组,它是组长,就不行,所以得先让它不是组长,只要不是第一个进程就好了。

void Daemon()
{
    if(fork() > 0) exit(0);
    //下面的就是子进程了
    pid_t ret = setid();
    if((int)ret == -1)
    {
        lodMessage(Fatal, "daemon error, code: %d, string: %s", errno, strerror(errno));
        exit(SETSID_ERR);
    }
}

以及还需要忽略异常信号等其它。

#pragma once

#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "log.hpp"
#include "err.hpp" 

void Daemon()
{
    //1. 忽略信号
    //这里就忽略两个信号,还可以忽略其它信号
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);
    //2. 避免成为组长
    if(fork() > 0) exit(0);
    //下面的就是子进程了
    //3. 新建会话,自己成为会话的话首进程
    pid_t ret = setid();
    if((int)ret == -1)
    {
        logMessage(Fatal, "daemon error, code: %d, string: %s", errno, strerror(errno));
        exit(SETSID_ERR);
    }
    //4. 可选: 更改守护进程的工作路径
    //因为我们自定义的一些头文件,这里就不改路径了
    //chdir("/")//更改为根目录
    //5. 处理012问题
    //Linux中有个/dev/null文件,任何向里面输入的内容都会被抛弃,不会被提取内容
    int fd = open("/dev/null", O_RDWR);//读写方式打开
    if(fd < 0)
    {
        logMessage(Fatal, "open error, code: %d, string: %s", errno, strerror(errno));
        exit(OPEN_ERR);
    }
    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    close(fd);
}

在tcp_server.cc文件中,守护进程后还会有start函数,这个函数里可能有cout,cin等,守护进程后使用这些就会出错,所以要把错误重定向到/dev/null中。守护进程本质是孤儿进程的一种。这时候再次启动服务端就可以了。启动后用命令

ps ajx | head -1 && ps -axj | grep tcp_server

grep后面的就是进程名字。可以发现TTY是?,SID是一个新的组。关闭云服务器后,这个服务端依然可以提供服务。用jobs查看不到。

想要关闭服务端,kill -9 SID就可以。但是还有一个问题,把标准输入输出错误都重定向到/dev/null了,那么日志打印的消息程序员也就看不到了,就不知道服务器会出什么问题了,所以我们还得更改一下log.hpp,让它把消息打印到当前路径的一个文件中。

#pragma once

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdarg>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>

const std::string filename0 = "log/tcpserver.log.Debug";
const std::string filename1 = "log/tcpserver.log.Info";
const std::string filename2 = "log/tcpserver.log.Warning";
const std::string filename3 = "log/tcpserver.log.Error";
const std::string filename4 = "log/tcpserver.log.Fatal";
const std::string filename5 = "log/tcpserver.log.Unknown";


enum
{
    Debug = 0,//调试信息
    Info,//正常信息
    Warning,//告警,不影响运行
    Error,//一般错误
    Fatal,//严重错误
    Unknown
};

static std::string toLevelString(int level, std::string& filename)
{
    switch(level)
    {
    case Debug:
        filename = filename0;
        return "Debug";
    case Info:
        filename = filename1;
        return "Info";
    case Warning:
        filename = filename2;
        return "Warning";
    case Error:
        filename = filename3;
        return "Error";
    case Fatal:
        filename = filename4;
        return "Fatal";
    default:
        filename = filename5;
        return "Unknown";
    }
}

static std::string getTime()
{
    time_t curr = time(nullptr);//拿到当前时间
    struct tm *tmp = localtime(&curr);//这个结构体有对于时间单位的int变量
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d", tmp->tm_year + 1900, tmp->tm_mon + 1, tmp->tm_mday, \
        tmp->tm_hour, tmp->tm_min, tmp->tm_sec);//这些tm_的变量就是结构体中自带的,tm_year是从1900年开始算的,所以+1900
    return buffer;
}

//日志格式: 日志等级 时间 pid 消息体
//logMessage(DEBUG, "hello: %d, %s", 12, s.c_str()); 12以%d形式打印, s.c_str()%s形式打印
void logMessage(int level, const char* format, ...)//...就是可变参数,format是输出格式
{
    //写入到两个缓冲区中
    char logLeft[1024];//用来显示日志等级,时间,pid
    std::string filename;
    std::string level_string = toLevelString(level, filename);
    std::string curr_time = getTime();
    snprintf(logLeft, sizeof(logLeft), "[%s] [%s] [%d] ", level_string.c_str(), curr_time.c_str(), getpid());
    char logRight[1024];//用来显示消息体
    va_list p;
    va_start(p, format);
    //直接用这个接口来对format进行操作,提取信息
    vsnprintf(logRight, sizeof(logRight), format, p);
    va_end(p);
    //打印
    printf("%s%s\n", logLeft, logRight);
    //format是一个字符串,里面有格式,比如%d, %c,通过这个就可以用arg来提取参数
    //保存到文件中
    FILE* fp = fopen(filename.c_str(), "a");
    if(fp == nullptr) return ;
    fprintf(fp, "%s%s\n", logLeft, logRight);
    fflush(fp);
    fclose(fp);
    //va_list p;//char*
    //下面是三个宏函数
    //int a = va_arg(p, int);//根据类型提取参数
    //va_start(p, format);//让p指向可变参数部分的起始地址
    //va_end(p);//把p置为空, p = NULL
}

8、其它

man inet_addr会看到很多接口,inet_ntoa是把四字节IP转换为字符串,但它是C接口,返回类型是char*,也就是说返回了指针,返回了地址,而字符串是系统在内存中申请了一块空间来存储,这个位置不需要我们手动释放,但频繁调用,后面的会覆盖前面的地址,也就是说这个接口不是线程安全的,所以在多线程场景中会出问题。不过到现在为止,应当是加上了线程安全,可以用这段代码测试

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

void* Func1(void* p)
{
    struct sockaddr_in* addr = (struct sockaddr_in*)p;
    while (1)
    {
        char* ptr = inet_ntoa(addr->sin_addr);
        printf("addr1: %s\n", ptr);
    }  
    return NULL;
}

void* Func2(void* p) 
{
    struct sockaddr_in* addr = (struct sockaddr_in*)p;
    while (1) 
    {
        char* ptr = inet_ntoa(addr->sin_addr);
        printf("addr2: %s\n", ptr);
    }
    return NULL;
}

int main()
{
    pthread_t tid1 = 0;
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;
    pthread_create(&tid1, NULL, Func1, &addr1);
    pthread_t tid2 = 0;
    pthread_create(&tid2, NULL, Func2, &addr2);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

四字节ip转为字符串还可以用inet_ntop,把IP地址转为字符串,把二进制转为文本,src参数就是IP地址,dst是一个char类型的缓冲区,定义一个buffer[]来保存转化好的字符串。

TCP协议中,服务器监听后,客户端就可以连接了,客户端的connect实际上是在发送报文,操作系统底层进行三次握手处理连接过程,处理完后服务端的accept接口就把这个创建好的连接给用户使用;close时是进行四次挥手来断开连接。建立和断开连接是用户让系统做的。建立时,客户端完成两次操作,服务端完成一次;断开时,双方都close,一次close对应两次操作。

结束。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

黑豹程序员-放大招-架构师学习路线图

文章目录 全栈软件架构师技术路线六环能力图一、开发基础二、增强软件三、海量数据四、软件智能五、并发增强六、桌面开发 全栈软件架构师技术路线 六环能力图 作为软件开发&#xff0c;我们的任务就是开发软件业务系统。 如果要做好一个软件系统需要的技能是非常多的。我归纳…

分布式链路追踪--SkyWalking7.0.0+es7.0.0

分布式链路追踪–SkyWalking ​ 微服务的出现&#xff0c;的确解决了一些业务痛点&#xff0c;但是也造成了新的问题比如随着调用链的拉长&#xff0c;如果想要知道请求为什么这么慢&#xff0c;这个请求到底经历了哪些环节&#xff0c;又依赖了哪些东西&#xff0c;在微服务架…

基于Java的婚纱影楼管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

Bluespec SytemVerilog 握手协议接口转换

01、引言 由于接口控制信号上的差异&#xff0c;要实现Bluespec SystemVerilog(BSV)生成的代码和外部Verilog代码之间的正确交互是一件比较麻烦同时容易出错的事情。在BSV中, 模块之间的交互都是基于Action或ActionValue这两类method完成。下图展示了使用BSV设计的某一模块的接…

综合应用QGIS软件,实现商场选址分析

一、实验要求 ①离城市主要交通道路50米内&#xff0c;保证商场交通的便利性。 ②在居民区100米内&#xff0c;便于居民步行到商场。 ③距离停车场100米内&#xff0c;便于顾客停车。 ④距离其他商场500米范围之外&#xff0c;减少竞争压力。 二、实验数据 ①城市地区主要…

机器学习算法基础--层次聚类法

文章目录 1.层次聚类法原理简介2.层次聚类法基础算法演示2.1.Single-linkage的计算方法演示2.2.Complete-linkage的计算方法演示2.3.Group-average的计算方法演示 3.层次聚类法拓展算法介绍3.1.质心法原理介绍3.2.基于中点的质心法3.3.Ward方法 4.层次聚类法应用实战4.1.层次聚…

Java21 新特性

文章目录 1. 概述2. JDK21 安装与配置3. 新特性3.1 switch模式匹配3.2 字符串模板3.3 顺序集合3.4 记录模式&#xff08;Record Patterns&#xff09;3.5 未命名类和实例的main方法&#xff08;预览版&#xff09;3.6 虚拟线程 1. 概述 2023年9月19日 &#xff0c;Oracle 发布了…

【Linux】完美解决ubuntu18.04下vi不能使用方向键和退格键

今天在刚安装完ubuntu18.04&#xff0c;发现在使用vi命令配置文件时使用方向键并不能移动光标&#xff0c;而是出现一堆奇怪的英文字母&#xff0c;使用退格键也不能正常地删除内容&#xff0c;用惯了CentOS的我已经感觉到ubuntu没有centos用着丝滑&#xff0c;但是没办法&…

C++ -- 学习系列 std::deque 的原理与使用

一 deque 是什么? std::deque 是 c 一种序列式容器&#xff0c;其与 vector 类似&#xff0c;其底层内存都是连续的&#xff0c;不同的地方在于&#xff0c; vector 是一端开口&#xff0c;在一端放入数据与扩充空间&#xff0c;而 deque 是双端均开口&#xff0c;都可以放…

lv5 嵌入式开发-10 信号机制(下)

目录 1 信号集、信号的阻塞 2 信号集操作函数 2.1 自定义信号集 2.2 清空信号集 2.3 全部置1 2.4 将一个信号添加到集合中 2.5 将一个信号从集合中移除 2.6 判断一个信号是否在集合中 2.7 设定对信号集内的信号的处理方式(阻塞或不阻塞) 2.8 使进程挂起&#xff08;…

NLP 01(介绍)

一、NLP 自然语言处理 (Natural Language rrocessing,简称NLP) 是计算机科学与语言学中关注于计算机与人类语言间转换的领域。 1.1 发展 规则&#xff1a;基于语法 自然语言处理的应用场景: 语音助手 机器翻译 搜索引擎 智能问答

Windows下安装MySQL8详细教程

Windows下安装MySQL8详细教程 因为需要在Windows下安装MySQL8的数据库&#xff0c;做一个临时数据库环境。 1.准备软件 使用社区版本&#xff0c;下载地址如下&#xff1a; https://dev.mysql.com/downloads/mysql/ 使用8.0.16版本&#xff0c;需要在归档中查找 选择版本&a…

pysimpleGui 使用之sg.SaveAs使用

SaveAs与FileBrowse使用一样需要给指定target参数&#xff0c;保存路径 layout [ [ sg.FileBrowse( button_text“请选择单个文件”, # 按钮文本 target“single_path”, # 把选择后的路径保存到key为input_path的对象 # file_types((“All Files”, “.”),), # 默认筛选全部…

Python+Yolov8路面桥梁墙体裂缝识别

程序示例精选 PythonYolov8路面桥梁墙体裂缝识别 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《PythonYolov8路面桥梁墙体裂缝识别》编写代码&#xff0c;代码整洁&#xff0c;规则&#…

如何应用MBTI职业性格测试来做职业规划

想要有一个不错的职业发展&#xff0c;需要做好职业规划。通常来说&#xff0c;职业规划可以分为三个组成&#xff0c;即定位、目标和路径。应用MBTI职业性格测试&#xff0c;可以对上述三个组成有更清晰的认识&#xff0c;帮助人们完成适合自己的职业规划。 职业性格和职业定…

Pytorch之ResNet图像分类

&#x1f482; 个人主页:风间琉璃&#x1f91f; 版权: 本文由【风间琉璃】原创、在CSDN首发、需要转载请联系博主&#x1f4ac; 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦 目录 前言 一、ResNet网络结构 1.residual结构 2.BN(Batch Normalization)层…

反射知识点学习

文章目录 1. Java 反射机制原理示意图1.1 反射相关的主要类1.2 反射的优点和缺点1.3 反射调用优化-关闭访问检查 2. Class 类2.1 基本介绍2.2 Class类的常用方法2.3 获取 Class 类对象 3. 哪些类型有 Class 对象4. 类加载4.1 基本说明4.2 类加载时机4.3 类加载过程图4.4 类加载…

国庆作业1

使用消息队列实现进程之间的通信 代码 write.c #include <myhead.h> //消息结构体 typedef struct {long msgtype; //消息类型char data[1024]; //消息正文 }Msg_ds;#define SIZE sizeof(Msg_ds)-sizeof(long) //正文大小 int main(int argc, cons…

算法-位运算-只出现一次的数字 II

算法-位运算-只出现一次的数字 II 1 题目概述 1.1 题目出处 https://leetcode.cn/problems/bitwise-and-of-numbers-range/description/?envTypestudy-plan-v2&envIdtop-interview-150 1.2 题目描述 2 逐个按位与运算 2.1 思路 最简单的就是直接挨个做与运算&#x…

transformers简介

目录 1、前言 2、网络结构 &#xff08;1&#xff09;、Transformers的总体架构可以分为四部分 &#xff08;2&#xff09;、输入文本包含 &#xff08;3&#xff09;、输出部分包含 &#xff08;4&#xff09;、编码器部分 &#xff08;5&#xff09;、解码器部分 1、前…