小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系统编程专栏<—请点击
linux网络编程专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

【linux】网络套接字编程(五)TCP服务器与客户端的实现——多进程版,多线程版——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】网络套接字编程(六)TCP服务器与客户端的实现——线程池版,客户端如何实现断线自动重连,服务器与客户端实现英汉翻译


一、线程池版

  1. 在之前的文章中,TCP服务器与客户端的版本小编已经讲解多进程,到多线程的实现,多线程比多进程更优,因为线程的创建成本要低于进程,所以多线程的性能更好,但是小编觉得还不够,性能还是不够,还想继续优化一下,那么我们该如何下手呢?线程池
  2. 在之前的文章中,小编讲解了线程池 线程池的讲解,详情请点击<——,原理就是提前创建出一批线程,如果队列中没有任务,那么线程就会去条件变量下休眠等待,然后基于生产者消费者模型,将线程要执行的工作以任务的形式放到队列中,当队列中有任务的时候,线程就会被唤醒,然后去队列中拿任务,然后执行任务
  3. 所以相较于多线程版,线程池在客户端的连接到来前已经提前创建了一批线程,然后线程池将这些线程管理起来,当客户端的连接服务器之后,线程池就会唤醒一个线程,然后让这个线程直接去和客户端进行通信,当通信完成之后,这个线程又会去条件变量下休眠,所以线程池中的线程执行通信任务在这个过程中省去了创建和释放的成本,而多线程版当连接到来的时候,需要创建线程进行通信,当通信完成之后,需要释放线程,所以多线程相较于线程池仍旧很重,所以我们可以采用线程池版在多线程版的基础上再次进行进一步的优化,提高性能

简单打印

  1. 所以小编这里就在上一篇文章的多线程版的代码上直接进行修改,并且将线程池的代码也引入进来,文章讲解以及代码链接如下
    (一)多线程版的讲解,详情请点击<——
    (二)线程池的讲解,详情请点击<——
  2. 由于线程池是提前创建一批线程,那么也就注定了线程池中线程的数量是有上限的,所以如果类似于上文的多线程版的服务器创建线程,然后让线程为客户端提供长服务,即while(true)一直进行通信,那么也就注定了这一个线程会被一直被一个客户端所占用,所使用,那么一旦客户端较多,超过了线程池的线程数量的上限,所以服务器就会出现对于客户端的连接请求不响应的情况,那儿这不是我们想要看到的
  3. 所以我们该如何解决呢?既然长服务不行, 那么就让线程池中的线程提供短服务不就好了,什么是短服务?即客户端向服务端发送数据,服务端接收然后响应将处理完成的数据发送回给客户端,客户端接收到处理完成的数据,完成一次短服务,所以这就是短服务
  4. 那么我们就将一次短服务看作一个任务,这个短服务的执行速度很快,如果短服务的任务很多的情况下,所以也就意味着线程池中的多个线程都可以来执行这个短服务,所以线程池中的多个线程都可以被调用起来
  5. 如果客户端想要进行长时间通信那么怎么办?很简单,那么将短服务的代码逻辑构成一个死循环,每次用户想要进行通信的时候就会发送一个短服务,如果想要进行多次通信那么就会发送多次短服务,所以短服务可以有效的避免线程池中的一个线程被长时间占用而造成的线程池中的线程被调用占用至上限进而造成的服务器对于客户端的连接不响应的情况
Task.hpp
  1. 那么这里线程池中的线程执行的任务,即Task.hpp,那么线程和客户端进行什么通信呢?这里我们和上文中的多线程一样,让线程拿到客户端发来的数据,然后打印,打印结束之后,进行简单的处理,然后发送回客户端即可,所以Task.hpp的类的成员变量中就需要包含sockfd,客户端的IP地址和端口号的字段,那么在构造函数中队对应的字段进行初始化即可
  2. 所以这里我们将上文多线程执行的服务Service中拿过来,去掉while(true)死循环即是一个短服务的代码逻辑,由于包含日志的打印,所以这里我们需要日志的头文件,然后声明外部日志对象lg即可,所以我们将短服务的代码逻辑添加到Task执行的任务operator()中即可,当执行完一次短服务之后,此时已经线程已经完成了与客户端的短服务通信的任务,所以此时close关闭网络文件描述符sockfd即可
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include "Log.hpp"

extern Log lg;

class Task
{
public:
    Task()
    {}

    Task(int sockfd, const std::string& clientip, const uint16_t& clientport)
        : sockfd_(sockfd)
        , clientip_(clientip)
        , clientport_(clientport)
    {}

    void operator()()
    {
        char inbuffer[4096];
        ssize_t n = read(sockfd_, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            inbuffer[n] = 0;
            std::cout << "client say# " << inbuffer << std::endl;

            std::string echo_string = "tcpserver echo# ";
            echo_string += inbuffer;

            n = write(sockfd_, echo_string.c_str(), echo_string.size());                
            if(n < 0)
                lg(Info, "write err, errno: %d, errstr: %s", errno, strerror(errno));
        }
        else if(n == 0)
        {
            lg(Info, "%s:%d quit, server colse sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
        }
        else
        {
            //异常
            lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", 
                sockfd_, clientip_.c_str(), clientport_);
        }

        close(sockfd_);
    }

    ~Task()
    {}

private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
};
  1. 那么既然任务的执行,以及Task.hpp的成员函数已经被我们修改了,所以在线程池的代码中,我们还应该注释掉对应GetResult方法的调用
static void* HandlerTask(void* args)
{
    ThreadPool* tp = static_cast<ThreadPool*>(args);
    std::string name = tp->GetThreadName(pthread_self());

    while(true)
    {
        tp->Lock();

        while(tp->IsQueueEmpty())
        {
            tp->ThreadSleep();
        }

        T t = tp->Pop();

        tp->Unlock();

        t();
        // std::cout << name << " run" << ", result: " << t.GetResult() << std::endl;
    }


    return nullptr;
}
TcpClient.cc
  1. 那么客户端的代码逻辑中仅需要进行调整即可,服务器字段service的初始化放到死循环外边,然后死循环中包含一次短服务的逻辑,即socket创建套接字获取网络文件描述符sockfd,然后connect连接服务器,write写入发送数据,read接收读取数据,最后close关闭网络文件描述符sockfd即可
  2. 为什么每次的短服务都需要重新创建sockfd,然后最后关闭网络文件描述符?因为当前建立起的信道,在write写入发送数据,read接收读取数据之后,服务端认为已经结束了一次短服务通信,所以服务端就会关闭信号,即关闭对应的读写端,即close关闭网络文件描述符,所以既然当前信道已经失效了,所以自然每次的短服务都需要重新创建sockfd,然后最后关闭网络文件描述符
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void Usage(const std::string& str)
{
    std::cout << "\n\tUsage: " << str << " serverip serverport" << std::endl; 
}

// ./tcpclient serverip serverport
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 0;
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
    socklen_t len = sizeof(server);

    while(true)
    {
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(sockfd < 0)
        {
            std::cerr << "socket create err" << std::endl;
            return 1;
        }

        //客户端发起connect请求的时候,操作系统会自动进行端口号的随机bind绑定
        int n = connect(sockfd, (struct sockaddr*)&server, len);
        if(n < 0)
        {
            std::cerr << "connect err..." << std::endl;
            return 2;
        }

        std::string message;
        char inbuffer[4096];

        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        n = write(sockfd, message.c_str(), message.size());
        if(n < 0)
            std::cerr << "write err" << std::endl;

        n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }

        close(sockfd);
    }

    return 0;
}
TcpServer.hpp
  1. 那么此时我们在服务器的启动函数StartServer中,上来先启动线程池,创建好一批线程,开始没有任务,所以这一批线程就会去条件变量下等待,然后在StartServer的死循环内当客户端连接成功服务器的时候,那么进行进行任务的创建,以及线程池的Push入任务了,所以线程池就会自动唤醒在条件变量下等待的线程,然后被唤醒的线程就会执行任务了
void StartServer()
{
    ThreadPool<Task>::GetInstance()->Start();
    lg(Info, "tcpserver is running...");
    
    for (;;)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        
        int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
        if (sockfd < 0)
        {
            lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
            continue;
        }
        uint16_t clientport = ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));

        lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", 
            sockfd, clientip, clientport);

        //version 3 多线程版
        // ThreadData* td = new ThreadData(sockfd, clientip, clientport, this);

        // pthread_t tid;
        // pthread_create(&tid, nullptr, Routine, td);

        //version 4 线程池版
        Task t(sockfd, clientip, clientport);
        ThreadPool<Task>::GetInstance()->Push(t);
    }
}
测试
  1. 所以接下来我们运行服务器,线程池成功启动,服务器开始运行
    在这里插入图片描述

  2. 那么接下来运行客户端连接服务器,连接成功
    在这里插入图片描述

  3. 然后客户端开始输入数据,可以看到客户端的每次的输入都会变成一次新的连接,即客户端的每次输入都是一次短服务的过程
    在这里插入图片描述
    在这里插入图片描述

  4. 最后我们ctrl+c退出客户端即可,无误
    在这里插入图片描述

断线自动重连

在这里插入图片描述

  1. 相信屏幕面前的不少读者友友都玩过王者荣耀这款游戏,那么在对局中,如果突然手机断网了(或者王者荣耀服务器崩溃终止了),王者荣耀客户端会尝试自动重连,那么如果在这个断线自动重连的这个过程中,如果手机网络恢复了(王者荣耀服务器维修运行好了),那么就会重连成功,并且短时间内加载对局,即会观察到人物,血条,野怪快速变化,更新,在这个变化的过程中,你是无法操作的,只有当变化更新,即数据同步到你手机的王者荣耀客户端上之后(数据可以进行同步说明王者荣耀服务器在你断线的过程中是对局没有丢包的),你才可以继续进行对局,所以此时就可以开心的玩元歌了
  2. 那么究竟这个客户端断线自动重连是如何做的,如果今天小编也想要在我们自己的客户端上添加断线自动重连应该如何做呢?如下
  3. 其实我们在客户端中要进行修改的模块很简单,仅仅只需要修改connect连接模块即可
  4. 首先设置两个变量,一个bool类型的isreconnect变量默认为false,即默认不进行重连,然后还有一个int类型的计数cnt变量,默认为5,即断线自动重连5次,如果连接5次不成功,那么则直接退出客户端
  5. 所以我们可以基于connect连接使用一个do - while循环,即上来直接进行连接,因为我们本来的代码逻辑中也是上来直接进行连接,客户端本来的代码逻辑中如果连接失败则退出客户端,但是这里的自动重连逻辑中我们不这样做,如果连接失败,那么将isreconnect变量设置为true,然后将计数cnt减减,并且这里再进行sleep休眠2秒,让连接的过程慢一点便于我们观察到断线自动重连的过程
  6. 所以接下来while(cnt && isreconnect)中cnt大于0,并且isreconnect为真,那么就会继续do进行连接,如果连接成功了,那么connect的返回值会返回0,所以此时我们就break退出do - while,然后在外部进行判断,此时cnt仍然大于0,所以就不会终止客户端,否则,如果connect连接了5次都失败了,那么cnt会减到0,那么此时就打印信息,然后退出客户端即可
  7. 那么如果上来connect就连接成功,那么此时isreconnect仍然是false,所以while(cnt && isreconnect)中的isreconnect也就不会成立,进而也就不会继续重新连接,因为此时连接已经成功了,cnt仍然为5,所以if(cnt == 0)也就不成立,进而就不会退出客户端,会继续向后与服务器进行通信无误
  8. 那么如果上来connect连接不成功,那么此时isreconnect就会被设置为true,所以while(cnt && isreconnect)中的isreconnect也就会成立,当连接了几次之后,连接成功了,所以此时就需要退出do - while循环了,但是如果这里对于connect的返回值 大于等于0 不判断,那么那么此时isreconnect仍然是true,所以while(cnt && isreconnect)中的isreconnect仍然为true所以就会继续进行判断,明明已经连接成功了,已经不再需要进行自动重连了,但是这里由于对于connect的返回值 大于等于0 不判断,导致此时isreconnect仍然是true,进而就会继续执行connect
  9. 然后由于此时已经连接成功了,那么再次进行连接的话,系统会判定为错误操作,进而connect会返回小于0的数,进而造成cnt继续减减,那么继续进行重复的连接,直到cnt变成0,退出循环,所以接下来if(cnt == 0)就会成立,所以就会打印信息,终止客户端,明明此时已经连接成功了,由于没有对connect的返回值是否 大于等于0 进行判断,导致do while 未能正常退出导致的终止客户端是很难受的,所以我们不仅要判断connect的返回值小于0的情况,对于connect的返回值大于等于0的情况,我们也应该进行判断只有这样判断成立之后,断线自动重连的do while才会退出循环,进而才不会导致客户端进程被终止
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void Usage(const std::string& str)
{
    std::cout << "\n\tUsage: " << str << " serverip serverport" << std::endl; 
}

// ./tcpclient serverip serverport
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 0;
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
    socklen_t len = sizeof(server);

    while(true)
    {
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(sockfd < 0)
        {
            std::cerr << "socket create err" << std::endl;
            return 1;
        }

        bool isreconnect = false;
        int cnt = 5;
        
        do
        {
            int n = connect(sockfd, (struct sockaddr*)&server, len);
            if(n < 0)
            {
                //此时连接失败,那么进行断线自动重连
                isreconnect = true;
                cnt--;
                sleep(2);
                std::cerr << "connect error..., reconnect: " << cnt << std::endl;
            }
            else  //这里必须有对connect的返回值 大于等于0 的else的判断
                break;
        }while(cnt && isreconnect);

        if(cnt == 0)
        {
            std::cerr << "user offline..." << std::endl;
            return 2;
        }

        //客户端发起connect请求的时候,操作系统会自动进行端口号的随机bind绑定
        std::string message;
        char inbuffer[4096];

        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        int n = write(sockfd, message.c_str(), message.size());
        if(n < 0)
            std::cerr << "write err" << std::endl;

        n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }

        close(sockfd);
    }

    return 0;
}
测试
  1. 所以这里小编就采用服务器终止,然后让客户端进行操作之后,发现断开了连接,所以while循环中的短服务就会失败,那么就会再次进行while循环进行自动重连
  2. 所以这里我们终止服务器,然后让客户端进行操作,在这个期间小编将服务器重新运行起来,期望可以观察到客户端断线自动重连现象成功,如下,无误
    在这里插入图片描述
  3. 同样的,如果在客户端5次自动重连的过程中如果服务器不重新启动,那么也就意味着客户端5次自动重连不会成功,进而导致客户端打印信息,然后客户端被终止,我们期望这个现象也可以看到,如下,无误

在这里插入图片描述

英汉翻译

  1. 小编上面客户端与服务器中线程池中的线程执行的还都是比较简单的对字符串进行处理,然后将处理完成的数据发送回客户端,可是今天小编想要实现一个英汉翻译的任务该如何做呢?
  2. 即客户端发送英语单词给服务器,服务器接收到这个英语单词之后,在服务器的本地词库中查询这个英语单词,如果找到了那么将英语单词对应的汉语发送给客户端,如果找不到这个英语单词那么发送unknow表示找不到,所以该如何实现呢?
  3. 并且小编还想使用文件操作从文件中读取单词,那么所以此时我们在当前目录下就应该有一个单词文件叫做dict.txt,那么小编这里简要的将小学英语常用词汇200词,以冒号进行分隔,那么所有的英语以及对应的汉语就应该是这种形式 Monday:星期一,那么单词文件dict.txt的内容如下
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
egg: 鸡蛋
fish: 鱼
girl: 女孩
boy: 男孩
book: 书
pen: 钢笔
pencil: 铅笔
bag: 书包
school: 学校
teacher: 老师
student: 学生
friend: 朋友
mother: 妈妈
father: 爸爸
sister: 姐妹
brother: 兄弟
grandma: 奶奶;外婆
grandpa: 爷爷;外公
family: 家庭
home: 家
room: 房间
bed: 床
table: 桌子
chair: 椅子
door: 门
window: 窗户
blackboard: 黑板
white: 白色
black: 黑色
red: 红色
blue: 蓝色
green: 绿色
yellow: 黄色
pink: 粉色
orange: 橙色;橙子
purple: 紫色
one: 一
two: 二
three: 三
four: 四
five: 五
six: 六
seven: 七
eight: 八
nine: 九
ten: 十
eleven: 十一
twelve: 十二
thirteen: 十三
fourteen: 十四
fifteen: 十五
sixteen: 十六
seventeen: 十七
eighteen: 十八
nineteen: 十九
twenty: 二十
thirty: 三十
forty: 四十
fifty: 五十
sixty: 六十
seventy: 七十
eighty: 八十
ninety: 九十
one hundred: 一百
big: 大的
small: 小的
long: 长的
short: 短的;矮的
tall: 高的
fat: 胖的
thin: 瘦的
happy: 开心的
sad: 难过的
good: 好的
bad: 坏的
new: 新的
old: 旧的;老的
hot: 热的
cold: 冷的
warm: 温暖的
cool: 凉爽的
nice: 美好的
good morning: 早上好
good afternoon: 下午好
good evening: 晚上好
good night: 晚安
hello: 你好
hi: 嗨
bye: 再见
goodbye: 再见
thank you: 谢谢你
sorry: 对不起
please: 请
yes: 是
no: 不
ok: 好的
this: 这个
that: 那个
these: 这些
those: 那些
my: 我的
your: 你的;你们的
his: 他的
her: 她的
it: 它
is: 是
am: 是
are: 是
was: 是(过去式)
were: 是(过去式)
have: 有
has: 有(第三人称单数)
had: 有(过去式)
do: 做;助动词
does: 做;助动词(第三人称单数)
did: 做;助动词(过去式)
go: 去
come: 来
see: 看见
look: 看
watch: 观看
read: 读
write: 写
draw: 画
sing: 唱
dance: 跳舞
run: 跑
jump: 跳
walk: 走
eat: 吃
drink: 喝
sleep: 睡觉
play: 玩
toy: 玩具
ball: 球
car: 汽车
bus: 公交车
bike: 自行车
plane: 飞机
train: 火车
ship: 船
sun: 太阳
moon: 月亮
star: 星星
sky: 天空
cloud: 云
rain: 雨;下雨
snow: 雪;下雪
wind: 风
tree: 树
flower: 花
grass: 草
bird: 鸟
rabbit: 兔子
tiger: 老虎
lion: 狮子
panda: 熊猫
elephant: 大象
monkey: 猴子
pig: 猪
cow: 牛
sheep: 羊
horse: 马
day: 天;日
week: 周
month: 月
year: 年
Monday: 星期一
Tuesday: 星期二
Wednesday: 星期三
Thursday: 星期四
Friday: 星期五
Saturday: 星期六
Sunday: 星期日
January: 一月
February: 二月
March: 三月
April: 四月
May: 五月
June: 六月
July: 七月
August: 八月
September: 九月
October: 十月
November: 十一月
December: 十二月
breakfast: 早餐
lunch: 午餐
dinner: 晚餐
water: 水
milk: 牛奶
rice: 米饭
bread: 面包
meat: 肉
vegetable: 蔬菜
fruit: 水果
  1. 那么我们就可以使用一个Init类去完成对dict.txt文件的读取,并且提供一个Translation函数用于进行单词的查找翻译,既然是对字符串的快速查找,所以这里我们就可以使用unordered_map哈希表进行查找,即将dict.txt文件的内容读取到哈希表中,然后就可以快速查找对应的单词,所以Init的类中就应该有一个unordered_map类型对象dict
  2. 那么我们定义出文件的路径dictname,即当前路径下,那么我们就可以一次读取一行,然后由于小编对于单词的英语和汉语是采用 : 进行的分隔,所以就可以编写一个Split函数,用于分隔一行中的英语和汉语,所以对于这个分隔符sep我们也定义出来
  3. 那么接下来就可以编写Init类了,成员变量应该有一个哈希表dict这一点之前我们已经分析过了,那么接下来我们看一下Init的构造函数,所以上来我们就可以使用c++中的文件操作打开文件,即定义一个文件打开对象in打开当前路径下的单词文件dict.txt,但是打开文件有坑会失败,所以我们进行对应日志的打印,然后终止进程,如果打开单词文件dict.txt成功,那么就可以使用getline从in中读取一行到string类型的line中,所以采用while(std::getline(in, line))的方式进行读取,如果文件中的仍然有行未被读取,那儿就会一直进行读取,一次读取一行
  4. 所以此时当我们读取完成一行之后,本质上一行中放置的是 Monday:星期一 这种形式的字符串,所以我们就可以使用Split函数以sep对应 : 作为分隔符,然后将英语单词Monday放到part1中,然后将对应的汉语放到part2中,所以此时一行中就被分隔开了,得到了英语单词和对应的汉语
  5. 所以此时我们就可以将英语单词作为key,汉语作为val插入到哈希表dict中,然后while循环结束,单词文件中所有的以sep对应的 : 为分隔符的英语单词和对应汉语就被插入到了哈希表dict中了,在最后文件打开对象in已经使用完成,所以为了防止内存泄漏,我们调用文件打开对象的close方法关闭文件打开对象即可
  6. 那么我们要知道,如果单词文件dict.txt中的英语单词和对应汉语如果不是以 : 进行分隔的,那么此时Split就无法进行分隔了,所以在Split中我们应该对分隔的结果进行判断,如果分隔成功,那么返回true,如果分隔失败,那么返回false,只有当分隔成功的时候,我们才向哈希表dict中进行插入
  7. 所以接下来我们看Split这个分隔函数,拿到要进行分隔的一行字符串,字符串是以sep对应的 : 为分隔符进行分割的,冒号左侧是英语单词,冒号右侧是对应的汉语,即 Monday:星期一 这种形式,所以我们就可以使用string中的find进行查找sep对应的分隔符 : 如果查找成功,那么就会返回分隔符 : 对应的下标pos,否则返回npos,那么我们就可以对pos进行判断了,如果pos等于npos,说明此时没有找到分隔符,那么我们就直接返回false即可,否则即为找到了
  8. 所有我们就使用string中的substr进行分隔即可,英语单词对应的下标位置是从0到pop位置,因为我们向substr传入的参数是左闭右开形式的,所以substr只会生成子串到分隔符的前一个位置,那么汉语则是pos + 1到最后,因为pos是分隔符对应的下标位置,我们要的汉语被放在了分隔符的下一个位置,所以这里的汉语生成的起始位置应该是pos + 1到最后
  9. substr还有一个特性,如果只传入一个参数,那么会自动从pot + 1位置处开始一直到当前字符串的最后都生成一个子串,正好符合我们生成汉语子串的需求,所以最后分隔成功,我们返回true即可
  10. 对于Split的参数设计我们将part1和part2通过传入指针将其设置成输出型参数,那么从Split中切分生成的子串,在外部就可以直接拿到结果
  11. 那么下面我们看一下Translation这个翻译函数,由于此时英语单词以及对应的汉语都已经被放到了哈希表dict中了,所以这里我们直接使用unordered_map中的find功能进行查找即可,find的返回结果是一个迭代器iter,如果find没有查找到就会返回npos,所以我们进行判断如果没有查找到那么返回"unkonw"表示未知单词即可,否则则是查找到了,此时我们返回迭代器iter的second对应的汉语即可
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include <unistd.h>
#include "Log.hpp"

const std::string dictname = "./dict.txt";
const std::string sep = ":";

class Init
{
public:
    Init()
    {
        std::ifstream in(dictname);
        if(!in.is_open())
        {
            lg(Fatal, "ifstream open %s error", dictname.c_str());
            exit(1);
        }

        std::string line;
        while(std::getline(in, line))
        {
            std::string part1, part2;
            if(Split(line, &part1, &part2))
                dict.insert({ part1, part2 });
        }

        in.close();
    }

    bool Split(const std::string line, std::string* part1, std::string* part2)
    {
        size_t pos = line.find(sep);
        if(pos == std::string::npos)
            return false;

        *part1 = line.substr(0, pos);
        *part2 = line.substr(pos + 1);

        return true;
    }    

    std::string Translation(const std::string& key)
    {
        auto iter = dict.find(key);
        if(iter == dict.end())
            return "unknow";

        return iter->second;
    }

private:
    std::unordered_map<std::string, std::string> dict;
};
  1. 那么下面我们仅需要在服务端的线程池执行的线程中执行的任务,即在Tash.hpp中将对于字符串的处理转化为调用Init类对象的Translation函数从返回值中拿到汉语即可
  2. 那么这也就意味着我们在Tash.hpp中需要包含有一个Init的类对象init,那么我们包含Init.hpp头文件,然后将这个Init实例化的对象init定义成全局的即可,所以在operator()中就可以包含进行调用Init类对象的Translation函数从返回值中拿到汉语了
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include "Log.hpp"
#include "Init.hpp"


extern Log lg;
Init init;


class Task
{
public:
    Task()
    {}

    Task(int sockfd, const std::string& clientip, const uint16_t& clientport)
        : sockfd_(sockfd)
        , clientip_(clientip)
        , clientport_(clientport)
    {}

    void operator()()
    {
        char inbuffer[4096];
        ssize_t n = read(sockfd_, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            inbuffer[n] = 0;
            std::cout << "client say# " << inbuffer << std::endl;
            
            std::string echo_string = init.Translation(inbuffer);

            // std::string echo_string = "tcpserver echo# ";
            // echo_string += inbuffer;

            n = write(sockfd_, echo_string.c_str(), echo_string.size());                
            if(n < 0)
                lg(Info, "write err, errno: %d, errstr: %s", errno, strerror(errno));
        }
        else if(n == 0)
        {
            lg(Info, "%s:%d quit, server colse sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
        }
        else
        {
            //异常
            lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", 
                sockfd_, clientip_.c_str(), clientport_);
        }

        close(sockfd_);
    }

    ~Task()
    {}

private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
};
测试
  1. 那么接下来我们编译一下代码,然后运行即可,先运行服务器
    在这里插入图片描述

  2. 然后运行客户端
    在这里插入图片描述

  3. 接下来就可以在客户端输入英语单词,然后服务器中线程池的线程拿到英语单词之后,就会进行查找拿到汉语并返回,所以客户端就可以接收到汉语并打印了,无误
    在这里插入图片描述

二、源代码

TcpServer.hpp

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"

const int defaultfd = -1;
const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";
const int backlog = 10;
extern Log lg;

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError
};

// class TcpServer;
//
// class ThreadData
// {
// public:
//     ThreadData(int fd, const std::string& ip, const uint16_t& p, TcpServer* t)
//         : sockfd(fd)
//         , clientip(ip)
//         , clientport(p)
//         , tsvr(t)
//     {}

// public:
//     int sockfd;
//     std::string clientip;
//     uint16_t clientport;
//     TcpServer* tsvr;
// };


class TcpServer
{
public:
    TcpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
        : listensock_(defaultfd), port_(port), ip_(ip)
    {}

    void InitServer()
    {
        listensock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock_ < 0)
        {
            lg(Fatal, "create socket error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg(Info, "create socket success, listensock_: %d", listensock_);

        //防止偶发性服务器无法立即重启
        int opt = 1;
        setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));

        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(port_);
        inet_aton(ip_.c_str(), &(server.sin_addr));
        socklen_t len = sizeof(server);

        if (bind(listensock_, (struct sockaddr *)&server, len) < 0)
        {
            lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }
        lg(Info, "bind socket success, listensock_: %d", listensock_);

        if (listen(listensock_, backlog) < 0)
        {
            lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }
        lg(Info, "listen socket success, listensock_: %d", listensock_);
    }

    // static void* Routine(void* args)
    // {
    //     pthread_detach(pthread_self());

    //     ThreadData* td = static_cast<ThreadData*>(args);
    //     td->tsvr->Service(td->sockfd, td->clientip, td->clientport);

    //     close(td->sockfd);
    //     delete td;

    //     return nullptr;
    // }

    void StartServer()
    {
        ThreadPool<Task>::GetInstance()->Start();
        lg(Info, "tcpserver is running...");
        
        for (;;)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            
            int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
            if (sockfd < 0)
            {
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));

            lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", 
                sockfd, clientip, clientport);
            //version 1 单进程/单线程版
            // Service(sockfd, clientip, clientport);
            // close(sockfd);

            //version 2 多进程版
            // pid_t id = fork();
            // if(id == 0)
            // {
            //     //child
            //     close(listensock_);
            //     if(fork() > 0)
            //         exit(0);
            //     //grandchild
            //     Service(sockfd, clientip, clientport);
            //     close(sockfd);

            //     exit(0);
            // }
            // close(sockfd);
            // pid_t rid = waitpid(id, nullptr, 0);
            // if(rid == id)
            //     std::cout << "father wait success" << std::endl;

            //version 3 多线程版
            // ThreadData* td = new ThreadData(sockfd, clientip, clientport, this);

            // pthread_t tid;
            // pthread_create(&tid, nullptr, Routine, td);

            //version 4 线程池版
            Task t(sockfd, clientip, clientport);
            ThreadPool<Task>::GetInstance()->Push(t);
        }
    }

    // void Service(int sockfd, const std::string& clientip, const uint16_t& clientport)
    // {
    //     while (true)
    //     {
    //         char inbuffer[4096];
    //         ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
    //         if (n > 0)
    //         {
    //             inbuffer[n] = 0;
    //             std::cout << "client say# " << inbuffer << std::endl;

    //             std::string echo_string = "tcpserver echo# ";
    //             echo_string += inbuffer;

    //             write(sockfd, echo_string.c_str(), echo_string.size());                
    //         }
    //         else if(n == 0)
    //         {
    //             lg(Info, "%s:%d quit, server colse sockfd: %d", clientip.c_str(), clientport, sockfd);
    //             break;
    //         }
    //         else  
    //         {
    //             //异常
    //             lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", 
    //                 sockfd, clientip.c_str(), clientport);
    //             break;
    //         }
    //     }
    // }

    ~TcpServer()
    {
        if (listensock_ > 0)
            close(listensock_);
    }

private:
    int listensock_;
    uint16_t port_;
    std::string ip_;
};

Main.cc

#include <iostream>
#include <memory>
#include "TcpServer.hpp"


void Usage(const std::string str)
{
    std::cout << "\n\tUsage: " << str << " port[1024+]\n" << std::endl; 
}


int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(UsageError);
    }

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<TcpServer> server(new TcpServer(port));
    server->InitServer();
    server->StartServer();


    return 0;
}

TcpClient.cc

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void Usage(const std::string& str)
{
    std::cout << "\n\tUsage: " << str << " serverip serverport" << std::endl; 
}

// ./tcpclient serverip serverport
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 0;
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
    socklen_t len = sizeof(server);

    while(true)
    {
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(sockfd < 0)
        {
            std::cerr << "socket create err" << std::endl;
            return 1;
        }

        bool isreconnect = false;
        int cnt = 5;
        
        do
        {
            int n = connect(sockfd, (struct sockaddr*)&server, len);
            if(n < 0)
            {
                //此时连接失败,那么进行断线自动重连
                isreconnect = true;
                cnt--;
                sleep(2);
                std::cerr << "connect error..., reconnect: " << cnt << std::endl;
            }
            else  
                break;
        }while(cnt && isreconnect);

        if(cnt == 0)
        {
            std::cerr << "user offline..." << std::endl;
            return 2;
        }

        //客户端发起connect请求的时候,操作系统会自动进行端口号的随机bind绑定
        std::string message;
        char inbuffer[4096];

        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        int n = write(sockfd, message.c_str(), message.size());
        if(n < 0)
            std::cerr << "write err" << std::endl;

        n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }

        close(sockfd);
    }

    // std::string message;
    // char inbuffer[4096];
    //
    // while(true)
    // {
    //     std::cout << "Please Enter# ";
    //     std::getline(std::cin, message);

    //     int n = write(sockfd, message.c_str(), message.size());
    //     if(n < 0)
    //     {
    //         std::cerr << "write err" << std::endl;
    //         break;
    //     }

    //     n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
    //     if(n > 0)
    //     {
    //         inbuffer[n] = 0;
    //         std::cout << inbuffer << std::endl;
    //     }
    //     else
    //     {
    //         break;
    //     }
    // }
    //
    // close(sockfd);


    return 0;
}

Tash.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include "Log.hpp"
#include "Init.hpp"


extern Log lg;
Init init;


class Task
{
public:
    Task()
    {}

    Task(int sockfd, const std::string& clientip, const uint16_t& clientport)
        : sockfd_(sockfd)
        , clientip_(clientip)
        , clientport_(clientport)
    {}

    void operator()()
    {
        char inbuffer[4096];
        ssize_t n = read(sockfd_, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            inbuffer[n] = 0;
            std::cout << "client say# " << inbuffer << std::endl;
            
            std::string echo_string = init.Translation(inbuffer);

            // std::string echo_string = "tcpserver echo# ";
            // echo_string += inbuffer;

            n = write(sockfd_, echo_string.c_str(), echo_string.size());                
            if(n < 0)
                lg(Info, "write err, errno: %d, errstr: %s", errno, strerror(errno));
        }
        else if(n == 0)
        {
            lg(Info, "%s:%d quit, server colse sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
        }
        else
        {
            //异常
            lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", 
                sockfd_, clientip_.c_str(), clientport_);
        }

        close(sockfd_);
    }

    ~Task()
    {}

private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
};

Init.hpp

#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include <unistd.h>
#include "Log.hpp"

const std::string dictname = "./dict.txt";
const std::string sep = ":";

class Init
{
public:
    Init()
    {
        std::ifstream in(dictname);
        if(!in.is_open())
        {
            lg(Fatal, "ifstream open %s error", dictname.c_str());
            exit(1);
        }

        std::string line;
        while(std::getline(in, line))
        {
            std::string part1, part2;
            if(Split(line, &part1, &part2))
                dict.insert({ part1, part2 });
        }

        in.close();
    }

    bool Split(const std::string line, std::string* part1, std::string* part2)
    {
        size_t pos = line.find(sep);
        if(pos == std::string::npos)
            return false;

        *part1 = line.substr(0, pos);
        *part2 = line.substr(pos + 1);

        return true;
    }    

    std::string Translation(const std::string& key)
    {
        auto iter = dict.find(key);
        if(iter == dict.end())
            return "unknow";

        return iter->second;
    }

private:
    std::unordered_map<std::string, std::string> dict;
};

ThreadPool.hpp

#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <string>
#include <pthread.h>


static int defaultnum = 5;


struct ThreadInfo
{
    pthread_t tid;
    std::string threadname;
};

template<class T>
class ThreadPool
{
private:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }

    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }

    void WakeUp()
    {
        pthread_cond_signal(&cond_);
    }

    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }

    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }

    std::string GetThreadName(pthread_t tid)
    {
        for(auto& ti : threads_)
        {
            if(ti.tid == tid)
                return ti.threadname;
        }

        return "None";
    }

    T Pop()
    {
        T out = tasks_.front();
        tasks_.pop();

        return out;
    }

    static void* HandlerTask(void* args)
    {
        ThreadPool* tp = static_cast<ThreadPool*>(args);
        std::string name = tp->GetThreadName(pthread_self());

        while(true)
        {
            tp->Lock();

            while(tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }

            T t = tp->Pop();

            tp->Unlock();

            t();
            // std::cout << name << " run" << ", result: " << t.GetResult() << std::endl;
        }


        return nullptr;
    }

public:
    void Start()
    {
        int n = threads_.size();
        for(int i = 0; i < n; i++)
        {
            threads_[i].threadname = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, (void*)this);
        }
    }

    void Push(const T& in)
    {
        Lock();

        tasks_.push(in);
        WakeUp();

        Unlock();
    }

    static ThreadPool<T>* GetInstance()
    {
        if(tp_ == nullptr)
        {
            pthread_mutex_lock(&lock_);
            if(tp_ == nullptr)
            {
                std::cout << "log: singleton create done first" << std::endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
        }

        return tp_;
    }

private:
    ThreadPool(int num = defaultnum):threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }

    ThreadPool(const ThreadPool<T>& ) = delete;
    const ThreadPool<T>& operator=(const ThreadPool<T>& ) = delete;

    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }

private:
    std::queue<T> tasks_;
    std::vector<ThreadInfo> threads_;

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;
    
    static ThreadPool<T>* tp_;
    static pthread_mutex_t lock_;
};

template<class T>
ThreadPool<T>* ThreadPool<T>::tp_ = nullptr;

template<class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

Log.hpp

#pragma once

#include <iostream>
#include <string>
#include <ctime>
#include <cstdio>
#include <cstdarg>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define SIZE 1024

#define Info   0
#define Debug  1
#define Warning 2
#define Error  3
#define Fatal  4

#define Screen 1     //输出到屏幕上
#define Onefile 2    //输出到一个文件中
#define Classfile 3  //根据事件等级输出到不同的文件中

#define LogFile "log.txt" //日志名称


class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }

    void Enable(int method) //改变日志打印方式
    {
        printMethod = method;
    }

    ~Log()
    {}

    std::string levelToString(int level)
    {
        switch(level)
        {
            case Info:
                return "Info";
            case Debug:
                return "Debug";
            case Warning:
                return "Warning";
            case Error:
                return "Error";
            case Fatal:
                return "Fata";
            default:
                return "";
        }
    }

    void operator()(int level, const char* format, ...)
    {
        //默认部分 = 日志等级 + 日志时间
        time_t t = time(nullptr);
        struct tm* ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), 
        ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, 
        ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        char logtxt[2 * SIZE];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        printLog(level, logtxt);
    }

    void printLog(int level, const std::string& logtxt)
    {
        switch(printMethod)
        {
            case Screen:
                std::cout << logtxt << std::endl;
                break;
            case Onefile:
                printOneFile(LogFile, logtxt);
                break;
            case Classfile:
                printClassFile(level, logtxt);
                break;
            default:
                break;
        }
    }

    void printOneFile(const std::string& logname, const std::string& logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if(fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }

    void printClassFile(int level, const std::string& logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level);

        printOneFile(filename, logtxt);
    }


private:
    int printMethod;
    std::string path;
};

Log lg;

makefile

all:tcpserver tcpclient

tcpserver:Main.cc
	g++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.cc
	g++ -o $@ $^ -std=c++11

.PHONT:clean
clean:
	rm -f tcpserver tcpclient

dict.txt

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
egg: 鸡蛋
fish: 鱼
girl: 女孩
boy: 男孩
book: 书
pen: 钢笔
pencil: 铅笔
bag: 书包
school: 学校
teacher: 老师
student: 学生
friend: 朋友
mother: 妈妈
father: 爸爸
sister: 姐妹
brother: 兄弟
grandma: 奶奶;外婆
grandpa: 爷爷;外公
family: 家庭
home: 家
room: 房间
bed: 床
table: 桌子
chair: 椅子
door: 门
window: 窗户
blackboard: 黑板
white: 白色
black: 黑色
red: 红色
blue: 蓝色
green: 绿色
yellow: 黄色
pink: 粉色
orange: 橙色;橙子
purple: 紫色
one: 一
two: 二
three: 三
four: 四
five: 五
six: 六
seven: 七
eight: 八
nine: 九
ten: 十
eleven: 十一
twelve: 十二
thirteen: 十三
fourteen: 十四
fifteen: 十五
sixteen: 十六
seventeen: 十七
eighteen: 十八
nineteen: 十九
twenty: 二十
thirty: 三十
forty: 四十
fifty: 五十
sixty: 六十
seventy: 七十
eighty: 八十
ninety: 九十
one hundred: 一百
big: 大的
small: 小的
long: 长的
short: 短的;矮的
tall: 高的
fat: 胖的
thin: 瘦的
happy: 开心的
sad: 难过的
good: 好的
bad: 坏的
new: 新的
old: 旧的;老的
hot: 热的
cold: 冷的
warm: 温暖的
cool: 凉爽的
nice: 美好的
good morning: 早上好
good afternoon: 下午好
good evening: 晚上好
good night: 晚安
hello: 你好
hi: 嗨
bye: 再见
goodbye: 再见
thank you: 谢谢你
sorry: 对不起
please: 请
yes: 是
no: 不
ok: 好的
this: 这个
that: 那个
these: 这些
those: 那些
my: 我的
your: 你的;你们的
his: 他的
her: 她的
it: 它
is: 是
am: 是
are: 是
was: 是(过去式)
were: 是(过去式)
have: 有
has: 有(第三人称单数)
had: 有(过去式)
do: 做;助动词
does: 做;助动词(第三人称单数)
did: 做;助动词(过去式)
go: 去
come: 来
see: 看见
look: 看
watch: 观看
read: 读
write: 写
draw: 画
sing: 唱
dance: 跳舞
run: 跑
jump: 跳
walk: 走
eat: 吃
drink: 喝
sleep: 睡觉
play: 玩
toy: 玩具
ball: 球
car: 汽车
bus: 公交车
bike: 自行车
plane: 飞机
train: 火车
ship: 船
sun: 太阳
moon: 月亮
star: 星星
sky: 天空
cloud: 云
rain: 雨;下雨
snow: 雪;下雪
wind: 风
tree: 树
flower: 花
grass: 草
bird: 鸟
rabbit: 兔子
tiger: 老虎
lion: 狮子
panda: 熊猫
elephant: 大象
monkey: 猴子
pig: 猪
cow: 牛
sheep: 羊
horse: 马
day: 天;日
week: 周
month: 月
year: 年
Monday: 星期一
Tuesday: 星期二
Wednesday: 星期三
Thursday: 星期四
Friday: 星期五
Saturday: 星期六
Sunday: 星期日
January: 一月
February: 二月
March: 三月
April: 四月
May: 五月
June: 六月
July: 七月
August: 八月
September: 九月
October: 十月
November: 十一月
December: 十二月
breakfast: 早餐
lunch: 午餐
dinner: 晚餐
water: 水
milk: 牛奶
rice: 米饭
bread: 面包
meat: 肉
vegetable: 蔬菜
fruit: 水果

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐