mosquitto客户端配置参数中的6个属性指针管理(mosquitto2.0.15客户端源码分析之二)

前言

mosquitto2.0.15定义了一个struct mqtt5__property 结构体用来支持MQTT5.0协议中的属性功能。
在《mosquitto客户端配置参数管理(mosquitto2.0.15客户端源码分析之一)》一文中,我们详细地介绍了源代码如何从配置文件或命令行输入方式读入参数并赋值的过程,配置参数中与MQTT5.0增加的属性部分的参数,则极少涉及。
事实上,mosquitto2.0.15客户端源码在struct mosq_config 结构体中定义了6个指针,用来保存需要设置的 MQTT 消息中的属性。
本文详细讨论了mosquitto2.0.15客户端源码中如何初始化和管理这些指针。

一、struct mosquitto_property结构体

struct mqtt5__property 是Mosquitto 2.0.15源码中用于支持MQTT 5.0属性功能的一个数据结构,它定义了消息中可能出现的不同属性值以及属性值的数据类型。结构体定义如下:

struct mqtt__string {
	char *v;
	uint16_t len;
};
struct mqtt5__property {
	struct mqtt5__property *next;
	union {
		uint8_t i8;
		uint16_t i16;
		uint32_t i32;
		uint32_t varint;
		struct mqtt__string bin;
		struct mqtt__string s;
	} value;
	struct mqtt__string name;
	int32_t identifier;
	bool client_generated;
};
  • struct mqtt5__property 中的字符串,都是动态生成的,使用struct mqtt__string表达,包含一个char类型指针v和一个uint16_t类型的长度len。
  • 属性值value的类型,使用union类型,方便按照需求灵活定义,可以是下面的类型:
    1. uint8_t i8:一个8位整型。
    2. uint16_t i16:一个16位整型。
    3. uint32_t i32:一个32位整型。
    4. uint32_t varint:一个可变长度整型,由MQTT协议规定。
    5. struct mqtt__string bin:一个mqtt__string类型的二进制数据。
    6. struct mqtt__string s:一个mqtt__string类型的文本字符串。
  • name:属性的名称,一个mqtt__string类型的字符串。
  • identifier:属性的标识符,一个32位整型。
  • client_generated:该属性是否是由客户端生成的,一个bool类型。
    这个结构体可以用于MQTTv5协议中的属性传输和封装,通过属性名和值的组合体现了MQTTv5协议的灵活性和可扩展性。

二、六个指向mosquitto_property类型的指针

mosquitto2.0.15客户端源码在struct mosq_config 结构体中定义了6个指针,用来保存需要设置的 MQTT 消息中的属性。

struct mosq_config
 {
  ...
	mosquitto_property *connect_props;
	mosquitto_property *publish_props;
	mosquitto_property *subscribe_props;
	mosquitto_property *unsubscribe_props;
	mosquitto_property *disconnect_props;
	mosquitto_property *will_props;
	...
}

这六个指针,保存的属性如下:

  • cfg->connect_props用于存储MQTT连接时的属性。
  • cfg->publish_props用于存储MQTT发布消息时的属性。
  • cfg->subscribe_props用于存储MQTT订阅主题时的属性。
  • cfg->unsubscribe_props用于存储MQTT取消订阅主题时的属性。
  • cfg->disconnect_props用于存储MQTT断开连接时的属性。
  • cfg->will_props用于存储MQTT遗嘱消息的属性。

这些属性都是关于MQTT协议中消息传递时的一些元数据信息,通过这些属性可以更准确地控制MQTT消息的传递过程。例如,连接属性可以包括客户端ID、清除会话标志、用户名、密码等信息。订阅属性可以包括QoS等级、订阅标识符等信息。发布属性可以包括QoS等级、保留标志、主题别名等信息。

这些属性可以是在MQTT中定义的标准属性,也可以是用户自定义的属性。这些属性的设置和获取都使用MQTT协议中定义的方法和规则,可以确保确保协议的正确性和兼容性。

三、配置属性参数过程

1.从配置文件或命令行输入的参数初始化属性

(1)从配置文件或命令行输入中获取参数(client_config_line_proc() 函数)

client_config_line_proc 函数是 Mosquitto2.0.15中的一个函数,它的功能是逐行解析配置文件或命令行参数,并将解析后的值保存在 mosq_config 结构体对象cfg中,我们在《mosquitto客户端配置参数管理(mosquitto2.0.15客户端源码分析之一)》已经详细讨论了这个函数。下面这段代码是 client_config_line_proc 函数中节选的,当函数判断参数是否为"-D"或"–property",即输入的参数为MQTT属性参数,此时需要调用cfg_parse_property函数进行解析,解析完成后,将协议版本设置为MQTT_PROTOCOL_V5。

int client_config_line_proc(struct mosq_config *cfg, int pub_or_sub, int argc, char *argv[])
{
  ...
		}else if(!strcmp(argv[i], "-D") || !strcmp(argv[i], "--property")){
			i++;
			if(cfg_parse_property(cfg, argc, argv, &i)){
				return 1;
			}
			cfg->protocol_version = MQTT_PROTOCOL_V5;
		}
	...
}

(2)参数解析和初始化(cfg_parse_property()函数)

  • 函数功能:解析mosquitto配置文件中的属性值

  • 函数参数:
    struct mosq_config *cfg: 配置文件结构体指针
    int argc: 参数个数
    char *argv[]: 参数数组
    int *idx: 当前处理的参数在数组中的位置

  • 函数流程:

  1. 检查参数合法性,如果参数个数小于2,返回MOSQ_ERR_INVAL(无效参数)。
  2. 在配置文件结构体中查找当前参数,如果未找到,返回MOSQ_ERR_UNKNOWN(未知参数)。
  3. 根据参数在配置文件结构体中对应字段的类型,解析参数值,并存储到配置文件结构体中对应字段。
  • 解析过程:
    cfg_parse_property函数用于解析mosquitto配置文件中的属性值,函数通过传递参数argc、argv以及cfg结构体实现对配置文件的解析。函数首先检查传入参数是否合法,如果参数个数小于2,则直接返回MOSQ_ERR_INVAL,代表参数无效。随后函数在cfg结构体中查找当前参数的信息,如果未找到,则返回MOSQ_ERR_UNKNOWN,代表未知参数。最后,函数根据参数在cfg结构体中对应字段的类型,对参数进行解析,并将结果存储到cfg结构体中对应字段中。由于不同参数有不同的解析方式,因此函数主要是通过switch语句根据参数类型进行相应的解析,例如对于字符串类型,函数使用字符串拷贝函数strdup进行拷贝。函数执行成功后返回MOSQ_ERR_SUCCESS,下面我们会逐段对代码进行分析。
int cfg_parse_property(struct mosq_config *cfg, int argc, char *argv[], int *idx)
{
	char *cmdname = NULL, *propname = NULL;
	char *key = NULL, *value = NULL;
	int cmd, identifier, type;
	mosquitto_property **proplist;
	int rc;
	long tmpl;
	size_t szt;

	/* idx now points to "command" */
	if((*idx)+2 > argc-1){
		/* Not enough args */
		fprintf(stderr, "Error: --property argument given but not enough arguments specified.\n\n");
		return MOSQ_ERR_INVAL;
	}

上段代码除了定义函数需要的局部变量外,检查传入参数是否合法,如果参数个数小于2,则直接返回MOSQ_ERR_INVAL,代表参数无效。

	cmdname = argv[*idx];
	if(mosquitto_string_to_command(cmdname, &cmd)){
		fprintf(stderr, "Error: Invalid command given in --property argument.\n\n");
		return MOSQ_ERR_INVAL;
	}

上段代码首先从argv[*idx+0],获得命令的名称cmdname ,然后将命令名称字符串转对应成整型数cmd,cmd为属性设置的三个要素之一。
转换调用了mosquitto_string_to_command函数,通过字符串比较的方式将字符串命令转换为对应的枚举指令。

	propname = argv[(*idx)+1];
	if(mosquitto_string_to_property_info(propname, &identifier, &type)){
		fprintf(stderr, "Error: Invalid property name given in --property argument.\n\n");
		return MOSQ_ERR_INVAL;
	}

上段代码首先从argv[(*idx)+1]获得属性名称propname ,用属性名称对应出identifier--属性标识符, type--属性类型
以上代码段调用mosquitto_string_to_property_info()函数,用于将属性名字符串转换为对应属性的标识符和类型。
需要注意的是,当propname = “user-property”,此时是用户自定义属性,会得到下面的结果:

  • identifier = MQTT_PROP_USER_PROPERTY;
  • type = MQTT_PROP_TYPE_STRING_PAIR;
	if(mosquitto_property_check_command(cmd, identifier)){
		fprintf(stderr, "Error: %s property not allowed for %s in --property argument.\n\n", propname, cmdname);
		return MOSQ_ERR_INVAL;
	}

代码到此已经的到了属性初始化需要的3个要素cmd, identifier, type
上面的代码段调用mosquitto_property_check_command函数,检查上面的命令、标识符对(cmd–identifier)是否合法。
在 MQTT 协议中,每种控制报文(cmd),如 CONNECT、PUBLISH、SUBSCRIBE、PINGREQ 等等,可以支持特定的标识符。

	if(identifier == MQTT_PROP_USER_PROPERTY){
		if((*idx)+3 > argc-1){
			/* Not enough args */
			fprintf(stderr, "Error: --property argument given but not enough arguments specified.\n\n");
			return MOSQ_ERR_INVAL;
		}

		key = argv[(*idx)+2];
		value = argv[(*idx)+3];
		(*idx) += 3;
	}else{
		value = argv[(*idx)+2];
		(*idx) += 2;
	}

MQTT_PROP_USER_PROPERTY是MQTT 5.0协议中定义的一个属性码,用于表示用户自定义属性。当客户端和服务端之间进行MQTT消息传递时,可以通过这个属性码将自定义的键值对属性信息传递给对方。MQTT_PROP_USER_PROPERTY属性码为MQTT 5.0协议提供了更加灵活的消息传递方式,开发者可以根据自己的需求携带自定义的信息,并对其进行处理。举个例子,假设我们要将一些额外的信息携带到MQTT消息中,比如机器的识别码、时间戳等等,就可以利用MQTT_PROP_USER_PROPERTY属性码,在消息中添加自定义的键值对属性信息。
当属性为用户自定义(标识符:MQTT_PROP_USER_PROPERTY)时,需要比其他属性多一个参数:
key = argv[(*idx)+2];
value = argv[(*idx)+3];
得出的是一个键值对。

		switch(cmd){
		case CMD_CONNECT:
			proplist = &cfg->connect_props;
			break;

		case CMD_PUBLISH:
			if(identifier == MQTT_PROP_TOPIC_ALIAS){
				cfg->have_topic_alias = true;
			}
			if(identifier == MQTT_PROP_SUBSCRIPTION_IDENTIFIER){
				fprintf(stderr, "Error: %s property not supported for %s in --property argument.\n\n", propname, cmdname);
				return MOSQ_ERR_INVAL;
			}
			proplist = &cfg->publish_props;
			break;

		case CMD_SUBSCRIBE:
			if(identifier != MQTT_PROP_SUBSCRIPTION_IDENTIFIER && identifier != MQTT_PROP_USER_PROPERTY){
				fprintf(stderr, "Error: %s property not supported for %s in --property argument.\n\n", propname, cmdname);
				return MOSQ_ERR_NOT_SUPPORTED;
			}
			proplist = &cfg->subscribe_props;
			break;

		case CMD_UNSUBSCRIBE:
			proplist = &cfg->unsubscribe_props;
			break;

		case CMD_DISCONNECT:
			proplist = &cfg->disconnect_props;
			break;

		case CMD_AUTH:
			fprintf(stderr, "Error: %s property not supported for %s in --property argument.\n\n", propname, cmdname);
			return MOSQ_ERR_NOT_SUPPORTED;

		case CMD_WILL:
			proplist = &cfg->will_props;
			break;

		case CMD_PUBACK:
		case CMD_PUBREC:
		case CMD_PUBREL:
		case CMD_PUBCOMP:
		case CMD_SUBACK:
		case CMD_UNSUBACK:
			fprintf(stderr, "Error: %s property not supported for %s in --property argument.\n\n", propname, cmdname);
			return MOSQ_ERR_NOT_SUPPORTED;

		default:
			return MOSQ_ERR_INVAL;
	}

上面代码用于根据不同的命令类型和属性标识符,将需要初始化的属性指针赋值给proplist 。

	switch(type){
		case MQTT_PROP_TYPE_BYTE:
			tmpl = atol(value);
			if(tmpl < 0 || tmpl > UINT8_MAX){
				fprintf(stderr, "Error: Property value (%ld) out of range for property %s.\n\n", tmpl, propname);
				return MOSQ_ERR_INVAL;
			}
			rc = mosquitto_property_add_byte(proplist, identifier, (uint8_t )tmpl);
			break;
		case MQTT_PROP_TYPE_INT16:
			tmpl = atol(value);
			if(tmpl < 0 || tmpl > UINT16_MAX){
				fprintf(stderr, "Error: Property value (%ld) out of range for property %s.\n\n", tmpl, propname);
				return MOSQ_ERR_INVAL;
			}
			rc = mosquitto_property_add_int16(proplist, identifier, (uint16_t )tmpl);
			break;
		case MQTT_PROP_TYPE_INT32:
			tmpl = atol(value);
			if(tmpl < 0 || tmpl > UINT32_MAX){
				fprintf(stderr, "Error: Property value (%ld) out of range for property %s.\n\n", tmpl, propname);
				return MOSQ_ERR_INVAL;
			}
			rc = mosquitto_property_add_int32(proplist, identifier, (uint32_t )tmpl);
			break;
		case MQTT_PROP_TYPE_VARINT:
			tmpl = atol(value);
			if(tmpl < 0 || tmpl > UINT32_MAX){
				fprintf(stderr, "Error: Property value (%ld) out of range for property %s.\n\n", tmpl, propname);
				return MOSQ_ERR_INVAL;
			}
			rc = mosquitto_property_add_varint(proplist, identifier, (uint32_t )tmpl);
			break;
		case MQTT_PROP_TYPE_BINARY:
			szt = strlen(value);
			if(szt > UINT16_MAX){
				fprintf(stderr, "Error: Property value too long for property %s.\n\n", propname);
				return MOSQ_ERR_INVAL;
			}
			rc = mosquitto_property_add_binary(proplist, identifier, value, (uint16_t )szt);
			break;
		case MQTT_PROP_TYPE_STRING:
			rc = mosquitto_property_add_string(proplist, identifier, value);
			break;
		case MQTT_PROP_TYPE_STRING_PAIR:
			rc = mosquitto_property_add_string_pair(proplist, identifier, key, value);
			break;
		default:
			return MOSQ_ERR_INVAL;
	}

上面代码根据不同的数据类型动态分配内存,初始化参数,并将地址赋值给proplist 指针。
如果type = MQTT_PROP_TYPE_STRING_PAIR时,此时为用户自定义属性,初始化使用键值对key, value和属性标识符赋初值,其余情况则使用value和属性标识符赋初值。


	if(rc){
		fprintf(stderr, "Error adding property %s %d\n", propname, type);
		return rc;
	}
	return MOSQ_ERR_SUCCESS;

2.用默认参数初始化属性(当前源码中只有连接属性cfg->connect_props)

int client_config_load(struct mosq_config *cfg, int pub_or_sub, int argc, char *argv[])
{
...
	if(cfg->protocol_version == 5){
		...
		if(cfg->session_expiry_interval > 0){
			...
			rc = mosquitto_property_add_int32(&cfg->connect_props, MQTT_PROP_SESSION_EXPIRY_INTERVAL, (uint32_t )cfg->session_expiry_interval);
			...
		}
	}else{
		...
	}
...
}

可以看到在上面的代码里,调用mosquitto_property_add_int32()函数给cfg->connect_props指针分配了内存,同时用MQTT_PROP_SESSION_EXPIRY_INTERVAL,(uint32_t )cfg->session_expiry_interval对这个新对象赋值。
需要注意的是,这个初始化代码片段会释放前面用配置文件或命令行参数所初始化的cfg->connect_props对象的内存,并重新设置。在源码里,只有cfg->connect_props是这样编码的,
事实上,其他属性指针的初始化也可以照此办理。
在实际使用时,如果希望使用配置文件或命令行参数初始化cfg->connect_props,则需要屏蔽这部分代码。
经过这段代码设置,cfg->connect_props赋值如下:

  • cfg->connect_props->client_generated = true;
  • cfg->connect_props->identifier=MQTT_PROP_SESSION_EXPIRY_INTERVAL;
  • cfg->connect_props->value.i32=(uint32_t )cfg->session_expiry_interval;
  • 如果使用无限会话过期间隔(cfg->session_expiry_interval=UINT32_MAX(0xffffffffui32)),则必须提供客户端ID不能为空。

3.对6个属性对象检查数据类型是否正确,命令、标识符对是否匹配

int client_config_load(struct mosq_config *cfg, int pub_or_sub, int argc, char *argv[])
{
...
	rc = mosquitto_property_check_all(CMD_CONNECT, cfg->connect_props);
	if(rc){
		err_printf(cfg, "Error in CONNECT properties: %s\n", mosquitto_strerror(rc));
		return 1;
	}
	rc = mosquitto_property_check_all(CMD_PUBLISH, cfg->publish_props);
	if(rc){
		err_printf(cfg, "Error in PUBLISH properties: %s\n", mosquitto_strerror(rc));
		return 1;
	}
	rc = mosquitto_property_check_all(CMD_SUBSCRIBE, cfg->subscribe_props);
	if(rc){
		err_printf(cfg, "Error in SUBSCRIBE properties: %s\n", mosquitto_strerror(rc));
		return 1;
	}
	rc = mosquitto_property_check_all(CMD_UNSUBSCRIBE, cfg->unsubscribe_props);
	if(rc){
		err_printf(cfg, "Error in UNSUBSCRIBE properties: %s\n", mosquitto_strerror(rc));
		return 1;
	}
	rc = mosquitto_property_check_all(CMD_DISCONNECT, cfg->disconnect_props);
	if(rc){
		err_printf(cfg, "Error in DISCONNECT properties: %s\n", mosquitto_strerror(rc));
		return 1;
	}
	rc = mosquitto_property_check_all(CMD_WILL, cfg->will_props);
	if(rc){
		err_printf(cfg, "Error in Will properties: %s\n", mosquitto_strerror(rc));
		return 1;
	}

	return MOSQ_ERR_SUCCESS;
}

上面的代码调用mosquitto_property_check_all()函数对cfg->connect_propscfg->publish_propscfg->subscribe_propscfg->unsubscribe_propscfg->disconnect_props cfg->will_props 属性指针所指的对象进行检查,确保属性的数据类型和命令、标识符对(cmd–identifier)的合法性。

具体来说,mosquitto_property_check_all()函数有以下几个功能:

  1. 检查数据类型是否正确。在MQTTv5中,属性是以<属性标识符,属性值>的形式传递的。属性值的数据类型可能是整数、字符串、二进制数据等,mosquitto_property_check_all()函数会根据属性标识符和属性值的类型进行检查,确保数据类型匹配。

  2. 调用mosquitto_property_check_command函数,检查命令、标识符对(cmd–identifier)是否合法。在MQTTv5中,每个属性都有一个对应的命令和标识符。mosquitto_property_check_all()函数会调用mosquitto_property_check_command函数来检查命令、标识符对是否正确。如果不正确,就会返回错误码,提示调用者出错。

  3. 对于属性列表中未知的属性,将其丢弃。在MQTTv5中,属性列表中可能会包含一些未知的属性,这些属性可能是由MQTTv5协议的更新引入的。mosquitto_property_check_all()函数会忽略这些未知的属性,确保属性列表的正确性。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值