【计算机网络】网络编程套接字(二)

文章目录

  • 网络编程套接字(二)
      • 简单TCP服务器实现
        • 创建套接字
        • 服务器绑定
        • 服务器监听
        • 服务器接收连接
        • 服务器处理请求
      • 简单TCP客户端实现
        • 创建套接字
        • 客户端发起连接
        • 客户端发起请求
      • 服务器简单测试
      • 服务器简单测评
      • 多进程版TCP服务器
        • 捕捉SIGCHLD信号
        • 孙子进程提供服务
      • 多线程版TCP服务器
      • 线程池版TCP服务器

网络编程套接字(二)

简单TCP服务器实现

我们将会使用到的头文件放在comm.h文件中

#include <iostream>
#include <memory.h>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

创建套接字

创建过程和UDP服务器几乎完全一样,除了使用的是TCP服务器使用的是流式服务(SOCK_STREAM),UDP使用的是数据包服务(SOCK_DGRAM)

#include "comm.h"class TcpServer {
public:TcpServer(){};void InitServer(){sock = socket(AF_INET, SOCK_STREAM, 0);     if (sock < 0) {std::cerr << "socket error" << std::endl;exit(2);}}~TcpServer(){if (sock >= 0) close(sock);}
private:void Socket();int sock;
};

服务器绑定

绑定的过程和UDP服务器也是相同的,可以看着复习一下

     // 2 服务器绑定sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {std::cerr << "bind error" << std::endl;exit(3);}std::cout << "server bind success" << std::endl;             

定义好struct sockaddr_in结构体后,最好使用memset对该结构体进行初始化,也可以使用bzero函数进行清空

void bzero(void *s, size_t n);

服务器监听

TCP服务器是面向连接的,,客户端在正式向TCP服务器发送数据之前必须先于TCP服务器建立连接,然后才可以进行通信

因此TCP服务器需要时刻注意是否有客户端发来连接请求,需要将TCP创建的套接字设置成监听状态

int listen(int socket, int backlog);
  • socket : 需要设置为监听状态的套接字对应的文件描述符
  • backlog: 全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接会先被放入连接队列,该参数代表这个连接队列的最大长度,一般设置成5或者10即可
  • 监听成功返回0,失败返回-1同时设置错误码
	  // 3 服务器监听if (listen(listen_sock, 10) < 0) {std::cerr << "listen error" << std::endl;exit(4);}

这里我们发现上文的sockfd其实是一个被监听的文件描述符,为了变量命名更容易让人理解,我们把sockfd改为listen_sock,并且在初始化TCP服务器中,只有套接字创建成功,绑定成功,监听成功,TCP服务器的初始化才算完成

vim 替换单词

全文替换 ::#sockfd#listen_sock#g 使用 :#str1#str2#g进行全文替换,将str1全部替换成str2

局部替换: : 20, 30s#str1#str2#g (将20到30行内的str1替换成str2)

当前行替换: : s#str1#str2#g (将光标所在行内的str1 替换成 str2)

服务器接收连接

TCP服务器初始化后就可以开始运行了,但是TCP服务器与客户端在进行网络通信之前,服务器需要先获取到客户端的连接请求

 int accept(int socket, struct sockaddr *restrict address,socklen_t *restrict address_len);
  • socket : 特定的监听套接字,表示从该监听文件中获取连接

  • address : 对端网络相关的属性信息

  • addrlen: 传入希望读取到的address结构体的长度,返回实际读取到的address结构体的长度,是一个输入输出参数

  • 获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时设置错误码

监听套接字和accept函数返回套接字的区别

  • 监听套接字:用于获取连接请求信息,accept函数不断从监听文件中获取新连接
  • accept返回套接字:用于为这个连接提供服务,进行真正的业务数据传输

服务端获取连接

  • accept函数获取连接时可能会失败,但是服务器不能因为获取某一个连接失败就退出,因此服务端获取连接失败后还需要继续获取连接

  • 如果需要将获取到的客户端IP地址和端口号信息进行输出,需要调用inet_ntoa函数将整数IP转换成字符串IP,调用ntohs函数将网络序列转换成主机序列

  • inet_ntoa函数会先将整数IP转换成主机序列,然后再将其转换成字符串IP

 void Start() {for (;;) {// 获取连接struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));socklen_t len = sizeof(peer);int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);if (sock < 0) {std::cerr << "accept error, continue" << std::endl;continue;}std::string client_ip = inet_ntoa(peer.sin_addr);  short client_port = ntohs(peer.sin_port);cout << "get a new link->"  << sock << "[" << client_ip << ":" << client_port << "]" << endl;Service(sock, client_ip, client_port);      // 进行业务处理}}

服务器处理请求

现在服务器已经可以和客户端建立连接了,接下来就是到了通信阶段。我们只需要通过对accept函数打开的网络文件进行读写,就可以完成网络数据的传输和接收了。为了能让双方都可以看到现象,这里就实现一个简单的回声TCP服务器

ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);
ssize_t read(int fildes, void *buf, size_t nbyte);
  • fd:特定的文件描述符,表示从该文件描述符中读取数据

  • buffer : 数据的存储位置,表示将读取数据到该缓冲区中

  • count : 期望从该文件描述符中读取的字节数

  • 返回值大于零代表本次读取到的字节数,等于零表示对端对出,小于零说明读取出现错误

ssize_t pwrite(int fildes, const void *buf, size_t nbyte, off_t offset);
ssize_t write(int fildes, const void *buf, size_t nbyte);
  • fd: 特定的文件描述符,表示将把数据写入该文件描述符对应的文件

  • buffer : 需要写入的数据

  • count :期望写入的字节数

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码被设置

还需要注意到,服务端读取的数据是从服务套接字中获取的,如果客户端断开连接服务结束那么需要关闭对应文件。因为文件描述符本质就是数组的下标,是有限的,如果一直占用会导致文件描述符泄漏

void Service(int sock, std::string client_ip, short client_port) {for (;;) {
#define BUFFER_SIZE 128char buffer[BUFFER_SIZE];ssize_t size = read(sock, buffer, BUFFER_SIZE - 1);  				// 读取请求if (size > 0) {          // 读取成功buffer[size] = 0;std::cout << client_ip << ":" << client_port << " # " << buffer << std::endl;std::string response = "tcp server say # ";response += buffer;if (write(sock, response.c_str(), response.size()) < 0) {       // 发送响应std::cerr << "write response error" << std::endl;} else {std::cout << "send response success" << std::endl;}} else if (size == 0) { // 对端关闭连接std::cout << client_ip << ":" << client_port << " quit..." << std::endl;close(sock);break;} else {                // 读取失败std::cerr << "read request error" << std::endl;std::cout << client_ip << ":" << client_port << " quit..." << std::endl;close(sock);break;}}}

简单TCP客户端实现

创建套接字

class TcpClient{
public:  TcpClient(std::string& ip, short port): sockfd(-1), server_ip(ip), server_port(port){}~TcpClient(){if (sockfd > 0) close (sockfd);};int ClientInit() {// 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {std::cerr << "socket error" << std::endl;exit(2);}std::cout << "socket success" << std::endl;return 0;}
private:int sockfd;std::string server_ip;short server_port;
};

客户端发起连接

由于客户端不需要用户手动绑定也不需要监听,所以客户端创建好套接字后就可以直接向服务端发起连接请求

 int connect(int socket, const struct sockaddr *address, socklen_t address_len);
  • socket : 特定的套接字,表示通过该套接字发起连接请求
  • address : 对端的网络相关信息
  • addrlen: 传入的addr结构体的长度
  • 绑定成功返回0,连接失败返回-1,同时错误码被设置
  void Start() {// 发送连接请求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());if (connect(sockfd, (struct sockaddr*)&server, sizeof(server)) < 0) {std::cerr << "connect error" << std::endl;exit(3);}std::cout << "connect success" << std::endl;Request();}

客户端发起请求

这里的代码逻辑非常简单,可以稍微看一看

void Request() {for (; ;) {std::string msg;getline(cin, msg);if (msg == "quit") {break;}write(sockfd, msg.c_str(), msg.size());#define BUFFER_SIZE 128char buffer[BUFFER_SIZE];ssize_t size = read(sockfd, buffer, sizeof(buffer) - 1);if (size > 0) {buffer[size] = 0;std::cout << buffer << std::endl;}}}

服务器简单测试

首先使用telnet进行连接测试,可以看到服务器可以正常建立连接。控制telnet给服务器发送信息服务器可以接收并能返回响应

使用netstat命令查看,可以看到一个名为tcp_server的进程正处于监听状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kiPnBeEe-1688734778610)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707112949813.png)]

使用我们的客户端连接,可以看到服务端可以打印客户端的IP地址和端口号以及发送的数据,客户端也可以接收服务器发来的响应。客户端一旦退出,服务器也会立刻接收到并作出反应。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pm6hk0UN-1688734711199)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707113253782.png)]

服务器简单测评

当我们仅仅使用一个客户端连接服务端时,客户端可以正常享受服务器的服务。但是若再来一个客户端时,虽然新来的客户端也可以成功建立连接,但是我们的服务器正在为第一个客户端提供服务,无法立马处理第二个客户端的请求。只有等第一个客户端推出后,才能对第二个客户端发来的数据进行打印

单执行流服务器

这是因为我们的服务器是一个单执行流的,这种服务一一次只能为一个客户端提供服务。

单执行流服务器为什么可以同时和多个客户端建立连接

当服务端在给第一个客户端提供服务期间,第二个客户端发送连接请求时是成功的,这是因为连接其实已经建立,只是服务端还没有调用accept函数将连接获取上来罢了

前文在介绍listen接口的时候提到一个参数backlog,实际上操作系统在底层会为我们维护一个连接队列。服务端没有accept的新连接会被放在这个连接队列中,而这个队列的最大长度是由backlog决定的。所以虽然服务端没有使用accept获取第二个客户端发来的请求,但实际上链接已经建成了

那么如何解决服务器只能给一个客户端提供服务这个问题呢?? 很简单只要提供多进程版的服务器或者多线程版的就可以了

多进程版TCP服务器

当服务端调用accept函数获取到新连接,并未新连接打开网络文件后,让当前执行流调用fork()函数创键子进程,让子进程为父进程获取到的链接提供服务,由于父子进程是两个不同的执行流,父进程创建子进程后可以继续从监听套接字中获取新连接,不需要关心服务

子进程会继承父进程的文件描述符表

文件描述符表是隶属于一个进程的,子进程创建后会"复制"一份父进程的文件描述符表,之后父子进程之间会保持独立性。对于套接字文件(网络文件)也是一样的,父进程创建的子进程也会继承父进程的套接字文件信息,此时子进程也就能对特定的套接字文件进行读写操作

等待子进程问题

当父进程创建子进程后,父进程是必须等待子进程退出的,以防止子进程变成僵尸进程造成内存泄漏。如果服务端进行阻塞式等待子进程,那么服务端还是必须等待客户端的服务完毕才能获取下一个服务请求,显然不合理。若采用非阻塞方式等待子进程,那么服务端就必须将所有子进程的PID保存下来,并每隔一段时间要对所有链接进行检测,显然非常麻烦

总之,无论采用阻塞或非阻塞的方式等待子进程,都不能很好的帮助我们将获取链接和提供服务分离。

不等待子进程退出的方式

1、 捕捉SIGCHLD信号,将其处理动作设置成忽略

2、让父进程创建子进程,子进程再创建孙子进程,子进程退出,孙子进程就被操作系统领养并为客户端进行服务

捕捉SIGCHLD信号

  void Start() {    signal(SIGCHLD, SIG_IGN); 								// 忽略SIGCHLD信号    for (;;) {    // 获取连接    struct sockaddr_in peer;    memset(&peer, 0, sizeof(peer));    socklen_t len = sizeof(peer);    int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);    if (sock < 0) {    std::cerr << "accept error, continue" << std::endl;    continue;    }    std::string client_ip = inet_ntoa(peer.sin_addr);      short client_port = ntohs(peer.sin_port);    cout << "get a new link->"  << sock << "[" << client_ip << ":" << client_port << "]" << endl;                                                                                               pid_t id = fork();    								// 创建子进程执行服务if (id == 0) {    Service(sock, client_ip, client_port);    exit(0);    }    }    }    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sMi1OtBx-1688734711199)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707123144765.png)]

孙子进程提供服务

  void Start() {// signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号for (;;) {// 获取连接struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));socklen_t len = sizeof(peer);int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);if (sock < 0) {std::cerr << "accept error, continue" << std::endl;continue;}std::string client_ip = inet_ntoa(peer.sin_addr);  short client_port = ntohs(peer.sin_port);cout << "get a new link->"  << sock << "[" << client_ip << ":" << client_port << "]" << endl;pid_t id = fork();if (id == 0) {    // 子进程pid_t id = fork();if(id == 0){    // 孙子进程Service(sock, client_ip, client_port);exit(0);}exit(0);}waitpid(id, NULL, 0);  // 直接将子进程回收了}}
while :; do ps -axj | head -1 && ps -axj | grep tcp_server | grep -v grep; echo "############"; sleep 1; done  # 监视脚本

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aZOZXrxO-1688734711200)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707151802143.png)]

可以看到子进程直接就推出了,孙子进程正在为客户端提供服务。当客户端推出后,孙子进程直接就被操作系统回收了。它的PPID为1号进程,表明这是一个孤儿进程

多线程版TCP服务器

创建进程的成本非常高,而创建线程的成本就会小很多,因为线程本质就是再进程地址空间中运行的,创建出来的线程共享大部分资源。因此实现多执行流的服务器最好采用多线程进行实现

while :; do ps -aL | head -1 && ps -aL | grep tcp_server| grep -v grep; echo "#########################"; sleep 1; done

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ljGpvcxw-1688734711200)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707155728792.png)]

// 1、参数列表
struct Args{Args(int _sock, std::string& _ip, short _port): sock(_sock), ip(_ip), port(_port) {}int sock;std::string ip;short port;
};void Start() {for (;;) {// 获取连接struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));socklen_t len = sizeof(peer);int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);if (sock < 0) {std::cerr << "accept error, continue" << std::endl;continue;}std::string client_ip = inet_ntoa(peer.sin_addr);  short client_port = ntohs(peer.sin_port);cout << "get a new link->"  << sock << "[" << client_ip << ":" << client_port << "]" << endl;// 2、编写多线程部分pthread_t tid; struct Args *args = new struct Args(sock, client_ip, client_port);pthread_create(&tid, NULL, Service, (void*)args); pthread_detach(tid);}}// 3、将Service函数改为静态函数,使用struct Args* 指针将三个参数构成结构体传进去static void* Service(void* arg) {struct Args* args = (struct Args*)arg;int sock = args->sock;std::string client_ip = args->ip;short client_port = args->port;delete args;for (;;) {
#define BUFFER_SIZE 128char buffer[BUFFER_SIZE];ssize_t size = read(sock, buffer, BUFFER_SIZE - 1);if (size > 0) {          // 读取成功buffer[size] = 0;std::cout << client_ip << ":" << client_port << " # " << buffer << std::endl;std::string response = "tcp server say # ";response += buffer;if (write(sock, response.c_str(), response.size()) < 0) {std::cerr << "write response error" << std::endl;} else {std::cout << "send response success" << std::endl;}} else if (size == 0) { // 对端关闭连接std::cout << client_ip << ":" << client_port << " quit..." << std::endl;close(sock);break;} else {                // 读取失败std::cerr << "read request error" << std::endl;std::cout << client_ip << ":" << client_port << " quit..." << std::endl;close(sock);break;}}}

该对线程服务器存在的问题

  • 每当新线程到来时,服务端的主线程才会为客户端创建新线程,而服务结束又会将该线程销毁。就像我们去食堂吃饭,我们去了食堂阿姨才开始做饭。效率低下。
  • 如果有大量的客户端连接请求到来,计算机就要一一创建服务线程,线程越多CPU的压力也就越大。因为CPU要在这些线程之间来回切换,线程间切换的成本就变得很高,此外线程变多,每个线程被调度到的时间就变长了,用户体验变差

解决方案

  • 可以预先创建一批线程,当有客户端请求连接到来时就为这些线程提供服务。而不是客户端来了才创建线程
  • 当某个线程对客户端提供服务完成后,不让该线程推出,让该线程继续给下一个客户端提供服务。如果没有可以让线程先进入休眠状态
  • 服务端创建的一批线程数量不能太多。此外,如果有海量客户端接连到来,可以将这些新来的连接放在等待队列中进行排队,等服务端这一批线程有空闲线程后再将连接拿上来处理

实际解决上述问题就是要让我们再服务端引入线程池。线程池可以预先存储线程并使线程循环往复的工作,并且线程池中还有一个任务队列可以用于存储任务。如果有任务就从任务队列中Pop任务,并调用任务对应的Run函数对任务进行处理,如果没有任务就进入休眠状态

线程池版TCP服务器

线程池的实现在多线程那一章已经讲过了,这里直接套用了

#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>#define THREAD_NUM 5template<typename T>
class Thread_Pool{
public:static Thread_Pool* GetInstance(size_t _thread_num = THREAD_NUM);static void* Routine(void* arg);~Thread_Pool();void PushTask(const T& task);void PopTask(T* task);private:Thread_Pool(size_t _thread_num);privatebool IsEmpty() { return task_que.empty(); }void QueueLock() { pthread_mutex_lock(&mtx); }void QueueUnLock() { pthread_mutex_unlock(&mtx); }void Wait() { pthread_cond_wait(&cond, &mtx); }void Wakeup() { pthread_cond_signal(&cond); }private:size_t thread_num;std::queue<T> task_que;pthread_mutex_t mtx;pthread_cond_t cond;static Thread_Pool* instance;
};template<typename T>
Thread_Pool<T>* Thread_Pool<T>::instance = nullptr;template<typename T>
Thread_Pool<T>* Thread_Pool<T>::GetInstance(size_t _thread_num) {if (instance == nullptr) instance = new Thread_Pool(_thread_num);return instance;
}template<typename T>
Thread_Pool<T>::Thread_Pool(size_t _thread_num) : thread_num(_thread_num){pthread_mutex_init(&mtx, NULL);pthread_cond_init(&cond, NULL);for (int i = 0; i < thread_num; i++) {pthread_t tid;pthread_create(&tid, NULL, Routine, (void*)this);pthread_detach(tid);}}template<typename T>
Thread_Pool<T>::~Thread_Pool() {pthread_mutex_destroy(&mtx);pthread_cond_destroy(&cond);
}template<typename T> 
void Thread_Pool<T>::PushTask(const T& task) {QueueLock();task_que.push(task);QueueUnLock();Wakeup();
}template<typename T>
void* Thread_Pool<T>::Routine(void* arg) {Thread_Pool<T>* tp = (Thread_Pool<T>*)arg;tp->QueueLock();while (tp->IsEmpty()) {tp->Wait();}T* task = new T;tp->PopTask(task);tp->QueueUnLock();task->Run();// std::cout << task << std::endl; // for testdelete task;
}template<typename T>
void Thread_Pool<T>::PopTask(T* task) {*task = task_que.front();task_que.pop();
}

现在想向服务器中引入线程池,因此在服务器类中新增一个线程池的指针成员

  • 在实例化服务器对象时,先将线程池指针初始化为空
  • 当服务器初始化完毕,进入正常运行阶段使用GetInstance接口获取单例线程池。
  • 主线程之后就用于获取连接,然后将获取到的客户端ip, port以及打开的网络文件sockfd打包成一个任务交给线程池的任务队列

线程池中的线程就通过不断获取任务队列中的任务,通过task中包含的信息为客户端提供服务

这实际也是一个生产消费模型,其中监听进程就是任务的生产者,线程池中的若干线程就是消费者,交易场所就是线程池中的任务队列

任务类的设计

任务类中必须包含服务器和客户端进行通信所需要的数据信息,包含网络套接字,客户端的IP,客户端的端口号。表示该任务是为哪一个客户端提供服务的,使用的是哪一个网络文件

任务类中还必须带有一个Run()方法,线程池中的线程拿到数据后交给Run方法对任务进行处理(通信),这个方法实际就是上文实现的Service函数,将其放入任务类中充当Run()方法,但是这样实际上并不利于软件分层。我们可以给任务类新增一个仿函数,当任务执行Run方法处理任务时就可以以回调的方式处理该任务

#pragma once 
#include <iostream>
#include "handler.hpp"class Task{
public:Task() {};~Task() {};Task(int _sockfd, std::string& _client_ip, short _client_port): sockfd(_sockfd), client_ip(_client_ip), client_port(_client_port){}void Run(){ handler(sockfd, client_ip, client_port); }
private:int sockfd;std::string client_ip;short client_port;Handler handler;};

仿函数类 Handler类

使用Handler类可以让我们的服务器处理不同的任务,实际想要怎么处理这个任务得由Handler函数定。如果想让服务器处理其它任务,只需要修改Handler当中的()重载函数即可,比如可以增加一个int 类型参数flag,当flag ==1 , flag == 2 ……的时候的就可以提供不同的处理方法

#include "comm.h"class Handler{
public:Handler(){};~Handler(){};void operator()(int sock, std::string client_ip, int client_port) {for (;;) {
#define BUFFER_SIZE 128char buffer[BUFFER_SIZE];ssize_t size = read(sock, buffer, BUFFER_SIZE - 1);if (size > 0) {          // 读取成功buffer[size] = 0;std::cout << client_ip << ":" << client_port << " # " << buffer << std::endl;std::string response = "tcp server say # ";response += buffer;if (write(sock, response.c_str(), response.size()) < 0) {std::cerr << "write response error" << std::endl;} else {std::cout << "send response success" << std::endl;}} else if (size == 0) { // 对端关闭连接std::cout << client_ip << ":" << client_port << " quit..." << std::endl;close(sock);break;} else {                // 读取失败std::cerr << "read request error" << std::endl;std::cout << client_ip << ":" << client_port << " quit..." << std::endl;close(sock);break;}}}
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wIk5pgoM-1688734711200)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707205621139.png)]

现在无论有多少个客户端发送请求,服务端只会有5个线程为其提供服务,线程池中的线程数不会因为客户端的增多而增多。这些线程也不会因为客户端的退出而退出
参考文章:「2021dragon」的文章
原文链接:https://blog.csdn.net/chenlong_cxy/article/details/124650187

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

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

相关文章

【RuoYi-Cloud-Plus】学习笔记 09 - Sentinel(四)熔断降级知识整理

文章目录 前言参考目录版本说明学习笔记1、包结构2、DegradeSlot3、DegradeRule4、DegradeRuleManager5、CircuitBreaker5.1 CircuitBreaker.State6、AbstractCircuitBreaker6.1、AbstractCircuitBreaker#fromCloseToOpen6.2、AbstractCircuitBreaker#fromHalfOpenToOpen6.3、A…

支付宝接入

支付宝接入 python-alipay-sdk pycryptodome一、电脑网站支付 1.1 获取支付宝密钥 沙箱网址 1.APPID 2.应用私钥 3.支付宝公钥1.2 存放密钥 在与 settings.py 的同级目录下创建 pem 文件夹pem 文件夹下创建 app_private_key.pem 和 alipay_public_key.pem app_private_key…

神经网络初谈

文章目录 简介神经网络的发展历程神经网络的初生神经网络的第一次折戟神经网络的新生&#xff0c;Hinton携BP算法登上历史舞台命途多舛&#xff0c;神经网络的第二次寒冬神经网络的重生&#xff0c;黄袍加身&#xff0c;一步封神神经网络的未来&#xff0c;众说纷纭其他时间点 …

STM32 Proteus仿真LCD12864俄罗斯方块-FZ0063

STM32 Proteus仿真LCD12864俄罗斯方块-FZ0063 Proteus仿真小实验&#xff1a; STM32 Proteus仿真LCD12864俄罗斯方块-FZ0063 功能&#xff1a; 硬件组成&#xff1a;STM32F103R6单片机 LCD12864显示器多个按键 1.标准俄罗斯方块经典游戏玩法&#xff0c;带计时&#xff0c…

Kong 服务和路由的添加

管理服务 这里参考DB-less-Mode&#xff0c;因为使用的是yaml配置文件的形式&#xff0c;所以所有的相关配置只需要往初始化的kong.yml文件中添加就可以了&#xff0c;就像nginx的配置文件 DB-less-Mode 创建服务 vim /etc/kong/kong.yml services: - name: my-service #…

MySQL---表数据高效率查询(简述)

目录 前言 一、聚合查询 &#x1f496;聚合函数 &#x1f496;GROUP BY子句 &#x1f496;HAVING 二、联合查询 &#x1f496;内连接 &#x1f496;外连接 &#x1f496;自连接 &#x1f496;子查询 &#x1f496;合并查询 &#x1f381;博主介绍&#xff1a;博客名…

Idea 修改默认 Maven 为自己的

每次我们打开新项目时,都要去配置一遍 maven,很麻烦,其实可以去修改 idea 里面默认的 maven 配置,这样后面不管是打开新项目还是老项目,就都是用的自己的 maven 了. 1.文件->新项目设置->新项目的设置 File->Other Settings -> Settings for New Project 2.然后和…

git下载源码及环境搭建之数据库(二)

学习目标&#xff1a; 数据库 新项目使用 数据库文件 的配置 及相关属性的设置 步骤&#xff1a; 数据库 下图所示为开发时所用数据库 第一步&#xff1a;新建一个数据库 注意&#xff1a; 字符集与排序规则我们应该选择utf-8 相关 选中新创建的表&#xff0c;点击备份—还…

MySQL单表查询练习题

目录 第一题 第二题 第三题 第一题 1.创建数据表pet&#xff0c;并对表进行插入、更新与删除操作&#xff0c;pet表结构如表8.3所示。 (1&#xff09;首先创建数据表pet&#xff0c;使用不同的方法将表8.4中的记录插入到pet表中。 mysql> create table pet( name varchar(…

centos7.9php8swoole5swoft2环境安装遇到确实redis扩展的解决办法

1、环境介绍 运行系统&#xff1a;centos7.9 php版本&#xff1a;php8.0.29 swoole版本&#xff1a;swoole5 swoft版本&#xff1a;swoft2.02、遇到的问题 The requested PHP extension ext-redis * is missing from your system. Install or enable PHPs redis extension。这…

python爬虫哪个库用的最多

目录 常用的python爬虫库有哪些 1. Requests&#xff1a; 2. BeautifulSoup&#xff1a; 3. Scrapy&#xff1a; 4. Selenium&#xff1a; 5. Scrapy-Redis&#xff1a; 哪个爬虫库用的最多 Scrapy示例代码 总结 常用的python爬虫库有哪些 Python拥有许多常用的爬虫库…

Java反射与“整活--(IOC容器)”

文章目录 前言反射什么是反射基本操作获取类对象获取类属性获取类方法方法的执行对构造方法的操作 注解定义获取注解 整活&#xff08;IOC容器&#xff09;项目结构IOC/DI流程ApplicationContextBeanDefinitionReaderBeanDefinitionBeanWrappergetBean&#xff08;&#xff09;…

Django_admin数据管理后台

目录 一、基础操作 二、自定义后台操作数据行为 源码等资料获取方法 admin数据管理后台是django内置的一个后台管理界面&#xff0c;能查看已注册模型类的数据结构&#xff0c;以及对数据的增删改。 一、基础操作 1.1 检查项目目录下的urls.py有没有如下配置 1.2 创建djan…

技术讨论:我心中TOP1的编程语言

欢迎关注博主 六月暴雪飞梨花 或加入【六月暴雪飞梨花】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和技术…

Flutter——最详细(NavigationBar)使用教程

NavigationBar简介 Material 3 导航栏组件! 导航栏提供了一种持久且便捷的方式来在应用程序的主要目的地之间进行切换。 使用场景&#xff1a; 底部菜单栏模块 属性作用onDestinationSelected选择索引回调监听器selectedIndex目前选定目的地的索引destinations存放菜单按钮back…

七大排序算法——冒泡排序,通俗易懂的思路讲解与图解(完整Java代码)

文章目录 一、排序的概念排序的概念排序的稳定性七大排序算法 二、冒泡排序核心思想代码实现 三、性能分析四、七大排序算法 一、排序的概念 排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或…

C++之工厂模式

目录 一、为什么要使用工厂模式 优点 缺点 二、简单工厂&#xff08;Simple Factory&#xff09; 好处&#xff1a; 不足&#xff1a; 三、工厂方法&#xff1a; 好处&#xff1a; 不足&#xff1a; 四、抽象工厂&#xff08;Abstract Factory&#xff09; 一、为什…

【HCIA】10.VLAN间通信

VLAN间通信的解决方法 使用路由器的物理接口 路由器三层接口作为网关&#xff0c;转发本网段前往其它网段的流量。路由器三层接口无法处理携带VLAN Tag的数据帧&#xff0c;因此交换机上联路由器的接口需配置为Access。路由器的一个物理接口作为一个VLAN的网关&#xff0c;因此…

Django_re_path_使用正则匹配url

与path定义的路由相比&#xff0c;re_path 定义的路由可以使用正则表达式匹配url。 需要注意的是&#xff1a; 如果未定义匹配结果的变量名&#xff0c;匹配的结果默认传入视图的第2个形参。如果定义了匹配结果的变量名&#xff0c;匹配的结果会传给视图的同名字段&#xff0…

【GitOps系列】从零上手GitOps

文章目录 GitOps 介绍如何将业务代码构建为容器镜像&#xff1f;如何将容器镜像部署到K8s&#xff1f;K8s如何实现自动扩容和自愈&#xff1f;1.传统的扩容和自愈2.k8s自愈机制3.k8s弹性扩容 如何借助GitOps实现应用秒级自动发布和回滚&#xff1f;1.传统 K8s 应用发布流程2.从…