嵌入式应用综合小项目-在线词典开发

前言

该项目来自华清远见Level 8项目,该项目实现的主要功能为:用户在客户端输入想要查询的单词,客户端会通过socket发送其到服务器。服务器查询数据库,如果有查到相应的释义就将查到的释义经服务器发给客户端。没找到有同样发回对应的提示信息。
在这里插入图片描述
该项目能够实现以下功能

  • 能够实现用户的注册,注册的信息保存在数据库中
  • 能够实现用户登录,通过查询所保存的用户数据库信息,判断是否可以登陆
  • 能够实现单词的查询,如果用户选择的是查询模式,就查询数据库并返回相应的释义
  • 用户查询的记录将被保存在数据库中,用户如果想查询之前的查询记录,也可以查询到

1 流程解析

1.1 客户端

定义了一个结构体MSG来传输数据,如下图右上角所示,type需要处理的类型,有注册(R:1),登录(L:2),查找(Q:3),历史(H:4)name和data为发送的用户名和数据。主要流程为,首先,创建socket连接,采用的是TCP,然后打印提示词,引导用户选择要注册、登录还是退出。用户输入后,判断用户选项,并根据选项跳转到对应的功能中。

  • 注册模块中,给type类型赋值注册状态,并引导用户输入用户名、密码。然后通过TCP将msg结构体发给服务器,服务器判断type类型,并跳转到相应的功能处,判断该用户名是否存在,如果存在返回already exit给客户端,如果不存在将其加入数据库当中,并返回OK给客户端。
  • 登录模块中,先给type类型赋值登录状态,并引导用户输入用户名、密码。然后通过TCP将msg结构体发给服务器,服务器判断type类型,并跳转到相应的功能处,判断该用户名是否存在,如果不存在返回给客户端usr/pwd wrong,如果存在返回OK给客户端。然后跳转到查询模块。
  • 查询模块中,先是引导用户输入相应的功能,如果是查询单词,则跳到对应的查询模块,同样先给type类型赋值查询状态,然后读取单词,这时候如果输入“#”号则退出。读取到单词后将其发送给服务器,服务器判断type类型,并跳转到相应的功能处,服务器搜寻数据库,如果该单词不存在则返回给客户端not found,如果存在则把释义发给客户端,并获取当地时间,连同查询的单词一并插sqlite3的记录表中,客户端把收到的释义打印出来
  • 查询模块中,先是引导用户输入相应的功能,如果用户是查询历史,则跳到对应的历史模块,同样先给type类型赋值历史状态,然后发送给服务器,服务器判断type类型,并跳转到相应的功能处,服务器搜寻数据库,服务器把查询历史连同时间一并发给客户端
    在这里插入图片描述

1.2 服务器

GPS模块通过串口将数据发给主控芯片,ATGM336H会一次性返回多条信息,其中信息头的第一个是消息ID,标示着通过什么定位系统的采集的数据。简单的含义如下,具体含义可以查询使用手册
在这里插入图片描述

2 sqlite3基础使用

单词的查询通过数据库完成,下面简单介绍其基本使用。

2.1 apt指令直接安装

	sudo apt-get install sqlite3 //安装sqlite3
	 
	sudo apt-get install libsqlite3-dev // 安装sqlite3编译需要的工具包
	 
	sqlite3 -version//用来检查是否安装成功

2.2 sqlite3基本命令

1、创建表

 create table table_name(id integer, name char, tel integer);  //创建一个表

2、插入数据

 insert into table_name values(1, "www", 6666);    

3、查询相关数据

select * from table_name;  //查询所有数据
 
select * from table_name where id =       //按指定内容查询

4、更新数据

update table_name set name = "change" where id = 866;  //

5、删除数据

delete from table_name where id = 666;  //删除id为666的所有信息

6、删除表

drop table table_name; //删除整个表

2.4sqlite3 API编程

sqlite3_open()
功能描述:打开一个数据库,如果该数据库不存在,sqlite则会自动创建

int sqlite3_open(const char *dbname,sqlite3 **db)

参数解析:

  • 第一个参数是特定文件名(xx.db)
  • 第二个参数是sqlite3 **结构体指针,成为数据库句柄(相当于是数据库的描述符)
    返回值:成功则返回SQLITE_OK(0),失败则返回其他错误信息(非0值)

sqlite3_exec()
功能描述:编译和执行sql语句,将查询到的结果返回给回调函数callback

int sqlite3_open(const char *dbname,sqlite3 **db)

参数解析:

  • 第一个参数是打开的数据库句柄
  • 第二个参数是一个字符指针,表示所要执行的sql语句字符串,以’\0’结尾
  • 第三个参数是一个回调函数,用来处理查询结果,如果不需要回调则NUL填L(比如insert或delete操作时),一般用于SELECT
  • 第四个参数是传给回调函数的指针参数,如果不需要传递,则值填NULL
  • 第五个参数是返回错误信息
    返回值:成功则返回SQLITE_OK(0),失败则返回其他错误信息(非0值)

sqlite3_get_table()
功能描述:主要是用于以非回调的方式进行SELECT查询

int sqlite3_get_table(sqlite3 *db, const char *zsql,
        char ***pazResult, int *nrow, int *ncolumn,char **zErrmsg);
 

参数解析:

  • 第一个参数是一个数据库句柄,是打开数据库得到的指针
  • 第二个参数是一条sql语句,与sqlite3_exec()中的一样,以’\0’结尾的字符串
  • 第三个参数是查询的结果,是一个一维数组,它的内存布局是:字段名称,后面是紧接着每个字段的值(就是列表名,后面跟着一个一个数据)
  • 第四个参数是查询出多少条记录(即查出多少行,不包括字段名的那一行)
  • 第五个参数是有多少字段(多少列)
  • 第六个参数是返回错误信息,这里是指针的指针
    返回值:成功则返回SQLITE_OK(0),失败则返回其他错误信息(非0值)

sqlite3_close()

int sqlite3_close(sqlite3  *db)
//参数说明:db需要关闭的数据库文件。
//功能描述:关闭之前调用的数据库连接,所有与连接相关的语句都应该在关闭之前完成
//返回值:成功则返回SQLITE_OK(0),失败则返回其他错误信息(非0值),其中如果是查询没有完成,则返回SQLITE_BUSY
 

3. 本项目中sqlite3开发

linux@linux:~/Desktop/dictionary$ sqlite3 my.db
SQLite version 3.8.2 2013-12-06 14:53:30
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .database
seq  name             file                                                      
---  ---------------  ----------------------------------------------------------
0    main             /home/linux/Desktop/dictionary/my.db                      
sqlite> create table usr(name text, passwd text);
sqlite> create table record(name text, date text, word text);
sqlite> :q

4. 程序

4. 1客户端代码

client.c

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

//socket
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>         //sockaddr in this file
#include <netinet/in.h>         //sockaddr_in in this file
#include <arpa/inet.h>

#include <string.h>


#include <fcntl.h> // for open
#include <unistd.h> // for close

#define     N       32

#define     R       1   //user  -   register
#define     L       2   //user  -   login
#define     Q       3   //user  -   query
#define     H       4   //user  -   history

//定义通信双方的结构体
typedef struct client
{
    int type;
    char name[N];
    char data[256];
}MSG;

int  do_register(int sockfd, MSG *msg)
{
    msg->type = R;
    printf("Input name:");
    scanf("%s",msg->name);
    getchar();

    printf("Input passwd:");
    scanf("%s",msg->data);

    if(send(sockfd, msg, sizeof(MSG),0) < 0)//(msg已经是指针,不用取地址)是大写MSG不是小写,小写是地址(永远4字节)
	{
		printf("fail to send.\n");
		return -1;
	}
 
	if(recv(sockfd, msg, sizeof(MSG), 0) < 0)
	{
		printf("Fail to recv.\n");
		return -1;
	}
 
	// ok !  or  usr alread exist.
	printf("%s\n", msg->data);
 
	return 0;

}
 
int do_login(int sockfd, MSG *msg)
{
	msg->type = L;
    printf("Input name:");
    scanf("%s",msg->name);
    getchar();

    printf("Input passwd:");
    scanf("%s",msg->data);

    if(send(sockfd, msg, sizeof(MSG),0) < 0)//(msg已经是指针,不用取地址)是大写MSG不是小写,小写是地址(永远4字节)
	{
		printf("fail to send.\n");
		return -1;
	}
 
	if(recv(sockfd, msg, sizeof(MSG), 0) < 0)
	{
		printf("Fail to recv.\n");
		return -1;
	}
 
    if ( strncmp(msg->data, "OK", 3) == 0)//看看是否为OK,比较3位因为还有\0
    {
        printf("Login ok!\n");
        return 1;
    }
    else
    {
        printf("%s",msg->data);
    }
    
	return 0;
}
 
int do_query(int sockfd, MSG *msg)
{
    msg->type = Q;
	printf("--------------------------\n");
 
	while(1)
	{
		printf("Input word:");
		scanf("%s",msg->data);
		getchar();
 
		//输入#返回上级菜单
		if(strncmp(msg->data, "#", 1) == 0) 
		{
			break;
		}

        //将要查询的单词发送给服务器
		if(send(sockfd, msg, sizeof(MSG),0) < 0) //send
		{
			perror("write"); //Fail to send.\n
			return -1;
		}

        //等待接收服务器,传递回来的单词的注释信息
		if(recv(sockfd, msg, sizeof(MSG), 0) < 0) //recv
		{
			perror("read"); //Fail to recv.\n
			return -1;
		}
 
		printf("%s\n",msg->data);
	}
	return 0;
}
 
int do_history(int sockfd, MSG *msg)
{
	msg->type = H;

	send(sockfd, msg, sizeof(MSG), 0);
	
	// 接受服务器,传递回来的历史记录信息
	while(1)
	{
		recv(sockfd, msg, sizeof(MSG), 0);

		if(msg->data[0] == '\0')
			break;

		//输出历史记录信息
		printf("%s\n", msg->data);
	}

	return 0;
}

// .server 192.168.58.128 10000(端口号) 分别是argv[0],argv[1],argv[2]
int main(int argc, const char *argv[])
{
    int sockfd; //socket handle
    struct sockaddr_in serveraddr; //网络信息结构体
	int n;
	MSG  msg;   

    if (argc != 3)
    {
        printf("Usage:%s serverip port.\n", argv[0]);
        return -1;
    }

    //创建套接字
    if ((sockfd = socket(AF_INET,  SOCK_STREAM, 0)) < 0)
    {
        perror("fail to socket.\n");
        return -1;
    }

    bzero(&serveraddr, sizeof(serveraddr));     //Clear the structure
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);  //inet_addr: The dotted decimal string is converted to a binary address
    //atoi: convert to a int number
    //htons:Convert native data to network data
    serveraddr.sin_port = htons(atoi(argv[2])); //将本机数据转换为网络数据,atoi功能将字符串转换为整数 

    //连接服务器
    if(connect(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr)) < 0)
    {
        perror("fail to connect");
        return -1;
    }
    
 	//一级菜单
	while(1)
	{
		printf("***************************************************\n");
		printf("* 1.register          2.login           3.quit    *\n");
		printf("***************************************************\n");
		printf("please choose:");
		scanf("%d",&n);
 
		getchar();                  //读走垃圾字符
 
		switch(n)
		{
		case 1:
			do_register(sockfd, &msg);
			break;
		case 2:
			if(do_login(sockfd, &msg) == 1)
			{
				goto next;
			}
			break;
		case 3:
			close(sockfd);
			exit(0);
			break;
		default:
			printf("Invalid data cmd.\n");
		}
 
	}
   
//二级菜单
next:
	while(1)
	{
		printf("*****************************************************\n");
		printf("* 1.query_word   2.history_record   3.quit          *\n");
		printf("*****************************************************\n");
		printf("Please choose:");
		scanf("%d", &n);
		getchar();
 
		switch(n)
		{
		case 1:
			do_query(sockfd, &msg);
			break;
		case 2:
			do_history(sockfd, &msg);
			break;
		case 3:
			close(sockfd);
			exit(0);
			break;
		default :
			printf("Invalid data cmd.\n");
		}
 
	}
    
    return 0;
}

4.2 服务端代码

server.c

//C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h> // for open
#include <unistd.h> // for close

//socket
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>         //sockaddr in this file
#include <netinet/in.h>         //sockaddr_in in this file
#include <arpa/inet.h>

#include <sqlite3.h>
#include <signal.h>
#include <time.h>

#define     N       32

#define     R       1   //user  -   register
#define     L       2   //user  -   login
#define     Q       3   //user  -   query
#define     H       4   //user  -   history

#define DATABASE "my.db"

//定义通信双方的结构体
typedef struct client
{
    int type;
    char name[N];
    char data[256];
}MSG;


int do_client(int acceptfd,sqlite3 *db);  
void  do_register(int acceptfd, MSG *msg,sqlite3 *db);
int do_login(int sacceptfd, MSG *msg,sqlite3 *db);
int do_query(int sacceptfd, MSG *msg,sqlite3 *db);
int do_history(int acceptfd, MSG *msg,sqlite3 *db);
int history_callback(void* arg,int f_num,char** f_value,char** f_name);
int do_searchword(int acceptfd, MSG *msg, char word[]);
int get_date(char *date);

// .server 192.168.58.128 10000(端口号) 分别是argv[0],argv[1],argv[2]
int main(int argc, const char *argv[])
{
    int sockfd; //socket handle
    struct sockaddr_in serveraddr; //网络信息结构体
	int n;
	sqlite3 *db;
	int acceptfd;
	pid_t pid;
	MSG  msg;   

    if (argc != 3)
    {
        printf("Usage:%s serverip port.\n", argv[0]);
        return -1;
    }

    //打开数据库
    if(sqlite3_open(DATABASE, &db) != SQLITE_OK)
    {
        printf("%s\n",sqlite3_errmsg(db));
        return -1;
    }
    else
    {
        printf("open DATABASE success.\n");
    }
    

    //创建套接字
    if ((sockfd = socket(AF_INET,  SOCK_STREAM, 0)) < 0)
    {
        perror("fail to socket.\n");
        return -1;
    }

    bzero(&serveraddr, sizeof(serveraddr));     //Clear the structure
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);  //inet_addr: The dotted decimal string is converted to a binary address
    //atoi: convert to a int number
    //htons:Convert native data to network data
    serveraddr.sin_port = htons(atoi(argv[2])); //将本机数据转换为网络数据,atoi功能将字符串转换为整数 

    //绑定(建立套接字与地址联系)
    if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) <0 )
    {
        perror("fail to bind\n");
        return -1;
    }

	//将套接字设为监听模式
	if(listen(sockfd,5) < 0)//监听个数5
	{
		printf("fail to listen");
		return -1;
	}

/*SIGCHLD 信号
 *当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号;
 *当父进程的某个子进程因收到信号而停止(暂停运行)或恢复时,内核也可能向父进程发送该信号。
*/
	//处理僵尸进程
	signal(SIGCHLD,SIG_IGN); //SIG_IGN :这个符号表示忽略该信号。
    
 	while (1)
    {
       if((acceptfd = accept(sockfd, NULL, NULL)) < 0)
       {
            perror("fail to accept");
            return -1;
       }

    //如果成功了,应创建父子进程,父进程永远接收客户端请求,子进程处理请求的具体内容
       if ((pid = fork()) < 0)
       {
        perror("fail to fork.\n");
        return -1;
       }
		else if(pid == 0)
		{
			//处理客户端具体的消息(子进程)
			close(sockfd);//监听套接字不用了,关闭
			do_client(acceptfd,db);
 
		}
		else
		{
			//接收客户端的请求(父进程)
			close(acceptfd);//接收套接字不用了,关闭
		}
 
    }
    

    
    return 0;
}

int do_client(int acceptfd,sqlite3 *db)
{
	MSG msg;
	while(recv(acceptfd,&msg,sizeof(msg),0) > 0)//接收成功
	{
			printf("type:%d\n", msg.type);
			switch(msg.type)
			{
			case R:
				do_register(acceptfd, &msg, db);
				break;
			case L:
				do_login(acceptfd, &msg, db);
				break;
			case Q:
				do_query(acceptfd, &msg, db);
				break;
			case H:
				do_history(acceptfd, &msg, db);
				break;
			default:
				printf("Invalid data msg.\n");
			}
 
	}
 
		printf("client exit.\n");//客户端退出就会跳出while循环
		close(acceptfd);
		exit(0);
 
		return 0;
}
 
 
void  do_register(int acceptfd, MSG *msg, sqlite3 *db)
{
    char *errmsg;
	char sql[512];//原本视频中是[128],会有警告
    sprintf(sql, "insert into usr values('%s', %s);", msg->name, msg->data);
	printf("%s\n", sql);
 
	if(sqlite3_exec(db,sql, NULL, NULL, &errmsg) != SQLITE_OK)
	{
		printf("%s\n", errmsg);
		strcpy(msg->data, "usr name already exist.");
	}
	else
	{
		printf("client  register ok!\n");
		strcpy(msg->data, "OK!");
	}
 
	if(send(acceptfd, msg, sizeof(MSG), 0) < 0)
	{
		perror("fail to send");
		return ;
	}
 
	return ;

}
 
int do_login(int acceptfd, MSG *msg, sqlite3 *db)
{
    char sql[512] = {};//128太小会警告
	char *errmsg;
	int nrow;
	int ncloumn;
	char **resultp;
 
	sprintf(sql, "select * from usr where name = '%s' and pass = '%s';", msg->name, msg->data);
	printf("%s\n", sql);
 
	if(sqlite3_get_table(db, sql, &resultp, &nrow, &ncloumn, &errmsg)!= SQLITE_OK)
	{
		printf("%s\n", errmsg);
		return -1;
	}
	else
	{
		printf("get_table ok!\n");
	}
 
	// 查询成功,数据库中拥有此用户
	if(nrow == 1)
	{
		strcpy(msg->data, "OK");//这里把OK放进data里,client.c中才会那么比较
		send(acceptfd, msg, sizeof(MSG), 0);
		return 1;
	}
 
	if(nrow == 0) // 密码或者用户名错误,或者写else就可以
	{
		strcpy(msg->data,"usr/passwd wrong.");
		send(acceptfd, msg, sizeof(MSG), 0);
	}
 
	return 0;
}

int do_searchword(int acceptfd, MSG *msg, char word[])
{
	FILE * fp;
	int len = 0;
	char temp[512] = {};
	int result;
	char *p;
 
 
	//打开文件,读取文件,进行比对
	
	if((fp = fopen("dict.txt", "r")) == NULL)
	{
		perror("fail to fopen.\n");
		strcpy(msg->data, "Failed to open dict.txt");
		send(acceptfd, msg, sizeof(MSG), 0);
		return -1;
	}
 
	//打印出,客户端要查询的单词
	len = strlen(word);
	printf("%s , len = %d\n", word, len);
 
	//读文件,来查询单词
	while(fgets(temp, 512, fp) != NULL)  //每次读取512字节
	{
 
	//	printf("temp:%s\n", temp);
 
		// abandon  ab
		result = strncmp(temp,word,len);  
 
		if(result < 0)  //不相等
		{
			continue;
		}                
		if(result > 0 || ((result == 0) && (temp[len]!=' ')))  //第一个字符串>第二个或者两者相同但后面不是空格,表示在注释里出现的单词
		{
			break;
		}
 
		// 表示找到了,查询的单词result == 0 且 temp[len] == ' '。
		p = temp + len; //  abandon   v.akdsf dafsjkj 
	//	printf("found word:%s\n", p);
		while(*p == ' ')
		{
			p++;
		}
		// 找到了注释,跳跃过所有的空格
 
		strcpy(msg->data, p);
		printf("found word:%s\n", msg->data);
 
		// 注释拷贝完毕之后,应该关闭文件
		fclose(fp);
		return 1;
	}
 
	fclose(fp);
 
	return 0;
}
 
int get_date(char *date)
{
	time_t t;
	struct tm *tp;
 
	time(&t);
 
	//进行时间格式转换
	tp = localtime(&t);
 
	sprintf(date, "%d-%d-%d %d:%d:%d", tp->tm_year + 1900, tp->tm_mon+1, tp->tm_mday, 
			tp->tm_hour, tp->tm_min , tp->tm_sec);
	printf("get date:%s\n", date);
 
	return 0;
}

int do_query(int acceptfd, MSG *msg, sqlite3 *db)
{
    char word[64];
	int found = 0;
	char date[128] = {};
	char sql[128] = {};
	char *errmsg;
 
	//拿出msg结构体中,要查询的单词
	strcpy(word, msg->data);
 
	found = do_searchword(acceptfd, msg, word);
	printf("查询一个单词完毕.\n");
 
	// 表示找到了单词,那么此时应该将 用户名,时间,单词,插入到历史记录表中去。
	if(found == 1)
	{
		// 需要获取系统时间
		get_date(date);
 
        sprintf(sql, "insert into record values('%s', '%s', '%s')", msg->name, date, word);
 
		if(sqlite3_exec(db, sql, NULL, NULL, &errmsg) != SQLITE_OK)
		{
			printf("%s\n", errmsg);
			return -1;
		}
		else
		{
			printf("Insert record done.\n");
		}
 
	}
	else  //表示没有找到
	{
		strcpy(msg->data, "Not found!");
	}
 
	// 将查询的结果,发送给客户端
	send(acceptfd, msg, sizeof(MSG), 0);
 
	return 0;
}
 
// 得到查询结果,并且需要将历史记录发送给客户端
int history_callback(void* arg,int f_num,char** f_value,char** f_name)
{
	// record  , name  , date  , word 
	int acceptfd;
	MSG msg;
 
	acceptfd = *((int *)arg);
 
	sprintf(msg.data, "%s , %s", f_value[1], f_value[2]);
 
	send(acceptfd, &msg, sizeof(MSG), 0);
 
	return 0;
}


int do_history(int acceptfd, MSG *msg, sqlite3 *db)
{
    char sql[128] = {};
	char *errmsg;

	sprintf(sql, "select * from record where name = '%s'", msg->name);

	//查询数据库
	if(sqlite3_exec(db, sql, history_callback,(void *)&acceptfd, &errmsg)!= SQLITE_OK)
	{
		printf("%s\n", errmsg);
	}
	else
	{
		printf("Query record done.\n");
	}

	// 所有的记录查询发送完毕之后,给客户端发出一个结束信息
	msg->data[0] = '\0';

	send(acceptfd, msg, sizeof(MSG), 0);

	return 0;
}

5. 总结

以上即是本次的内容。详情可以见华清远见视频。

相关推荐

  1. 嵌入单片机项目开发的基本思想分享

    2024-07-18 23:52:04       20 阅读
  2. Rust 嵌入开发

    2024-07-18 23:52:04       55 阅读
  3. rust嵌入开发

    2024-07-18 23:52:04       44 阅读
  4. 嵌入开发】29

    2024-07-18 23:52:04       46 阅读
  5. 嵌入开发】72

    2024-07-18 23:52:04       41 阅读
  6. 嵌入开发】70

    2024-07-18 23:52:04       42 阅读
  7. 嵌入开发】94

    2024-07-18 23:52:04       49 阅读
  8. 嵌入开发】98

    2024-07-18 23:52:04       37 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-18 23:52:04       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-18 23:52:04       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-18 23:52:04       58 阅读
  4. Python语言-面向对象

    2024-07-18 23:52:04       69 阅读

热门阅读

  1. 什么是虚拟DOM?什么是diff算法?

    2024-07-18 23:52:04       20 阅读
  2. 什么是CAP理论?

    2024-07-18 23:52:04       20 阅读
  3. Hive修改表中的某个值

    2024-07-18 23:52:04       23 阅读
  4. HTTP请求五类状态码详细介绍,以及部分处理思路

    2024-07-18 23:52:04       24 阅读
  5. SQL FOREIGN KEY

    2024-07-18 23:52:04       19 阅读
  6. MySQL——在查询中使用关键字

    2024-07-18 23:52:04       21 阅读
  7. 确定Python 数据的数据结构

    2024-07-18 23:52:04       23 阅读
  8. 深入了解Memcached:缓存技术的利器

    2024-07-18 23:52:04       24 阅读
  9. antlr4私人学习笔记

    2024-07-18 23:52:04       19 阅读