KVstore :键值映射存储服务器

概述:本文介绍kv存储服务,所谓kv即key-value映射,用户存储键值对,提供:1.根据键查找值 2.根据键修改值 3.根据键删除值

效果:kv存储是运行在服务器上的一个进程,客户端通过套接字与服务器上的kvstore进程进行通信,客户端发送由协议规定的请求例如 SET name01 wjq ,kvstore服务器接收到请求并解析,回复结果 SUCCESS; 又例如客户端发送 GET name01 ,接收到服务端的回复 wjq

实现思路:

1.首先我们需要做到kvstore与客户端通信,这里使用tcp,也就是说设计之初kvstore就是一个支持百万级并发连接的tcp服务器:这里使用一个reactor模型,直接附上代码,tcp服务器不在本文讲解范围内




#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>

#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <pthread.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <sys/time.h>


#include "kvstore.h"



// listenfd
// EPOLLIN --> 
int accept_cb(int fd);
// clientfd
// 
int recv_cb(int fd);
int send_cb(int fd);

// conn, fd, buffer, callback


int epfd = 0;
struct conn_item connlist[1048576] = {0}; // 1024  2G     2 * 512 * 1024 * 1024 
// list
struct timeval zvoice_king;
// 
// 1000000

#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)


int set_event(int fd, int event, int flag) {

	if (flag) { // 1 add, 0 mod
		struct epoll_event ev;
		ev.events = event ;
		ev.data.fd = fd;
		epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
	} else {
	
		struct epoll_event ev;
		ev.events = event;
		ev.data.fd = fd;
		epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
	}

	

}

int accept_cb(int fd) {

	struct sockaddr_in clientaddr;
	socklen_t len = sizeof(clientaddr);
	
	int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
	if (clientfd < 0) {
		return -1;
	}
	set_event(clientfd, EPOLLIN, 1);

	connlist[clientfd].fd = clientfd;
	memset(connlist[clientfd].rbuffer, 0, BUFFER_LENGTH);
	connlist[clientfd].rlen = 0;
	memset(connlist[clientfd].wbuffer, 0, BUFFER_LENGTH);
	connlist[clientfd].wlen = 0;
	
	connlist[clientfd].recv_t.recv_callback = recv_cb;
	connlist[clientfd].send_callback = send_cb;

	if ((clientfd % 1000) == 999) {
		struct timeval tv_cur;
		gettimeofday(&tv_cur, NULL);
		int time_used = TIME_SUB_MS(tv_cur, zvoice_king);

		memcpy(&zvoice_king, &tv_cur, sizeof(struct timeval));
		
		printf("clientfd : %d, time_used: %d\n", clientfd, time_used);
	}

	return clientfd;
}

int recv_cb(int fd) { // fd --> EPOLLIN

	char *buffer = connlist[fd].rbuffer;
	int idx = connlist[fd].rlen;
	
	int count = recv(fd, buffer, BUFFER_LENGTH, 0);
	if (count == 0) {
		printf("disconnect\n");

		epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);		
		close(fd);
		
		return -1;
	}

	
	connlist[fd].rlen = count;


	kvstore_request(&connlist[fd]); 
	connlist[fd].wlen = strlen(connlist[fd].wbuffer);

	set_event(fd, EPOLLOUT, 0);   

	
	return count;
}


int send_cb(int fd) {

	char *buffer = connlist[fd].wbuffer;
	int idx = connlist[fd].wlen;

	int count = send(fd, buffer, idx, 0);

	set_event(fd, EPOLLIN, 0);

	return count;
}




int init_server(unsigned short port) {

	int sockfd = socket(AF_INET, SOCK_STREAM, 0);

	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0, sizeof(struct sockaddr_in));

	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(port);

	if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))) {
		perror("bind");
		return -1;
	}

	listen(sockfd, 10);

	return sockfd;
}




int epoll_entry(void) {

	int port_count = 20;
	unsigned short port = 2048;
	int i = 0;

	
	epfd = epoll_create(1); // int size

	for (i = 0;i < port_count;i ++) {
		int sockfd = init_server(port + i);  // 2048, 2049, 2050, 2051 ... 2057
		connlist[sockfd].fd = sockfd;
		connlist[sockfd].recv_t.accept_callback = accept_cb;
		set_event(sockfd, EPOLLIN, 1);
	}

	gettimeofday(&zvoice_king, NULL);

	struct epoll_event events[1024] = {0};
	
	while (1) { // mainloop();

		int nready = epoll_wait(epfd, events, 1024, -1); // 

		int i = 0;
		for (i = 0;i < nready;i ++) {

			int connfd = events[i].data.fd;
			if (events[i].events & EPOLLIN) { //

				int count = connlist[connfd].recv_t.recv_callback(connfd);
				//printf("recv count: %d <-- buffer: %s\n", count, connlist[connfd].rbuffer);

			} else if (events[i].events & EPOLLOUT) { 
				// printf("send --> buffer: %s\n",  connlist[connfd].wbuffer);
				
				int count = connlist[connfd].send_callback(connfd);
			}

		}

	}


	//getchar();
	//close(clientfd);

}
函数epoll_entry实现了与客户端之间的通信,并通过kvstore_request(&connlist[fd])这个函数实现了处理客户端请求,并将处理结果发送给客户端

2.kvstore存储引擎的实现

概要:由于服务器要将客户端请求存储的内容存储起来,有两种方式,一是存储到数据库,二是存储到服务端本地

为了简单实现业务,本文使用存储到本地进行讲解,采用的数据结构是哈希表

先介绍哈希表的实现以及为kvstore封装的接口:

/*
 * 单线程版本,没有做线程安全!
 *
 */


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>

#include "kvstore.h"


#define MAX_KEY_LEN	128
#define MAX_VALUE_LEN	512


#define MAX_TABLE_SIZE	102400

#define ENABLE_POINTER_KEY	1 


typedef struct hashnode_s { // hash node

#if ENABLE_POINTER_KEY
    char *key;
    char *value;
#else
    char key[MAX_KEY_LEN];
    char value[MAX_VALUE_LEN];
#endif
    struct hashnode_s *next;

} hashnode_t;


typedef struct hashtable_s { // hash table

    hashnode_t **nodes; // hashnode_t * 类型的 *nodes,也就是存放着hashnode_t类型的指针的数组nodes

    int max_slots;
    int count;

} hashtable_t;


hashtable_t Hash;



static int _hash(char *key, int size) { // hash函数,使用key确定hash值

    if (!key) return -1;

    int sum = 0;
    int i = 0;

    while (key[i] != 0) { // 使用ASCII计算hash值,由于key是字符数组,该方法通用
        sum += key[i];
        i ++;
    }

    return sum % size; // 返回hash值
}


hashnode_t *_create_node(char *key, char *value) {

    hashnode_t *node = (hashnode_t *)kvstore_malloc(sizeof(hashnode_t));
    if (!node) return NULL; // malloc filed

#if ENABLE_POINTER_KEY

    // 为节点的成员分配空间
    node->key = kvstore_malloc(strlen(key) + 1);
    if (!node->key) {
        kvstore_free(node); // node分配成功但key失败
        return NULL;
    }
    strcpy(node->key, key);

    node->value = kvstore_malloc(strlen(value) + 1);
    if (!node->value) {
        kvstore_free(node->key); // node和key分配成功但value失败
        kvstore_free(node);
        return NULL;
    }
    strcpy(node->value, value);

#else

	strncpy(node->key, key, MAX_KEY_LEN);
	strncpy(node->value, value, MAX_VALUE_LEN);
	
#endif
    // 初始化 next
    node->next = NULL;

    return node;
}



int init_hashtable(hashtable_t *hash) {

    if (!hash) return -1;

    hash->nodes = (hashnode_t**)kvstore_malloc(sizeof(hashnode_t *) * MAX_TABLE_SIZE);
    if (!hash->nodes) return -1;

    hash->max_slots = MAX_TABLE_SIZE;
    hash->count = 0;

    return 0;
}

void dest_hashtable(hashtable_t *hash) { // 销毁哈希表

    if (!hash) return;

    // 遍历释放数组中所有链表
    int i = 0;
    for (i = 0; i < hash->max_slots; i++) {
        hashnode_t *node = hash->nodes[i];

        while (node != NULL) {
            hashnode_t *tmp = node; // 保存当前节点
            node = node->next; // 移动到下一个节点
            hash->nodes[i] = node; // 更新头指针,在这段代码中没有作用

            kvstore_free(tmp); // 释放当前节点 

        }
    }

    kvstore_free(hash->nodes); // 释放哈希表的数组成员
}


int put_kv_hashtable(hashtable_t *hash, char *key, char *value) {

    if (!hash || !key || !value) return -1;

    int idx = _hash(key, MAX_TABLE_SIZE); // 哈希值作为数组下标

    hashnode_t *node = hash->nodes[idx]; // 获取正确数组位置的头指针
#if 1
    while (node != NULL) {    // 如果已经存在,直接退出,不重复插入
        if (strcmp(node->key, key) == 0) {
            return 1;
        }
        node = node->next;
    }
#endif

    hashnode_t *new_node = _create_node(key, value);
    // 头插法
    new_node->next = hash->nodes[idx];
    hash->nodes[idx] = new_node; // 更新头节点指针

    hash->count ++;

    return 0;
}


char *get_kv_hashtable(hashtable_t *hash, char *key) { // search

    if (!hash || !key) return NULL;

    int idx = _hash(key, MAX_TABLE_SIZE);

    hashnode_t *node = hash->nodes[idx]; // 确定数组索引

    while (node != NULL) { // 遍历查找

        if (strcmp(node->key, key) == 0) {
            return node->value;
        }

        node = node->next;
    }

    return NULL;
}


int count_kv_hashtable(hashtable_t *hash) {
	return hash->count;
}


int delete_kv_hashtable(hashtable_t *hash, char *key) { // 根据key删除节点

    if (!hash || !key) return -1;

    int idx = _hash(key, MAX_TABLE_SIZE); // 哈希值作为索引
    // 先判断头指针
    hashnode_t *head = hash->nodes[idx];
    if (head == NULL) return -1;

    // 遍历链表
    hashnode_t *cur = hash->nodes;
    hashnode_t *prev = NULL;
    while (cur != NULL) {

        if (strcmp(cur->key, key) == 0) break;
        prev = cur;
        cur = cur->next;
    }

    if (cur == NULL) return -1; // 没找到

    if (prev == NULL) { // 如果要删除的是头节点
        hash->nodes[idx] = cur->next; // 删除cur
    } else { // 不是头节点
        prev->next = cur->next; // 删除cur
    }

// 释放cur节点的空间
#if ENABLE_POINTER_KEY
	if (cur->key) {
		kvstore_free(cur->key);
	}
	if (cur->value) {
		kvstore_free(cur->value);
	}
	kvstore_free(cur);
#else
	free(cur);
#endif
	hash->count --; // 更新count

    return 0;
}


int exit_kv_hashtable(hashtable_t *hash, char *key) { // 判断是否存在该key的映射value

    char *value = get_kv_hashtable(hash, key);
    if (value) return 1;
    else return 0;
}



int kvs_hash_modify(hashtable_t *hash, char *key, char *value) { // 先查找key再修改value

    if (!hash || !key || !value) return -1;

    int idx = _hash(key, MAX_TABLE_SIZE);

    hashnode_t *node = hash->nodes[idx];

    while (node != NULL) {

        if (strcmp(node->key, key) == 0) {
            // 先释放原空间,避免内存泄漏
            kvstore_free(node->value); // 释放原value指向的空间
            node->value = NULL; // 避免使用悬空指针
            // 新分配空间
            node->value = kvstore_malloc(strlen(value) + 1); 
            if (node->value) { // 分配成功
                strcpy(node->value, value);
                return 0;
            } else 
                assert(0);
        }

        node = node->next;
    }

    return -1;
}


int kvs_hash_count(hashtable_t *hash) {
	return hash->count;
}



// 再封装一层接口:使用第三方库时,对库函数进行一层封装,适配自己的代码,
// 排查问题或更新迭代时只需要修改这一层接口的内容就行,不需要在源代码主体上修改,相当于做了一层隔离

int kvstore_hash_craete(hashtable_t *hash) {

    return init_hashtable(hash);
}


void kvstore_hash_destory(hashtable_t *hash) {

	return dest_hashtable(hash);

}


int kvs_hash_set(hashtable_t *hash, char *key, char *value) {

	return put_kv_hashtable(hash, key, value);

}


char *kvs_hash_get(hashtable_t *hash, char *key) {

	return get_kv_hashtable(hash, key);

}


int kvs_hash_delete(hashtable_t *hash, char *key) {

	return delete_kv_hashtable(hash, key);

}

对于哈希表的设计与实现,注释说的很清楚了,最后封装的接口是用在接下来的kvstore主程序中的

3.kvstore主体

概要:这份代码集成了前面的tcp服务epoll_entry、存储组件哈希表以及最后要介绍的:对客户端请求进行解析处理的组件

先介绍kvstore主程序:

int init_kvengine(void) {

	kvstore_hash_create(&Hash);
}


int exit_kvengine(void) {
    
	kvstore_hash_destory(&Hash);

}





int main() {


	init_kvengine(); // 创建存储引擎,这里是哈希表
	
	epoll_entry();  // 启动tcp服务器,处理并回复客户端请求

	exit_kvengine(); // 销毁哈希表

}

而这里调用的init_kvengine();实际上就是前面的哈希表代码中的:

int init_hashtable(hashtable_t *hash) {

    if (!hash) return -1;

    hash->nodes = (hashnode_t**)kvstore_malloc(sizeof(hashnode_t *) * MAX_TABLE_SIZE);
    if (!hash->nodes) return -1;

    hash->max_slots = MAX_TABLE_SIZE;
    hash->count = 0;

    return 0;
}

4.请求解析

我们对于kvstore主程序中的存储引擎、tcp服务都介绍完了,接下来介绍最核心的请求解析函数:

这两个函数位于epoll_entry的kvstore_request(&connlist[fd])函数中:

int kvstore_request(struct conn_item *item) {

    char *msg = item->rbuffer;
    char *tokens[KVSTORE_MAX_TOKENS];

    int count = kvstore_split_token(msg, tokens); // 解析请求

    kvstore_parser_protocol(item, tokens, count); // 生成回复内容

    return 0;
}

这个函数做到了对用户请求的解析以及回复,而依赖的是以下两个函数:

解析请求:

int kvstore_split_token(char *msg, char **tokens) { // 将msg字符串进行分割,结果保存在tokens字符串数字里

    if (msg == NULL || tokens == NULL) return -1; // 参数检查

    int idx = 0;

    char *token = strtok(msg, " "); // 对msg按空格“ ”进行分割,返回第一个子字符串

    while (token != NULL) { // 获取剩余的子字符串
        tokens[idx++] = token; // 将子字符串保存在字符串数组里
        token = strtok(NULL, " "); // 固定写法,依次获取除第一个外,剩余的子字符串
    }

    return idx; // 返回子字符串的个数
}

我们能对用户请求按空格进行分割的原因是,kvstore规定了应用层协议,只有按协议规定发送的请求才能被正确处理,就像linux shell 中的命令的名称以及使用方法一样

处理并回复:

int kvstore_parser_protocol(struct conn_item *item, char **tokens, int count) {

    if (item == NULL || tokens[0] == NULL || count == 0) return -1; // 检查参数

    char *msg = item->wbuffer; // 获取写缓冲区
	memset(msg, 0, BUFFER_LENGTH);

    // 对用户的命令的解析结果, 例如 SET name wjq 解析结果如下:
    char *command = tokens[0];  // SET
	char *key = tokens[1];      // name
	char *value = tokens[2];    // wjq
    

    int cmd = KVS_CMD_START;

    
    for (cmd = KVS_CMD_START; cmd < KVS_CMD_SIZE; cmd++) { // 查找比对tokens里的命令

        if (strcmp(commands[cmd], command) == 0) {
            break; // 找到了或者不存在
        }
    }

    // 匹配命令并回复结果
    switch (cmd) {
       
        case KVS_CMD_HSET: {    // SET :添加

			int res = kvstore_hash_set(key, value); // 调用哈希表的函数
			if (!res) {
				snprintf(msg, BUFFER_LENGTH, "SUCCESS");
			} else {
				snprintf(msg, BUFFER_LENGTH, "FAILED");
			}
			break;
		}
		
		case KVS_CMD_HGET: {   // GET :查询

			char *val = kvstore_hash_get(key); // 调用哈希表提供的接口
			if (val) {
				snprintf(msg, BUFFER_LENGTH, "%s", val);
			} else {
				snprintf(msg, BUFFER_LENGTH, "NO EXIST");
			}
			
			break;
		}
		case KVS_CMD_HDEL: { // DEL : 删除

			int res = kvstore_hash_delete(key);
			if (res < 0) {  // server
				snprintf(msg, BUFFER_LENGTH, "%s", "ERROR");
			} else if (res == 0) {
				snprintf(msg, BUFFER_LENGTH, "%s", "SUCCESS");
			} else {
				snprintf(msg, BUFFER_LENGTH, "NO EXIST");
			}
			
			break;
		}
        case KVS_CMD_HMOD: { // MOD : 修改

			int res = kvstore_hash_modify(key, value);
			if (res < 0) {  // server
				snprintf(msg, BUFFER_LENGTH, "%s", "ERROR");
			} else if (res == 0) {
				snprintf(msg, BUFFER_LENGTH, "%s", "SUCCESS");
			} else {
				snprintf(msg, BUFFER_LENGTH, "NO EXIST");
			}
			
			break;
		}

		case KVS_CMD_HCOUNT: { // COUNT : 查询数量
			int count = kvstore_hash_count();
			if (count < 0) {  // server
				snprintf(msg, BUFFER_LENGTH, "%s", "ERROR");
			} else {
				snprintf(msg, BUFFER_LENGTH, "%d", count);
			}
			break;
		}
		
		default: {
			printf("cmd: %s\n", commands[cmd]);
			assert(0);
		}
		
    }

}

可以看到解析查询的过程就是将用户按我们指定协议输入的请求,分成几段,为每一条请求进行一次解析、处理

增删改查用到了哈希表这个数据结构提供的函数,而只有按空格将字符串分割这个函数是我们自行设计的,难度并不大

至此,kvstore的设计实现已经全部完成

推荐学习https://xxetb.xetslk.com/s/p5Ibb

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Linux入门】基础开发工具

本篇博客整理了Linux&#xff08;centOS版本&#xff09;中基础开发工具的用途和用法&#xff0c;旨在透过开发工具的使用&#xff0c;帮助读者更好地理解可执行程序的编写、编译、运行等。 目录 一、软件包管理器 yum 1.软件的下载与安装 2.Linux应用商店&#xff1a;yum …

【JAVA项目】基于SSM的【寝室管理系统设计】

技术简介&#xff1a;采用B/S架构、ssm 框架和 java 开发的 Web 框架&#xff0c; eclipse开发工具。 系统简介&#xff1a;寝室管理设计的主要使用者分为管理员、宿舍长和学生&#xff0c;实现功能包括管理员权限&#xff1a;首页、个人中心、学生管理、宿舍号管理、宿舍长管理…

使用快捷键的方式把多个关键字文本快速替换(快速替换AE脚本代码)

首先&#xff0c;需要用到的这个工具&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 这里做AE(Adobe After Effact)里的脚本规则&#xff0c;把英文替换成中文&#xff0c;如下 swap thisComp.layer(“Segment settings”).effect("%&…

谷歌免费的机器学习课程

虽然这样的课程收藏了不少&#xff0c;但是很少有看的下去的&#xff0c;可能我就是这样的收藏党吧。 具体可以跳转链接查看 机器学习工程师学习路径

Springboot(SSM)项目实现数据脱敏

目录 一、引入hutool的依赖 二、sql脚本 三、自定义注解代码 3.1 自定义注解 3.2 自定义一个枚举,用于定义脱敏的类型 3.3 序列化 四、使用脱敏注解 4.1 Person.java 4.2 controller 4.3 dao 五、源代码参考 一、引入hutool的依赖 <dependency><groupId>…

皮内针可以治腱鞘炎吗?如何用皮内针治疗腱鞘炎?

点击文末领取揿针的视频教程跟直播讲解 腕部腱鞘炎是什么&#xff1f; 腱鞘是近关节处的半圆形结构&#xff0c;环形包绕肌腱组织&#xff0c;起到固定肌腱的作用。当关节活动时&#xff0c;肌腱与腱鞘之间会产生相互摩擦&#xff0c;如果两者摩擦过度就会引起炎症&#xff0…

时间复杂度空间复杂度 力扣:转轮数组,消失的数字

1. 算法效率 如何衡量一个算法的好坏&#xff1f;一般是从时间和空间的维度来讨论复杂度&#xff0c;但是现在由于计算机行业发展迅速&#xff0c;所以现在并不怎么在乎空间复杂度了下面例子中&#xff0c;斐波那契看上去很简洁&#xff0c;但是复杂度未必如此 long long Fib…

基于改进暗原色先验和颜色校正的水下图像增强,Matlab实现

博主简介&#xff1a; 专注、专一于Matlab图像处理学习、交流&#xff0c;matlab图像代码代做/项目合作可以联系&#xff08;QQ:3249726188&#xff09; 个人主页&#xff1a;Matlab_ImagePro-CSDN博客 原则&#xff1a;代码均由本人编写完成&#xff0c;非中介&#xff0c;提供…

【OpenNJet下一代云原生之旅】

OpenNJet下一代云原生之旅 1、OpenNJet的定义OpenNJet架构图 2、OpenNJet的特点性能无损动态配置灵活的CoPilot框架支持HTTP/3支持国密企业级应用高效安全 3、OpenNJet的功能特性4、OpenNJet的安装使用编译安装配置yum源创建符号连接修改配置编译 5、通过 OpenNJet 部署 WEB SE…

数字化战略|数字化建设总体规划蓝图PPT(建议收藏)

摘要 这份头部咨询公司关于数字化转型的报告为企业管理者和技术人员提供了一份详尽的数字化转型指南。报告从战略出发&#xff0c;详细阐述了数字生态体系建设、数字化核心方案构建、管理协同能力提升以及数据集中管理和应用能力增强等关键环节。对于从业者而言&#xff0c;报…

CogVLM/CogAgent环境搭建推理测试

引子 对于多模态大语言模型&#xff0c;一直没有怎么接触。刚巧一朋友有问到这方面的问题&#xff0c;也就顺手调研下。智谱AI的东西一直以来&#xff0c;还是很不错的。ChatGLM的忠实fans&#xff0c;看到白嫖网站github上有他们开源的多模态CogVLM/CogAgent&#xff0c;那就…

【JAVA项目】基于SSM的【电动车智能充电服务平台】

技术简介&#xff1a;采用SSM技术、MYSQL等技术实现。 系统简介&#xff1a;电动车智能充电服务平台实现了首页、个人中心、用户管理、充电桩管理、电池商品管理、托送服务管理、我的钱包管理、充值信息管理、消费信息管理、购买订单管理、配送信息管理、服务订单管理、系统管理…

43.139.152.26 C07L01P02 数字8

题目描述 不超过 N 位的正整数中包含有多少数字 8 &#xff1f; 输入格式 一行 1 个正整数 N &#xff0c;范围 [1,16]。 输出格式 一个整数。 输入数据 1 2输出数据 1 20样例解释 8,18,28,38,48,58,68,78,88, 98 与 80,81,82,83,84,85,86,87,89 这些数中有 8 &#xff0…

菜鸡学习netty源码(四)—— EventLoop

1.概述 我们前面进行过分析,channel为netty网络操作的抽象类,EventLoop负责处理注册到其上的Channel处理的I/O事件;EventLoopGroup是一个EventLoop的分组,它可以获取到一个或者多个的EventLoop对象。 2.类关系图 NioEventLoopGroup的类继承图,蓝色部分为对应的java类,绿…

使用Jellyfin创建媒体库

目录 前言 1.介绍Jellyfin 2.安装 3.设置时注意点 4.效果 5.内存占用 前言 分为客户端和服务器端&#xff0c;这里讲的是服务器端的安装 1.介绍Jellyfin Jellyfin 是一个免费开源的媒体服务器软件&#xff0c;它允许您管理和播放您的媒体文件。这些媒体文件可以包括电…

crossover软件是干嘛的 CrossOver软件好用吗 crossover最新2024使用方法教程 MacBook怎么安装exe软件

CrossOver软件是干嘛的 CrossOver由CodeWeavers公司开发&#xff0c;目的是使linux和Mac OS X操作系统和windows系统兼容。这样使用户可以将windows系统上的应用在linux或Mac os上运行。 CrossOver让您可以在Mac和Linux系统上运行Microsoft Windows应用&#xff0c;不必购买W…

第19章 基于质量特性的测试技术

一、功能性测试 &#xff08;一&#xff09;测试方法 等价类边界值法因果图法判定表法场景法 &#xff08;二&#xff09;用例 1、正常用例 2、异常用例 &#xff08;三&#xff09;完备性 1、功能覆盖率 2、X1-A/B 功能覆盖率X&#xff1a;软件实际功能覆盖文档中所有…

Linux的socket详解

一、本机直接的进程通信方式 管道&#xff08;Pipes&#xff09;&#xff1a; 匿名管道&#xff08;Anonymous pipes&#xff09;&#xff1a;通常用于父子进程间的通信&#xff0c;它是单向的。命名管道&#xff08;Named pipes&#xff0c;也称FIFO&#xff09;&#xff1a;允…

2023第十四届蓝桥杯国赛C/C++ 大学 A 组 圆上的连线

思路&#xff1a;很显然总的方案数等于挑选偶数点的方案数乘以对应偶数点的连线方案数之和&#xff0c;挑选偶数点的方案数靠组合数得出&#xff0c;偶数点的连线方案数就是个卡特兰数。具体为什么是卡特兰数&#xff0c;可以任选一个点&#xff0c;枚举这个点所连边的位置&…

Linux搭建sqlilabs靶场

提前准备&#xff1a; 文章中所使用到的Linux系统&#xff1a;Ubantu20.4sqlilabs靶场下载地址&#xff1a;GitHub - Audi-1/sqli-labs: SQLI labs to test error based, Blind boolean based, Time based. 一. 安装phpstudy phpstudy安装命令&#xff1a;wget -O install.sh h…