• input子系统框架、外设驱动开发


    一、input子系统基本框架

    Linux内核为了两个目的:

    1. 简化纯输入类外设(如:键盘、鼠标、游戏杆、轨迹球、触摸屏。。。等等)的驱动开发
    2. 统一输入类外设产生的数据格式(struct input_event),更加方便应用层编程

    设计了输入子系统

    在这里插入图片描述

    事件处理层:接收来自核心层上报的事件,并选择对应的handler(事件处理器 struct input_handler)去处理。内核维护着多个事件处理器对象,每个input_handler对象专门处理一类事件,所有产生同类事件的设备驱动共用同一个handler。

    设备驱动层:主要实现获取硬件设备的数据信息(包括触摸屏被按下、按下位置、鼠标移动、键盘按下等等),并转换为核心层定义的规范事件后提交给核心层,该层每个设备对应一个struct input_dev对象,

    核心层:负责连接设备驱动层和事件处理层,为设备驱动层提供输入设备驱动的接口(struct input_dev)以及输入设备驱动的注册函数(input_register_device),为事件处理层提供输入事件驱动的接口;通知事件处理层对事件进行处理。

    二、驱动开发步骤

    /*init或probe函数中:
    1. 创建struct input_dev对象input_allocate_device
    2. 设置事件类型以及相关参数set_bit
    3. 注册struct input_dev对象input_register_device
    */
    
    /*exit或remove函数中:
    1. 注销struct input_dev对象input_unregister_device
    2. 销毁struct input_dev对象input_free_device
    */
    
    /*上报事件
    	两种事件上报方式:
    	1. 对有中断支持的输入设备:在其中断处理函数(上半部或下半部)中上报事件
    	2. 对无中断支持的输入设备:使用workqueue循环定时上报(struct delayed_work)
    	主要函数:
    	input_event
    	input_report_abs
    	input_sync
    */
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    相关接口:

    /*_init*/
    struct input_dev *input_allocate_device(void)/*
    	创建一个输入设备对象。
    	返回一个指向 input_dev 结构的指针,该结构用于表示输入设备。
    	*/
    	
    void set_bit(struct input_dev *dev, unsigned long whichbits)/*
    	设置输入设备的事件类型。dev 是输入设备对象的指针,whichbits 是一个位掩码,用于指定事件类型。
    	例如,使用 set_bit(dev, EV_KEY) 可以设置输入设备支持按键事件。
    
    	whichbits:
    		EV_KEY:按键事件。这个事件类型用于处理键盘、鼠标等输入设备的按键事件
    		EV_ABS:绝对坐标事件。这个事件类型用于处理绝对坐标的事件,例如触摸屏的触摸位置。
    	*/
    
    void input_set_abs_params(struct input_dev *dev, unsigned int axis, int min, int max, int fuzz, int flat)
    	/*
    	配置输入设备的绝对坐标参数。
    	dev 是输入设备对象的指针,
    	axis 是要配置的坐标轴(如ABS_X、ABS_Y等),
    	min 和 max 分别是坐标轴的最小值和最大值,
    	fuzz 和 flat 是用于指定坐标轴的模糊度和平坦度参数。
    	*/
    	
    int input_register_device(struct input_dev *dev)
    	/*
    	注册输入设备到内核。将输入设备对象注册到内核,以便它可以开始上报事件。
    	*/
    
    /*_exit*/
    void input_unregister_device(struct input_dev *dev)//注销输入设备。从内核中注销输入设备,停止事件的上报。
    void input_free_device(struct input_dev *dev)//释放输入设备。释放输入设备对象的内存。
    
    /*上报事件*/
    void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)
    	/*
    	上报一个通用输入事件。通常,这个函数用于上报不是绝对坐标或按键事件的事件。
    	dev 是输入设备对象的指针,
    	type 表示事件类型,
    	code 表示事件代码(例如,按键代码),
    	value 表示事件的值。
    	*/
    
    void input_report_key(struct input_dev *dev, unsigned int code, int value)
    	/*
    	上报按键事件。
    	dev 是输入设备对象的指针,
    	code 表示按键事件的代码,
    	value 表示按键事件的值(0表示松开,1表示按下)。
    	*/
    	
    void input_report_abs(struct input_dev *dev, unsigned int code, int value)
    	/*
    	上报绝对坐标事件。
    	dev 是输入设备对象的指针,
    	code 表示绝对坐标事件的代码,
    	value 表示绝对坐标的值。
    	*/
    
    void input_sync(struct input_dev *dev)//上报完成后需要调用这些函数来通知系统处理完整事件, 这个函数告诉内核事件已经完整,可以处理了。
    
    /*应用层数据类型*/
    struct input_event { 		//这是一个用于表示输入事件的结构体。它包含以下字段:
    	struct timeval time; 	//事件的时间戳。
    	__u16 type; 			//事件类型,如 EV_KEY(按键事件)、EV_ABS(绝对坐标事件)等。
    	__u16 code; 			//事件代码,具体表示事件的含义,例如按下哪个键或是哪个绝对坐标轴。
    	__s32 value; 			//事件的值,通常表示按键的状态(按下或松开)或绝对坐标的值。
    }
    
    • 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

    三、key2-input版代码解析

    key2.c

    #include           // Linux内核模块头文件
    #include           // 内核相关功能的头文件
    #include               // 文件系统相关功能的头文件
    #include             // GPIO库的头文件
    #include        // 中断处理相关功能的头文件
    #include          // Open Firmware GPIO相关功能的头文件
    #include           // Open Firmware中断相关功能的头文件
    #include             // 字符设备相关功能的头文件
    #include             // 等待队列相关功能的头文件
    #include            // 调度相关功能的头文件
    #include             // poll相关功能的头文件
    #include               // 内存管理相关功能的头文件
    #include            // 输入子系统相关功能的头文件
    #include            // 延时相关功能的头文件
    #include             // 内存分配相关功能的头文件
    #include            // 用户态内核态数据传输相关功能的头文件
    
    struct fs4412key2_dev
    {
    	struct input_dev *pdev;     // 输入设备结构体指针,用于注册输入事件
    
    	int gpio;                   // GPIO引脚的编号
    	int irqno;                  // 中断编号
    };
    
    struct fs4412key2_dev *pgmydev = NULL; // 指向驱动程序数据结构的指针
    
    // 中断处理函数,处理按键中断
    irqreturn_t key2_irq_handle(int no, void *arg)
    {
    	struct fs4412key2_dev *pmydev = (struct fs4412key2_dev *)arg;
    	int status1 = 0;
    	int status2 = 0;
    
    	// 读取GPIO引脚状态两次,用于防抖
    	status1 = gpio_get_value(pmydev->gpio);
    	mdelay(1);
    	status2 = gpio_get_value(pmydev->gpio);
    
    	// 如果两次状态不一致,认为是抖动,不处理
    	if (status1 != status2)
    	{
    		return IRQ_NONE;
    	}
    
    	// 根据按键状态生成输入事件
    	if (status1)
    	{
    		input_event(pmydev->pdev, EV_KEY, KEY_2, 0); // 按键释放事件
    		input_sync(pmydev->pdev);                   // 同步输入事件
    	}
    	else
    	{
    		input_event(pmydev->pdev, EV_KEY, KEY_2, 1); // 按键按下事件
    		input_sync(pmydev->pdev);                   // 同步输入事件
    	}
    
    	return IRQ_HANDLED;
    }
    
    // 模块初始化函数
    int __init fs4412key2_init(void)
    {
    	int ret = 0;
    
    	struct device_node *pnode = NULL;
    
    	// 查找设备树节点
    	pnode = of_find_node_by_path("/mykey2_node");
    	if (NULL == pnode)
    	{
    		printk("find node failed\n");
    		return -1;
    	}
    
    	// 分配驱动程序数据结构内存
    	pgmydev = (struct fs4412key2_dev *)kmalloc(sizeof(struct fs4412key2_dev), GFP_KERNEL);
    	if (NULL == pgmydev)
    	{
    		printk("kmalloc for struct fs4412key2_dev failed\n");
    		return -1;
    	}
    
    	// 从设备树中获取GPIO引脚编号
    	pgmydev->gpio = of_get_named_gpio(pnode, "key2-gpio", 0);
    
    	// 从设备树中获取中断编号
    	pgmydev->irqno = irq_of_parse_and_map(pnode, 0);
    
    	// 分配并注册输入设备
    	pgmydev->pdev = input_allocate_device();
    	set_bit(EV_KEY, pgmydev->pdev->evbit);
    	set_bit(KEY_2, pgmydev->pdev->keybit);
    	ret = input_register_device(pgmydev->pdev);
    
    	// 请求中断处理函数
    	ret = request_irq(pgmydev->irqno, key2_irq_handle, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, "fs4412key2", pgmydev);
    	if (ret)
    	{
    		printk("request_irq failed\n");
    		input_unregister_device(pgmydev->pdev);
    		input_free_device(pgmydev->pdev);
    		kfree(pgmydev);
    		pgmydev = NULL;
    		return -1;
    	}
    	return 0;
    }
    
    // 模块卸载函数
    void __exit fs4412key2_exit(void)
    {
    	// 释放中断
    	free_irq(pgmydev->irqno, pgmydev);
    
    	// 注销输入设备
    	input_unregister_device(pgmydev->pdev);
    	input_free_device(pgmydev->pdev);
    
    	// 释放驱动程序数据结构内存
    	kfree(pgmydev);
    	pgmydev = NULL;
    }
    
    MODULE_LICENSE("GPL"); // 指定模块许可证
    module_init(fs4412key2_init); // 指定模块初始化函数
    module_exit(fs4412key2_exit); // 指定模块卸载函数
    
    
    • 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

    testkey2.c

    #include 
    #include 
    #include 
    #include 
    #include 
    
    #include 
    
    int main(int argc,char *argv[])
    {
    	int fd = -1;
    	struct input_event evt;
    	if(argc < 2)
    	{
    		printf("Argument is too few\n");
    		return 1;
    	}
    
    	/*open*/
    	fd = open(argv[1],O_RDONLY);
    	if(fd < 0)
    	{
    		printf("open %s failed\n",argv[1]);
    		return 2;
    	}
    
    	/*init mpu6050*/
    
    	while(1)
    	{
    		read(fd,&evt,sizeof(evt));
    		if(evt.type == EV_KEY && evt.code == KEY_2)
    		{
    			if(evt.value)
    			{
    				printf("KEY2 DOWN\n");
    			}
    			else
    			{
    				printf("KEY2 UP\n");
    			}
    		}
    	}
    
    	/*close*/
    	close(fd);
    	fd = -1;
    	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

    四、mpu6050-input版代码解析

    mpu6050drv.c

    #include           // Linux内核模块头文件
    #include           // 内核相关功能的头文件
    #include               // 文件系统相关功能的头文件
    #include              // I2C总线相关功能的头文件
    #include             // 字符设备相关功能的头文件
    #include             // 等待队列相关功能的头文件
    #include            // 调度相关功能的头文件
    #include             // poll相关功能的头文件
    #include             // 内存分配相关功能的头文件
    #include               // 内存管理相关功能的头文件
    #include            // 输入子系统相关功能的头文件
    #include               // I/O内存操作相关功能的头文件
    #include            // 用户态内核态数据传输相关功能的头文件
    #include             // 原子操作相关功能的头文件
    
    /****************MPU6050内部寄存器地址****************/
    
    #define	SMPLRT_DIV		0x19	//陀螺仪采样率,典型值:0x07(125Hz)
    #define	CONFIG			0x1A	//低通滤波频率,典型值:0x06(5Hz)
    #define	GYRO_CONFIG		0x1B	//陀螺仪自检及测量范围,典型值:0x18(不自检,2000deg/s)
    #define	ACCEL_CONFIG	0x1C	//加速计自检、测量范围及高通滤波频率,典型值:0x18(不自检,2G,5Hz)
    #define	ACCEL_XOUT_H	0x3B
    #define	ACCEL_XOUT_L	0x3C
    #define	ACCEL_YOUT_H	0x3D
    #define	ACCEL_YOUT_L	0x3E
    #define	ACCEL_ZOUT_H	0x3F
    #define	ACCEL_ZOUT_L	0x40
    #define	TEMP_OUT_H		0x41
    #define	TEMP_OUT_L		0x42
    #define	GYRO_XOUT_H		0x43
    #define	GYRO_XOUT_L		0x44
    #define	GYRO_YOUT_H		0x45
    #define	GYRO_YOUT_L		0x46
    #define	GYRO_ZOUT_H		0x47
    #define	GYRO_ZOUT_L		0x48
    #define	PWR_MGMT_1		0x6B	//电源管理,典型值:0x00(正常启用)
    #define	WHO_AM_I		0x75	//IIC地址寄存器(默认数值0x68,只读)
    #define	SlaveAddress	0x68	//MPU6050-I2C地址
    
    // 定义MPU6050设备结构体
    struct mpu6050_dev
    {
    	struct input_dev * pinput;   // 输入设备结构体指针
    
    	struct i2c_client *pclient; // I2C客户端结构体指针
    
    	struct delayed_work work;   // 延迟工作结构体
    };
    
    struct mpu6050_dev *pgmydev = NULL; // 指向MPU6050设备数据结构的指针
    
    // 读取MPU6050寄存器的函数
    int mpu6050_read_byte(struct i2c_client *pclt, unsigned char reg)
    {
    	int ret = 0;
    	char txbuf[1] = {reg};
    	char rxbuf[1] = {0};
    
    	struct i2c_msg msg[2] = 
    	{
    		{pclt->addr, 0, 1, txbuf},
    		{pclt->addr, I2C_M_RD, 1, rxbuf}
    	};
    
    	ret = i2c_transfer(pclt->adapter, msg, ARRAY_SIZE(msg));
    	if (ret < 0)
    	{
    		printk("ret = %d, in mpu6050_read_byte\n", ret);
    		return ret;
    	}
    
    	return rxbuf[0];
    }
    
    // 写入MPU6050寄存器的函数
    int mpu6050_write_byte(struct i2c_client *pclt, unsigned char reg, unsigned char val)
    {
    	int ret = 0;
    	char txbuf[2] = {reg, val};
    
    	struct i2c_msg msg[1] = 
    	{
    		{pclt->addr, 0, 2, txbuf},
    	};
    
    	ret = i2c_transfer(pclt->adapter, msg, ARRAY_SIZE(msg));
    	if (ret < 0)
    	{
    		printk("ret = %d, in mpu6050_write_byte\n", ret);
    		return ret;
    	}
    
    	return 0;
    }
    
    // 延迟工作函数,用于读取MPU6050传感器数据
    void mpu6050_work_func(struct work_struct *pwk)
    {
    	struct mpu6050_dev *pmydev = container_of((struct delayed_work *)pwk, struct mpu6050_dev, work);
    	unsigned short ax = 0;
    	unsigned short ay = 0;
    	unsigned short az = 0;
    	unsigned short gx = 0;
    	unsigned short gy = 0;
    	unsigned short gz = 0;
    	unsigned short temp = 0;
    
    	// 读取加速度和陀螺仪数据
    	ax = mpu6050_read_byte(pmydev->pclient, ACCEL_XOUT_L);
    	ax |= (mpu6050_read_byte(pmydev->pclient, ACCEL_XOUT_H) << 8);
    	input_report_abs(pmydev->pinput, ABS_X, ax);
    
    	ay = mpu6050_read_byte(pmydev->pclient, ACCEL_YOUT_L);
    	ay |= (mpu6050_read_byte(pmydev->pclient, ACCEL_YOUT_H) << 8);
    	input_report_abs(pmydev->pinput, ABS_Y, ay);
    
    	az = mpu6050_read_byte(pmydev->pclient, ACCEL_ZOUT_L);
    	az |= (mpu6050_read_byte(pmydev->pclient, ACCEL_ZOUT_H) << 8);
    	input_report_abs(pmydev->pinput, ABS_Z, az);
    
    	gx = mpu6050_read_byte(pmydev->pclient, GYRO_XOUT_L);
    	gx |= (mpu6050_read_byte(pmydev->pclient, GYRO_XOUT_H) << 8);
    	input_report_abs(pmydev->pinput, ABS_RX, gx);
    
    	gy = mpu6050_read_byte(pmydev->pclient, GYRO_YOUT_L);
    	gy |= (mpu6050_read_byte(pmydev->pclient, GYRO_YOUT_H) << 8);
    	input_report_abs(pmydev->pinput, ABS_RY, gy);
    
    	gz = mpu6050_read_byte(pmydev->pclient, GYRO_ZOUT_L);
    	gz |= (mpu6050_read_byte(pmydev->pclient, GYRO_ZOUT_H) << 8);
    	input_report_abs(pmydev->pinput, ABS_RZ, gz);
    
    	temp = mpu6050_read_byte(pmydev->pclient, TEMP_OUT_L);
    	temp |= (mpu6050_read_byte(pmydev->pclient, TEMP_OUT_H) << 8);
    	input_report_abs(pmydev->pinput, ABS_MISC, temp);
    
    	input_sync(pmydev->pinput);
    	schedule_delayed_work(&pgmydev->work, msecs_to_jiffies(1000)); // 延迟1秒后再次读取数据
    }
    
    // 初始化MPU6050传感器
    void init_mpu6050(struct i2c_client *pclt)
    {
    	mpu6050_write_byte(pclt, PWR_MGMT_1, 0x00);
    	mpu6050_write_byte(pclt, SMPLRT_DIV, 0x07);
    	mpu6050_write_byte(pclt, CONFIG, 0x06);
    	mpu6050_write_byte(pclt, GYRO_CONFIG, 0xF8);
    	mpu6050_write_byte(pclt, ACCEL_CONFIG, 0x19);
    }
    
    // I2C设备驱动的探测函数
    static int mpu6050_probe(struct i2c_client *pclt, const struct i2c_device_id *pid)
    {
    	int ret = 0;
    
    	pgmydev = (struct mpu6050_dev *)kmalloc(sizeof(struct mpu6050_dev), GFP_KERNEL);
    	if (NULL == pgmydev)
    	{
    		printk("kmalloc failed\n");
    		return -1;
    	}
    	memset(pgmydev, 0, sizeof(struct mpu6050_dev));
    
    	pgmydev->pclient = pclt;
    
    	init_mpu6050(pgmydev->pclient);
    
    	pgmydev->pinput = input_allocate_device();
    
    	set_bit(EV_ABS, pgmydev->pinput->evbit);
    	input_set_abs_params(pgmydev->pinput, ABS_X, -32768, 32767, 0, 0);
    	input_set_abs_params(pgmydev->pinput, ABS_Y, -32768, 32767, 0, 0);
    	input_set_abs_params(pgmydev->pinput, ABS_Z, -32768, 32767, 0, 0);
    	input_set_abs_params(pgmydev->pinput, ABS_RX, -32768, 32767, 0, 0);
    	input_set_abs_params(pgmydev->pinput, ABS_RY, -32768, 32767, 0, 0);
    	input_set_abs_params(pgmydev->pinput, ABS_RZ, -32768, 32767, 0, 0);
    	input_set_abs_params(pgmydev->pinput, ABS_MISC, -32768, 32767, 0, 0);
    
    	ret = input_register_device(pgmydev->pinput);
    	if (ret)
    	{
    		printk("input_register_device failed\n");
    
    		input_free_device(pgmydev->pinput);
    		pgmydev->pinput = NULL;
    
    		kfree(pgmydev);
    		pgmydev = NULL;
    		return -1;
    	}
    
    	INIT_DELAYED_WORK(&pgmydev->work, mpu6050_work_func);
    
    	schedule_delayed_work(&pgmydev->work, msecs_to_jiffies(1000)); // 初始化后立即开始读取数据
    
    	return 0;
    }
    
    // I2C设备驱动的卸载函数
    static int mpu6050_remove(struct i2c_client *pclt)
    {
    	cancel_delayed_work(&pgmydev->work);
    
    	input_unregister_device(pgmydev->pinput);
    
    	input_free_device(pgmydev->pinput);
    	pgmydev->pinput = NULL;
    
    	kfree(pgmydev);
    	pgmydev = NULL;
    
    	return 0;
    }
    
    // 匹配设备树中的MPU6050节点
    struct of_device_id mpu6050_dt[] =
    {
    	{.compatible = "invensense,mpu6050"},
    	{}
    };
    
    // 定义MPU6050设备驱动的ID
    struct i2c_device_id mpu6050_ids[] =
    {
    	{"mpu6050", 0},
    	{}
    };
    
    // 定义MPU6050设备驱动结构体
    struct i2c_driver mpu6050_driver =
    {
    	.driver = {
    		.name = "mpu6050",
    		.owner = THIS_MODULE,
    		.of_match_table = mpu6050_dt,
    	},
    	.probe = mpu6050_probe,
    	.remove = mpu6050_remove,
    	.id_table = mpu6050_ids,
    };
    
    // 注册MPU6050设备驱动
    module_i2c_driver(mpu6050_driver);
    
    MODULE_LICENSE("GPL"); // 指定模块许可证
    
    
    • 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
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
  • 相关阅读:
    Minio入门系列【3】MinIO Client使用详解
    python 实现邮件发送详细解析 附代码(全)
    容器安全之镜像扫描
    Python安装教程
    前端在请求数据时使用节流来防止多个重复请求
    项目九:学会python爬虫数据保存(小白圆满级)
    Android 使用webView加载html页面
    聊聊流式数据湖Paimon(五)
    4、数据结构
    http客户端Feign
  • 原文地址:https://blog.csdn.net/qq_36091214/article/details/132795451