v0.23.0的版本使用了v1.8.3版本的zengl语言库,增加了bltTrim,bltFatalErrorCallback,bltToLower,bltToUpper等模块函数,修复了bltJsonEncode等模块函数的bug,在日志中记录了404和403错误具体的原因,支持中文url路径等

    页面导航:

项目下载地址:

    zenglServer源代码的相关地址:https://github.com/zenglong/zenglServer  当前版本对应的tag标签为:v0.23.0

zenglServer v0.23.0:

    v0.23.0的版本使用了v1.8.3版本的zengl语言库,增加了bltTrim,bltFatalErrorCallback,bltToLower,bltToUpper等模块函数,修复了bltJsonEncode等模块函数的bug,在日志中记录了404和403错误的具体原因,以及支持中文url路径等。

    当前版本还在CentOS 8.2和ubuntu 20.04中也进行了编译和测试。不过在CentOS 8.x的系统中安装ImageMagick相关的底层库时,如果使用之前介绍过的 yum install ImageMagick ImageMagick-devel 方式来安装的话,有可能会安装失败,此时,可以通过以下命令来进行安装:

dnf install -y epel-release
dnf config-manager --set-enabled PowerTools
dnf install -y ImageMagick ImageMagick-devel

    此外,在安装redis模块所需的hiredis的底层库时,在centos 8.x中使用yum命令安装hiredis-devel时,可能需要先安装epel仓库,可以通过 yum -y install epel-release 命令来安装epel仓库。

zengl v1.8.3:

    当前版本使用了v1.8.3版本的zengl语言库,该版本的zengl语言库修复了zenglApi_ReUse接口可能导致Call接口无法获取正确参数的问题,以及修复在linux系统中,ReUse后因重复关闭文件可能报的段错误问题。还完善了语法错误检测,尤其是和print相关的表达式的语法检测。和zengl v1.8.3版本相关的具体内容请参考zengl语言栏目。

    可以使用-v参数,来查看当前zenglServer的版本号,以及zenglServer所使用的zengl语言的版本号信息:

[root@localhost zenglServer]# ./zenglServer -v
zenglServer version: v0.23.0
zengl language version: v1.8.3
[root@localhost zenglServer]# 

bltTrim模块函数:

    当前版本增加了bltTrim模块函数,可以去除字符串左右两侧的指定字符,该模块函数定义在module_builtin.c文件中:

/**
 * bltTrim模块函数,去除字符串左右两侧的指定字符
 * 第一个参数str必须是字符串类型,表示需要进行操作的源字符串
 * 第二个参数chars是可选参数,也必须是字符串类型,表示需要去除哪些字符,默认值为' \t\n\r\v',表示需要去除字符串左右两侧的空格符,\t(tab制表符),\n(换行符)等
 *  - chars字符串中的每个字符都会被去除掉
 * 第三个参数mode也是可选参数,必须是整数类型,表示去除模式,有三种模式:
 *  - 当mode参数的值为1时,表示只去除字符串左侧的字符
 *  - 当mode参数的值为2时,表示只去除字符串右侧的字符
 *  - 当mode参数的值为3时,表示左右两侧的字符都要去除掉,mode的默认值就是3,也就是左右两侧的字符都会被去除掉
 *
 * 返回值:此模块函数会将处理后(也就是去除了左右两侧的指定字符)的字符串作为结果返回
 *
 * 示例:
 * 	use builtin;
	def TRIM_LEFT 1;
	def TRIM_RIGHT 2;
	def TRIM_BOTH 3;

	test = '
		hello world !!!!!
	  ';

	print 'test: ' + test;
	print '----------------------------------------';
	print '[bltTrim(test)]: ' + '[' + bltTrim(test) + ']';
	print '----------------------------------------';
	print "[bltTrim(test, ' \\n\\t', TRIM_LEFT)]: " + '[' + bltTrim(test, ' \n\t', TRIM_LEFT) + ']';
	print '----------------------------------------';
	print "[bltTrim(test, ' \\n\\t', TRIM_RIGHT)]: " + '[' + bltTrim(test, ' \n\t', TRIM_RIGHT) + ']';
	print '----------------------------------------';
	print '[bltTrim("  hahahaha~~~~  ", \' \', TRIM_BOTH)]: ' + '[' + bltTrim("  hahahaha~~~~  ", ' ', TRIM_BOTH) + ']';
	print '----------------------------------------';

	以上代码对三种模式都进行了测试,执行结果类似如下所示:

	test:
		hello world !!!!!

	----------------------------------------
	[bltTrim(test)]: [hello world !!!!!]
	----------------------------------------
	[bltTrim(test, ' \n\t', TRIM_LEFT)]: [hello world !!!!!
	  ]
	----------------------------------------
	[bltTrim(test, ' \n\t', TRIM_RIGHT)]: [
		hello world !!!!!]
	----------------------------------------
	[bltTrim("  hahahaha~~~~  ", ' ', TRIM_BOTH)]: [hahahaha~~~~]
	----------------------------------------

	上面的bltTrim(test)表示将test字符串变量左右两侧的空格符,换行符等都去除掉,
	bltTrim(test, ' \n\t', TRIM_LEFT)表示只将test左侧的空格符,换行符,回车符去除掉
	bltTrim(test, ' \n\t', TRIM_RIGHT)表示只将test右侧的空格符,换行符,回车符去除掉
	bltTrim("  hahahaha~~~~  ", ' ', TRIM_BOTH)表示将"  hahahaha~~~~  "字符串左右两侧的' '(空格符)给去除掉

	模块函数版本历史:
	 - v0.23.0版本新增此模块函数
 */
ZL_EXP_VOID module_builtin_trim(ZL_EXP_VOID * VM_ARG, ZL_EXP_INT argcount)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	const char * func_name = "bltTrim";
	if(argcount < 1)
		zenglApi_Exit(VM_ARG,"usage: %s(str[, chars = ' \t\n\r\v'[, mode = 3]]): str", func_name);
	zenglApi_GetFunArg(VM_ARG,1,&arg);
	if(arg.type != ZL_EXP_FAT_STR) {
		zenglApi_Exit(VM_ARG,"the first argument [str] of %s must be string", func_name);
	}
	char * str = (char *)arg.val.str;
	int str_len = strlen(str);
	char * chars = " \t\n\r\v";
	int chars_len = strlen(chars);
	int mode = 3;
	if(argcount > 1) {
		zenglApi_GetFunArg(VM_ARG,2,&arg);
		if(arg.type != ZL_EXP_FAT_STR) {
			zenglApi_Exit(VM_ARG,"the second argument [chars] of %s must be string", func_name);
		}
		chars = (char *)arg.val.str;
		chars_len = strlen(chars);
		if(argcount > 2) {
			zenglApi_GetFunArg(VM_ARG,3,&arg);
			if(arg.type != ZL_EXP_FAT_INT) {
				zenglApi_Exit(VM_ARG,"the third argument [mode] of %s must be integer", func_name);
			}
			mode = (int)arg.val.integer;
		}
	}
	char * start = str;
	char * end = (str_len > 0) ? (str + str_len) : str;
	if (chars_len <= 0) {
		zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_STR, str, 0, 0);
		return;
	}
	if(!st_trim_mask_init) {
		memset(st_trim_mask, 0, 256);
		st_trim_mask_init = ZL_EXP_TRUE;
	}
	unsigned char c;
	for(int i = 0; i < chars_len ;i++) {
		c = (unsigned char)chars[i];
		st_trim_mask[c] = 1;
	}

	if(mode & 1) {
		while(start != end) {
			if(st_trim_mask[(unsigned char)*start])
				start++;
			else
				break;
		}
	}
	if(mode & 2) {
		while(start != end) {
			if(st_trim_mask[(unsigned char)*(end-1)])
				end--;
			else
				break;
		}
	}

	for(int i = 0; i < chars_len ;i++) {
		c = (unsigned char)chars[i];
		st_trim_mask[c] = 0;
	}

	char orig_end_char = *end;
	*end = '\0';
	zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_STR, start, 0, 0);
	*end = orig_end_char;
}

    在上面C代码相关的注释中,已经指明了bltTrim模块函数的每个参数的具体含义,以及模块函数的使用示例。

    为了测试该模块函数,当前版本在根目录的my_webroot子目录中增加了v0_23_0的目录,并在v0_23_0目录内增加了test_trim.zl的测试脚本,该测试脚本的代码如下:

use builtin;
def TRIM_LEFT 1;
def TRIM_RIGHT 2;
def TRIM_BOTH 3;

test = '  
	hello world !!!!!   	
  ';

print 'test: ' + test;
print '----------------------------------------';
print '[bltTrim(test)]: ' + '[' + bltTrim(test) + ']';
print '----------------------------------------';
print "[bltTrim(test, ' \\n\\t', TRIM_LEFT)]: " + '[' + bltTrim(test, ' \n\t', TRIM_LEFT) + ']';
print '----------------------------------------';
print "[bltTrim(test, ' \\n\\t', TRIM_RIGHT)]: " + '[' + bltTrim(test, ' \n\t', TRIM_RIGHT) + ']';
print '----------------------------------------';
print '[bltTrim("  hahahaha~~~~  ", \' \', TRIM_BOTH)]: ' + '[' + bltTrim("  hahahaha~~~~  ", ' ', TRIM_BOTH) + ']';
print '----------------------------------------';

    该脚本在命令行中的执行结果如下:

[root@localhost zenglServer]# ./zenglServer -r "/v0_23_0/test_trim.zl"
test:   
	hello world !!!!!   	
  
----------------------------------------
[bltTrim(test)]: [hello world !!!!!]
----------------------------------------
[bltTrim(test, ' \n\t', TRIM_LEFT)]: [hello world !!!!!   	
  ]
----------------------------------------
[bltTrim(test, ' \n\t', TRIM_RIGHT)]: [  
	hello world !!!!!]
----------------------------------------
[bltTrim("  hahahaha~~~~  ", ' ', TRIM_BOTH)]: [hahahaha~~~~]
----------------------------------------
[root@localhost zenglServer]# 

    上面的bltTrim(test)表示将test字符串变量左右两侧的空格符,换行符等都去除掉。bltTrim(test, ' \n\t', TRIM_LEFT)表示只将test左侧的空格符,换行符,tab制表符去除掉。bltTrim(test, ' \n\t', TRIM_RIGHT)表示只将test右侧的空格符,换行符,tab制表符去除掉。bltTrim("  hahahaha~~~~  ", ' ', TRIM_BOTH)表示将"  hahahaha~~~~  "字符串左右两侧的' '(空格符)给去除掉。

bltFatalErrorCallback模块函数:

    当前版本增加了bltFatalErrorCallback模块函数,该模块函数可以设置,当发生严重的运行时错误时,需要执行的脚本中的回调函数。该模块函数的C代码定义在module_builtin.c文件中:

/**
 * bltFatalErrorCallback模块函数,设置当发生严重的运行时错误时,需要执行的脚本中的回调函数名,如果是类中定义的方法,还可以设置相关的类名
 * 第一个参数function_name表示需要设置的脚本中的回调函数名(必须是字符串类型,且不能为空字符串)
 * 第二个参数class_name是可选参数,表示需要设置的类名(也必须是字符串类型,如果第一个参数function_name是某个类中定义的方法的话,就可以通过此参数来设置类名)
 * 		- 默认值是空字符串,表示不设置类名,当需要跳过该参数设置第三个default_cmd_action参数时,也可以手动传递空字符串来表示不设置类名
 * 第三个参数default_cmd_action也是可选的,表示在命令行模式下,当执行完运行时错误回调函数后,是否还需要执行默认的输出错误信息到命令行的动作,
 * 		- 默认值为1,表示需要执行默认动作,如果脚本回调函数里已经将错误信息输出到了命令行的话,就可以将该参数设置为0,表示不需要再执行默认动作了
 *
 * 示例:
	use builtin;
	def WRITE_MODE 1;
	def APPEND_MODE 2;
	def TRUE 1;
	def FALSE 0;
	def DEFAULT_LEN -1;

	fun fatal_error(error, stack)
		print '\n hahaha fatal error [' + bltDate('%Y-%m-%d %H:%M:%S') + ']: \n' + error + ' backtrace: \n' + stack + '\n';
		bltWriteFile('fatal_error.log', bltDate('%Y-%m-%d %H:%M:%S') + ' - ' + error + ' backtrace: \n' + stack + '\n', DEFAULT_LEN, APPEND_MODE);
	endfun

	bltFatalErrorCallback('fatal_error', '', FALSE);

	class Test
		fun test()
			a = bltTestHa();
		endfun
	endclass

	Test.test();

	以上代码会将fatal_error脚本函数设置为运行时错误回调函数,当脚本准备执行bltTestHa函数时,由于bltTestHa函数没有被定义过,也不属于有效的模块函数,
	因此,在执行bltTestHa函数时就会发生运行时错误,此时就会调用上面的fatal_error脚本函数来处理该运行时错误,zenglServer会将错误信息和函数栈追踪信息
	以参数的形式传递给fatal_error脚本函数,也就是上面的error及stack参数,其中error表示具体的错误信息,stack表示发生错误时的脚本函数栈追踪信息,
	上面在fatal_error脚本回调函数中,在将错误信息和函数栈追踪信息通过print打印出来后,还会将这些信息写入到fatal_error.log日志文件中

	上面这段脚本的执行结果类似如下所示:

	hahaha fatal error [2020-10-24 21:55:44]:

	err: run func err , function 'bltTestHa' is invalid pc=89 (解释器运行时错误:函数'bltTestHa'无效)

	source code info: [ bltTestHa ] 17:7 <'my_webroot/v0_23_0/test_fatal_error.zl'>
	backtrace:
	my_webroot/v0_23_0/test_fatal_error.zl:17 Test:test
	my_webroot/v0_23_0/test_fatal_error.zl:21

	模块函数版本历史:
	 - v0.23.0版本新增此模块函数
 */
ZL_EXP_VOID module_builtin_fatal_error_callback(ZL_EXP_VOID * VM_ARG, ZL_EXP_INT argcount)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	const char * func_name = "bltFatalErrorCallback";
	if(argcount < 1)
		zenglApi_Exit(VM_ARG,"usage: %s(function_name[, class_name[, default_cmd_action]])", func_name);
	zenglApi_GetFunArg(VM_ARG,1,&arg);
	if(arg.type != ZL_EXP_FAT_STR) {
		zenglApi_Exit(VM_ARG,"the first argument [function_name] of %s must be string", func_name);
	}
	char * function_name = (char *)arg.val.str;
	if(strlen(function_name) == 0) {
		zenglApi_Exit(VM_ARG,"the first argument [function_name] of %s can't be empty", func_name);
	}
	// 在设置运行时错误回调函数名之前,先将之前可能设置过的回调函数相关的函数名,类名等重置为默认值,可以防止受到之前设置的影响
	fata_error_free_all_ptrs();
	fatal_error_set_function_name(function_name);
	char * class_name = NULL;
	if(argcount > 1) {
		zenglApi_GetFunArg(VM_ARG,2,&arg);
		if(arg.type != ZL_EXP_FAT_STR) {
			zenglApi_Exit(VM_ARG,"the second argument [class_name] of %s must be string", func_name);
		}
		class_name = (char *)arg.val.str;
		if(strlen(class_name) > 0) {
			fatal_error_set_class_name(class_name);
		}
		if(argcount > 2) {
			zenglApi_GetFunArg(VM_ARG,3,&arg);
			if(arg.type != ZL_EXP_FAT_INT) {
				zenglApi_Exit(VM_ARG,"the third argument [default_cmd_action] of %s must be integer", func_name);
			}
			fatal_error_set_default_cmd_action((int)arg.val.integer);
		}
	}
	zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_INT, ZL_EXP_NULL, 0, 0);
}

    在上面C代码的注释中,已经给出了该模块函数的各个参数的含义,以及模块函数的使用示例。

bltWriteFile模块函数增加mode可选参数:

    当前版本为bltWriteFile模块函数增加了一个mode可选参数,通过该参数可以设置在将数据写入文件时,是在文件的开头写入数据,还是在文件的结尾写入数据。该参数的具体用法,可以参考该模块函数的C代码的注释部分,和此模块函数相关的C代码也定义在module_builtin.c文件里:

/**
 * bltWriteFile模块函数,用于将字符串或者指针所指向的数据写入到指定的文件中
 * 例如:
 * body = rqtGetBody(&body_count, &body_source);
 * bltWriteFile('body.log', body);
 * bltWriteFile('body_source.log', body_source, body_count);
 * 该例子中,rqtGetBody会返回请求主体数据的字符串格式,同时将主体数据的字节数及指针值分别写入
 * 到body_count和body_source变量里,当然指针在zengl内部是以和指针长度一致的长整数的形式保存的,
 * 当请求主体数据中只包含字符串时,上面两个bltWriteFile写入文件的数据会是一样的,
 * 当主体数据中还包含了上传的文件时,两者就不一样了,body只会显示字符串能显示的开头的部分,直到被NULL字符阻止,
 * body_source配合body_count则可以将所有主体数据(包括上传的文件的二进制数据)都写入到文件中,
 * 从例子中可以看出,bltWriteFile模块函数既可以写入字符串,也可以写入指针指向的二进制数据
 *
 * 第一个参数filename表示需要写入的文件名(是相对于当前主执行脚本的文件路径)
 *
 * 第二个参数ptr|string表示需要写入文件的字符串,或者是指向了需要写入的二进制数据的整数类型的指针值
 *
 * 第三个参数length是可选参数(必须是整数类型),可以设置写入数据的长度
 *  - 当没提供length参数时,或者length参数小于0时,会自动使用字符串或指针指向的二进制数据的实际可用长度
 *
 * 第四个参数mode也是可选参数(必须是整数类型)
 *  - 当mode值为1时表示从文件的开始位置处写入数据(同时会清空文件中的原有数据),默认值就是1,也就是默认情况下,是从文件开头写入数据
 *  - 当mode值为2时表示从文件的结尾位置处写入数据(不会清空文件的原有数据,只是在原数据的基础上追加数据)
 *
 * 模块函数版本历史:
	 - v0.2.0版本新增此模块函数
	 - v0.23.0版本新增了mode可选参数,用于设置是在开头写入数据,还是在结尾追加数据
 */
ZL_EXP_VOID module_builtin_write_file(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT argcount)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	const char * func_name = "bltWriteFile";
	if(argcount < 2)
		zenglApi_Exit(VM_ARG,"usage: %s(filename, ptr|string[, length[, mode]])", func_name);
	zenglApi_GetFunArg(VM_ARG,1,&arg);
	if(arg.type != ZL_EXP_FAT_STR) {
		zenglApi_Exit(VM_ARG,"the first argument of %s must be string", func_name);
	}
	char * filename = arg.val.str;
	zenglApi_GetFunArg(VM_ARG,2,&arg);
	void * ptr = ZL_EXP_NULL;
	int ptr_size = 0;
	char * string = ZL_EXP_NULL;
	MAIN_DATA * my_data = zenglApi_GetExtraData(VM_ARG, "my_data");
	if(arg.type == ZL_EXP_FAT_STR) {
		ptr = arg.val.str;
		ptr_size = strlen(arg.val.str);
	}
	else if(arg.type == ZL_EXP_FAT_INT) {
		ptr = (void *)arg.val.integer;
		int ptr_idx = pointer_list_get_ptr_idx(&(my_data->pointer_list), ptr);
		if(ptr_idx < 0) {
			zenglApi_Exit(VM_ARG,"runtime error: the second argument [ptr] of %s is invalid pointer", func_name);
		}
		ptr_size = my_data->pointer_list.list[ptr_idx].ptr_size;
	}
	else {
		zenglApi_Exit(VM_ARG,"the second argument [ptr|string] of %s must be integer or string", func_name);
	}
	int length = ptr_size;
	int write_mode = WRITE_FILE_MODE_WRITE;
	if(argcount > 2) {
		zenglApi_GetFunArg(VM_ARG,3,&arg);
		if(arg.type != ZL_EXP_FAT_INT) {
			zenglApi_Exit(VM_ARG,"the third argument [length] of %s must be integer", func_name);
		}
		length = (int)arg.val.integer;
		if(length < 0 || length > ptr_size) {
			length = ptr_size;
		}
		if(argcount > 3) {
			zenglApi_GetFunArg(VM_ARG,4,&arg);
			if(arg.type != ZL_EXP_FAT_INT) {
				zenglApi_Exit(VM_ARG,"the fourth argument [mode] of %s must be integer", func_name);
			}
			write_mode = (int)arg.val.integer;
			if(write_mode != WRITE_FILE_MODE_WRITE && write_mode != WRITE_FILE_MODE_APPEND) {
				zenglApi_Exit(VM_ARG,"the fourth argument [mode] of %s must be %d(for write mode) or %d(for append mode)",
								func_name, WRITE_FILE_MODE_WRITE, WRITE_FILE_MODE_APPEND);
			}
		}
	}
	char full_path[FULL_PATH_SIZE];
	builtin_make_fullpath(full_path, filename, my_data);
	FILE * fp = NULL;
	if(write_mode == WRITE_FILE_MODE_APPEND)
		fp = fopen(full_path, "ab");
	else
		fp = fopen(full_path, "wb");
	if(fp != NULL) {
		size_t retval = fwrite(ptr, 1, length, fp);
		fclose(fp);
		zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_INT, ZL_EXP_NULL, (ZL_EXP_LONG)retval, 0);
	}
	else { // 如果打开文件失败,则将错误记录到日志中
		zenglApi_Exit(VM_ARG,"%s <%s> failed [%d] %s", func_name, full_path, errno, strerror(errno));
	}
}

    可以看到,当第四个mode参数为1时表示从文件的开始位置处写入数据(同时会清空文件中的原有数据,默认值就是1),当mode参数的值为2时,表示从文件的结尾位置处写入数据,也就是追加数据。

test_fatal_error.zl脚本:

    为了测试上面介绍的bltFatalErrorCallback模块函数,以及bltWriteFile模块函数新增的mode参数,当前版本在 my_webroot/v0_23_0/ 目录中增加了test_fatal_error.zl的测试脚本,该脚本的代码如下:

use builtin;
def WRITE_MODE 1;
def APPEND_MODE 2;
def TRUE 1;
def FALSE 0;
def DEFAULT_LEN -1;

fun fatal_error(error, stack)
	print '\n hahaha fatal error [' + bltDate('%Y-%m-%d %H:%M:%S') + ']: \n' + error + ' backtrace: \n' + stack + '\n';
	bltWriteFile('fatal_error.log', bltDate('%Y-%m-%d %H:%M:%S') + ' - ' + error + ' backtrace: \n' + stack + '\n', DEFAULT_LEN, APPEND_MODE);
endfun

bltFatalErrorCallback('fatal_error', '', FALSE);

class Test
	fun test()
		a = bltTestHa();
	endfun
endclass

Test.test();

    该测试脚本会通过bltFatalErrorCallback模块函数,将脚本中的fatal_error函数设置为运行时错误回调函数,由于bltTestHa是无效的函数,因此,脚本在执行bltTestHa时就会发生运行时错误,并将该错误交由fatal_error脚本函数去处理。

    在fatal_error脚本函数中会将error即运行时错误信息和stack函数栈追踪信息给打印出来,同时还会将这些信息以追加的方式,写入到fatal_error.log日志文件中。

    该测试脚本在命令行中的执行结果如下:

[root@localhost zenglServer]# ./zenglServer -r "/v0_23_0/test_fatal_error.zl"

 hahaha fatal error [2020-11-10 18:00:37]: 

 err: run func err , function 'bltTestHa' is invalid pc=89 (解释器运行时错误:函数'bltTestHa'无效)

 source code info: [ bltTestHa ] 17:7 <'my_webroot/v0_23_0/test_fatal_error.zl'>
 backtrace: 
 my_webroot/v0_23_0/test_fatal_error.zl:17 Test:test
 my_webroot/v0_23_0/test_fatal_error.zl:21 


[root@localhost zenglServer]# cat my_webroot/v0_23_0/fatal_error.log 
..................................................................

2020-11-10 18:00:37 - 
 err: run func err , function 'bltTestHa' is invalid pc=89 (解释器运行时错误:函数'bltTestHa'无效)

 source code info: [ bltTestHa ] 17:7 <'my_webroot/v0_23_0/test_fatal_error.zl'>
 backtrace: 
 my_webroot/v0_23_0/test_fatal_error.zl:17 Test:test
 my_webroot/v0_23_0/test_fatal_error.zl:21 

[root@localhost zenglServer]# 

    通过上面的error错误信息和backtrace函数栈追踪信息,我们就可以清楚的知道运行时错误发生的具体原因。在本例中,就是因为脚本在第21行通过Test类的test方法,在第17行的第7列调用了bltTestHa这个无效的函数导致的。

bltToLower模块函数:

    当前版本新增了bltToLower模块函数,可以将字符串中的字符都转为小写,并将转换的结果返回给调用者。该模块函数相关的C源码定义在module_builtin.c文件中:

/**
 * bltToLower和bltToUpper模块函数,最终都会通过下面这个函数来执行具体的转换大小写的操作,
 * 该函数则会通过tolower的C库函数来将所有的字符都转为小写字符,或者通过toupper的C库函数将所有的字符都转为大写字符
 */
static char * to_lower_upper(ZL_EXP_VOID * VM_ARG, char * str, ZL_EXP_BOOL is_tolower)
{
	int str_len = strlen(str);
	char * result = (char *)zenglApi_AllocMem(VM_ARG, (str_len + 1));
	for(int i = 0 ; i < str_len; i++) {
		result[i] = is_tolower ? tolower(str[i]) : toupper(str[i]);
	}
	result[str_len] = '\0';
	return result;
}

.......................................................................

/**
 * bltToLower模块函数,将字符串转为小写,并将转换的结果返回给调用者
 * 第一个参数str必须是字符串类型,表示需要进行转换的源字符串
 *
 * 示例:
	use builtin,request;

	headers = rqtGetHeaders();
	for(i=0; bltIterArray(headers,&i,&k,&v); )
		print k +": " + v + '<br/>';
		lowers[bltToLower(k)] = bltToLower(v);
	endfor

	print '=============================================<br/>lowers: <br/>';

	for(i=0; bltIterArray(lowers,&i,&k,&v); )
		print k +": " + v + '<br/>';
	endfor

	执行的结果类似如下:

	Host: 192.168.1.113:8083
	User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0
	Accept: text/html,application/xhtml+xml,application/xml;q=0.9
	Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
	Accept-Encoding: gzip, deflate
	Connection: keep-alive
	Upgrade-Insecure-Requests: 1
	=============================================
	lowers:
	host: 192.168.1.113:8083
	user-agent: mozilla/5.0 (windows nt 10.0; win64; x64; rv:81.0) gecko/20100101 firefox/81.0
	accept: text/html,application/xhtml+xml,application/xml;q=0.9
	accept-language: zh-cn,zh;q=0.8,zh-tw;q=0.7,zh-hk;q=0.5,en-us;q=0.3,en;q=0.2
	accept-encoding: gzip, deflate
	connection: keep-alive
	upgrade-insecure-requests: 1

	可以看到经过bltToLower转换后,所有的字符都转为了小写字符

	模块函数版本历史:
	 - v0.23.0版本新增此模块函数
 */
ZL_EXP_VOID module_builtin_to_lower(ZL_EXP_VOID * VM_ARG, ZL_EXP_INT argcount)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	const char * func_name = "bltToLower";
	if(argcount < 1)
		zenglApi_Exit(VM_ARG,"usage: %s(str)", func_name);
	zenglApi_GetFunArg(VM_ARG,1,&arg);
	if(arg.type != ZL_EXP_FAT_STR) {
		zenglApi_Exit(VM_ARG,"the first argument [str] of %s must be string", func_name);
	}
	char * str = (char *)arg.val.str;
	char * result = to_lower_upper(VM_ARG, str, ZL_EXP_TRUE);
	zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_STR, result, 0, 0);
	zenglApi_FreeMem(VM_ARG, result);
}

    可以看到,bltToLower模块函数,最终会通过to_lower_upper函数中的tolower的底层C库函数,来将字符串中的每个字符都转为小写字符。

bltToUpper模块函数:

    当前版本还增加了bltToUpper模块函数,用于将字符串中的字符都转为大写字符,并将转换的结果返回给调用者。该模块函数也定义在module_builtin.c文件中:

/**
 * bltToUpper模块函数,将字符串转为大写,并将转换的结果返回给调用者
 * 第一个参数str必须是字符串类型,表示需要进行转换的源字符串
 *
 * 示例:
	use builtin,request;

	headers = rqtGetHeaders();
	for(i=0; bltIterArray(headers,&i,&k,&v); )
		print k +": " + v + '<br/>';
		uppers[bltToUpper(k)] = bltToUpper(v);
	endfor

	print '=============================================<br/>uppers: <br/>';

	for(i=0; bltIterArray(uppers,&i,&k,&v); )
		print k +": " + v + '<br/>';
	endfor

	执行的结果类似如下:

	Host: 192.168.1.113:8083
	User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0
	Accept: text/html,application/xhtml+xml,application/xml;q=0.9
	Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
	Accept-Encoding: gzip, deflate
	Connection: keep-alive
	Upgrade-Insecure-Requests: 1
	Cache-Control: max-age=0
	=============================================
	uppers:
	HOST: 192.168.1.113:8083
	USER-AGENT: MOZILLA/5.0 (WINDOWS NT 10.0; WIN64; X64; RV:81.0) GECKO/20100101 FIREFOX/81.0
	ACCEPT: TEXT/HTML,APPLICATION/XHTML+XML,APPLICATION/XML;Q=0.9
	ACCEPT-LANGUAGE: ZH-CN,ZH;Q=0.8,ZH-TW;Q=0.7,ZH-HK;Q=0.5,EN-US;Q=0.3,EN;Q=0.2
	ACCEPT-ENCODING: GZIP, DEFLATE
	CONNECTION: KEEP-ALIVE
	UPGRADE-INSECURE-REQUESTS: 1
	CACHE-CONTROL: MAX-AGE=0

	可以看到经过bltToUpper转换后,所有的字符都转为了大写字符

	模块函数版本历史:
	 - v0.23.0版本新增此模块函数
 */
ZL_EXP_VOID module_builtin_to_upper(ZL_EXP_VOID * VM_ARG, ZL_EXP_INT argcount)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	const char * func_name = "bltToUpper";
	if(argcount < 1)
		zenglApi_Exit(VM_ARG,"usage: %s(str)", func_name);
	zenglApi_GetFunArg(VM_ARG,1,&arg);
	if(arg.type != ZL_EXP_FAT_STR) {
		zenglApi_Exit(VM_ARG,"the first argument [str] of %s must be string", func_name);
	}
	char * str = (char *)arg.val.str;
	char * result = to_lower_upper(VM_ARG, str, ZL_EXP_FALSE);
	zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_STR, result, 0, 0);
	zenglApi_FreeMem(VM_ARG, result);
}

test_lower_upper.zl脚本:

    为了测试新增的bltToLower和bltToUpper模块函数,当前版本在 my_webroot/v0_23_0/ 目录中增加了test_lower_upper.zl的测试脚本,该测试脚本的代码如下:

use builtin,request;

print '<!Doctype html>
<html>
<head><meta http-equiv="content-type" content="text/html;charset=utf-8" />
<title>测试bltToLower和bltToUpper</title>
</head>
<body>';

headers = rqtGetHeaders();
for(i=0; bltIterArray(headers,&i,&k,&v); )
	print k +": " + v + '<br/>';
	lowers[bltToLower(k)] = bltToLower(v);
	uppers[bltToUpper(k)] = bltToUpper(v);
endfor

print '=============================================<br/>转为小写: <br/>';

for(i=0; bltIterArray(lowers,&i,&k,&v); )
	print k +": " + v + '<br/>';
endfor

print '=============================================<br/>转为大写: <br/>';

for(i=0; bltIterArray(uppers,&i,&k,&v); )
	print k +": " + v + '<br/>';
endfor

print '</body></html>';

    该脚本会通过bltToLower模块函数将headers请求头数组中的key和值都转为小写并存入lowers数组中,还会通过bltToUpper模块函数将headers里的key和值都转为大写并存入uppers数组中,最后将lowers和uppers数组里的成员都打印出来。该测试脚本在web端的执行结果如下:

test_lower_upper.zl测试脚本

    从上图中可以看到,经过bltToLower和bltToUpper模块函数转换后,lowers数组里的key和值都是小写的了,而uppers数组里的key和值都是大写的了。

修复bltJsonEncode模块函数的Bug:

    当前版本修复了bltJsonEncode模块函数可能会生成无法被客户端解析的json字符串的bug,我们通过以下代码片段来说明该bug:

use builtin;

data['name'] = 'zenglong';
data['job'] = 'programmer';
data['test'] = 'i will be unset';
bltUnset(&data['test']);
data['hobby'] = 'play game';

print bltJsonEncode(data);

    以上代码片段在之前的版本中执行时,生成的json字符串会是:{"name":"zenglong","job":"programmer",,"hobby":"play game"},也就是之前的版本会因为数组中间的data['test']成员被unset移除掉了,而在生成json时会在中间多出一个英文逗号,也就是在"hobby"前面多了一个英文逗号,从而会导致生成的这个json无法被浏览器之类的客户端解析。

    当前版本处理了这个问题,不会因为数组中间的某些成员被bltUnset之类的模块函数,重置为了NONE类型(数组成员的初始化类型),而在生成的json中多出一些无法解析的英文逗号。修复的代码位于module_builtin.c文件中:

/**
 * 将zengl脚本中的数组转为json格式,并追加到infoString动态字符串
 * 如果数组中还包含了数组,那么所包含的数组在转为json时,会递归调用当前函数
 * 如果数组成员有对应的哈希key(字符串作为key),那么生成的json会用大括号包起来
 * 例如:{"hello":"world","name":"zengl"}
 * 如果数组成员没有哈希key,那么生成的json会用中括号包起来
 * 例如:[1,2,3,3.14159,"zengl language"]
 */
static void builtin_write_array_to_string(ZL_EXP_VOID * VM_ARG, BUILTIN_INFO_STRING * infoString, ZENGL_EXPORT_MEMBLOCK memblock)
{
	................................................................................
	if(count > 0)
	{
		for(i=1,process_count=0; i<=size && process_count < count; i++)
		{
			mblk_val = zenglApi_GetMemBlock(VM_ARG,&memblock,i);
			zenglApi_GetMemBlockHashKey(VM_ARG,&memblock,i-1,&key);
			switch(mblk_val.type)
			{
			case ZL_EXP_FAT_INT:
			case ZL_EXP_FAT_FLOAT:
			case ZL_EXP_FAT_STR:
			case ZL_EXP_FAT_MEMBLOCK:
				if(process_count == 0) {
					if(key != ZL_EXP_NULL) {
						builtin_make_info_string(VM_ARG, infoString, "{");
						make_object = ZL_EXP_TRUE;
					}
					else {
						builtin_make_info_string(VM_ARG, infoString, "[");
						make_object = ZL_EXP_FALSE;
					}
				}
				process_count++;
				break;
			default: // 如果是其他类型,例如NONE成员(没有被赋值初始化过的成员),则跳过不处理
				continue;
			}
			................................................................................
		}
		................................................................................
	}
	................................................................................
}

    上面的C函数中在将zengl数组转为json时,如果遇到NONE之类的成员,则会跳过不处理,也就不会生成多余的英文逗号了。

修复bltMustacheFileRender模块函数的Bug:

    当前版本修复了bltMustacheFileRender模块函数的Bug,之前的版本在使用该模块函数解析模板时,如果遇到类似 <%> header.tpl%> 这样的模版标签时,会解析失败,这段标签假定使用<%作为标签起始符,使用%>作为标签结束符,中间的 > header.tpl 表示将header.tpl模板包含进来(在起始符右侧的>是包含其他模板的指令符号)。当前版本修复了这个问题,从而可以解析这样的模板标签。修复的代码位于 crustache/crustache.c 文件中:

static int
find_mustache(
	size_t *mst_pos,
	size_t *mst_size,
	crustache_template *template,
	size_t i,
	const char *buffer,
	size_t size)
{
	const char *mst_start;
	const char *mst_end;
	
	mst_start = railgun(
		buffer + i, size - i,
		template->mustache_open.chars,
		template->mustache_open.size);

	if (mst_start == NULL)
		return 0; /* no mustaches found */

	// 将下面这段代码,以及紧跟着的if条件判断给注释掉,因为这段代码会导致类似 <%> header.tpl%> 这样的模版标签解析出错
	/* mst_end = railgun(
		buffer + i, size - i,
		template->mustache_close.chars,
		template->mustache_close.size); */

	// if (mst_end == mst_start) {
		mst_end = railgun(
			mst_start + template->mustache_open.size,
			buffer + size - (template->mustache_open.size + mst_start),
			template->mustache_close.chars,
			template->mustache_close.size);
	// }

	if (mst_end == NULL || mst_end < mst_start) {
		template->error_pos = mst_end ? (mst_end - buffer) : (mst_start - buffer);
		return CR_EPARSE_MISMATCHED_MUSTACHE;
	}

	/* Greedy matching */
	if (mst_end + 1 == railgun(
		mst_end + 1, buffer + size - mst_end - 1,
		template->mustache_close.chars,
		template->mustache_close.size))
		mst_end = mst_end + 1;

	*mst_pos = mst_start - buffer;
	*mst_size = mst_end + template->mustache_close.size - mst_start;

	return 1; /* one mustache found */
}

    上面将find_mustache这个C函数里的 mst_end = railgun( buffer + i, size - i .... 这段语句以及紧跟着的if条件判断给注释掉了,因为这段语句会导致在解析 <%> header.tpl %> 这样的标签时,当解析出左侧的<%的起始符后,会直接从起始符的最后一个字符开始,这里就是从起始符的%开始搜索结束符,这样在遇到%右侧的>符号时,直接将<%> header.tpl %> 也就是header.tpl左侧的 %> 当成了结束符,从而导致解析失败,注释掉上面的相关语句后,在解析出起始符后,会从起始符的后面一个字符(本例中就是从<%后面的>字符)开始搜索结束符,从而可以避免这个问题。

test_bugfix.zl脚本:

    为了测试上面修复过的两个模块函数的Bug,当前版本在 my_webroot/v0_23_0/ 目录中增加了test_bugfix.zl的测试脚本,该测试脚本的代码如下:

use builtin;

data['name'] = 'zenglong';
data['job'] = 'programmer';
data['test'] = 'i will be unset';
bltUnset(&data['test']);
data['hobby'] = 'play game';

print bltJsonEncode(data);

print bltMustacheFileRender('test.tpl');

    上面脚本中用到的test.tpl测试模板也位于 my_webroot/v0_23_0/ 目录中,该测试模板的内容如下:

{{=<% %>=}}
<%> header.tpl%>
<%> footer.tpl%>

    test.tpl模板中会先通过 {{=<% %>=}} 模板语句将 <% 设置为模板标签新的起始符,以及将 %> 设置为模板标签新的结束符。接着就可以用 <%> header.tpl %> <%> footer.tpl %> 这样的模板标签语句将header.tpl和footer.tpl子模板的内容给包含进来了。

    上面的test_bugfix.zl的测试脚本在命令行中的执行结果如下:

[root@localhost zenglServer]# ./zenglServer -r "/v0_23_0/test_bugfix.zl"
{"name":"zenglong","job":"programmer","hobby":"play game"}

<html>
<head>
	<title>test</title>
</head>
<body>
	<h3>I'm in header</h3>

	<h3>I'm in footer!!</h3>
</body>
</html>


[root@localhost zenglServer]#

当发生段错误时将函数栈追踪信息写入日志:

     当前版本中,当zenglServer在命令行模式下的主进程或web模式下的工作子进程,因为严重的段错误导致进程挂掉时,会通过dump_process_segv_fault的C函数将段错误相关的函数栈追踪信息记录到日志中,从而可以分析出段错误发生的原因。该C函数定义在main.c文件中:

/**
 * 当zenglServer的命令行模式下的主进程或web模式下的工作子进程因为严重的段错误导致进程挂掉时,
 * 会通过下面这个C函数将段错误相关的函数栈追踪信息记录到日志中,从而可以分析出段错误发生的原因,
 * 由于记录在日志中的函数栈追踪信息里的地址是十六进制格式的地址,所以,还需要通过addr2line命令将这些地址转为具体的函数名(包括这些函数所在的C文件路径及行号信息)
 * 例如:addr2line 0x46a161 -e zenglServer -f 假设该命令中的0x46a161是日志中记录的函数地址的十六进制格式,那么得到的结果类似如下所示:
 * zenglrun_RunInsts (函数名)
 * /root/zenglServerTest/zengl/linux/zenglrun_main.c:1245 (函数所在的C文件路径及行号信息)
 */
static void dump_process_segv_fault()
{
	void *buffer[100] = {0};
	size_t size;
	char **strings = NULL;
	size_t i = 0;

	size = backtrace(buffer, 100);
	write_to_server_log_pipe(WRITE_TO_PIPE_, "segv fault backtrace() returned %d addresses \n", size);
	strings = backtrace_symbols(buffer, size);
	if (strings == NULL) {
		write_to_server_log_pipe(WRITE_TO_PIPE_, "error: backtrace_symbols return NULL");
		exit(EXIT_FAILURE);
	}

	for (i = 0; i < size; i++)
	{
		write_to_server_log_pipe(WRITE_TO_PIPE_, "%s\n", strings[i]);
	}

	free(strings);
	strings = NULL;
	exit(0);
}

    当zenglServer工作进程发生段错误时,相关信息都会记录到日志文件中,以下是一个段错误发生时,记录在日志中的相关信息:

[root@localhost zenglServer]# cat logfile
.......................................................................................
Sat Nov  7 16:09:29 2020
recv [client_socket_fd:8] [lst_idx:0] [pid:2267] [tid:2272]:

request header: Host: 192.168.0.106:8083 | User-Agent: Mozilla/5.0 (Windows NT 10.0; ......................................................

url: /
url_path: /
segv fault backtrace() returned 6 addresses
zenglServer: child(0) ppid:2265() [0x40741d]
/lib64/libc.so.6(+0x363b0) [0x7f6d50a243b0]
zenglServer: child(0) ppid:2265() [0x4090ef]
zenglServer: child(0) ppid:2265() [0x408afd]
/lib64/libpthread.so.0(+0x7e65) [0x7f6d53173e65]
/lib64/libc.so.6(clone+0x6d) [0x7f6d50aec88d]
child PID 2267 exited normally.  Exit number:  0
.......................................................................................
[root@localhost zenglServer]#

    通过日志中的十六进制地址,再结合addr2line工具,我们就可以知道错误发生的具体位置,例如上面的 0x408afd 的地址我们就可以用下面的命令查看到该地址所在的函数名,以及该地址对应的具体的C文件名和行列号信息:

[root@localhost zenglServer]# addr2line 0x408afd -e zenglServer -f
routine
/root/zenglServerMaster/zenglServer/main.c:1769
[root@localhost zenglServer]#

对常见请求头key进行大小写转换处理:

    由于某些CDN(例如cloudflare)会将客户端传递过来的某些http请求头key都转为小写。因此,当前版本会对一些常见的请求头key进行大小写转换,从而确保模块函数和脚本中都可以使用一致的大小写方式来访问这些http请求头信息。相关的C函数代码定义在module_request.c文件中:

/**
 * 将请求头中的key转为指定的格式,例如:content-type,content-Type,CONTENT-TYPE等请求头字段key都会被转为Content-Type,
 * cookie,COOKIE,cookiE等也都会被转为Cookie,这样转为统一的格式后,就始终可以通过Content-Type的key来获取内容类型,以及
 * 使用Cookie的key来获取cookie的值等,像cloudflare之类的cdn可能会将客户端传递过来的请求头key转为小写,所以需要将一些常规的key
 * 通过下面的函数转为指定的格式,方便在模块函数中以及在脚本中使用统一的key来访问请求头中的数据。
 *
 * 这里只对比较常见的key,例如content-type,content-length等进行了转换,
 * 其他的请求头key需要自行在脚本中进行处理(例如可以通过bltToLower模块函数生成全是小写的请求头key等)。
 */
static ZL_EXP_CHAR *  get_final_header_field(ZL_EXP_CHAR * field)
{
	ZL_EXP_CHAR * from[CONVERT_HEADER_FIELD_LEN] = {
		"host", "user-agent", "accept-language", "accept-encoding", "content-type", "content-length",
		"origin", "connection", "referer", "accept", "cookie"
	};
	ZL_EXP_CHAR * to[CONVERT_HEADER_FIELD_LEN] = {
		"Host", "User-Agent", "Accept-Language", "Accept-Encoding", "Content-Type", "Content-Length",
		"Origin", "Connection", "Referer", "Accept", "Cookie"
	};
	ZL_EXP_CHAR * result = field;
	int field_len = strlen(field);
	for(int i = 0; i < CONVERT_HEADER_FIELD_LEN; i++) {
		if(strlen(from[i]) == field_len) {
			if(strncasecmp(from[i], field, field_len) == 0) {
				result = to[i];
				break;
			}
		}
	}
	return result;
}

500错误加入Content-Type:

    在web端执行脚本时,如果发生500错误,当前版本会在响应头中加入Content-Type,以防止某些CDN加入错误的Content-Type,相关的C代码位于main.c文件中:

static int routine_process_client_socket(CLIENT_SOCKET_LIST * socket_list, int lst_idx)
{
	....................................................................
	if(zenglApi_Run(VM, full_path) == -1) //编译执行zengl脚本
	{
		// 如果执行失败,则显示错误信息,并抛出500内部错误给客户端
		fatal_error_set_error_string(zenglApi_GetErrorString(VM));
		if(fatal_error_callback_exec(VM, full_path, fatal_error_get_error_string()) == -1) {
			write_to_server_log_pipe(WRITE_TO_PIPE_, "zengl run fatal error callback of <%s> failed: %s\n",
					full_path, zenglApi_GetErrorString(VM));
		}
		else {
			write_to_server_log_pipe(WRITE_TO_PIPE_, "zengl run <%s> failed: %s\n",
					full_path, fatal_error_get_error_string());
		}
		client_socket_list_append_send_data(socket_list, lst_idx, "HTTP/1.1 500 Internal Server Error\r\n", 36);
		client_socket_list_append_send_data(socket_list, lst_idx, "Content-Type: text/html\r\n", 25);
		dynamic_string_append(&my_data.response_body, "500 Internal Server Error", 25, 200);
		status_code = 500;
	}
	....................................................................
}

    上面代码中,当在web端执行zengl脚本失败,并抛出500错误时,还会将Content-Type响应头设置为text/html类型。

处理rqtGetBody在没有POST请求主体数据时报500错误的问题:

    之前的版本,如果在web端直接访问 my_webroot/v0_2_0/post.zl 脚本(不通过相同目录内的form.html提交表单数据),会报500错误。这是因为在之前的版本中,rqtGetBody模块函数在没有POST请求主体数据时,获取相关的数据指针和数据长度时,会因为数据长度为0无法加入到内部的指针列表中,从而会报相关的错误。当前版本修复了这个问题,修复方案就是当没有请求主体数据时,会跳过加入指针列表的操作,而直接将空指针设置到返回参数中,相关的修复代码位于module_request.c文件里:

...........................................................................
ZL_EXP_VOID module_request_GetBody(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT argcount)
{
	...........................................................................

	if(argcount == 1 || argcount == 2) {
		...........................................................................
		if(argcount == 2) {
			...........................................................................
			// 只有当body_count大于0时,也就是存在请求主体数据时,才将请求主体数据相关的指针设置到第二个参数
			if(body_count > 0) {
				int ret_set_ptr = pointer_list_set_member(&(my_data->pointer_list), my_parser_data->request_body.str, body_count, NULL);
				if(ret_set_ptr != 0) {
					zenglApi_Exit(VM_ARG, "rqtGetBody add pointer to pointer_list failed, pointer_list_set_member error code:%d", ret_set_ptr);
				}
				arg.val.integer = (ZL_EXP_LONG)my_parser_data->request_body.str;
			}
			else // 如果body_count不大于0,则说明没有请求主体数据,则将空指针(也就是值为0的指针值)设置到第二个参数
				arg.val.integer = 0;
			zenglApi_SetFunArg(VM_ARG,2,&arg);
		}
	}
	...........................................................................
}

将403和404错误的具体原因记录到日志:

    之前的版本在web端抛出403和404错误时,并没有将导致这些错误的具体原因记录在日志中。对于403错误,有可能是直接访问目录导致的,也有可能是没有权限访问文件导致的。对于404错误,则有可能是文件不存在导致的,也有可能是因为别的原因无法打开文件导致的。

    因此,当前版本就将导致这些错误的具体原因也记录到了日志中。相关的C代码位于main.c文件里:

static int routine_process_client_socket(CLIENT_SOCKET_LIST * socket_list, int lst_idx)
{
	................................................................................
		doc_fd = open(full_path, O_RDONLY);
		if(doc_fd == -1) {
			// 如果open函数返回-1,则说明无法打开文件,就设置403或404状态码,并将打开文件失败的具体原因记录到日志中
			if(config_verbose)
				write_to_server_log_pipe(WRITE_TO_PIPE, "open file failed: [%d] %s\n", errno, strerror(errno));
			else
				write_to_server_log_pipe(WRITE_TO_PIPE_, "open file failed: [%d] %s | ", errno, strerror(errno));
			................................................................................
		}
	................................................................................
	// 非常规文件,直接返回403禁止访问
	if(!S_ISREG(filestatus.st_mode)) {
		status_code = 403;
		default_output_html = DEFAULT_OUTPUT_HTML_403;
		is_reg_file = ZL_EXP_FALSE;
		if(config_verbose)
			write_to_server_log_pipe(WRITE_TO_PIPE, "directory have no index.html, directory are not allowed directly access\n");
		else
			write_to_server_log_pipe(WRITE_TO_PIPE_, "directory have no index.html, directory are not allowed directly access | ");
	}
	................................................................................
}

    从上面的代码中可以看到,如果无法打开文件,则会根据errno即文件打开失败的错误码将 open file failed..... 信息作为404或403错误的原因记录到日志中。如果是直接访问目录,且目录中没有包含index.html,则会将directory have no index.html..... 的信息作为403错误的具体原因记录到日志中。以下是一个404错误在日志中的记录情况:

[root@localhost zenglServer]# cat logfile
.................................................................................
-----------------------------------
Fri Nov  6 11:07:23 2020
recv [client_socket_fd:9] [lst_idx:0] [pid:3875] [tid:3879]:

request header: Host: 192.168.0.100:8083 | User-Agent: Mozilla/5.0 (Windows ........................

url: /v0_23_0/v0_2_0/post.zl
url_path: /v0_23_0/v0_2_0/post.zl
full_path: my_webroot/v0_23_0/v0_2_0/post.zl
open file failed: [2] No such file or directory
status: 404, content length: 99
response header: HTTP/1.1 404 Not Found
Content-Type: text/html
Content-Length: 99
Connection: Closed
Server: zenglServer
free socket_list[0]/list_cnt:0 epoll_fd_add_count:0 pid:3875 tid:3879
.................................................................................
[root@localhost zenglServer]#

    可以看到上面这个404错误,是由于 No such file or directory 也就是文件不存在导致的。

为403和404设置默认的显示内容:

    之前的版本在发生403错误和404错误(如果根目录中又没有404.html)时,只会返回相应的状态码。当前版本则在发生403错误时,或者在web根目录中没有404.html文件并发生了404错误时,会输出一段默认的显示内容。相关的C代码位于main.c文件中:

....................................................................................

// 当web根目录中没有定义404.html时,就会将下面这个宏定义的字符串,作为404错误的默认输出内容返回给客户端
#define DEFAULT_OUTPUT_HTML_404 "<html><head><title>404 Not Found</title></head><body><center><h1>404 Not Found</h1></center></body></html>"
// 当发生403错误时,会将下面这个宏定义的字符串作为结果返回给客户端
#define DEFAULT_OUTPUT_HTML_403 "<html><head><title>403 Forbidden</title></head><body><center><h1>403 Forbidden</h1></center></body></html>"

....................................................................................
static int routine_process_client_socket(CLIENT_SOCKET_LIST * socket_list, int lst_idx)
{
	....................................................................................
		// 如果不是zengl脚本,则直接打开full_path对应的文件,如果打不开,说明文件不存在或者没有权限打开文件
		// 如果文件不存在则打开web根目录中的404.html文件,并设置404状态码,如果是没有权限打开文件,
		// 则设置403状态码,并设置错误的默认输出内容
		doc_fd = open(full_path, O_RDONLY);
		if(doc_fd == -1) {
			// 如果open函数返回-1,则说明无法打开文件,就设置403或404状态码,并将打开文件失败的具体原因记录到日志中
			if(config_verbose)
				write_to_server_log_pipe(WRITE_TO_PIPE, "open file failed: [%d] %s\n", errno, strerror(errno));
			else
				write_to_server_log_pipe(WRITE_TO_PIPE_, "open file failed: [%d] %s | ", errno, strerror(errno));
			// 如果是没有权限打开文件,则设置403状态码,并设置403错误的默认输出内容
			if(errno == EACCES) {
				status_code = 403;
				default_output_html = DEFAULT_OUTPUT_HTML_403;
			}
			else {
				full_length = root_length;
				full_length += main_full_path_append(full_path, full_length, FULL_PATH_SIZE, "/404.html");
				full_path[full_length] = '\0';
				stat(full_path, &filestatus);
				doc_fd = open(full_path, O_RDONLY);
				status_code = 404;
				// 如果web根目录中的404.html文件不存在或者无法打开,则设置404错误的默认输出内容
				if(doc_fd == -1)
					default_output_html = DEFAULT_OUTPUT_HTML_404;
			}
		}
	....................................................................................
}

    从上面的代码中可以看到,当web根目录中没有404.html时,就会将DEFAULT_OUTPUT_HTML_404对应的字符串即 "<html><head><title>404 Not Found</title></head><body><center><h1>404 Not Found</h1></center></body></html>",作为404错误的默认输出内容返回给客户端。同理,当发生403错误时,则会将DEFAULT_OUTPUT_HTML_403宏对应的字符串 "<html><head><title>403 Forbidden</title></head><body><center><h1>403 Forbidden</h1></center></body></html>" 作为结果返回给客户端。

支持中文url路径:

    当前版本可以通过utf8格式的url编码来访问中文路径,在 my_webroot/v0_23_0/ 目录中增加了一个名为"测试.zl"的脚本,该脚本的代码如下:

use builtin,request;
headers = rqtGetHeaders();
print 'user agent: ' + headers['User-Agent'] + '<br/>';

query_string = rqtGetQueryAsString();
if(query_string)
	print 'query string: ' + query_string + '<br/>';
	querys = rqtGetQuery();
	// 通过bltIterArray模块函数来迭代数组成员
	for(i=0;bltIterArray(querys,&i,&k,&v);)
		print k +": " + v + '<br/>';
	endfor
endif
print 'test...';

    以下是"测试.zl"脚本在命令行中的执行情况:

[root@localhost zenglServer]# ./zenglServer -r "/v0_23_0/%E6%B5%8B%E8%AF%95.zl?a=123&b=456"
user agent: 0<br/>
query string: a=123&b=456<br/>
a: 123<br/>
b: 456<br/>
test...
[root@localhost zenglServer]#

    "测试.zl"这个中文脚本文件名对应的utf8格式的url编码是 %E6%B5%8B%E8%AF%95.zl ,因此,上面就通过该url编码来执行了"测试.zl"这个使用中文名字的脚本。以下是该脚本在浏览器中的执行情况:

"测试.zl"脚本

    在火狐之类的浏览器中直接输入中文路径,例如上面的/v0_23_0/测试.zl.....,浏览器会直接将中文转为utf8格式的url编码,因此,在这些浏览器的地址栏中可以直接输入中文来访问中文url路径。

    当前版本会对路径中的url编码进行解码,从而实现对中文路径的访问,相关的C代码位于main.c文件中:

static int routine_process_client_socket(CLIENT_SOCKET_LIST * socket_list, int lst_idx)
{
	.........................................................................
	// 对客户端传递过来的url路径信息进行url解码,这样在linux中就可以访问utf8编码的中文路径了
	gl_request_url_decode(decode_url_path, url_path, strlen(url_path));
	write_to_server_log_pipe(WRITE_TO_PIPE, "url_path: %s\n", decode_url_path);
	.........................................................................
}

.......................................................................................
static int main_run_cmd(char * run_cmd)
{
	.........................................................................
	// 对客户端传递过来的url路径信息进行url解码,这样在linux中就可以访问utf8编码的中文路径了
	gl_request_url_decode(decode_url_path, url_path, strlen(url_path));
	write_to_server_log_pipe(WRITE_TO_PIPE_, "url_path: %s\n", decode_url_path);
	.........................................................................
}

结束语:

    真金不怕沙埋,就算埋得再深,再久,一样会发光的。

—— 创业年代

 

上下篇

下一篇: zenglServer v0.24.0 增加bltVersionCompare,mysqlAffectedRows模块函数,为bltStr增加format可选参数

上一篇: zenglServer v0.22.0 增加支付宝支付测试脚本,增加bltUrlEncode等模块函数

相关文章

zenglServer v0.19.0 增加redis缓存相关的模块

zenglServer v0.12.0 图形验证码, 新增模块函数

zenglServer v0.18.0 直接在命令行中执行脚本

zenglServer v0.17.0 设置精简日志模式,设置允许上传文件大小,日志分割等

zenglServer v0.10.1 添加bltInt,bltFloat,bltHtmlEscape模块函数,使用v1.8.1版本的zengl语言库

zenglServer v0.3.0 mysql模块