目录
本文说明
一:字典程序的几个问题
1:字典的本质
2:翻译功能的本质
3:让服务端和翻译功能相关联
二:字典类(Dict.hpp)
1:加载词典(Load)
2:翻译单词(Translate)
三:服务端(UdpServer.hpp)
四:Main.cc
五:客户端(UdpClient.cc)
六:日志文件(Log.hpp)
七:makefile
八:运行效果
本文说明
本文旨在实现UDP编程下的字典程序,输入英文返回中文,在其中不大改之前的echo程序的代码,而是让翻译功能和服务端类分离开,形成松耦合的效果~~
其中涉及到的socket接口、echo程序、日志文件都在以下博客中:
socket接口及echo程序:https://blog.csdn.net/shylyly_/article/details/151292001
日志文件的实现:https://blog.csdn.net/shylyly_/article/details/151263351
一:字典程序的几个问题
1:字典的本质
直接导入一个名为Dict.txt的文件即可,文件内容如下,暂且导入这点词汇,作为测试
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
2:翻译功能的本质
既然我们是松耦合,那么我们选择在新文件中创建一个字典类Dict来完成关于字典的操作;字典的操作分为两步:①导入词汇,②翻译
①导入词汇:只需把词汇插入进哈希表unordered_map
②翻译:这样我们的翻译就可以直接使用哈希表unordered_map进行,翻译可以分为两种!第一种是英文作为K值,中文作为V值,使用哈希表自带的[ ]即可形成翻译效果,第二种是通过遍历得到对应的K值的迭代器,然后打印迭代器的second即可
3:让服务端和翻译功能相关联
如果是强耦合,则很简单,我们只需要在服务端类中大改代码,成员变量增加哈希表等,成员函数增加翻译函数等,但是这是不优雅的写法
松耦合让我们拥有了一个独立的字典类,该类中有翻译功能,而我们的服务端类相使用字典类中的翻译函数,只需使用bind实现,一个类绑定另一个类中的函数即可!
bind的讲解博客:https://blog.csdn.net/shylyly_/article/details/151109228
所以我们的服务端类的构造函数的参数就应该新增一个去接受字典类的翻译函数,然后使用bind把服务端类构造时的参数和字典类的翻译函数绑定即可,这样我们的服务端运行起来依旧只需要输入端口号,这才优雅
二:字典类(Dict.hpp)
#pragma once#include <iostream> //C++头文件
#include <unordered_map> //哈希表
#include <fstream> //C++的文件操作
#include <string> //使用string类型
#include "Log.hpp" //包含日志文件// 字典命名空间
namespace dict_ns
{const std::string defaultpath = "./Dict.txt"; // 字典文件的默认文件路径 为当前路径下的Dict.txt文件const std::string sep = ": "; // 字典文件中键值对的分隔符// 字典类class Dict{private:bool Load() // 私有方法:从文件加载字典数据到哈希表{std::ifstream in(_dict_conf_filepath); // 创建输入文件流if (!in.is_open()) // 检查文件是否成功打开 及其打开失败的处理{LOG(FATAL, "open %s error\n", _dict_conf_filepath.c_str());return false;}std::string line;while (std::getline(in, line)) // 逐行读取文件{if (line.empty()) // 如果读到空行(什么字符都没有的一行)continue; // 则跳过此空行 回到while里的getline 去读取下一行// 来到这 则代表改行不是空行auto pos = line.find(sep); // 则需要先找到分隔符的 pos为分隔符的起始位置if (pos == std::string::npos) // 如果没有分隔符 代表格式不对!continue; // 则继续continent 回到while里的getline 去读取下一行// 来到这 代表一定有分隔符std::string word = line.substr(0, pos); // 则提取改行中的单词if (word.empty()) // 如果分隔符前面没有单词continue; // 则继续continent 回到while里的getline 去读取下一行// 来到这 代表已经提取到了单词 则下面进行提取到中文std::string han = line.substr(pos + sep.size()); // 提取出中文if (han.empty()) // 如果分割符后面没有中文continue; // 则继续continent 回到while里的getline 去读取下一行LOG(DEBUG, "load info, %s: %s\n", word.c_str(), han.c_str()); // 顺便使用日志 打印一下提取到的单词和中文_dict.insert(std::make_pair(word, han)); // 把提取到的单词和中文插入到哈希表}in.close(); // 关闭文件LOG(DEBUG, "load %s success\n", _dict_conf_filepath.c_str()); // 顺便使用日志 打印加载成功 以及词典的文件名字return true;}public:// 构造函数 内部直接调用加载函数// 构造函数接收一个 path 参数,path参数默认缺省为"./Dict.txt" 也可以自己传参指定路径// 再用 path 的值来初始化成员变量 _dict_conf_filepathDict(const std::string &path = defaultpath) : _dict_conf_filepath(path){Load(); // 调用加载函数}// 翻译函数std::string Translate(const std::string &word, bool &ok) // 第一个参数是单词 第二个是bool值(用来反映翻译是否可靠){ok = true;auto iter = _dict.find(word); // 单词作为哈希表的find接口的参数,去查找是否存在if (iter == _dict.end()) // 如果find的返回值是end(),代表没找到{ok = false; // 则返回的不是翻译 而是提示语句 所以bool值为false 表示返回的值不可靠return "未找到";}// return _dict[word];return iter->second; // 返回v值}// 析构函数~Dict(){}private:std::unordered_map<std::string, std::string> _dict; // 哈希表std::string _dict_conf_filepath; // 词典的路径};
}
1:加载词典(Load)
字典类的重点在于如何把文件中每一行的单词和中文加载到哈希表中,我们将次逻辑封装进了Load函数中,代码逻辑如下:
①:创建一个 ifstream
(输入文件流)对象 in,并且打开词典文件(路径:
_dict_conf_filepath)
std::ifstream in(_dict_conf_filepath); // 创建输入文件流
if (!in.is_open()) // 检查文件是否成功打开 及其打开失败的处理
{LOG(FATAL, "open %s error\n", _dict_conf_filepath.c_str());return false;
}
②:使用getline函数读取词典文件中的每一行,而每一行不一定都是 apple: 苹果 这样的格式,所以我们需要对不符合要求的行进行跳过,而不符合要求的行有几种情况:
情况1:直接就是空行
情况2:不是空行 但没有分隔符
情况3:有分隔符 但是没有单词
情况4:有分隔符 但是没有中文
遇到以上的4中情况,只需使用关键字continue即可,其会跳过后序代码,来到while中的getline再次取到下一行!因为每次不管是否合规,我们都已经读取到了一整行,所以continue之后,回到最初的while中getline就会读取到下一行
Load函数代码:
bool Load() // 私有方法:从文件加载字典数据到哈希表{std::ifstream in(_dict_conf_filepath); // 创建输入文件流if (!in.is_open()) // 检查文件是否成功打开 及其打开失败的处理{LOG(FATAL, "open %s error\n", _dict_conf_filepath.c_str());return false;}std::string line;while (std::getline(in, line)) // 逐行读取文件 (std::getline(输入流对象, 字符串变量);){if (line.empty()) // 如果读到空行(什么字符都没有的一行)continue; // 则跳过此空行 回到while里的getline 去读取下一行// 来到这 则代表改行不是空行auto pos = line.find(sep); // 则需要先找到分隔符的 pos为分隔符的起始位置if (pos == std::string::npos) // 如果没有分隔符 代表格式不对!continue; // 则继续continent 回到while里的getline 去读取下一行// 来到这 代表一定有分隔符std::string word = line.substr(0, pos); // 则提取改行中的单词if (word.empty()) // 如果分隔符前面没有单词continue; // 则继续continent 回到while里的getline 去读取下一行// 来到这 代表已经提取到了单词 则下面进行提取到中文std::string han = line.substr(pos + sep.size()); // 提取出中文if (han.empty()) // 如果分割符后面没有中文continue; // 则继续continent 回到while里的getline 去读取下一行LOG(DEBUG, "load info, %s: %s\n", word.c_str(), han.c_str()); // 顺便使用日志 打印一下提取到的单词和中文_dict.insert(std::make_pair(word, han)); // 把提取到的单词和中文插入到哈希表}in.close(); // 关闭文件LOG(DEBUG, "load %s success\n", _dict_conf_filepath.c_str()); // 顺便使用日志 打印加载成功 以及词典的文件名字return true;}
理解了四种需要continue的情况之后,还需要理解怎么从一个合格的行中,摘取出单词和中文,这就需要使用到string的substr接口了!关键点在于我们知道分隔符是冒号空格,我们已经将其定义成了sep!
所以当我们不是情况1空行的时候,我们首先要做的就是查找到我们的分隔符,如果找不到就是情况2,反之找到了分隔符,我们的find函数就会返回分隔符的第一个字符也就是冒号的下标,所以此时我们就使用substr去摘取单词!代码如下:
std::string word = line.substr(0, pos); // 则提取改行中的单词
if (word.empty()) // 如果分隔符前面没有单词continue;
参数0代表从下标为0处开始摘取单词,pos代表摘取的字符的个数,冒号的下标刚好就是单词中字符的格式,所以直接line.substr(0, pos)即可!
再下来就是摘取中文,此时我们还会再使用substr,第一个参数应该是中文的第一个字符的下标,所以我们只需pos(冒号的下标)+分隔符的size即可让pos移动到中文的第一个字符的下标,代码:
// 来到这 代表已经提取到了单词 则下面进行提取到中文
std::string han = line.substr(pos + sep.size()); // 提取出中文
if (han.empty()) // 如果分割符后面没有中文continue;
substr的第二个参数不填,代表sunstr会一直向后取到line这个字符串,也就是我们getline获取到的当前行的末尾,我们无需再传递参数
2:翻译单词(Translate)
而翻译函数就更简单了,我们只需对接收到的参数,也就是一个英文单词,将其作为哈希表的k值即可,返回v值即可达到翻译的效果
而Translate参数中的bool值,只是用来避免我们词典中没有这个单词,返回未找到,但是用户刚好传进来的单词的翻译就是未找到的情况,所以bool值为true代表翻译是可靠的,反之代表翻译不可靠,也就是买找到,打印未找到
当然,你也可以完全不用这个bool值参数,因为我们本来就是翻译一个单词,干脆把没找到翻译的情况,我们打印"该单词未被收录"即可,因为不会有一个单词的翻译会是"该单词未被收录"这么长!
// 翻译函数
std::string Translate(const std::string &word, bool &ok) // 第一个参数是单词 第二个是bool值(用来反映翻译是否可靠)
{ok = true;auto iter = _dict.find(word); // 单词作为哈希表的find接口的参数,去查找是否存在if (iter == _dict.end()) // 如果find的返回值是end(),代表没找到{ok = false; // 则返回的不是翻译 而是提示语句 所以bool值为false 表示返回的值不可靠return "未找到";}// return _dict[word];return iter->second; // 返回v值
}
三:服务端(UdpServer.hpp)
#pragma once#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <functional>
#include "Log.hpp"// 一些发生错误时候 返回的枚举变量
enum
{SOCKET_ERROR = 1,BIND_ERROR,USAGE_ERROR
};// socket接口默认的返回值
const static int defaultfd = -1;// typedef std::function<std::string(const std::string&)> func_t;
using func_t = std::function<std::string(const std::string &, bool &ok)>; // 定义一个函数类型 别名为func_tclass UdpServer
{
public:// 构造函数 socket的返回值 端口号 是否运行 翻译函数UdpServer(uint16_t port, func_t func) : _sockfd(defaultfd), _port(port), _isrunning(false), _func(func){}// 析构函数~UdpServer(){}// 初始化服务端void InitServer(){// 1:创建udp socket 套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET代表网络通信 SOCK_DGRAM代表数据报 所以就是UDP 第三个参数填写0即可// 创建套接字失败 打印语句提醒if (_sockfd < 0){LOG(FATAL, "socket error, %s, %d\n", strerror(errno), errno);exit(SOCKET_ERROR);}// 创建套接字成功 打印语句提醒LOG(INFO, "socket create success, sockfd: %d\n", _sockfd);// 2.0 填充sockaddr_in结构struct sockaddr_in local; // 网络通信 所以定义struct sockaddr_in类型的变量bzero(&local, sizeof(local)); // 先把结构体情况 好习惯local.sin_family = AF_INET; // 填写第一个字段 (地址类型)local.sin_port = htons(_port); // 填写第二个字段PORT (需先转化为网络字节序)local.sin_addr.s_addr = INADDR_ANY; // 填写第三个字段IP (直接填写0即可,INADDR_ANY就是为0的宏)// 2:bind绑定// 我们填充好的local和我们创建的套接字进行绑定(绑定我们接收信息发送信息的端口)int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));// 绑定失败 打印语句提醒if (n < 0){LOG(FATAL, "bind error, %s, %d\n", strerror(errno), errno);exit(BIND_ERROR);}// 绑定成功 打印语句提醒LOG(INFO, "socket bind success\n");}// 启动服务端(不想让网络通信模块和业务模块进行强耦合)void Start(){// 先把bool值置为true 代表服务端在运行_isrunning = true;while (true) // 服务端都是死循环{char request[1024]; // 对方端发来的信息 存储在buffer中struct sockaddr_in peer; // 对方端的网络属性socklen_t len = sizeof(peer); // 必须初始化成为sizeof(peer) 不能初始化为0// 我们要让server先收数据ssize_t n = recvfrom(_sockfd, request, sizeof(request) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){request[n] = 0; // 添加字符串终止符 方便得到正确完整的单词bool ok; // 定义bool值std::string response = _func(request, ok); // 将翻译请求,回调出去,在外部进行数据处理// 再把翻译得到的中文,发回给对方sendto(_sockfd, response.c_str(), response.size(), 0, (struct sockaddr *)&peer, len);}}_isrunning = false;}private:int _sockfd; // socket的返回值 在多个接收中都需要使用uint16_t _port; // 服务器所用的端口号bool _isrunning; // 反映是否在运行的bool值// 给服务器设定回调,用来执行翻译的函数func_t _func;
};
解释:
服务端和上篇博客的echo程序的服务端大致相同,因为我们是松耦合!将翻译函数全部交给了字典类中的翻译函数,所以服务端只需要额外理解我们如果引入一个承载字典类中的Translate函数的载体就行!
①:我们需要先定义出一个函数类型,再给其起一个别名,代码:
using func_t = std::function<std::string(const std::string &, bool &ok)>;
// 定义一个函数类型 别名为func_t
这个类型就是我们字典类中的类型,这样才能完美的将字典类的Translate函数bind到func_t上
②:然后我们需要利用定义出的类型去创建一个成员变量,因为只有创建除了成员变量,我们才可以通过构造函数的参数进行绑定!
func_t _func;
③:start中调用_func函数
前面准备工作做好之后,我们需要就是在start函数中进行调用_func函数,也就是我们接收到客户端传来的单词,我们就要把单词作为参数传递进_func函数中,再把_func函数的返回值,也就是翻译得到的中文,发送给客户端即可!
// 我们要让server先收数据
ssize_t n = recvfrom(_sockfd, request, sizeof(request) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0)
{request[n] = 0; // 添加字符串终止符 方便得到正确完整的单词bool ok; // 定义bool值std::string response = _func(request, ok); // 将翻译请求,回调出去,在外部进行数据处理//再把翻译得到的中文,发回给对方sendto(_sockfd, response.c_str(), response.size(), 0, (struct sockaddr *)&peer, len);
}
这样在我们进行bind操作之后,就可以实现松耦合的翻译功能了!
四:Main.cc
#include <iostream>
#include <memory>
#include "Dict.hpp"
#include "UdpServer.hpp"using namespace dict_ns; // 打开字典类的命名空间// 运行服务端的时候,因为IP在服务端文件中给定0了
// 所以只需要使用者只需要给服务端一个PORT即可
void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " local_port\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(USAGE_ERROR);}EnableScreen(); // 表示把日志打印到屏幕上Dict dict; // 定义字典类对象uint16_t port = std::stoi(argv[1]); // 从main的参数列表中获取到PORT// 创建服务端对象 并且使用bind把字典类的成员函数绑定到服务器类的构造参数中std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, std::bind(&Dict::Translate, &dict, std::placeholders::_1, std::placeholders::_2));usvr->InitServer(); // 初始化服务端usvr->Start(); // 启动服务端return 0;
}
解释:
重点就在于bind的使用!首先需要包含字典文件已经打开字典类的命名空间,然后创建一个字典类的对象,这样,我们下一步进行创建服务端对象的时候,就可以进行bind了。bind的语法就不再赘述了,都整理在了博客中:https://blog.csdn.net/shylyly_/article/details/151109228
bind之后,就相当于我们服务端类中的_func函数就拥有了实体了,其才能够真正的实现翻译功能
五:客户端(UdpClient.cc)
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// 启动客户端 需要用户输入服务端的IP+PORT
void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1]; // 从main的参数列表中获取到服务端IPuint16_t serverport = std::stoi(argv[2]); // 从main的参数列表中获取到服务端PORT// 1. 创建socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;}// 2. 无需显式的bind OS会自己做// 用户已经传递服务端的IP+PORT// 所以我们sendto就知道了 给哪个服务端传递信息// 所以需要事先构建好服务端的网络属性struct sockaddr_in server; // 定义出struct sockaddr_in的变量memset(&server, 0, sizeof(server)); // 清零server.sin_family = AF_INET; // 地址类型字段的填写server.sin_port = htons(serverport); // PORT字段的填写server.sin_addr.s_addr = inet_addr(serverip.c_str()); // IP字段的填写std::string message; // 存放用户输入的信息// 3. 直接通信即可while (true){std::cout << "Please Enter# "; // 提示用户可以输入你想向服务端发送的信息了std::getline(std::cin, message); // 获取到用户输入的信息 cin到message中// 发送信息sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));// 构建出接收服务端的网络属性结构体struct sockaddr_in peer;socklen_t len = sizeof(peer);char buffer[1024];// 接收信息ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);// 接收成功if (n > 0){buffer[n] = 0; // 添加字符串终止符std::cout << "server echo# " << buffer << std::endl; // 打印服务器回复的信息}}return 0;
}
解释:客户端代码与echo程序一模一样,不再赘述
六:日志文件(Log.hpp)
#pragma once#include <iostream> //C++必备头文件
#include <cstdio> //snprintf
#include <string> //std::string
#include <ctime> //time
#include <cstdarg> //va_接口
#include <sys/types.h> //getpid
#include <unistd.h> //getpid
#include <thread> //锁
#include <mutex> //锁
#include <fstream> //C++的文件操作std::mutex g_mutex; // 定义全局互斥锁
bool gIsSave = false; // 定义一个bool类型 用来判断打印到屏幕还是保存到文件
const std::string logname = "log.txt"; // 保存日志信息的文件名字// 日志等级
enum Level
{DEBUG = 0,INFO,WARNING,ERROR,FATAL
};// 将日志写进文件的函数
void SaveFile(const std::string &filename, const std::string &message)
{std::ofstream out(filename, std::ios::app);if (!out.is_open()){return;}out << message << std::endl;out.close();
}// 日志等级转字符串--->字符串才能表示等级的意义 0123意义不清晰
std::string LevelToString(int level)
{switch (level){case DEBUG:return "Debug";case INFO:return "Info";case WARNING:return "Warning";case ERROR:return "Error";case FATAL:return "Fatal";default:return "Unknown";}
}// 获取当前时间的字符串
// 时间格式包含多个字符 所以干脆糅合成一个字符串
std::string GetTimeString()
{// 获取当前时间的时间戳(从1970-01-01 00:00:00开始的秒数)time_t curr_time = time(nullptr);// 将时间戳转换为本地时间的tm结构体// tm结构体包含年、月、日、时、分、秒等字段struct tm *format_time = localtime(&curr_time);// 检查时间转换是否成功if (format_time == nullptr)return "None";// 缓冲区用于存储格式化后的时间字符串char time_buffer[1024];// 格式化时间字符串:年-月-日 时:分:秒snprintf(time_buffer, sizeof(time_buffer), "%d-%02d-%02d %02d:%02d:%02d",format_time->tm_year + 1900, // tm_year: 从1900年开始的年数,需要加1900format_time->tm_mon + 1, // tm_mon: 月份范围0-11,需要加1得到实际月份format_time->tm_mday, // tm_mday: 月中的日期(1-31)format_time->tm_hour, // tm_hour: 小时(0-23)format_time->tm_min, // tm_min: 分钟(0-59)format_time->tm_sec); // tm_sec: 秒(0-60,60表示闰秒)return time_buffer; // 返回格式化后的时间字符串
}// 日志函数-->打印出日志
// 格式:时间 + 等级 + PID + 文件名 + 代码行数 + 可变参数
void LogMessage(int level, std::string filename, int line, bool issave, const char *format, ...)
{std::string levelstr = LevelToString(level); // 得到等级字符串std::string timestr = GetTimeString(); // 得到时间字符串pid_t selfid = getpid(); // 得到PID// 使用va_接口+vsnprintf得到用户想要的可变参数的字符串 存储与buffer中char buffer[1024];va_list arg;va_start(arg, format);vsnprintf(buffer, sizeof(buffer), format, arg);va_end(arg);std::lock_guard<std::mutex> lock(g_mutex); // 引入C++的RAII的锁 保护打印功能// 保存格式为时间 + 等级 + PID + 文件名 + 代码行数 + 可变参数 的日志信息 到message中std::string message = "[" + timestr + "]" + "[" + levelstr + "]" +"[" + std::to_string(selfid) + "]" +"[" + filename + "]" + "[" + std::to_string(line) + "] " + buffer;// 打印到屏幕if (!issave){std::cout << message << std::endl;}// 保存进文件else{SaveFile(logname, message);}
}// 宏定义 省略掉__FILE__ 和 __LINE__
#define LOG(level, format, ...) \do \{ \LogMessage(level, __FILE__, __LINE__, gIsSave, format, ##__VA_ARGS__); \} while (0)// 用户调用则意味着保存到文件
#define EnableScreen() (gIsSave = false)// 用户调用则意味着打印到屏幕
#define EnableFile() (gIsSave = true)
解释:日志博客:https://blog.csdn.net/shylyly_/article/details/151263351
七:makefile
.PHONY:all
all:udpserver udpclientudpserver:Main.ccg++ -o $@ $^ -std=c++14
udpclient:UdpClient.ccg++ -o $@ $^ -std=c++14
.PHONY:clean
clean:rm -f udpserver udpclient
八:运行效果
解释:
当我们运行服务端的时候,因为我们添加了日志,所以每次我们插入到哈希表中的单词中文,都会被打印出来,而在右侧客户端,我们就实现了翻译的功能
最后我也准备一份更全面的常用单词文件:
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
water: 水
food: 食物
house: 房子
home: 家
family: 家庭
friend: 朋友
money: 钱
time: 时间
work: 工作
school: 学校
one: 一
two: 二
three: 三
four: 四
five: 五
six: 六
seven: 七
eight: 八
nine: 九
ten: 十
red: 红色
blue: 蓝色
green: 绿色
yellow: 黄色
black: 黑色
white: 白色
orange: 橙色
purple: 紫色
pink: 粉色
brown: 棕色
rice: 米饭
noodle: 面条
bread: 面包
meat: 肉
fish: 鱼
chicken: 鸡肉
egg: 鸡蛋
milk: 牛奶
tea: 茶
coffee: 咖啡
tiger: 老虎
lion: 狮子
elephant: 大象
monkey: 猴子
bird: 鸟
horse: 马
cow: 牛
sheep: 羊
pig: 猪
sun: 太阳
moon: 月亮
star: 星星
rain: 雨
snow: 雪
wind: 风
cloud: 云
weather: 天气
hot: 热的
cold: 冷的
head: 头
eye: 眼睛
ear: 耳朵
nose: 鼻子
mouth: 嘴巴
hand: 手
foot: 脚
heart: 心脏
blood: 血液
bone: 骨头
doctor: 医生
nurse: 护士
engineer: 工程师
driver: 司机
cook: 厨师
farmer: 农民
worker: 工人
manager: 经理
artist: 艺术家
singer: 歌手
angry: 生气的
excited: 兴奋的
tired: 疲倦的
scared: 害怕的
surprised: 惊讶的
calm: 平静的
nervous: 紧张的
proud: 自豪的
shy: 害羞的
curious: 好奇的
walk: 走路
sit: 坐
stand: 站
sleep: 睡觉
eat: 吃
drink: 喝
read: 阅读
write: 写
speak: 说话
listen: 听
day: 天
night: 夜晚
morning: 早上
afternoon: 下午
evening: 晚上
week: 周
month: 月
year: 年
today: 今天
tomorrow: 明天
city: 城市
country: 国家
park: 公园
hospital: 医院
restaurant: 餐厅
hotel: 酒店
store: 商店
bank: 银行
station: 车站
airport: 机场
study: 学习
knowledge: 知识
exam: 考试
question: 问题
answer: 答案
class: 班级
lesson: 课程
homework: 作业
test: 测试
degree: 学位
computer: 电脑
phone: 电话
internet: 互联网
music: 音乐
movie: 电影
game: 游戏
sport: 运动
ball: 球
dance: 舞蹈
photo: 照片
father: 父亲
mother: 母亲
son: 儿子
daughter: 女儿
brother: 兄弟
sister: 姐妹
grandfather: 祖父
grandmother: 祖母
uncle: 叔叔
aunt: 阿姨
big: 大的
small: 小的
tall: 高的
short: 矮的
long: 长的
new: 新的
old: 旧的
young: 年轻的
beautiful: 美丽的
ugly: 丑陋的
rich: 富有的
poor: 贫穷的
strong: 强壮的
weak: 虚弱的
fast: 快的
slow: 慢的
easy: 容易的
difficult: 困难的
right: 正确的
wrong: 错误的
true: 真实的
false: 虚假的
open: 打开
close: 关闭
start: 开始
stop: 停止
buy: 买
sell: 卖
give: 给
take: 拿
help: 帮助
wait: 等待
play: 玩
sing: 唱歌
dance: 跳舞
swim: 游泳
fly: 飞
travel: 旅行
meet: 见面
think: 思考
know: 知道
understand: 理解
remember: 记住
forget: 忘记
live: 生活
die: 死亡
birth: 出生
growth: 成长
change: 改变
stay: 停留
leave: 离开
arrive: 到达
depart: 出发
win: 赢
lose: 输
success: 成功
failure: 失败
hope: 希望
dream: 梦想
plan: 计划
goal: 目标
problem: 问题
solution: 解决方案
reason: 原因
result: 结果
cause: 导致
effect: 影响
解释:彩色是因为是关键字