服务端Skynet(一)——源码浅析

news2024/11/16 1:29:04

服务端Skynet(一)——源码浅析

文章目录

  • 服务端Skynet(一)——源码浅析
    • 1、skynet的本质
    • 2、skynet基本的数据结构
      • 1、skynet_modules管理模块
      • 2、skynet_context模块
      • 3、skynet_message模块
    • 3、skynet启动服务步骤
    • 4、启动服务例子(logger)

参考文献:

skynet设计综述

skynet源码赏析

1、skynet的本质

Skynet 仅解决一个问题:把一个符合规范的 C 模块,从动态库(so 文件)中启动起来,绑定一个永不重复(即使模块退出)的数字 id 做为其 handle 。模块被称为服务(Service),服务间可以自由发送消息。每个模块可以向 Skynet 框架注册一个 callback 函数,用来接收发给它的消息。每个服务都是被一个个消息包驱动,当没有包到来的时候,它们就会处于挂起状态,对 CPU 资源零消耗。如果需要自主逻辑,则可以利用 Skynet 系统提供的 timeout 消息,定期触发。(Skynet 提供了名字服务,还可以给特定的服务起一个易读的名字,而不是用 id 来指代它。id 和运行时态相关,无法保证每次启动服务,都有一致的 id ,但名字可以。)

从上面的意思来看,skynet可以让我们写的不同的业务逻辑,独立运行在不同的上下文环境中,并且能够通过某种方式,相互协作,最终共同服务(actor模型)

skynet机制

在这里插入图片描述

  • 我们编写好的c文件,在编译成so库以后,在某个时机,调用该so库api的句柄,会被加载到一个modules列表中,一般这样的模块会被要求定义4种接口create,init,signal和release
  • 我们要创建一个新的,运行该业务逻辑的上下文环境时,则从modules列表中,找到对应的so库句柄,并且调用create接口,创建一个该类业务模块的数据实例,并且创建一个上下文环境(context),引用该类业务的接口和数据实例,该context会被存放在一个统一存放context的列表中,这种context被称之为服务
  • 一个服务,默认不会执行任何逻辑,需要别人向它发出请求时,才会执行对应的逻辑(定时器也是通过消息队列,告诉指定服务,要执行定时事件),并在需要时返回结果给请求者。请求者往往也是其他服务。服务间的请求、响应和推送,并不是直接调用对方的api来执行,而是通过一个消息队列,也就是说,不论是请求、回应还是推送,都需要通过这个消息队列转发到另一个服务中。skynet的消息队列,分为两级,一个全局消息队列,他包含一个头尾指针,分别指向两个隶属于指定服务的次级消息队列。skynet中的每一个服务,都有一个唯一的、专属的次级消息队列。
  • skynet一共有4种线程,monitor线程用于检测节点内的消息是否堵住,timer线程运行定时器,socket线程进行网络数据的收发,worker线程则负责对消息队列进行调度(worker线程的数量,可以通过配置表指定)。消息调度规则是,每条worker线程,每次从全局消息队列中pop出一个次级消息队列,并从次级消息队列中pop出一条消息,并找到该次级消息队列的所属服务,将消息传给该服务的callback函数,执行指定业务,当逻辑执行完毕时,再将次级消息队列push回全局消息队列中。因为每个服务只有一个次级消息队列,每当一条worker线程,从全局消息队列中pop出一个次级消息队列时,其他线程是拿不到同一个服务,并调用callback函数,因此不用担心一个服务同时在多条线程内消费不同的消息,一个服务执行,不存在并发,线程是安全的
  • socket线程、timer线程甚至是worker线程,都有可能会往指定服务的次级消息队列中push消息,push函数内有加一个自旋锁,避免同时多条线程同时向一个次级消息队列push消息的惨局。

2、skynet基本的数据结构

1、skynet_modules管理模块

/*
	一个模块被加载以后,将被放置到modules的skynet_module数组中,当要创建该module的实例时,将会从skynet_module中取出对应的模块,并调用create函数创建实例,然后将实例指针传入init函数完成初始化以后,赋值给context。
	一个C服务,定义以上四个接口时,一定要以文件名作为前缀,然后通过下划线和对应函数连接起来,因为skynet加载的时候,就是通过这种方式去寻找对应函数的地址的,比如一个c服务文件名为logger,那么对应的4个函数名则为logger_create、logger_init、logger_signal、logger_release(在程序中动态加载到skynet_module列表中,这里通过dlopen函数来获取so库的访问句柄,并通过dlsym将so库中对应的函数绑定到函数指针中)
*/
// skynet_module.h
typedef void * (*skynet_dl_create)(void);												//create
//skynet_context 对象会注册在 skynet_context list
typedef int (*skynet_dl_init)(void * inst, struct skynet_context *, const char * parm);		//init   
typedef void (*skynet_dl_release)(void * inst);											//release								
typedef void (*skynet_dl_signal)(void * inst, int signal);								 //signal
    
struct skynet_module {
    const char * name;          // C服务名称,一般是C服务的文件名
    void * module;              // 访问该so库的dl句柄,该句柄通过dlopen函数获得
    skynet_dl_create create;    // 绑定so库中的xxx_create函数,通过dlsym函数实现绑定,调用该create即是调用xxx_create
    skynet_dl_init init;        // 绑定so库中的xxx_init函数,调用该init即是调用xxx_init
    skynet_dl_release release;  // 绑定so库中的xxx_release函数,调用该release即是调用xxx_release
    skynet_dl_signal signal;    // 绑定so库中的xxx_signal函数,调用该signal即是调用xxx_signal
};
    
// skynet_module.c
#define MAX_MODULE_TYPE 32
    
struct modules {
    int count;                  // modules的数量
    struct spinlock lock;       // 自旋锁,避免多个线程同时向skynet_module写入数据,保证线程安全
    const char * path;          // 由skynet配置表中的cpath指定,一般包含./cservice/?.so路径
    struct skynet_module m[MAX_MODULE_TYPE];  // 存放服务模块的数组,最多32类
};
    
static struct modules * M = NULL;

2、skynet_context模块

/*
	对于一个新服务的创建流程:
	对应的module -> module实例化和初始化 -> 创建skynet_context上下文环境 -> module实例和模块与skynet_context关联 -> 放置到skynet_context list
	
	当一个消息送达一个context时,其callback函数就会被调用,callback函数一般在module的init函数里指定,调用callback函数时,会传入
        userdata(一般是instance指针),
        source(发送方的服务id),
        type(消息类型),
        msg和sz(数据及其大小),
	每个服务的callback处理各自的逻辑
*/

// skynet_server.c
struct skynet_context {
    void * instance;                // 由指定module的create函数,创建的数据实例指针,同一类服务可能有多个实例,
                                    // 因此每个服务都应该有自己的数据
        
    struct skynet_module * mod;     // 引用服务module的指针,方便后面对create、init、signal和release函数进行调用
    void * cb_ud;                   // 调用callback函数时,回传给callback的userdata,一般是instance指针
    skynet_cb cb;                   // 服务的消息回调函数,一般在skynet_module的init函数里指定
    struct message_queue *queue;    // 服务专属的次级消息队列指针
    FILE * logfile;                 // 日志句柄
    char result[32];                // 操作skynet_context的返回值,会写到这里
    uint32_t handle;                // 标识唯一context的服务id
    int session_id;                 // 在发出请求后,收到对方的返回消息时,通过session_id来匹配一个返回,对应哪个请求
    int ref;                        // 引用计数变量,当为0时,表示内存可以被释放
    bool init;                      // 是否完成初始化
    bool endless;                   // 消息是否堵住
    
    CHECKCALLING_DECL
};
    
// skynet_handle.c
// 这个结构用于记录,服务对应的别名,当应用层为某个服务命名时,会写到这里来
struct handle_name {
    char * name;                   // 服务别名
    uint32_t handle;               // 服务id
};
    
struct handle_storage {
    struct rwlock lock;            // 读写锁
    
    uint32_t harbor;               // harbor id
    uint32_t handle_index;         // 创建下一个服务时,该服务的slot idx,一般会先判断该slot是否被占用,后面会详细讨论
    int slot_size;                 // slot的大小,一定是2^n,初始值是4
    struct skynet_context ** slot; // skynet_context list
        
    int name_cap;                  // 别名列表大小,大小为2^n
    int name_count;                // 别名数量
    struct handle_name *name;      // 别名列表
};
    
static struct handle_storage *H = NULL;

3、skynet_message模块

/*
	skynet包含两级消息队列
		1、global_mq		包含一个head和tail指针  分别指向次级消息队列的头部和尾部
		2、次级消息队列(mq)	单线链表
		次级消息队列,实际上是一个数组,并且用两个int型数据,分别指向他的头部和尾部(head和tail),不论是head还是tail,当他们的值>=数组尺寸时,都会进行回绕(即从下标为0开始,比如值为数组的size时,会被重新赋值为0),在push操作后,head等于tail意味着队列已满(此时,队列会扩充两倍,并从头到尾重新赋值,此时head指向0,而tail为扩充前,数组的大小),在pop操作后,head等于tail意味着队列已经空了(后面他会从skynet全局消息队列中,被剔除掉)。
*/

/*
	1、消息驱动
	消息派发的机制:(worker线程 --> global_mq --(pop mq)--> mq --(pop msg)-->context的callback函数 --(push mq)-->global_mq)
    	工作线程,会从global_mq里pop一个次级消息队列来,然后从次级消息队列中,pop出一个消息,并传给context的callback函数,在完成驱动以后,再将次级消息队列push回global_mq中
*/
// skynet_mq.h
struct skynet_message {
    uint32_t source;            // 消息发送方的服务地址
        
    // 如果这是一个回应消息,那么要通过session找回对应的一次请求,在lua层,我们每次调用call的时候,都会往对  
    // 方的消息队列中,push一个消息,并且生成一个session,然后将本地的协程挂起,挂起时,会以session为key,协程句  
    // 柄为值,放入一个table中,当回应消息送达时,通过session找到对应的协程,并将其唤醒。后面章节会详细讨论
    int session; 
        
    void * data;        // 消息地址
    size_t sz;          // 消息大小
};
    
// skynet_mq.c
#define DEFAULT_QUEUE_SIZE 64
#define MAX_GLOBAL_MQ 0x10000
    
// 0 means mq is not in global mq.
// 1 means mq is in global mq , or the message is dispatching.
    
#define MQ_IN_GLOBAL 1
#define MQ_OVERLOAD 1024
    
struct message_queue {
    // 自旋锁,可能存在多个线程,向同一个队列写入的情况,加上自旋锁避免并发带来的发现,
    //后面会讨论互斥锁,自旋锁,读写锁和条件变量的区别
    struct spinlock lock;     
        
    uint32_t handle;                // 拥有此消息队列的服务的id
    int cap;                        // 消息大小
    int head;                       // 头部index
    int tail;                       // 尾部index
    int release;                    // 是否能释放消息
    int in_global;                  // 是否在全局消息队列中,0表示不是,1表示是
    int overload;                   // 是否过载
    int overload_threshold;
    struct skynet_message *queue;   // 消息队列
    struct message_queue *next;     // 下一个次级消息队列的指针
};
    
struct global_queue {
    struct message_queue *head;
    struct message_queue *tail;
    struct spinlock lock;
};
    
static struct global_queue *Q = NULL;


/*
2、消息写入
我们要向一个服务发消息,最终是通过调用skynet.send接口,将消息插入到该服务专属的次级消息队列的,次级消息队列的内容,并不是context结构的一部分(context只是引用了他的指针),因此,在一个服务执行callback的同时,其他服务(可能是多个线程内执行callback的其他服务)可以向它的消息队列里push消息,而mq的push操作,是加了一个自旋锁,以避免多个线程,同时操作一个消息队列。lua层的skynet.send接口,最终会调到c层的skynet_context_push。这个接口实质上,是通过handle将context指针取出来,然后再往消息队列里push消息:
*/
// skynet_server.c
int skynet_context_push(uint32_t handle, struct skynet_message *message) {
    struct skynet_context * ctx = skynet_handle_grab(handle);
    if (ctx == NULL) {
        return -1;
    }
    skynet_mq_push(ctx->queue, message);
    skynet_context_release(ctx);
    
    return 0;
}
    
// skynet_handle.c
struct skynet_context * 
skynet_handle_grab(uint32_t handle) {
    struct handle_storage *s = H;
    struct skynet_context * result = NULL;
    
    rwlock_rlock(&s->lock);
    
    uint32_t hash = handle & (s->slot_size-1);
    struct skynet_context * ctx = s->slot[hash];
    if (ctx && skynet_context_handle(ctx) == handle) {
        result = ctx;
        skynet_context_grab(result);
    }
    
    /*
    	因为我们访问一个服务的机会,远大于创建一个服务并写入列表的机会,因此这里用了读写锁,在通过handle获取context指针时,加了一个读取锁,这样当在读取的过程中,同时有新的服务创建,并且存在要扩充skynet_context list容量的风险,因此不论如何,他都应当被阻塞住,直到所有的读取锁都释放掉。
    */
    rwlock_runlock(&s->lock);
    
    return result;
}

3、skynet启动服务步骤

  1. 从modules列表中,查找对应的服务模块,如果找到则返回,否则到modules的path中去查找对应的so库,创建一个skynet_module对象(skynet_modules管理模块所示),将so库加载到内存,并将访问该so库的句柄和skynet_module对象关联(_try_open做了这件事),并将so库中的xxx_create,xxx_init,xxx_signal,xxx_release四个函数地址赋值给skynet_module的create、init、signal和release四个函数中,这样这个skynet_module对象,就能调用so库中,对应的四个接口(_open_sym做了这件事)。
  2. 创建一个服务实例即skynet_context对象,他包含一个次级消息队列指针,服务模块指针(skynet_module对象,便于他访问module自定义的create、init、signal和release函数),由服务模块调用create接口创建的数据实例等。
  3. 将新创建的服务实例(skynet_context对象)注册到全局的服务列表中(skynet_modules管理模块所示)。
  4. 初始化服务模块(skynet_module创建的数据实例),并在初始化函数中,注册新创建的skynet_context实例的callback函数。
  5. 将该服务实例(skynet_context实例)的次级消息队列,插入到全局消息队列中。
    经过上面的步骤,一个c服务模块就被创建出来了,在回调函数被指定以后,其他服务发送给他的消息,会被pop出来,最终传给服务对应的callback函数,最后达到驱动服务的目的。

创建c服务的工作,一般在c层进行,一般会调用skynet_context_new接口,如下所示:

// skynet_server.c
struct skynet_context * 
skynet_context_new(const char * name, const char *param) {
	struct skynet_module * mod = skynet_module_query(name);

	if (mod == NULL)
		return NULL;

	void *inst = skynet_module_instance_create(mod);
	if (inst == NULL)
		return NULL;
	struct skynet_context * ctx = skynet_malloc(sizeof(*ctx));
	CHECKCALLING_INIT(ctx)

	ctx->mod = mod;
	ctx->instance = inst;
	ctx->ref = 2;
	ctx->cb = NULL;
	ctx->cb_ud = NULL;
	ctx->session_id = 0;
	ctx->logfile = NULL;

	ctx->init = false;
	ctx->endless = false;
	// Should set to 0 first to avoid skynet_handle_retireall get an uninitialized handle
	ctx->handle = 0;	
	ctx->handle = skynet_handle_register(ctx);
	struct message_queue * queue = ctx->queue = skynet_mq_create(ctx->handle);
	// init function maybe use ctx->handle, so it must init at last
	context_inc();

	CHECKCALLING_BEGIN(ctx)
	int r = skynet_module_instance_init(mod, inst, ctx, param);
	CHECKCALLING_END(ctx)
	if (r == 0) {
		struct skynet_context * ret = skynet_context_release(ctx);
		if (ret) {
			ctx->init = true;
		}
		skynet_globalmq_push(queue);
		if (ret) {
			skynet_error(ret, "LAUNCH %s %s", name, param ? param : "");
		}
		return ret;
	} else {
		skynet_error(ctx, "FAILED launch %s", name);
		uint32_t handle = ctx->handle;
		skynet_context_release(ctx);
		skynet_handle_retire(handle);
		struct drop_t d = { handle };
		skynet_mq_release(queue, drop_message, &d);
		return NULL;
	}
}

// skynet_module.c
struct skynet_module * 
skynet_module_query(const char * name) {
	struct skynet_module * result = _query(name);
	if (result)
		return result;

	SPIN_LOCK(M)

	result = _query(name); // double check

	if (result == NULL && M->count < MAX_MODULE_TYPE) {
		int index = M->count;
		void * dl = _try_open(M,name);
		if (dl) {
			M->m[index].name = name;
			M->m[index].module = dl;

			if (_open_sym(&M->m[index]) == 0) {
				M->m[index].name = skynet_strdup(name);
				M->count ++;
				result = &M->m[index];
			}
		}
	}

	SPIN_UNLOCK(M)

	return result;
}



static void *
_try_open(struct modules *m, const char * name) {
	const char *l;
	const char * path = m->path;
	size_t path_size = strlen(path);
	size_t name_size = strlen(name);

	int sz = path_size + name_size;
	//search path
	void * dl = NULL;
	char tmp[sz];
	do
	{
		memset(tmp,0,sz);
		while (*path == ';') path++;
		if (*path == '\0') break;
		l = strchr(path, ';');
		if (l == NULL) l = path + strlen(path);
		int len = l - path;
		int i;
		for (i=0;path[i]!='?' && i < len ;i++) {
			tmp[i] = path[i];
		}
		memcpy(tmp+i,name,name_size);
		if (path[i] == '?') {
			strncpy(tmp+i+name_size,path+i+1,len - i - 1);
		} else {
			fprintf(stderr,"Invalid C service path\n");
			exit(1);
		}
		dl = dlopen(tmp, RTLD_NOW | RTLD_GLOBAL);
		path = l;
	}while(dl == NULL);

	if (dl == NULL) {
		fprintf(stderr, "try open %s failed : %s\n",name,dlerror());
	}

	return dl;
}

_open_sym(struct skynet_module *mod) {
	size_t name_size = strlen(mod->name);
	char tmp[name_size + 9]; // create/init/release/signal , longest name is release (7)
	memcpy(tmp, mod->name, name_size);
	strcpy(tmp+name_size, "_create");
	mod->create = dlsym(mod->module, tmp);
	strcpy(tmp+name_size, "_init");
	mod->init = dlsym(mod->module, tmp);
	strcpy(tmp+name_size, "_release");
	mod->release = dlsym(mod->module, tmp);
	strcpy(tmp+name_size, "_signal");
	mod->signal = dlsym(mod->module, tmp);

	return mod->init == NULL;
}

4、启动服务例子(logger)

1、启动skynet节点时,会启动一个logger c服务

// skynet_start.c
void
skynet_start(struct skynet_config * config) {
	...
	struct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);
	if (ctx == NULL) {
		fprintf(stderr, "Can't launch %s service\n", config->logservice);
		exit(1);
	}
	...
}

2、此时,skynet_module list列表中,搜索logger服务模块,如果没找到则在so库的输出路径中,寻找名为logger的so库,找到则将该so库加载到内存中,并将对应的logger_create,logger_init,logger_release函数地址分别赋值给logger模块中的create,init,release函数指针,此时skynet_module list中,多了一个logger模块。

3、创建服务实例,即创建一个skynet_context实例,为了使skynet_context实例拥有访问logger服务内部函数的权限,这里将logger模块指针,赋值给skynet_context实例的mod变量中。

4、创建一个logger服务的数据实例,调用logger服务的create函数:

// service_logger.c
struct logger {
	FILE * handle;
	int close;
};
struct logger *
logger_create(void) {
	struct logger * inst = skynet_malloc(sizeof(*inst));
	inst->handle = NULL;
	inst->close = 0;
	return inst;
}

此时,将新创建的数据实例赋值给skynet_context的instance变量,此时,一个服务对象运行时,所要用到的逻辑,能够通过mod变量,访问logger服务对应的函数,而通过instance可以找到该服务自己的数据块。

5、将新创建的skynet_context对象,注册skynet_context list中,此时skynet_context list多了一个logger服务实例

6、初始化logger服务,注册logger服务的callback函数:

// service_logger.c
static int
_logger(struct skynet_context * context, void *ud, int type, int session, uint32_t source, const void * msg, size_t sz) {
	struct logger * inst = ud;
	fprintf(inst->handle, "[:%08x] ",source);
	fwrite(msg, sz , 1, inst->handle);
	fprintf(inst->handle, "\n");
	fflush(inst->handle);
	return 0;
}
int
logger_init(struct logger * inst, struct skynet_context *ctx, const char * parm) {
	if (parm) {
		inst->handle = fopen(parm,"w");
		if (inst->handle == NULL) {
			return 1;
		}
		inst->close = 1;
	} else {
		inst->handle = stdout;
	}
	if (inst->handle) {
		skynet_callback(ctx, inst, _logger);
		skynet_command(ctx, "REG", ".logger");
		return 0;
	}
	return 1;
}
// skynet_server.c
void 
skynet_callback(struct skynet_context * context, void *ud, skynet_cb cb) {
	context->cb = cb;
	context->cb_ud = ud;
}

上面这段逻辑,将skynet_context的callback函数设置为logger服务的_logger函数,并将调用callback时,传入的userdata设置为先前创建的数据实例

7、为logger服务实例创建一个次级消息队列,并将队列插入到全局消息队列中

简单的启动初始化流程:

在这里插入图片描述

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

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

相关文章

实战Netty!基于私有协议,怎样快速开发网络通信服务?

前言 今天我们一起来来聊聊怎么使用netty。 在工作中&#xff0c;我经常使用netty开发一些服务&#xff0c;掌握netty的工作原理&#xff0c;开发一些服务端以及客户端是非常简单&#xff0c;本篇文章&#xff0c;我们就以具体的协议来进行一个简单的服务的开发。 正文 私有…

一键汇总报告模型可能会需要修改的地方

文章目录如何修改文件夹地址为指定地址?如何取消清除提醒, 避免每次点击弹窗选择?如何取消完成弹窗提醒?如果觉得反复打开工作簿太闪怎么办?如果有些报告的内容页不在第一个Sheet怎么办?如果想修改字段怎么办?运行程序前不想清除原有内容怎么办?报告页有内容但是没有被抓…

混合整数规划的机组组合附Matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

Redis常见面试题(2022)

Redis基础 什么是 Redis&#xff1f; Redis 是一个开源&#xff08;BSD 许可&#xff09;、基于内存、支持多种数据结构的存储系统&#xff0c;可以作为数据库、缓存和消息中间件。它支持的数据结构有字符串&#xff08;strings&#xff09;、哈希&#xff08;hashes&#xf…

多分类交叉熵理解

多分类交叉熵有多种不同的表示形式&#xff0c;如下图所示&#xff1a; 但是&#xff0c;有时候我们读论文会深陷其中不能自拔。 也有很多读者、观众会纠正其他作者的文章、视频的交叉熵形式。 实际上&#xff0c;上述三种形式都是没有问题的。 这里&#xff0c;我们就要了解…

多媒体内容理解在美图社区的应用实践

导读&#xff1a;移动互联网时代&#xff0c;图像和短视频等多媒体内容爆发&#xff0c;基于计算机视觉的AI算法是多媒体内容分析的基础。在美图社区智能化发展的过程中&#xff0c;视频和图像分类打标、去重以及质量评估的结果&#xff0c;在推荐、搜索以及人工审核等多个场景…

【R语言数据科学】:变量选择(三)主成分回归和偏最小二乘回归

变量选择(三)主成分回归和偏最小二乘回归 🌸个人主页:JOJO数据科学📝个人介绍:统计学top3高校统计学硕士在读💌如果文章对你有帮助,欢迎✌关注、👍点赞、✌收藏、👍订阅专栏✨本文收录于【R语言数据科学】本系列主要介绍R语言在数据科学领域的应用包括: R语言编…

多分类问题的precision和recall以及F1 scores的计算

对于多分类问题&#xff0c;首先&#xff0c;对于每一个类的精准率&#xff08;Precision&#xff09;和召回率&#xff08;Recall&#xff09;&#xff0c;定义和二分类问题一致&#xff0c;但是计算上不再需要TP,FP,FN等量了&#xff1a;&#xff09; 比如对A, B, C三类有如…

SpringBoot中如何集成ThymeLeaf呢?

转自: SpringBoot中如何集成ThymeLeaf呢&#xff1f; 下文笔者将讲述SpringBoot集成ThymeLeaf的方法&#xff0c;如下所示: 实现思路:1.在pom.xml中引入ThymeLeaf的相关依赖2.在Templates文件夹下编写相应的模板文件例: 1.pom.xml 添加ThymeLeaf依赖<!-- ThymeLeaf 依赖…

河南某商务楼BA系统设计

目 录 第一章 概述 3 第二章 设计任务与要求 4 第三章 设计依据和规范 4 第四章 系统设计 5 4.1系统选型 5 4.2 I/O点位设计 7 4.2.1暖通空调系统 11 4.2.2给排水系统 13 4.2.3电气系统 15 4.3线缆选型设计 17 4.4供电接地设计 17 4.5中央控制室设计 18 第五章 设备清单配置 18…

ASEMI代理力特LSIC1MO120E0080碳化硅MOSFET

编辑-Z 力特碳化硅MOS管LSIC1MO120E0080参数&#xff1a; 型号&#xff1a;LSIC1MO120E0080 漏极-源极电压&#xff08;VDS&#xff09;&#xff1a;1200V 连续漏电流&#xff08;ID&#xff09;&#xff1a;25A 功耗&#xff08;PD&#xff09;&#xff1a;214W 工作结温…

mysql数据库日志

1、日志类型 mysql日志在mysql事务章有事务日志相关的记录。初次之外&#xff0c;MySQL有不同类型的日志文件&#xff0c;用来存储不同类型的日志&#xff0c;分为二进制日志 、 错误日志 通用查询日志和 查询日志 &#xff0c;这也是常用的4种。MySQL 8又新增两种支持的日志&…

关于HTTPDNS,你知道多少?

导读&#xff1a; 全网域名劫持率高&#xff0c;域名解析失败、解析超时&#xff0c;IP调度不精准&#xff0c;域名解析变更生效不实时&#xff0c;这些问题是否一直困扰着你&#xff1f;作为网络请求最前置的环节&#xff0c;域名解析的稳定与精准程度直接决定了APP的访问体验…

实战讲解SpringCloud网关接口限流SpringCloudGateway+Redis(图+文)

1 缘起 最近补充微服务网关相关知识&#xff0c;学习了网关相关概念&#xff0c; 了解网关在微服务中存在的意义及其使命&#xff0c;如统一用户认证、接口权限控制、接口限流、接口熔断、黑白名单机制等&#xff0c; 打算通过实践的方式逐步学习网关的相关功能&#xff0c;同…

从零到一落地接口自动化测试

前段时间写了一系列自动化测试的文章&#xff0c;更多是从方法和解决问题思路角度阐述我的观点。 昨天花了几个小时看完了陈磊老师的《接口测试入门课》&#xff0c;有一些新的收获&#xff0c;结合我自己实践自动化测试的一些经验以及个人理解&#xff0c;这篇文章来聊聊新手…

主要控制系统之间的逻辑关系

电力行业 工控安全解决思路保障框架从电力行业对工控安全需求看&#xff0c;电力企业在主要是以合规性建设为主&#xff0c;在 2004 年原电监会 5 号令颁布开始&#xff0c;大部 分的电厂控制系统安全 建设已经按照 5 号令的要求进行了整改&#xff0c;形成“安全分区、网络专…

【2022硬件设计开源盛宴】一年一度的hackaday大赛结束,冠军便携式风力涡轮机,共提交326个电子作品,奖金池15万美元

https://hackaday.com/2022/11/05/ ... -years-competition/ &#xff08;1&#xff09;一年一度的Hackaday大赛结束&#xff0c;今年是第9届了&#xff0c;总奖金池是15万美元&#xff0c;冠军5万美元。前6届&#xff0c;冠军奖金非常高&#xff0c;像第3届冠军是最厉害的&am…

java设计模式之装饰者模式

一&#xff1a;装饰者模式 1.什么是装饰者模式? 装饰模式是一种结构型设计模式&#xff0c; 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。 装饰者模式的基本介绍 1.装饰者模式&#xff1a;动态的将新功能附加到对象上。在对象功能扩展方面&#xf…

Jasper 中如何将数据拆成多行并跨行累计

【问题】 I have a query that returns some summary records. For instance, loan amount, loan term, interest rate. Then I want to have a second row that builds out the detailed payment schedule. so the report would look like this: Loan Amt Term …

SpringBoot2

文章目录1.简介1.1 SpringBoot优缺点1.2 官方文档结构2. SpringBoot入门2.1 HelloWord2.2 依赖管理2.3 自动配置2.4 容器功能组件添加原生配置文件引入2.5 配置绑定ConfigurationPropertiesEnableConfigurationProperties2.6 自动配置原理底层总结最佳实践2.7 开发小技巧Lombok…