网络套接字-TCP服务器

news2024/11/17 6:44:28

一 前言

        前面已经写过udp服务器的实现了,那里说了很多编写服务器的所需知识,在tcp服务器实现中就不再赘述了。

二 服务端编写

        大致接口如下。

./server + port端口号 启动时指明端口号
void usage(const std::string proc)
{
    std::cout<<"Usage "<<proc<<" port"<<std::endl;
}

int main(int argc,char*argv[]) 解析命令行参数,第二个就是端口号
{
    if(argc != 2)
    {
        usage(argv[1]);
        std::cout<<"port error"<<strerror(errno)<<std::endl;
        exit(USAGE_ERR);
    }
    u_int16_t port = atoi(argv[1]);将字符串的端口号转为数字,后续传给服务端类。

    tcp->InitServer();
    tcp->start();
    return 0;
}

        然后我们就去实现类了,首先服务端必定要有端口号,而且一个公司部署的服务端的端口号是分配好的。还有个成员是保存套接字的,这个后面具体实现再提。

namespace server
{


    class TcpServer
    {
    public:
        TcpServer()
        {
            ;
        }
        ~TcpServer()
        {
            ;
        }
        void InitServer()
        {
 
        }
        void start()
        {
      
        }
 
    private:
        int listensocket_;
        int port_;
    };
};

1 创建套接字

        初始化,显然就是要打开网络文件,创建套接字,直接复制udp的实现。

             // 1 创建套接字
            listensocket_ = socket(AF_INET, SOCK_STREAM, 0);
            if (listensocket_ < 0)
            {
                std::cout << "socket err" << std::endl;
                exit(SOCKET_ERR);
            }
            std::cout << "socket success" << std::endl;

2  开始绑定

        注意:recv和sendto函数内部不会实现大小端转化,需要我们自己主机转网络序列。


            // 2 绑定端口号和ip地址
            struct sockaddr_in sock; // 头文件<netinet/in.h>
            bzero(&sock, sizeof(sock));
            sock.sin_addr.s_addr = INADDR_ANY; // 设置ip地址 表示所有的ip的地址
            sock.sin_port = htons(port_);
            sock.sin_family = AF_INET;
            if (bind(listensocket_, (sockaddr *)(&sock), sizeof(sock)) < 0)
            {
                std::cout << "bind error " << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
            std::cout << "bind success" << std::endl;

3 监听

        参数2后面提及协议再说。可以设个不大不小的整数。

        listensocket_是我们先前创建套接字返回的文件描述符,这个描述符被用来监听了,监听的是客户端的链接请求,因为请求也是通信,所以我们要先创建套接字,如果有链接请求从网络中来,os会把链接请求转成数据保存在监听套接字对应的文件中,上层就从这个文件中读取链接。

             // 3 开始监听
            if (listen(listensocket_, backlog)) // 返回0,监听成功
            {
                std::cout << "listen error " << strerror(errno) << std::endl;
                exit(LISTEN_ERR);
            }

4 启动服务器

        当有请求来了,我们当然要调用函数去处理,参数很熟悉,sockfd是我们先前用的listensockt_,因为请求都在这个文件内。

        void start()
        {
            while (true)
            {
                // 获取链接
                struct sockaddr_in sock; // 头文件<netinet/in.h>
    
                socklen_t len = sizeof(sock);
                int socket = accept(listensocket_, (sockaddr *)&sock, &len); // 不保存吗
                if (socket < 0)
                {
                    // std::cout << "accept err" << std::endl;
                    exit(SOCKET_ERR);
                }
                std::cout << "accept success" << std::endl;
                // 开始发消息,此时我们不能让主线程去发消息,不然就无法链接其它的客户端了
                std::string clientip = inet_ntoa(sock.sin_addr);
                u_int16_t clientport = ntohs(sock.sin_port);
                server(sock,clientip, clientport);

            }
        }
        void server(int sock, std::string ip, uint16_t port)
        {
            ;
        }

        此时收到请求后,我们也就获得了客户端的ip和端口,接下来就用ip和端口传给server函数实现通信。可是为什么accept又返回一个文件描述符,为什么要有两个套接字呢?在udp服务器中我们只创建了一个文件,意味着所有客户端发来的消息都在这个文件中,读取的时候其实很容易读取到a客户端的数据处理完发给了b客户端,那对于a客户端来说,数据就缺失了,所以udp的通信确实是不考虑完不完整的,所以tcp为了保证客户端数据的独立性,就每接收一次链接就创建一个文件,内部一定会把客户端的ip和端口号和这个文件绑定,这样下次客户端的数据来的时候就会根据ip和端口号判断放在哪个文件中了。

        接下来看看server函数内如何通信。直接调用read函数读,调用write函数写,奇怪,为什么先前udp服务器不可以用read,而是用recvfrom,也不是用write,而是用sendto,据我了解,read和write是面向字节流的,udp服务端我们打开文件的时候是指明面向数据报的,read用面向字节流读取面向数据报的文件会出问题。后面讲tcp,udp协议,了解了面向字节流和面向数据报就理解得更深刻了。

        void server(int sock, std::string ip, uint16_t port)
        {

            char buffer[1024] = {0};
            int n = read(sock, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                // 写回给客户端
                write(sock, ret.c_str(), ret.size());
            }
            else if (n == 0)
            {
                close(sock);
                std::cout << name << " 断开连接" << std::endl;
            }
            else
            {
                close(sock);
                exit(READ_ERR);
            }

        }

   可是直接输出又有点单调,所以我们做了些修改。调用回调方法对数据做处理。

    

         func_就是func_t定义的变量,可以接收一个可调用对象,这个可调用对象的类型是string(string),也就是返回值是string,参数也是string。 

   

       数据处理方法由外部定义。如下就是完整的定义服务端对象和启动。

std::string echo(std::string message)
{
    return message;
}
int main(int argc,char*argv[])
{
    if(argc != 2)
    {
        usage(argv[1]);
        std::cout<<"port error"<<strerror(errno)<<std::endl;
        exit(USAGE_ERR);
    }
    u_int16_t port = atoi(argv[1]);
    std::shared_ptr<TcpServer> tcp = std::make_shared<TcpServer>(echo,port);
    tcp->InitServer();
    tcp->start();
    return 0;
}

相应构造函数也要修改。

三 客户端编写

        同样客户端不用自己bind。那要不要listen,accept呢? 我认为是不需要的,这两个函数的意义在于等待别人来链接,一般来说都是客户端主动连接服务端,很少有服务端来主动找客户端的,并且获取链接方便后面给对方发数据,前面还说了可以保证服务端收到来自客户端数据的独立和安全。

        而客户端本身不用担心多个服务端发来的数据混杂,虽然我们手机上有多个客户端,但是我们的客户端都是一个个进程,创建的套接字文件是独立的,服务端是因为会有多个客户端和它通信才要创建多个文件,我们这里是一个客户端对应一个服务端。所以通信步骤如下。

1 创建套接字

2 发起链接

        用的是下面这个connect函数,参数列表也是非常熟悉。

        直接开始准备初始化sock结构体。

        因为我们不能一链接失败就退出,要尝试重连几次,就像我们打游戏没网,也是会有尝试重连选项的。

int main(int argc, char *argv[])  ip地址不能是任意的,必须知道服务端的ip地址和端口号
{
    if (argc != 3)
    {
        usage(argv[0]);
        std::cout << "stage error" << strerror(errno) << std::endl;
        exit(USAGE_ERR);
    }
    const std::string ip_ = argv[1];
    u_int16_t port = atoi(argv[2]);

    int socket_ = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_ < 0)
    {
        std::cout << "create socket error" << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
    }
    std::cout << "create socket successs" << std::endl;
    // 打开网络文件

    // 不用客户端自己绑定端口号和ip地址
    struct sockaddr_in sock;
    sock.sin_addr.s_addr = inet_addr(ip_.c_str()); // 将字符串类型的地址转为四字节地址,而且是网络字节序了
    sock.sin_port = htons(port);
    sock.sin_family = AF_INET;
    int len = sizeof(sock);
    // 开始连接
    int timenum = 5;
    while (connect(socket_, (sockaddr *)&sock, len) < 0)
    {
        sleep(1);
        std::cout << "开始重连:" << std::endl;
        timenum--;
        if (timenum == 0)
            break;
    }
    if (timenum <= 0)
    {
        std::cout << "链接失败" << std::endl;
        exit(CON_ERR);
    }
    return 0;
}

        接下来就是我们客户端直接把消息写到文件中,然后read读取服务端返回的数据。

       

四 实验测试 

        测试1

      服务端bug:如果客户端不给我们发消息,我们就会阻塞在read这里,就不能调用accept接收新连接了,所以我们应该安排一个线程去调用server函数来收发消息

          注意:我服务端代码里的server函数只能读一次和发一次消息给客户端,如果写成死循环后面引进线程池还会出问题,因为线程数量有限,多个客户端来链接,线程就会不够,这里不会,因为此时我们是一个线程负责收发消息给一个客户端。

    
        
void *recv(void *arg)
{
    // 收消息
    int socket_ = *((int *)arg);
    while (true)
    {
        char buffer[1024] = {0};
        int n = read(socket_, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = '\0';
            std::cout << "server echo# " << buffer << std::endl;
        }
        else if (n == 0)
        {
            close(socket_);
            std::cout << "server quit" << std::endl;
        }
        else
        {
            close(socket_);
            std::cout << "client read err" << strerror(errno) << std::endl;
            exit(READ_ERR);
        }
    }
}

         void start()
        {
            while (true)
            {
                // 获取链接
                struct sockaddr_in sock; // 头文件<netinet/in.h>
                bzero(&sock, sizeof(sock));
                socklen_t len = sizeof(sock);
                int socket = accept(listensocket_, (sockaddr *)&sock, &len); // 不保存吗
                if (socket < 0)
                {
                    lg_(ErrorLevel::Info,"accept err");
                    // std::cout << "accept err" << std::endl;
                    exit(SOCKET_ERR);
                }
                std::cout << "accept success" << std::endl;
                // 开始发消息,此时我们不能让主线程去发消息,不然就无法链接其它的客户端了
                std::string clientip = inet_ntoa(sock.sin_addr);
                u_int16_t clientport = ntohs(sock.sin_port);

                pthread_t id;
                pthread_create(&id,nullptr,recv,ThreadDta);
                                            
            }
        } 
    

        ThreadData类内包含ip和端口,server函数实现也被放入了静态成员函数recv中,我这里只是演示一下,只会把最后版本放出来下面我们引入线程池来做优化,我们希望在accept链接前就创建好线程了,提高响应速度。

引进线程池

        线程池:内部负责创建线程,我们外部构建任务,放入线程池中,内部线程池会去执行。

 先来看看任务构建,外部传入一个套接字,ip,端口号,可调用对象构建任务,线程拿到任务后调用可调用对象。


#define NUM 5
class Task
{
public:
    using func_t = std::function<void(int,std::string,u_int16_t)>;
    Task()
    {
        ;
    }
    Task(int sock,std::string ip,uint16_t port,func_t func)
        : socket_(sock), ip_(ip), port_(port),func_(func)
    {
        ;
    }
    void operator()()
    {
        func_(socket_,ip_,port_);
    }
    int socket_;
    func_t func_;
    std::string ip_;
    u_int16_t port_;
};

        使用如下,构建任务,并且入队列,这里面还用了bind语法。

        然后我大致说一下线程池内部实现,以及我们什么时候控制线程池去执行任务。成员如下,有任务队列保存任务,还有vector<Thread>保存多个线程。

        我们线程池内存的也不是线程id,而是封装后的Thread类。代码如下,可以不关心内部实现。

class Thread
{
public:
    typedef enum
    {
        NEW = 1,
        RUNING,
        EXIT
    }status;
    typedef void* (*fun_t)(void*);
    Thread()
    {
        ;
    }
    Thread(int num, fun_t fun, void* arg)
    :id_(0),fun_(fun),arg_(arg),status_(NEW)
    {
        name_ = "thread->" + std::to_string(num);
    }
    ~Thread()
    {
        ;
    }
    static void * threadRun(void*arg)
    {
        Thread* th = (Thread*) arg;
        th->fun_(th->arg_);
        return nullptr;
    }
    void Run()
    {
        int n = pthread_create(&id_,nullptr,threadRun,(void*)this);
        if(n != 0)//成功返回0,不成功返回错误码
            exit(4);
        status_ = RUNING;    
    }
    void join()
    {
        pthread_join(id_,nullptr);
        status_ = EXIT;
    }
    std::string getname()
    {
        return name_;
    }
    int getstatus()
    {
        return status_;
    }
    pthread_t  getid()
    {
        if(status_ == RUNING)
            return id_;
        else
        {
            std::cout<<name_<<" not create ";
            return 1;
        }    
    }
    pthread_t id_;
    std::string name_;//线程名
    status status_;//线程状态
    fun_t fun_;//线程执行函数
    void* arg_;//线程参数
};

        我们只需要知道我们要线程池初始化thread对象时要传一个执行函数和参数即可,i就是内部用来构建线程名的,不用关心。

        外部通过线程池的静态函数获取单例对象。

        线程池内调用Thread类内方法创建线程。

        Thread类内的Run方法。

        线程池内部实现。

template <class T>
class threadPool
{
    threadPool(int size = NUM) // vp_存的自定义类型要有默认构造,不然这里初始化会找不到默认构造!
        : vp_(size)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&Consumer, nullptr);
        pthread_cond_init(&Productor, nullptr);
    }
      ~threadPool()
    {
        for (auto &e : vp_) // 复用Thread join方法回收线程
        {
            e.join();
        }
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&Consumer);
        pthread_cond_destroy(&Productor);
    }
   threadPool(const threadPool<T>& sh) = delete;
	threadPool<T> operator=(const threadPool<T>& sh) = delete;
public:
    void init()
    {
        for (int i = 0; i < NUM; i++)
        {
            vp_[i] = (Thread(i, threadRun, this));//this指针是给内部传参数的
        }
    }
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    static void *threadRun(void *arg)
    {
        // pthread_detach(pthread_self());
        threadPool<T> *tp = static_cast<threadPool<T> *>(arg);
        // 执行任务
        while (true)
        {
            T data;
            tp->pop(data);
            data();
        }
    }
    bool Full()
    {
        return tasks_.size() == NUM;
    }
    bool Empty()
    {
        return tasks_.size() == 0;
    }
    void start()
    {
        for (auto &e : vp_) // 复用Thread Run方法创建线程
        {
            e.Run();
        }
    }
  
    void push(const T &data)
    {
        {
            LockGuard lg(&mutex_);
            while (Full())
            {
                pthread_cond_wait(&Productor, &mutex_);
            }
            tasks_.push(data);
        }
        pthread_cond_signal(&Consumer);

    }
    void pop(T &data)
    {

        {
            LockGuard lg(&mutex_);
            // 检查是否有任务
            while (Empty())
            {
                pthread_cond_wait(&Consumer, &mutex_);
            }
            data = tasks_.front();
            tasks_.pop();
        }
        pthread_cond_signal(&Productor);
    }
    static threadPool<T>* getthreadPool()
    {
        if(tp_ == nullptr)//减少加锁次数
        {
            LockGuard lg(&Poolmutex_);
            if (tp_ == nullptr)
            {    
                tp_ = new threadPool<T>();
                tp_->init();
                tp_->start();
            }

        }
        return tp_;    
    }
    std::queue<T> tasks_;    // 任务队列
    std::vector<Thread> vp_; // 线程池,不能存指针,内部要解引用访问的
    pthread_mutex_t mutex_;
    pthread_cond_t Consumer;
    pthread_cond_t Productor;
    static threadPool<T>* tp_;
    static pthread_mutex_t Poolmutex_;
};
template<class T>
threadPool<T>*threadPool<T>::tp_ = nullptr;
template<class T>
pthread_mutex_t threadPool<T>::Poolmutex_ = PTHREAD_MUTEX_INITIALIZER;

        综上,外部线程获取单例线程池对象并且push任务到队列,内部线程pop任务出来并执行。由于我们的线程池并没有设计扩容功能,所以线程是有限的,而线程池内部线程执行的任务是我们外部传入的server函数,如果server函数是个死循环,就会导致无法执行其它客户端的任务,所以我们把server函数内的死循环改了。

五 守护进程

1 概念介绍

        为什么要有守护进程呢?

        因为我们目前起的进程如果把中端窗口一关闭,此时就会导致服务停了,那总不能让屏幕一直开着吧,所以我们让这个进程在后台运行,也就是守护进程化。

我们以前在讲进程状态的时候,用过下面这条命令,但是有几个成员一直没提及,那就是PGID和SID以及TTY。

        PGID表是进程组,我们在命令行可以一次性起多个进程,这些进程都被归属于某个进程组被管理起来,进程组id一般是第一个创建的进程id,例如我们./server起了一个进程,这个进程内部也起了多个进程,此时这些进程都会被划分在一个进程组中,此时组长是server。

        tty表示终端文件,终端就是我们打开xshell显示的那个窗口,所以当我们在命令行输入指令,都是云服务将结果输出到文件,文件经过网络发送到我们的主机上。

在一个终端下起的进程的终端文件是一样的,父进程是一样的,都是bash,而且还都属于某个进程组,进程组以第一个创建的进程id命名,不是bash。例如sleep 100 | sleep 100 &我们就在后台一次性起了两个进程,它们属于一个进程组。

        所以./server不再是启动一个进程了,我们应该说是启动一个任务,因为serve程序内部可能也会创建进程,所以需要一个更大的概念-任务来描述,jobs可以查看当前会话的任务。

        左侧的是任务码,我们可以把一个后台拉到前台运行,fg+ 任务码,然后ctrl+c就可以结束这个进程了。

        将前台的任务回退为后台,Ctrl+z。

        细节,当我们把sleep拉回前台运行时,此时bash命令就没了,因为我们./server就是把一个任务变成了前台,bash就变成后台,一个终端只能有一个前台进程,可以简单理解前台进程就是要和我们的键盘交互的,我们只能给一个进程喂指令,所以一个终端只有一个前台进程,当我们ctrl+z把前台进程变成后台进程后,此时bash命令行又回来了,因为它自动把自己变成前台进程了。

        那什么是SID呢,我们称为会话id,22103其实是我们的bash进程,会话id以一个会话内的首进程id命名,什么是会话呢?当我们登录xshell时,linux就会建立一个会话,将bash以及bash创建进程管理在其中,有时候bash创建的进程(例如./server)内部又创建了许多进程,为了将server和它创建的进程关联起来,就有了进程组。       

        注销的理解:以前我们的电脑上是有个注销选项的,注销和重启是不同的,注销是删掉会话内的所有进程,重启则是整个系统进程都要重启了。守护进程化,就是对server进程独立开一个会话,这样用户注销就不会影响我这个守护进程了。使用如下接口。

返回值:会话ID。

2 守护进程化实现

void Daemon()
{

    pid_t ret = setsid();   
    if((int)ret == -1)
    {
        cout<<"setsid err:错误码: "<<errno<<" 错误信息:"<<strerror(errno)<<endl;
        exit(SETSID_ERR);
    }    
}

        然后我们在server.cc中调用一下,我们整个进程就变成守护进程啦。对进程组的其它进程无影响?如果我们是像这里一样实现的话,大概率会出错,首先我们这里只有一个server进程,

       

        server进程就必定是组长,组长进程不能从会话中独立出去,为什么不能走呢,我想是因为组长进程内有着管理组内进程的方法,如果走了就管理不了其它的进程了,深究的话就得讨论为什么组长走了就不能管理其它进程了,难道我们不能再选一个组长吗? 当然我说的容易,实现起来可能比较冗余,所以就禁止我们对组长进程进行守护进程化。

        优化如下。

        我们在Daemon()函数中创建子进程,让父进程退出,此时子进程就变成孤儿进程了,被os接管了,此时这个孤儿进程不会是组长,此时孤儿进程再将自己独立出去。

        变成守护进程后,一直在后台执行start函数,这个函数内是在接收链接,并且创建任务到队列中,让内部的线程池去执行通信任务。

然后我们还要忽略一些常见错误信号,让我们的守护进程不会随随便便就退出。

 signal(SIGPIPE,SIG_IGN);
    signal(SIGCHLD,SIG_IGN);

        已经成为守护进程了,就不应该和键盘显示器关联了,我们可以直接关闭显示器和键盘文件,但是这样我们使用cout,cin会直接出错。所以我们打开一个特殊文件,让cin读不到数据,直接返回,写入的数据也会被丢弃。

//守护进程化
void Daemon()
{

    子进程去创建新会话
    if(fork() > 0)
        exit(0);
    pid_t ret = setsid();   
    if((int)ret == -1)
    {
        cout<<"setsid err:错误码: "<<errno<<" 错误信息:"<<strerror(errno)<<endl;
        exit(SETSID_ERR);
    }    
    int fd = open("/dev/null",O_RDWR);
    if(fd < 0)
    {
        cout<<"open err:错误码: "<<errno<<" 错误信息:"<<strerror(errno)<<endl;
        exit(OPEN_ERR);
    }
    //关闭输入输出
    dup2(fd,0);
    dup2(fd,1);
    dup2(fd,2);
}

        守护进程只能kill掉。

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

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

相关文章

Py之scikit-learn-extra:scikit-learn-extra的简介、安装、案例应用之详细攻略

Py之scikit-learn-extra&#xff1a;scikit-learn-extra的简介、安装、案例应用之详细攻略 目录 scikit-learn-extra的简介 scikit-learn-extra的安装 scikit-learn-extra的案例应用 1、使用 scikit-learn-extra 中的 IsolationForest 模型进行异常检测 scikit-learn-extra…

Orbit 使用指南 10|在机器人上安装传感器 | Isaac Sim | Omniverse

如是我闻&#xff1a; 资产类&#xff08;asset classes&#xff09;允许我们创建和模拟机器人&#xff0c;而传感器 (sensors) 则帮助我们获取关于环境的信息&#xff0c;获取不同的本体感知和外界感知信息。例如&#xff0c;摄像头传感器可用于获取环境的视觉信息&#xff0c…

【小沐学Python】Python实现Web图表功能(Lux)

文章目录 1、简介2、安装3、测试3.1 入门示例3.2 入门示例2 结语 1、简介 https://github.com/lux-org/lux 用于智能可视化发现的 Python API Lux 是一个 Python 库&#xff0c;通过自动化可视化和数据分析过程来促进快速简便的数据探索。通过简单地在 Jupyter 笔记本中打印出…

我的风采——android studio

目录 实现“我的风采”页面要求理论代码生成apk文件 实现“我的风采”页面 要求 要求利用’java框架的边框布局实现“找的风采 ”页而&#xff0c;其中中间为你的生活照&#xff0c;左右和下面为按钮&#xff0c;上面为标签 理论 Java GUI编程是Java程序设计的重要组成部分…

QT(C++)-error LNK2038: 检测到“_ITERATOR_DEBUG_LEVEL”的不匹配项: 值“2”不匹配值“0”

1、项目场景&#xff1a; 在VS中采用QT&#xff08;C&#xff09;调试时&#xff0c;出现error LNK2038: 检测到“_ITERATOR_DEBUG_LEVEL”的不匹配项: 值“2”不匹配值“0”错误 2、解决方案&#xff1a; 在“解决方案资源管理器”中选中出现此类BUG的项目&#xff0c;右键-…

uniapp-Form示例(uviewPlus)

示例说明 Vue版本&#xff1a;vue3 组件&#xff1a;uviewPlus&#xff08;Form 表单 | uview-plus 3.0 - 全面兼容nvue的uni-app生态框架 - uni-app UI框架&#xff09; 说明&#xff1a;表单组建、表单验证、提交验证等&#xff1b; 截图&#xff1a; 示例代码 <templat…

PCIe总线-PCIe总线简介(一)

1.概述 早期的计算机使用PCI&#xff08;Peripheral Component Interconnect&#xff09;总线与外围设备相连&#xff0c;PCI总线使用单端并行信号进行数据传输&#xff0c;由于单端信号很容易被外部系统干扰&#xff0c;其总线频率很难进一步提高。目前&#xff0c;为了提高总…

k8s笔记27--快速了解 k8s pod和cgroup的关系

k8s笔记27--快速了解 k8s pod和 cgroup 的关系 介绍pod & cgroup注意事项说明 介绍 随着云计算、云原生技术的成熟和广泛应用&#xff0c;K8S已经成为容器编排的事实标准&#xff0c;学习了解容器、K8S技术对于新时代的IT从业者显得极其重要了。 之前在文章 docker笔记13–…

UDS升级入门,手把手教你——开篇

前面关于OTA的文章&#xff0c;写的比较乱&#xff0c;索性整了一个专栏&#xff0c;来认真梳理下&#xff0c;话不多开整。 准备工作&#xff1a; 1、QT环境 上位机开发 2、MDK环境&#xff0c;STM32F103&#xff0c;vscode MCU开发环境&#xff0c;调试 3、JFlash环境安…

【C语言数据结构】排序

1.排序的概念 在深入研究各个排序算法之前&#xff0c;首先&#xff0c;我们要对排序有个大概的了解&#xff0c;即与排序相关的一些概念 Q&#xff1a;什么是排序&#xff1f; A&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小…

基于Java中的SSM框架实现考研指导平台系统项目【项目源码+论文说明】

基于Java中的SSM框架实现考研指导平台系统演示 摘要 应对考研的学生&#xff0c;为了更好的使校园考研有一个更好的环境好好的学习&#xff0c;建议一个好的校园网站&#xff0c;是非常有必要的。提供学生的学习提供一个交流的空间。帮助同学们在学习高数、学习设计、学习统计…

使能 Linux 内核自带的 FlexCAN 驱动

一. 简介 前面一篇文章学习了 ALPHA开发板修改CAN的设备树节点信息&#xff0c;并加载测试过设备树文件&#xff0c;文件如下&#xff1a; ALPHA开发板修改CAN的设备树节点信息-CSDN博客 本文是学习使能 IMX6ULL的 CAN驱动&#xff0c;也就是通过内核配置来实现。 二. 使能…

Spring Cloud五:Spring Cloud与持续集成/持续部署(CI/CD)

Spring Cloud一&#xff1a;Spring Cloud 简介 Spring Cloud二&#xff1a;核心组件解析 Spring Cloud三&#xff1a;API网关深入探索与实战应用 Spring Cloud四&#xff1a;微服务治理与安全 文章目录 一、Spring Cloud在CI/CD中的角色1. 服务注册与发现&#xff1a;自动化管理…

YOLOV5 部署:TensorRT的安装和使用

1、介绍 TensorRT 可以加速神经网络的推理时间,常常在工业生产中使用 因为TensorRT需要使用到cuda和cudnn加速,所以需要安装这两个,安装的具体步骤参考前文: YOLOV5 部署:cuda和cuDNN安装-CSDN博客 2、TensorRT 下载 TensorRT下载地址:NVIDIA TensorRT Download | NV…

分类预测 | Matlab实现CNN-LSTM-Mutilhead-Attention卷积神经网络-长短期记忆网络融合多头注意力机制多特征分类预测

分类预测 | Matlab实现CNN-LSTM-Mutilhead-Attention卷积神经网络-长短期记忆网络融合多头注意力机制多特征分类预测 目录 分类预测 | Matlab实现CNN-LSTM-Mutilhead-Attention卷积神经网络-长短期记忆网络融合多头注意力机制多特征分类预测分类效果基本介绍模型描述程序设计参…

初识kafka-数据存储篇1

目录 背景 1 kafka总体体系结构 2 疑问解答 2.1 高吞吐低延迟 2.2 实现分布式存储和数据读取 2.3 如何保证数据不丢失 背景 最近在和产品过项目审批的时候&#xff0c;深刻感受到业务方对系统的时时响应提出了更高的要求。目前手上大部分的业务都是基础定时任务去实现的&…

[Java基础揉碎]单例模式

目录 什么是设计模式 什么是单例模式 饿汉式与懒汉式 饿汉式vs懒汉式 懒汉式存在线程安全问题 什么是设计模式 1.静态方法和属性的经典使用 2.设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、 以及解决问题的思考方式。设计模式就像是经典的棋谱&am…

使用 RunwayML 对图像进行 Camera 操作

RunwayML 是一個功能強大的平台&#xff0c;可以讓您使用 AI 和机器学习来增强您的图像和视频。 它提供一系列预训练模型&#xff0c;可用于各种任务&#xff0c;包括图像编辑、风格化和特效。 在本文中&#xff0c;我们将介绍如何使用 RunwayML 对图像进行 Camera 操作。我们…

游戏引擎中的地形系统

一、地形的几何 1.1 高度图 记录不同定点的高度&#xff0c;对每个网格/顶点应用高度、材质等信息&#xff0c;我们每个顶点可以根据高度改变位移 但是这种方法是不适用于开放世界的。很难直接画出几百万公里的场景 1.2 自适应网格细分 当fov越来越窄的时候&#xff0c;网格…

Stable diffusion(四)

训练自己的Lora 【DataSet】【Lora trainer】【SD Lora trainer】 前置的知识 batch size&#xff1a;模型一次性处理几张图片。一次性多处理图片&#xff0c;模型能够综合捕捉多张图片的特征&#xff0c;最终的成品效果可能会好。但是处理多个batch size也意味着更大的显存…