该版本新增session会话功能,服务端在新建会话时,可以将会话ID通过Set-Cookie响应头传给客户端(主要是web浏览器),这样,客户端在后续的请求中,会将会话ID通过Cookie传给服务端,服务端在接收到请求后,根据会话ID找到相应的会话文件,并读取或写入相关的会话数据。在写入会话文件时,如果是数组,则会被先转为json格式,再写入文件...

    页面导航: 项目下载地址:

    zenglServer源代码的相关地址:https://github.com/zenglong/zenglServer  当前版本对应的tag标签为:v0.6.0 ,包含两个分支:master主分支和develop开发分支。作者会在开发分支上进行日常的开发工作,在某个版本开发结束时,会合并到主分支上。

zenglServer v0.6.0:

    该版本新增session会话功能,服务端在新建会话时,可以将会话ID通过Set-Cookie响应头传给客户端(主要是web浏览器),这样,客户端在后续的请求中,会将会话ID通过Cookie传给服务端,服务端在接收到请求后,根据会话ID找到相应的会话文件,并读取或写入相关的会话数据。在写入会话文件时,如果是数组,则会被先转为json格式,再写入文件。

    在根目录的config.zl配置文件中,可以设置会话文件需要保存的目录,会话文件的超时时间,以及会话清理进程的清理时间间隔:

debug_mode = 1;
//debug_mode = 0;
// zl_debug_log = "zl_debug.log"; // zengl脚本的调试日志,可以输出相关的虚拟汇编指令

port = 8083; // 绑定的端口

if(!debug_mode)
	process_num = 3; // 进程数
else
	print '*** config is in debug mode ***';
	process_num = 1; // 进程数
endif

webroot = "my_webroot"; // web根目录

session_dir = "my_sessions"; // 会话目录
session_expire = 1440; // 会话默认超时时间(以秒为单位)
session_cleaner_interval = 3600; // 会话文件清理进程的清理时间间隔(以秒为单位)


    上面配置中,session_dir用于设置会话文件需要保存的目录(如果不设置,默认是sessions)。session_expire用于设置会话文件的过期时间(如果不设置,默认是1440秒),如果在过期时间内没有任何操作的话,会话数据就会失效。session_cleaner_interval用于设置会话文件清理进程的清理时间间隔(如果不设置,默认是3600秒),当前版本会创建一个清理进程,并在指定的时间间隔自动清理过期的会话文件。

    会话是通过session模块实现的,该模块对应的C源文件是:module_session.c,该模块目前定义了三个模块函数:sessGetData(根据会话文件名读取会话数据),sessSetData(将数据写入会话文件),以及sessMakeId(生成40个字符的随机字符串,可以用作会话文件名):

/*
 * module_session.c
 *
 *  Created on: 2017-12-3
 *      Author: zengl
 *
 * 该模块用于处理和session会话相关的内容
 * 在写入会话数据时,zengl脚本中的数组,会先转为json格式,再保存到session会话文件中
 * 在读取会话数据时,会先通过json-parser第三方解析程式,将json解析出来,再转为zengl数组等进行返回
 * json-parser的github地址:https://github.com/udp/json-parser
 */

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

/**
 * sessGetData模块函数,根据会话文件名,将会话数据转为zengl脚本可以识别的数据类型
 * 如果会话数据是一个json对象或者json数组,那么返回的就会是zengl数组,如果会话数据是整数,返回的也会是整数等
 */
ZL_EXP_VOID module_session_get_data(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT argcount)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	if(argcount != 1)
		zenglApi_Exit(VM_ARG,"usage:sessGetData(sess_file_name)");
	// 获取第一个参数,也就是会话文件名
	zenglApi_GetFunArg(VM_ARG,1,&arg);
	if(arg.type != ZL_EXP_FAT_STR) {
		zenglApi_Exit(VM_ARG,"the first argument of sessGetData must be string");
	}
	char filename[SESSION_FILEPATH_MAX_LEN];
	char * session_dir;
	long session_expire;
	// 先通过main_get_session_config函数获取会话目录,然后根据会话目录和会话文件名生成会话文件的相对路径
	main_get_session_config(&session_dir, &session_expire, NULL);
	session_make_filename(filename, session_dir, arg.val.str);

	int file_size;
	struct stat filestatus;
	if ( stat(filename, &filestatus) != 0) {
		session_return_empty_array(VM_ARG);
		return;
	}
	time_t cur_time = time(NULL);
	// 如果会话文件的修改时间(修改时间被用作超时时间)小于当前时间,则该会话文件已经超时,直接删除掉该文件,并返回空数组
	if(filestatus.st_mtime < cur_time) {
		remove(filename); // 删除超时的会话文件
		write_to_server_log_pipe(WRITE_TO_PIPE, "debug info: sessionGetData remove file: %s [m_time:%d < %d]\n", filename, filestatus.st_mtime, cur_time);
		session_return_empty_array(VM_ARG);
		return;
	}
	file_size = filestatus.st_size;
	FILE * fp = fopen(filename, "rb");
	// fp为NULL,直接返回空数组
	if (fp == NULL) {
		session_return_empty_array(VM_ARG);
		return;
	}
	char * file_contents = (char *)zenglApi_AllocMem(VM_ARG, file_size);
	int nread = fread(file_contents, file_size, 1, fp);
	if ( nread != 1 ) {
		fclose(fp);
		zenglApi_Exit(VM_ARG,"sessGetData error: Unable t read content of \"%s\"", filename);
	}
	fclose(fp);
	json_value * value;
	json_char * json = (json_char*)file_contents;
	json_settings settings = { 0 };
	settings.mem_alloc = my_json_mem_alloc;
	settings.mem_free = my_json_mem_free;
	settings.user_data = VM_ARG;
	json_char json_error_str[json_error_max];
	// 通过json-parser第三方解析程式来解析会话文件中的json数据,解析的结果是一个json_value结构
	value = json_parse_ex (&settings, json, file_size, json_error_str);
	if (value == NULL) {
		zenglApi_Exit(VM_ARG,"sessGetData error: Unable to parse data, json error: %s", json_error_str);
	}
	ZENGL_EXPORT_MEMBLOCK memblock;
	switch (value->type) {
	case json_none: // 将json中的null转为整数0
		zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_INT, ZL_EXP_NULL, 0, 0);
		break;
	case json_object:
	case json_array:
		// 如果是json对象或json数组,则创建一个memblock内存块
		if(zenglApi_CreateMemBlock(VM_ARG,&memblock,0) == -1) {
			zenglApi_Exit(VM_ARG,zenglApi_GetErrorString(VM_ARG));
		}
		// 通过process_json_object_array函数,循环将value中的json成员填充到memblock中
		process_json_object_array(VM_ARG, &memblock, value);
		zenglApi_SetRetValAsMemBlock(VM_ARG,&memblock);
		break;
	case json_integer:
		zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_INT, ZL_EXP_NULL, (ZL_EXP_LONG)value->u.integer, 0);
		break;
	case json_double:
		zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_FLOAT, ZL_EXP_NULL, 0, value->u.dbl);
		break;
	case json_string:
		zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_STR, value->u.string.ptr, 0, 0);
		break;
	case json_boolean: // 将json中的bool类型转为整数,例如:true转为1,false转为0
		zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_INT, ZL_EXP_NULL, (ZL_EXP_LONG)value->u.boolean, 0);
		break;
	default:
		zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_INT, ZL_EXP_NULL, 0, 0);
		break;
	}
	json_value_free_ex (&settings, value);
	zenglApi_FreeMem(VM_ARG, file_contents);
	// 设置会话文件session_expire秒后过期,其实就是将会话文件的修改时间设置为当前时间加上session_expire秒后的时间值
	// 因此,会话文件的修改时间就是过期时间
	session_set_expire(filename, session_expire);
}

/**
 * sessSetData模块函数,将zengl数据写入到sess_file_name会话文件
 * 在写入时,如果是zengl数组,则会被先转为json格式,再写入会话文件
 * 例如:
 * a['hello'] = 'world';
 * a['name'] = 'zengl';
 * sessSetData(sess_id, a);
 * 执行后,写入sess_id会话文件中的内容就会是: {"hello":"world","name":"zengl"}
 */
ZL_EXP_VOID module_session_set_data(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT argcount)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	if(argcount != 2)
		zenglApi_Exit(VM_ARG,"usage:sessSetData(sess_file_name, data)");
	// 获取第一个参数,也就是会话文件名
	zenglApi_GetFunArg(VM_ARG,1,&arg);
	if(arg.type != ZL_EXP_FAT_STR) {
		zenglApi_Exit(VM_ARG,"the first argument of sessSetData must be string");
	}
	char filename[SESSION_FILEPATH_MAX_LEN];
	char * session_dir;
	long session_expire;
	// 先通过main_get_session_config函数获取会话目录,然后根据会话目录和会话文件名生成会话文件的相对路径
	main_get_session_config(&session_dir, &session_expire, NULL);
	session_make_filename(filename, session_dir, arg.val.str);

	struct stat filestatus;
	if ( stat(filename, &filestatus) == 0) {
		time_t cur_time = time(NULL);
		if(filestatus.st_mtime < cur_time) {
			remove(filename); // 删除超时的会话文件,超时的会话文件,既不能进行读取操作,也不能进行写入操作
			write_to_server_log_pipe(WRITE_TO_PIPE, "debug info: sessSetData remove file: %s [m_time:%d < %d]\n", filename, filestatus.st_mtime, cur_time);
			zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_INT, ZL_EXP_NULL, 0, 0);
			return;
		}
	}

	FILE * session_file = fopen(filename, "w+");
	if(session_file == NULL) {
		zenglApi_Exit(VM_ARG,"sessSetData open file \"%s\" failed [%d] %s", filename, errno, strerror(errno));
	}
	zenglApi_GetFunArg(VM_ARG,2,&arg);
	switch(arg.type) {
	case ZL_EXP_FAT_MEMBLOCK:
		// 通过session_write_array_to_file函数将zengl数组转为json格式,并写入session_file会话文件
		session_write_array_to_file(VM_ARG, session_file, arg.val.memblock);
		break;
	case ZL_EXP_FAT_INT:
		fprintf(session_file, "%ld",arg.val.integer);
		break;
	case ZL_EXP_FAT_FLOAT:
		fprintf(session_file, "%.16g",arg.val.floatnum);
		break;
	case ZL_EXP_FAT_STR:
		fprintf(session_file, "%s",arg.val.str);
		break;
	default:
		fprintf(session_file, "null");
		break;
	}
	fclose(session_file);
	// 设置会话文件session_expire秒后过期,其实就是将会话文件的修改时间设置为当前时间加上session_expire秒后的时间值
	// 因此,会话文件的修改时间就是过期时间
	session_set_expire(filename, session_expire);
	// 执行成功,返回整数1
	zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_INT, ZL_EXP_NULL, 1, 0);
}

/**
 * sessMakeId模块函数,生成40个字符的随机字符串,并将该字符串作为结果返回
 * 该随机字符串可以用作会话文件名
 * 生成随机字符串时,所使用的random_get_bytes函数,是从libuuid库中移植过来的,
 * 其中会用到/dev/urandom或者/dev/random等,具有比较高的随机性
 */
ZL_EXP_VOID module_session_make_id(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT argcount)
{
	unsigned int v, i;
	char buf[50];
	char * p = buf;
	for (i = 0; i < 5; i++) {
		random_get_bytes(&v, sizeof(v));
		sprintf(p, "%08x", v);
		p += 8;
	}
	(*p) = '\0';
	zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_STR, buf, 0, 0);
}

/**
 * session模块的初始化函数,里面设置了与该模块相关的各个模块函数及其相关的处理句柄
 */
ZL_EXP_VOID module_session_init(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT moduleID)
{
	zenglApi_SetModFunHandle(VM_ARG,moduleID,"sessGetData", module_session_get_data);
	zenglApi_SetModFunHandle(VM_ARG,moduleID,"sessSetData", module_session_set_data);
	zenglApi_SetModFunHandle(VM_ARG,moduleID,"sessMakeId", module_session_make_id);
}


    为了测试这三个模块函数,在根目录的my_webroot目录中新增了v0_6_0目录,该目录中的test_sess_json.zl脚本用于测试读取会话数据,test_write_sess.zl脚本用于测试写入会话数据。

    test_sess_json.zl脚本的代码如下所示:

use builtin, request, session;

print '<!Doctype html>
<html>
<head><meta http-equiv="content-type" content="text/html;charset=utf-8" />
<title>显示会话json数据</title>
</head>
<body>';

cookies = rqtGetCookie();
sess_id = cookies['SESSION'];
if(!sess_id)
	print '暂无会话id';
	bltExit();
endif

print '会话中的json数组:<br/><br/>';
sessions = sessGetData(sess_id);
for(i=0; bltIterArray(sessions,&i,&k,&v); )
	//if(k == 'array')
	if(k == '4')
		for(j=0; bltIterArray(v,&j,&inner_k,&inner_v); )
			print ' -- ' + inner_k +": " + inner_v + '<br/>';
		endfor
	else
		print k +": " + v + '<br/>';
	endif
endfor

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


    上面脚本中,先根据Cookie中的SESSION获取到sess_id(会话ID),接着通过sessGetData模块函数,从会话文件中读取出会话数据,并将会话数据循环打印出来,脚本中直接使用会话ID作为会话文件名。

    test_write_sess.zl脚本的代码如下:

use builtin, request, session;

print '<!Doctype html>
<html>
<head><meta http-equiv="content-type" content="text/html;charset=utf-8" />
<title>显示会话json数据</title>
</head>
<body>';

fun get_default_data(sess_id)
	data['hello'] = 'world "世界你好"吗\\"\'?';
	print data['hello'];

	data['integer'] = 1213334;
	data['float'] = 123.121355;
	data['sess_id'] = sess_id;
	item_array = bltArray();
	item_array[] = 123;
	item_array[] = 15.34;
	item_array[] = "hello \"world\"";
	item_array[] = "走自己的路,让别人去说吧!!!";
	data[] = item_array;
	return data;
endfun

cookies = rqtGetCookie();
sess_id = cookies['SESSION'];
if(!sess_id)
	sess_id = sessMakeId();
	data = get_default_data(sess_id);
	rqtSetResponseHeader("Set-Cookie: SESSION="+sess_id+"; path=/");
else 
	data = sessGetData(sess_id);
	if(data['integer'])
		print ++data['integer'];
	else
		data = get_default_data(sess_id);
	endif
endif

sessSetData(sess_id, data);

print '设置会话数据成功<br/>';

print 'sess_id:' + sess_id + '<br/>';

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


    上面脚本主要用于写入会话数据,当Cookie中没有设置过SESSION,也就是客户端没有传会话ID过来时,会先调用sessMakeId生成一个新的会话ID,并将该会话ID通过Set-Cookie响应头反馈给客户端,如果客户端传了会话ID,则通过sessGetData读取原来的会话数据,在会话数据没过期时,对数据中的integer进行加一操作,如果会话过期,则重新获取默认数据,最后统一由sessSetData模块函数将会话数据写入会话文件。

    test_write_sess.zl脚本的测试结果如下:


图1: 写入会话数据

    test_sess_json.zl脚本的测试结果如下:


图2: 读取会话数据

    在会话目录中可以看到生成的会话文件的内容:

zengl@zengl-ubuntu:~/zenglServer$ cat my_sessions/85e8456ff9ef7d9dd383466e44a1ae53e17da4b5 
{"hello":"world \"世界你好\"吗\\\"'?","integer":1213334,"float":123.121355,"sess_id":"85e8456ff9ef7d9dd383466e44a1ae53e17da4b5","4":[123,15.34,"hello \"world\"","走自己的路,让别人去说吧!!!"]}


    可以看到,zengl脚本中的数组在写入会话文件时,被自动转为了json格式。

    还可以查看到会话文件的过期时间:

zengl@zengl-ubuntu:~/zenglServer$ stat my_sessions/85e8456ff9ef7d9dd383466e44a1ae53e17da4b5 
  文件:my_sessions/85e8456ff9ef7d9dd383466e44a1ae53e17da4b5
  大小:213       	块:8          IO 块:4096   普通文件
设备:815h/2069d	Inode:29365789    硬链接:1
权限:(0666/-rw-rw-rw-)  Uid:( 1000/   zengl)   Gid:( 1000/   zengl)
最近访问:2017-12-19 10:45:55.065385316 +0800
最近更改:2017-12-19 10:58:03.000000000 +0800
最近改动:2017-12-19 10:34:03.247485574 +0800
创建时间:-
zengl@zengl-ubuntu:~/zenglServer$ 


    可以看到,该会话文件的最近一次属性改动时间是10:34:03,过期时间是10:58:03,相差24分钟(也就是config.zl配置文件中设置的1440秒),会话文件的修改时间就是过期时间。如果对该会话文件使用sessGetData进行读取或者使用sessSetData进行写入操作的话,过期时间会自动延长1440秒。

    当前版本还新增了cleaner清理进程,该进程目前主要是用于定期清理过期的会话文件用的,可以通过ps命令查看到该进程:

zengl@zengl-ubuntu:~/zenglServer$ ps aux | grep zenglServer
zengl     4143  0.0  0.0  26416  2192 ?        Ss   10:22   0:00 zenglServer: master
zengl     4144  0.0  0.0 108344  2580 ?        Sl   10:22   0:00 zenglServer: child(0)
zengl     4145  0.0  0.0  26416   532 ?        S    10:22   0:00 zenglServer: cleaner
zengl@zengl-ubuntu:~/zenglServer$ 


    创建cleaner进程相关的C代码是在main.c中实现的:

/**
 * 创建cleaner清理进程,该进程会定期清理过期的会话文件
 */
void fork_cleaner_process()
{
	pid_t childpid = fork();

	if(childpid == 0) {
		// 设置cleaner进程的进程名
		snprintf(current_process_name, 0xff, "zenglServer: cleaner");
		// 将cleaner进程从父进程继承过来的信号处理函数取消掉
		if (!trap_signals(ZL_EXP_FALSE)) {
			fprintf(stderr, "Cleaner [pid:%d]: trap_signals() failed!\n", childpid);
			exit(1);
		}
		do {
			DIR * dp;
			struct dirent * ep;
			char * path = config_session_dir;
			char filename[SESSION_FILEPATH_MAX_LEN];
			int path_dir_len = strlen(path);
			int ep_name_len, left_len;
			struct stat ep_stat;
			strncpy(filename, path, path_dir_len);
			filename[path_dir_len] = '/';
			left_len = SESSION_FILEPATH_MAX_LEN - path_dir_len - 2;

			dp = opendir(path);
			if (dp != NULL)
			{
				time_t cur_time = time(NULL);
				time_t compare_time = (cur_time - 10); // 删除10秒前的超时会话文件,预留10秒,防止当前时间刚生成的会话文件被误删除
				int cpy_len;
				while((ep = readdir(dp)))
				{
					ep_name_len = strlen(ep->d_name);
					if(ep_name_len > 20) {
						cpy_len = (ep_name_len <= left_len) ?  ep_name_len : left_len;
						strncpy(filename + path_dir_len + 1, ep->d_name, cpy_len);
						filename[path_dir_len + 1 + cpy_len] = '\0';
						if(stat(filename, &ep_stat) == 0) {
							if(ep_stat.st_mtime < compare_time) {
								remove(filename);
								write_to_server_log_pipe(WRITE_TO_PIPE, "************ cleaner remove file: %s [m_time:%d < %d]\n", ep->d_name, ep_stat.st_mtime, compare_time);
							}
						}
						else
							write_to_server_log_pipe(WRITE_TO_PIPE, "!!!******!!! cleaner remove \"%s\" failed [%d] %s\n", filename, errno, strerror(errno));
					}
				}
				closedir(dp);
			}
			else {
				write_to_server_log_pipe(WRITE_TO_PIPE, "!!!******!!! cleaner opendir \"%s\" failed [%d] %s\n", path, errno, strerror(errno));
			}
			write_to_server_log_pipe(WRITE_TO_PIPE, "------------ cleaner sleep begin: %d\n", time(NULL));
			sleep(config_session_cleaner_interval);
			write_to_server_log_pipe(WRITE_TO_PIPE, "------------ cleaner sleep end: %d\n", time(NULL));
		} while(1);
	}
	else if(childpid > 0) { // childpid大于0,表示当前是主进程,就向日志中输出创建的子进程的信息
		write_to_server_log_pipe(WRITE_TO_LOG, "Master: Spawning cleaner [pid %d] \n", childpid);
		server_cleaner_process = childpid;
	}
}


    从上面的代码中,可以看到,cleaner进程每执行完一次清理操作后,都会根据session_cleaner_interval配置,sleep睡眠一段时间,从而实现定期清理会话文件。在清理文件时,还会将相关的清理信息(删除了哪个会话文件,删除的会话文件的过期时间等)写入日志中。

    以上就是当前版本的相关内容,就到这里,休息,休息一下 o(∩_∩)o~~

结束语:

    塞巴斯蒂安·凯恩:"达芬奇从来不睡,说是浪费时间"

——  《透明人》
 
上下篇

下一篇: zenglServer v0.7.0-v0.7.1 mustache模板解析

上一篇: zenglServer v0.5.0 设置响应头, 获取cookie

相关文章

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

zenglServer v0.25.0 使用v1.9.0版本的zengl语言库,增加backlog及timezone配置,增加bltSetTimeZone模块函数

zenglServer v0.13.0 目录入口文件以及模板路径调整

zenglServer v0.14.0 正则表达式模块

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

zenglServer v0.21.0 增加base64编解码相关的内建模块函数