C++进程(下)

四、多线程

4.1多线程基础

1> 多线程的引入:也是为了实现多任务并发执行的问题,能够实现多个阻塞任务同时执行

2> 多线程(LWP轻量级进程):线程是粒度更小的任务执行单元

3> 进程是资源分配的基本单位,而线程是任务器进行任务调度的最小单位

4> 一个进程可以拥有多个线程,同一个进程中的多个线程共享进程的资源

5> 由于线程是共用进程的资源,所以对于线程的切换而言,开销较小

6> 由于多个线程使用的是同一个进程的资源,那么,就会导致每个进程使用资源时,产生资源抢占问题,没有多进程安全

7> 每个进程至少有一个进程:主进程

8> 只要有一个线程中退出了进程,那么所有的线程也就结束了

9> 多个线程执行顺序:没有先后顺序,按时间片轮询,上下文切换,抢占CPU的方式进行

4.2多线程编程(重点)

由于C库没有提供有关线程的相关操作,对于多线程编程要依赖于第三方库,头文件:#include<pthread.h>。编译时:需要加上-lpthread选项,链接上对于的线程支持库

1、创建线程 pthread_create

cpp 复制代码
#include <pthread.h> //头文件
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *
(*start_routine) (void *), void *arg);
功能:创建一个分支线程
参数1:线程号,通过参数返回,用法:在外部定义一个该类型的变量,将地址传递入函数,调用结
束后,该变量中即是线程号
参数2:线程属性,一般填NULL,让系统使用默认属性创建一个线程
参数3:是一个回调函数,一个函数指针,需要向该参数中传递一个函数名,作为线程体执行函数
该函数由用户自己定义,参数是void*类型,返回值也是void *类型
参数4: 是参数3的参数,如果不想向线程体内传递数据,填NULL即可
返回值:成功返回0,失败返回一个错误码(非linux内核的错误码,是线程支持库中定义的一个错
误码)
Compile and link with -pthread. //编译时需要加上 -pthread选项

1>不向主线程传递数据

cpp 复制代码
#include<myhead.h>
//定义线程体函数
void *task(void *arg){
    while(1){
        printf("我是分支线程");
        sleep(1);
    }
}

int main(int argc, const char *argv[]){
    pthread_t tid = -1;//用于存储线程号的变量
    if(pthread_create(&tid, NULL, task, NULL) != 0){
        //参数2:表示让系统使用默认属性创建一个线程
        //参数3:线程体函数名
        //参数4:表示向线程体重传递的数据
        printf("tid create error\n");
        return -1;
    }
    printf("pthread_create success\n");
    while(1){
        printf("我是主线程\n");
        sleep(1);
    }
    while(1);//防止主线程结束


    return 0;
}

2>向主线程传递单个数据

cpp 复制代码
#include<myhead.h>
//定义线程体函数
void *task(void *arg){
    //arg --> &num 但是arg是一个void*类型的变量,需要转换为具体指针进行操作
    //(int*)arg -->将其转换为整型的指针
    //*(int*)arg -->num的值
    int key = *(int *)arg;
    printf("我是分支进程:num = %d\n", key);
}
int main(int argc, const char *argv[]){
    pthread_t tid = -1;//用于存储线程号的变量
    int num = 520;
    if(pthread_create(&tid, NULL, task, &num) != 0){
        //参数2:表示让系统使用默认属性创建一个线程
        //参数3:线程体函数名
        //参数4:表示向线程体重传递的数据
        printf("tid create error\n");
        return -1;
    }
    printf("pthread_create success, tid = %#lx\n", tid);
    printf("我是主线程\n");
    num = 1314;//主线程中更换数据
    printf("主线程中num = %d\n", num);
    while(1);//防止主线程结束


    return 0;
}

3> 向线程体中传入多个数据

cpp 复制代码
#include<myhead.h>
//定义信息结构体,用于向线程体传递数据
struct Info{
    int num;
    char name[20];
    double score;
};

//定义线程体函数
void *task(void *arg){
    Info buf = *((Info*)arg);//将结构体指针转换为结构体变量
    printf("分支线程中:num = %d, name = %s, score = %.2lf\n", buf.num, buf.name, buf.score);
}
int main(int argc, const char *argv[]){
    pthread_t tid = -1;//用于存储线程号的变量
    int num = 520;
    char name[20] = "zhangsan";
    double score = 99.5;
    //需求:将上面的三个数据全部传入线程体重
    Info buf = {num, "zhangsan", score};
    if(pthread_create(&tid, NULL, task, &num) != 0){
        //参数2:表示让系统使用默认属性创建一个线程
        //参数3:线程体函数名
        //参数4:表示向线程体重传递的数据
        printf("tid create error\n");
        return -1;
    }
    printf("pthread_create success, tid = %#lx\n", tid);
    printf("我是主线程\n");
    while(1);//防止主线程结束


    return 0;
}

2、获取线程号 pthread_self

cpp 复制代码
#include <pthread.h>
pthread_t pthread_self(void);
功能:获取当前线程的线程号
参数:无
返回值:返回调用线程的id号,不会失败

3、销毁线程 pthread_exit\

cpp 复制代码
#include <pthread.h>
void pthread_exit(void *retval);
功能:退出当前线程
参数:表示退出时的状态,一般填NULL
返回值:无

4、线程资源回收 phread_join

cpp 复制代码
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能:阻塞回收指定线程的资源
参数1:要回收的线程线程号
参数2:线程退出时的状态,一般填NULL
返回值:成功返回0,失败返回一个错误码

5、线程分离 phread_detach

cpp 复制代码
#include <pthread.h>
int pthread_detach(pthread_t thread);
功能:将指定线程设置成分离态,被设置成分离态的线程,退出后,资源由系统自动回收
参数:要分离的线程号
返回值:成功返回0,失败返回一个错误码
cpp 复制代码
#include<myhead.h>
//定义线程体函数
void *task(void *arg){
    printf("分支线程,tid = %#x\n", pthread_self());//2.调用函数,输出当前线程的线程号
    sleep(3);
    //3退出线程
    pthread_exit(NULL);
}

int main(){
    //1.定义一个线程号变量
    pthread_t tid = -1;
    //创建一个线程
    if(pthread_create(&tid, NULL, task, NULL) != 0){
        printf("pthread_create error\n");
        return -1;
    }
    printf("主线程,tid = %#x\n", tid);

    //4.回收分支线程的资源
    //pthread_join(tid, NULL);//前3秒,主线程处于休眠等待分支线程的结束
                            //分支线程处于休眠状态
    //将线程设置程分离态(非阻塞)
    pthread_detach(tid);
    sleep(5);
    //while(1);//防止主线程结束
    std::cout << "Hello, world" <<  std::endl;


    return 0;
}

练习:使用多线程完成两个文件的拷贝,线程1拷贝前一半内容,线程2拷贝后一半内容,主线程用于回 收两个分支线程的资源

cpp 复制代码
#include<myhead.h>
struct Info
{
    const char *srcfile;//要拷贝的源文件
    const char *destfile;//目标文件
    int start;//起始位置
    int len; //要拷贝的长度
};
//定义获取文件长度的函数
int get_file_len(const char *srcfile, const char *destfile){
    //定义两个文件描述符,分别作为源文件和目标文件的句柄
    int sfd, dfd;
    //以只读的形式打开目标文件
    if((sfd = open(srcfile, O_RDONLY)) == -1){
        perror("open srcfile error");
        return -1;
    }

    //以只写的形式打开目标文件
    if((dfd = open(destfile, O_RDWR|O_CREAT|O_TRUNC, 0644)) == -1){
        perror("open destfile error");
        return -1;
    }
    //获取源文件的长度
    int len = lseek(sfd, 0, SEEK_END);
    //关闭文件
    close(sfd);
    close(dfd);
    return len;
}
//定义线程体函数
void *task(void *arg){
    //将传入的数据解析处理
    const char*srcfile = ((struct Info*)arg)->srcfile;
    const char*destfile = ((struct Info*)arg)->destfile;
    int start = ((struct Info*)arg) -> start;
    int len = ((struct Info*)arg) -> len;
    //准备拷贝工作
    //定义两个文件描述符,分别作为源文件和目标文件的句柄
    int sfd, dfd;
    //以只读的形式打开源文件
    if((sfd = open(srcfile, O_RDONLY)) == -1){
        perror("open srcfile error");
        return NULL;
    }
    //以只写的形式打开目标文件
    if((dfd = open(destfile, O_RDWR)) == -1){
        perror("open destfile error");
        return NULL;
    }
    //偏移指针
    lseek(sfd, start, SEEK_SET);
    lseek(sfd, start, SEEK_SET);

    //拷贝工作
    int ret = 0;//记录每次读取的数据
    int count = 0;//记录拷贝的总个数
    char buf[128] = "";//数据搬运工
    while(1){
        ret = read(sfd, buf, sizeof(buf));
        //读取的数据放入count中
        count += ret;
        if(count >= len){
            //说明该部分的内容拷贝结束,还剩最后一次
            write(dfd, buf, ret - (count - len));
            break;
        }
        //其余的正常拷贝
        write(dfd, buf, ret);
    }
    //关闭文件描述符
    close(dfd);
    close(sfd);
}
int main(int argc, const char *argv[]){
    //判断传入的文件个数是否正确
    if(argc != 3){
        printf("input file error\n");
        printf("usage:./a.out srcfile destfile\n");
        return -1;
    }
    //获取源文件的长度,顺便将目标文件创建出来
    int len = get_file_len(argc[1], argv[2]);

    //创建两个线程
    pthread_t tid1, tid2;

    //定义向线程体函数传参的变量
    struct Info buf[2] = {{argv[1], argv[2], 0, len/2}, {argv[1], argv[2], len / 2, len-len/2}};
    if(pthread_create(&tid1, NULL, task, &buf[0]) != 0){
        printf("线程创建失败\n");
        return -1;
    }
    //主线程中完成对两个分支线程资源的回收
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    printf("拷贝成功\n");
    std::cout << "Hello, world" << std::endl;
    return 0;
}

4.3线程同步互斥机制(难点)

1、线程同步互斥机制的引入

1> 由于同一个进程的多个线程会共享进程的资源,这些被共享的资源称为临界资源

2> 多个线程对公共资源的抢占问题,访问临界资源的代码段称为临界区

3> 多个线程抢占进程资源的现象称为竞态

4> 为了解决竞态,我们引入了同步互斥机制

2、线程互斥机制---互斥锁

1> 互斥锁的本质也是一个特殊的临界资源,当该临界资源被某个线程所拥有后,其他线程就不能拥有该资源,直到,同有该资源的线程释放掉互斥锁后,其他吸线程才能进行抢占(同一时刻,一个互斥锁只能被一个线程拥有)

2> 互斥锁的相关API函数接口

cpp 复制代码
1、创建一个互斥锁:只需定义一个pthread_mutex_t 类型的变量即创建了一个互斥锁
pthread_mutex_t mutex;
2、初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //静态初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const
pthread_mutexattr_t *restrict attr);
功能:初始化互斥锁变量
参数1:互斥锁变量的地址,属于地址传递
参数2:互斥锁属性,一般填NULL,让系统自动设置互斥锁属性
返回值:成功返回0,失败返回错误码
3、获取锁资源
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:获取锁资源,如果要获取的互斥锁已经被其他线程锁定,那么该函数会阻塞,直到能够获取锁资源
参数:互斥锁地址,属于地址传递
返回值:成功返回0,失败返回一个错误码
4、释放锁资源
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:释放对互斥锁资源的拥有权
参数:互斥锁变量的地址
返回值:成功返回0,失败返回一个错误码
5、销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁互斥锁
参数:互斥锁变量的地址
返回值:成功返回0,失败返回一个错误码
cpp 复制代码
#include<myhead.h>
//11、创建一个互斥锁
pthread_mutex_t mutex;

//定义一个全局变量
int num = 520;//临界资源

//定义分支线程1
void *task1(void *arg){
    while(1){
        sleep(1);
        //33.获取锁资源
        pthread_mutex_lock(&mutex);
        num -= 10;//线程1将临界资源减少10
        printf("张三获取了10,剩余%d\n", num);
        //44、释放锁资源
        pthread_mutex_unlock(%mutex);
    }
}
//定义分支线程2
void *task2(void *arg){
    while(1){
        sleep(1);
        //33.获取锁资源
        pthread_mutex_lock(&mutex);
        num -= 20;//线程2将临界资源减少20
        printf("李四取了20, 剩余%d\n", num);
        //44.释放锁资源
        pthread_mutex_unlock(&mutex);
    }
}
int main(){
    //22.初始化互斥锁,参数NULL表示让系统自动分配互斥锁属性
    pthread_mutex_init(&mutex, NULL);
    //1.创建两个分支进程
    pthread_t tid1, tid2;
    if(pthread_create(&tid1, NULL, task1, NULL) != 0){
        printf("tid1 create error\n");
        return -1;
    }
    if(pthread_create(&tid2, NULL, task2, NULL) != 0){
        printf("tid2 create error\n");
        return -1;
    }
    printf("主线程:tid1 = %#X, tid2 = %#x\n", tid1, tid2);
    //阻塞等待线程结束
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    //55.释放锁资源
    pthread_mutex_destroy(&mutex);
    std::cout << "Hello, world" << std::endl;
    return 0;
}

3、线程同步机制---无名信号量

1> 线程同步:就是多个线程之间有先后顺序得执行,这样在访问临界资源时,就不会产生抢占的现象了

2> 同步机制常用于生产者消费者模型:消费者任务要想进行,必须先执行生产者线程,多个任务有顺序执行

3> 无名信号量:本质上也是一个特殊的临界资源,内部维护了一个value值,当某个进行想要执行之前,先申请该无名信号量的value资源,如果value值大于0,则申请源函数接触阻塞,继续执行后续操作。如果value值为0,则当前申请资源函数会处于阻塞状态,直到其他线程将该value值增加到大于0

4> 无名信号量的相关APP函数接口

cpp 复制代码
1、创建无名信号量:只需定义一个sem_t 类型的变量即可
sem_t sem;
2、初始化无名信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化无名信号量,最主要是初始化value值
参数1:无名信号量的地址
参数2:判断进程还是线程的同步
0:表示线程间同步
非0:表示进程间同步,需要创建在共享内存段中
参数3:无名信号量的初始值
返回值:成功返回0,失败返回-1并置位错误码
3、申请无名信号量的资源(P操作)
#include <semaphore.h>
int sem_wait(sem_t *sem);
功能:阻塞申请无名信号量中的资源,成功申请后,会将无名信号量的value进行减1操作,如果当
前无名信号量的value为0,则阻塞
参数:无名信号量的地址
返回值:成功返回0,失败返回-1并置位错误码
4、释放无名信号量的资源(V操作)
#include <semaphore.h>
int sem_post(sem_t *sem);
功能:将无名信号量的value值增加1操作
参数:无名信号量地址
返回值:成功返回0,失败返回-1并置位错误码
5、销毁无名信号量
#include <semaphore.h>
int sem_destroy(sem_t *sem);
功能:销毁无名信号量
参数:无名信号量地址
返回值:成功返回0,失败返回-1并置位错误码
cpp 复制代码
#include<myhead.h>
//11.创建无名信号量
sem_t sem;

//创建生产者线程
void *task1(void *arg){
    int num = 5;
    while(num--){
        sleep(1);
        printf("我生产了一辆宾利");
        //44.释放无名信号量资源
        sem_post(&sem);
    }
    //退出线程
    pthread_exit(NULL);
}
//创建消费者进程 
void *task2(void *arg){
    int num = 5;
    while(num--){
        //33.申请无名信号量的资源
        sem_wait(&sem);
        printf("我消费了一辆宾利, 很开心");
    }
    //退出线程
    pthread_exit(NULL);
}
int main(){
    //22.初始化无名信号量,第一个0表示用于线程间通信,第二个0表示初始值为0
    sem_init(&sem, 0, 0);
    //1.创建两个分支进程
    pthread_t tid1, tid2;
    if(pthread_create(&tid1, NULL, task2, NULL) != 0){
        printf("tid1 create error\n");
        return -1;
    }
    if(pthread_create(&tid2, NULL, task2, NULL) != 0){
        printf("tid2 create error\n");
        return -1;
    }
    printf("主线程:tid1 = %#x, tid2 = %#X\n", tid1, tid2);
    //2.阻塞等待进程结束
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    //55、销毁无名信号量
    sem_destroy(&sem);
    std::cout << "Hello,world" << std::endl;
    return 0;
}

练习:使用无名信号量完成,定义三个任务,任务1打印A,任务2打印B,任务3打印C,最终输出的结 果为ABCABCABCABC...

cpp 复制代码
#include<myhead.h>
//1、定义三个无名信号量
set_t sem1, sem2, sem3;
//定义三个任务,分别打印A、B、C
void *task1(void *arg){
    while(1){
        //申请sem1的资源
        sem_wait(&sem1);
        sleep(1);
        printf("A");
        fflush(stdout);//刷新缓冲区
        //释放sem2的资源
        sem_post(&sem2);
    }
}
void *task2(void *arg){
    while(1){
        //申请sem2的资源
        sem_wait(&sem2);
        sleep(1);
        printf("B");
        fflush(stdout);//刷新缓冲区
        //释放sem3的资源
        sem_post(&sem3);
    }
}
void *task3(void *arg){
    while(1){
        //申请sem3的资源
        sem_wait(&sem3);
        sleep(1);
        printf("C");
        fflush(stdout);//刷新缓冲区
        //释放sem1的资源
        sem_post(&sem1);
    }
}
int main(int argc, const char *argv[]){
    //初始化无名信号量
    sem_init(&sem1, 0, 1);
    sem_init(&sem2, 0, 0);
    sem_init(&sem3, 0, 0);

    //准备三个任务
    pthread_t tid1, tid2, tid3;

    //创建三个任务
    if(pthread_create(&tid1, NULL, task1, NULL) != 0){
        printf("tid1 create error\n");
        return -1;
    }
    if(pthread_create(&tid2, NULL, task2, NULL) != 0){
        printf("tid2 create error\n");
        return -1;
    }
    if(pthread_create(&tid3, NULL, task3, NULL) != 0){
        printf("tid3 create error\n");
        return -1;
    }
    //阻塞等待分支线程的结束
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);
    std:cout << "Hello, world" << std::endl;
    return 0;
}

4、线程同步机制---条件变量

1> 条件变量本质上也是一个临界资源,他维护了一个队列,当消费者进程想要执行时,先进入队列中等待生产者的唤醒。执行完生产者,再由生产者唤醒在队列中的消费者,这样就完成了生产者和消费者之间的同步关系。

2> 但是,多个消费者在进入休眠队列的过程是互斥的,所以,在消费者准备进入休眠队列时,需要使用互斥锁来进行互斥操作

3> 条件变量的API函数接口

cpp 复制代码
1、创建一个条件变量,只需定义一个pthread_cond_t类型的全局变量即可
pthread_cond_t cond;
2、初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //静态初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t
*restrict attr);
功能:初始化条件变量
参数1:条件变量的起始地址
参数2:条件变量的属性,一般填NULL
返回值:成功返回0,失败返回一个错误码
3、消费者线程进入等待队列
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t
*restrict mutex);
功能:将线程放入休眠等待队列,等待其他线程的唤醒
参数1:条件变量的地址
参数2:互斥锁,由于多个消费者线程进入等待队列时会产生竞态,为了解决竞态,需要使用一个互斥锁
返回值:成功返回0,失败返回错误码
4、生产者线程唤醒休眠队列中的任务
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒条件变量维护的队列中的所有消费者线程
参数:条件变量的地址
返回值:成功返回0,失败返回错误码
int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒条件变量维护的队列中的第一个进入队列的消费者线程
参数:条件变量的地址
返回值:成功返回0,失败返回错误码
5、销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁一个条件变量
参数:条件变量的地址
返回值:成功返回0,失败返回错误码
cpp 复制代码
#include<myhead.h>
//11、定义一个条件变量
pthread_cond_t cond;
//111、定义一个互斥锁
pthread_mutex_t mutex;

//创建生产者线程
void *task1(void *arg){
    sleep(3);
    printf("我生产了3辆特斯拉\n");
    //44.唤醒所有消费者进程
    pthread_cond_broadcast(&cond);
    //退出进程
    pthread_exit(NULL);
}
//创建消费者线程
void *task2(void *arg){
    //333、获取锁资源
    pthread_mutex_lock(&mutex);

    //33、进入休眠队列,等待生产者的唤醒
    pthread_cond_wait(&cond, &mutex);

    printf("%#x:消费了一辆特斯拉,很开心\n", pthread_self());

    //444、释放锁资源
    pthread_mutex_unlock(&mutex);
    //退出线程
    pthread_exit(NULL);
}
int main(){
    //22、初始化条件变量
    pthread_cond_init(&cond, NULL);
    //222、初始化互斥锁
    pthread_mutex_init(&mutex, NULL);
    //1、创建两个分支线程
    pthread_t tid1, tid2, tid3, tid4;
    if(pthread_create(&tid1, NULL, task1, NULL) != 0){
        printf("tid1 create error\n");
        return -1;
    }
    if(pthread_create(&tid2, NULL, task2, NULL) != 0){
        printf("tid2 create error\n");
        return -1;
    }
    if(pthread_create(&tid3, NULL, task2, NULL) != 0){
        printf("tid2 create error\n");
        return -1;
    }
    if(pthread_create(&tid4, NULL, task2, NULL) != 0){
        printf("tid2 create error\n");
        return -1;
    }
    printf("主线程:tid1 = %#x, tid2 = %#x, tid3 = %#x, tid4 = %#x\n", tid1, tid2, tid3, tid4);

    //2、阻塞等待线程结束
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);
    pthread_join(tid4, NULL);

    //55、销毁条件变量
    pthread_cond_destroy(&cond);
    //555、销毁互斥锁
    pthread_mutex_detroy(&mutex);
    std::cout << "Hello, world" << std::endl;
    return 0;
}

4.4、C++11中的多线程

1> C++11之后支持线程支持库

2> C++也支持线程创建、互斥锁、条件变量

3> 线程支持库需要引入头文件#include

4.1线程相关操作

1> 线程的创建:C++线程支持库,本质是面向对象的操作

2> 创建线程:可以使用构造函数完成

线程体函数种类:

1、可以是任意类型的函数,不必要是void*类型参数也是void*类型

2、可以是全局函数,也可以是类中成员函数当作线程体

3、可以是仿函数当作线程体函数

4、也可以是Lambda表达式当作线程体函数

cpp 复制代码
#include <myhead.h>
#include <thread>      // 线程支持库头文件
#include <iostream>
#include <string>
using namespace std;
/***************** 第一个测试线程体 *******************/
// 无参无返回值
void ThreadFun_1()
{
    cout << "ThreadFun_1 tid = " << this_thread::get_id() << endl;
    cout << "ThreadFun_1 test" << endl;
}
/***************** 第二个线程体测试 **********************/
// 有参无返回值函数
void ThreadFun_2(int num, const string& str)   // 使用 const 引用避免拷贝
{
    cout << "ThreadFun_2 tid = " << this_thread::get_id() << endl;
    cout << "num = " << num << " str = " << str << endl;
}
/******************* 第三个线程体测试 *********************/
class ThreadClass
{
public:
    string name;
    int age;

    // 类中成员函数作为线程体函数
    void ThreadClassFun()
    {
        cout << "ThreadFun_3 tid = " << this_thread::get_id() << endl;
        cout << "name = " << this->name << " age = " << age << endl;
    }
};
/********************** 主程序 **********************/
int main(int argc, const char *argv[])
{
    // 主程序就是主线程

    // 创建第一个分支线程,使用无参函数完成线程体
    thread th1(ThreadFun_1);

    // 创建第二个分支线程,并向线程体中传递数据
    string name = "zpp";
    thread th2(ThreadFun_2, 520, name);   // 可以直接向线程体传递参数

    // 创建第三个分支线程,传递类的成员函数
    ThreadClass test;
    test.name = "zhangsan";
    test.age = 18;
    thread th3(&ThreadClass::ThreadClassFun, &test);

    // 创建第四个分支线程,lambda 表达式当作线程体
    thread th4([](int key) {
        cout << "ThreadFun_4 tid = " << this_thread::get_id() << endl;
        cout << "key = " << key << endl;
    }, 999);

    // 阻塞回收分支线程
    th1.join();
    th2.join();
    th3.join();
    th4.join();

    cout << "Hello, World!" << endl;

    return 0;
}

3> 线程号获取:this_thread::get_id()

4> 有关线程的回收

1、阻塞方式回收线程:th1.join();

2、非阻塞形式回收线程:th.detach();

cpp 复制代码
#include <myhead.h>
#include <thread>   // 线程支持库头文件

/***************** 第一个测试线程体 *******************/
// 无参无返回值
void ThreadFun_1()
{
    for (int i = 0; i < 10; i++)
    {
        cout << "ThreadFun_1 tid = " << this_thread::get_id() << endl;
        cout << "ThreadFun_1 test" << endl;
        // 延时函数
        this_thread::sleep_for(1s);   // 等待1秒
    }
}

/********************** 主程序 **********************/
int main(int argc, const char *argv[])
{
    // 主程序就是主线程

    // 创建第一个分支线程,使用无参函数完成线程体
    thread th1(ThreadFun_1);   // 此时就创建了一个分支线程,线程体函数为无参无返回值函数

    // 阻塞回收分支线程
    // th1.join();

    // 将线程设置成分离态:主线程可以继续做自己其他事情
    th1.detach();

    std::cout << "Hello, World!" << std::endl;

    return 0;
}

4.2互斥锁的使用

1> 互斥锁本质上是完成将多个线程使用临界资源时,防止竞态

2> 需要引入头文件#include<metux>

3> 常用函数

cpp 复制代码
1、构造函数:创建一个互斥锁对象
2、lock():上锁
3、unlock()释放锁资源
cpp 复制代码
#include <myhead.h>
#include <thread>   // 线程支持库头文件
#include <mutex>    // 互斥锁头文件

mutex mux;          // 实例化一个互斥锁

/***************** 第一个测试线程体 *******************/
// 无参无返回值
void ThreadFun_1()
{
    mux.lock();     // 获取锁资源

    cout << "======================================" << endl;
    cout << "tid = " << this_thread::get_id() << endl;
    this_thread::sleep_for(1s);
    cout << "**************************************" << endl;

    mux.unlock();   // 释放锁资源
}

/********************** 主程序 **********************/
int main(int argc, const char *argv[])
{
    // 创建 10 个分支线程
    for (int i = 0; i < 10; i++)
    {
        thread th(ThreadFun_1);
        th.detach();    // 线程分离
    }

    // 等待子线程执行(因为 detach 后主线程无法 join)
    this_thread::sleep_for(20s);

    std::cout << "Hello, World!" << std::endl;

    // 再等一会儿,让输出完整显示
    this_thread::sleep_for(20s);

    return 0;
}

4.3条件变量

1> 实现一个生产者对应多个消费者问题

2> 需要引入头文件:#include<condition_variable>

3> 常用函数

cpp 复制代码
1、构造函数:创建并初始化一个条件变量
2、wait():将消费者线程放入等待队列中
3、唤醒线程:
cv.notify_one(); 唤醒一个线程
cv.notify_all(); 唤醒所有线程
cpp 复制代码
#include <myhead.h>
#include <condition_variable>   // 条件变量头文件
#include <thread>               // 线程支持库头文件
#include <mutex>                // 互斥锁

// 定义一个条件变量
condition_variable cv;
mutex mux;                      // 用于防止竞态的互斥锁

/***************** 生产者线程 *******************/
void ThreadWrite()
{
    for (int i = 0; i < 5; i++)
    {
        this_thread::sleep_for(2s);     // 每隔两秒生产一辆特斯拉
        cout << "我生产了一辆特斯拉" << endl;

        // 通知一个线程可以消费了
        cv.notify_one();
        // 通知所有线程
        // cv.notify_all();
    }
}

/***************** 消费者线程 *******************/
void ThreadRead()
{
    // 提前先进入消费者队列
    unique_lock<mutex> lock(mux);
    cv.wait(lock);                      // 等待被唤醒

    cout << "我消费了一辆特斯拉" << endl;

    lock.unlock();                      // 解锁
}

/********************** 主程序 **********************/
int main(int argc, const char *argv[])
{
    // 创建生产者线程
    thread th1(ThreadWrite);

    // 创建多个消费者
    for (int i = 0; i < 5; i++)
    {
        thread th2(ThreadRead);
        th2.detach();                   // 线程分离
    }

    th1.join();                         // 阻塞回收生产者线程

    std::cout << "Hello, World!" << std::endl;

    return 0;
}

五、网络编程基础

5.1网络编程的基本概念

1、为什么引入网络编程

1> 回顾之前学习的进程间通信方式:

内核提供三种:有名管道、无名管道、信号

system V提供三种:消息队列、共享内存、信号量级

2> 以上通信方式,仅仅局限于同一主机之间多个进程间通信方式,不能实现跨主机的通信方式

3> 如果想要实现跨主机的通信方式,我们引入的套接字的通信方式,就是该门课程要讲诉的内容

2、网络的发展历史

1> 第一阶段:APRAnet阶段----> 冷战的产物

阿帕网,是Internet的最早雏形,注意,此时的网络不能实现互联不同类型的计算机和不同类型的操作系统,并且没有纠错功能

2> TCP\IP两个协议的阶段

协议:在计算机网络中,要做到有条不紊的交换数据,需要遵循一些实现约定好的规则,这些规则明确规定了所交换的格式以及相关的同步问题。为了进行网络中数据交换而建立的规则、标准和约定统称为网络协议(protocol)

该阶段引入了两个协议(只有两个)

TCP:传输控制协议,用来检测网络传输中的差错控制。

IP:网际协议,用来专门负责不同网络中进行互联的协议。

3> OSI(开放系统互连模型)的七层网络体系结构

bash 复制代码
网络体系结构的概念
1> 每一层都有自己独立的分工,单每一层都不可获取
2> 通常将功能相近的协议组织在一起放在一层,称为协议栈,所以每一层中其实有多个协议
分层的好处:
1> 各层之间相互独立,每一层不需要知道下一层如何实现,而仅仅只需要知道该层通过层间接口锁提供的服
务
2> 稳定、灵活性好。当每一层出现变化时,只要层间接口保持不变,则当前层的上层和下次不受影响
3> 易于实现和维护,只要知道哪一层的功能,直接对指定层进行维护即可
4> 促进标准化工作,每一层所提供的服务和技术都有精确的说明
5> 结构上是不可分割的,各层之间都采用的最合适的技术来实现

1、OSI(开发系统互联模型)是由ISO(国际标准化组织)提出的理想化模型,一共有七层:物数 网传会表应

2、TCP\IP协议族的体系结构:也是互联网事实上的工业标准 一共提供的四层:应用层、传输层、网络层、链路层(网络接口层和物理)

3、虽然TCP\IP体系结构只有四层,但是做的事和OSI的七层体系结构是一样的

4、TCP\IP四层网络体系结构和OSI七层网络体系结构的对应关系如下

4> tcp\ip协议族中常见的协议

cpp 复制代码
应用层:
HTTP(Hypertext Transfer Protocol) 超文本传输协议
注意:每次使用的协议时由下层决定的,不能乱用
万维网的数据通信的基础
FTP(File Transfer Protocol) 文件传输协议
是用于在网络上进行文件传输的一套标准协议,使用TCP传输
TFTP(Trivial File Transfer Protocol) 简单文件传输协议
是用于在网络上进行文件传输的一套标准协议,使用UDP传输
SMTP(Simple Mail Transfer Protocol) 简单邮件传输协议
一种提供可靠且有效的电子邮件传输的协议
传输层:
TCP(Transport Control Protocol) 传输控制协议
是一种面向连接的、可靠的、基于字节流的传输层通信协议
UDP(User Datagram Protocol) 用户数据报协议
是一种无连接、不可靠、快速传输的传输层通信协议
网络层:
IP(Internetworking Protocol) 网际互连协议
是指能够在多个不同网络间实现信息传输的协议
ICMP(Internet Control Message Protocol) 互联网控制信息协议
用于在IP主机、路由器之间传递控制消息、ping命令使用的协议
IGMP(Internet Group Management Protocol) 互联网组管理
是一个组播协议,用于主机和组播路由器之间通信
链路层:
ARP(Address Resolution Protocol) 地址解析协议
通过IP地址获取对方mac地址
RARP(Reverse Address Resolution Protocol) 逆向地址解析协议
通过mac地址获取ip地址

3、网络通信中的封包和拆包过程

1> 案例引入

2> 网络通信中封包拆包过程的详细

3> 对等层(不同主机的同一层称为对等层)中数据包的名称

一帧数据的说明:

1、大小为:64--1518 (包含以太网首部14字节,以太网尾部4字节)

2、如果数据大于MTU(最大传输单元,linux中默认是1500),需要分成两个或多个数据包进行传输

3、可以使用ifconfig查看mtu的值

4、TCP和UDP区别

1> 两者都属于传输层的相关协议

2> TCP而言---->稳定

bash 复制代码
1、TCP提供了面向连接的、可靠的数据传输服务
2、传输过程中,能够保障数据无误、数据无丢失、数据无重复、数据无失序
TCP通信中会给每个数据包编上编号,该编号称为序列号
每个序列号都需要应打包应答,如果没有应答,则会将上面的数据包重复传输
3、TCP通信中,数据传输效率较低,耗费资源较多
4、数据收发是不同步的
为了提高传输效率,tcp通信中会将多个较小的,发送时间间隔较短的数据包,沾成一个数据包进行发
送,该现象称为沾包现象
5、TCP通信使用场景:对于传输质量要求较高的以及传输量较大的数据通信,在需要可靠传输信息的场合,一
般使用TCP通信
例如:账户和密码登录注册、大型文件的下载

3> UDP通信-----> 快速

cpp 复制代码
1、提供面向无连接、不保证数据可靠传输、尽最大努力传输的协议
2、数据传输过程中,可能会出现数据丢失、重复、失序、乱序等现象
3、数据传输效率较高、实时性高
4、限制每次传输的数据大小,多出部分会直接被忽略删除
5、数据的收发是同步的,不会沾包
6、使用场景:发送小尺寸的,在收到数据后给出应答比较困难的情况下,采用udp通信
例如:广播、组播、通信软件的音视频传输

5、网络字节序概念

1> 字节序:计算机在存储多字节整数时,根据主机的CPU处理架构不同,我们将主机分成大端存储的主机和小段存储的主机

大端存储:内存地址的低位存储的是数据的高位

小端存储:内存地址的低位存储的是数据的低位

2> 验证当前主机是大端存储还是小端存储

使用指针来进行判断

cpp 复制代码
#include <myhead.h>

int main(int argc, const char *argv[])
{
    // 定义一个整型变量,值为 0x12345678
    int num = 0x12345678;

    // 定义一个字符类型的指针,指向整型变量的起始地址
    char *ptr = (char *)&num;

    // 对 ptr 所指向的字节中的内容进行判断
    // 如果是 0x12 则说明是大端存储
    // 如果是 0x78 则说明是小端存储
    if (*ptr == 0x12)
    {
        cout << "big endian" << endl;
    }
    else if (*ptr == 0x78)
    {
        cout << "little endian" << endl;
    }
    else
    {
        // 异常情况(理论上不会发生)
        cout << "unknown endian" << endl;
    }

    std::cout << "Hello, World!" << std::endl;

    return 0;
}

使用共用体判断主机的大小端

cpp 复制代码
#include <myhead.h>

// 定义一个共用体类型:多个成员共享一个内存空间
// 共享的是所占内存空间最大的那个成员
union Info
{
    int num;    // 四字节整数
    char ch;    // 一字节
};

int main(int argc, const char *argv[])
{
    // 定义一个共用体变量
    Info temp;

    // 给整形成员赋值
    temp.num = 0x12345678;

    // 通过 ch 成员(首字节)判断大小端
    if (temp.ch == 0x12)
    {
        cout << "big endian" << endl;
    }
    else if (temp.ch == 0x78)
    {
        cout << "little endian" << endl;
    }
    else
    {
        cout << "unknown endian" << endl;
    }

    std::cout << "Hello, World!" << std::endl;

    return 0;
}

3> 由于不同主机之间存储方式不同,可能会出现,小端存储的主机中的多字节整数,在网络传输过程中,明明没有出现任何问题,但是,由于大小端存储的问题,导致,多字节整数传输出现错误

基于此,我们引入的网络字节序的概念,规定网络字节序都是大端存储的。

无论发送端是大端存储还是小端存储,在传输多字节整数时,一律先转换为网络字节序。经由 网络传输后,到达目的主机后,在转换为主机字节序即可

4> 系统给大家提供了一套有关网络字节序和主机字节序之间相互转换的函数

主机:host

网络:network

转换:to

cpp 复制代码
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//将4字节整数主机字节序转换为网络字节序,参数
是主机字节序,返回值是网络字节序
uint16_t htons(uint16_t hostshort);//将2字节整数主机字节序转换为网络字节序,参数
是主机字节序,返回值是网络字节序
uint32_t ntohl(uint32_t netlong);//将4字节整数的网络字节序转换为主机字节序,参数
是网络字节序,返回值是主机字节序
uint16_t ntohs(uint16_t netshort);//将2字节整数的网络字节序转换为主机字节序,参
数是网络字节序,返回值是主机字节序

5> 何时使用网络字节序转换函数

1、在进行多字节整数网络传输时,需要使用字节序转换函数

2、在进行单字节整数传输时,不需要使用

3、在网络中传输字符串时,也不需要使用

cpp 复制代码
#include <myhead.h>
#include <arpa/inet.h>   // htonl 函数头文件

int main(int argc, const char *argv[])
{
    // 定义一个 4 字节整数(主机字节序)
    int num = 0x12345678;

    // 调用 htonl 函数,将主机字节序转换为网络字节序
    int res = htonl(num);

    // 输出转换后的结果
    printf("res = %#x\n", res);   // 在小端机器上输出: 0x78563412

    std::cout << "Hello, World!" << std::endl;

    return 0;
}

6、IP地址

1> ip地址是主机在网络中的唯一标识,由两部分组成,分别是网络号和主机号。

网络号:确定计算机所从属的网络

主机号:标识该设备在该网络中的一个编号

2> 作用:在网络传输过程中,给网络传输载体必须添加的信息,指定源ip地址和目的ip地址,以 便于找到目的主机

3> IP地址的分类

1、IPv4:是使用4字节无符号整数表示的一个ip地址,取值范围 0, 2\^32-1 一共有四十多亿个,很明显不够,我们采用相关技术进行扩充 局域网扩充:为了解决ip地址不够用,让多个主机共享一个ip地址 WAN:wide area network (广域网) LAN:local area network(局域网)

2、IPv6:是使用16字节无符号整数表示的一个ip地址,取值范围 【0, 2^128-1】

3、注意:IPv6是不兼容IPv4的

4> IP地址的划分:由于IP地址比较庞大,我们将其进行分组,一共分为5类网络,分别是A类、B 类、C类、D类、E类网络

bash 复制代码
网络类型 取值范围                 网络号个数 主机号个数 用途
A类网络:1.0.0.0 --- 127.255.255.255 2^7     2^24     已经保留不供给使用
B类网络:128.0.0.0--191.255.255.255 2^14     2^16     名地址网管中心
C类网络:192.0.0.0--223.255.255.255 2^21     2^8     家庭、校园、公司使用
D类网络:224.0.0.0--239.255.255.255 组播IP
E类网络:240.0.0.0--255.255.255.255 保留、实验室使用

5> 特殊的IP地址

bash 复制代码
1、网络号 + 全为0的主机号:表示该网络,不分配给任何主机使用,例如:192.168.10.0
2、网络号 + 全为1的主机号:表示当前网络的广播地址,也不分配给任何主机使用,例如:
192.168.10.255
3、网络号 + 主机号为1:默认表示网关,当然可以自己制定网关ip
4、127.0.0.0:本地环回ip,当没有网络时,用于测试当前主机的ip
5、0.0.0.0:表示当前局域网中的任意一个主机号
6、255.255.255.255:一般表示广播地址

6> 点分十进制 为了方便记忆,我们将ip地址的每一个字节单独计算出十进制数据,并用点进行分割,这种方 式,称为点分十进制,在程序中使用的是字符串来存储的。但是,ip地址的本质是4字节无符号整数, 在网络中进行传输时,需要使用的是4字节无符号整数,而不是点分十进制的字符串。此时,就需要引 入关于点分十进制数据向4字节无符号整数转换的相关函数

地址:address

网络:network

转换:to

cpp 复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);//将点分十进制的ip地址转换为4字节无符号整数
的网络字节序,参数时点分十进制数据,返回值时4字节无符号整数
char *inet_ntoa(struct in_addr in);//将4字节无符号整数的网络字节序,转换为点分十
进制的字符串
cpp 复制代码
#include <myhead.h>
#include <arpa/inet.h>

int main(int argc, const char *argv[])
{
    const char *ip = "172.20.10.8";
    struct in_addr addr;

    // 将点分十进制 IP 转换为网络字节序的二进制形式
    if (inet_pton(AF_INET, ip, &addr) <= 0)
    {
        perror("inet_pton");
        return -1;
    }

    printf("ip_net = %#x\n", addr.s_addr);   // 0x080a14ac

    std::cout << "Hello, World!" << std::endl;

    return 0;
}

7、端口号(port)

1> 作用:为了区分同一个主机之间的每个进程的,使用端口号来进行标识

概念:端口是是一个 2 字节的无符号整数表示的数字,取值范围 【0, 65535】

2> 为什么不使用进程号标识,而使用端口号

答:因为进程号是进程的唯一标识,当同一个应用程序,关闭再打开后,并不是同一个进程号 了,但是是同一个应用程序

所以,端口号标识的是我们的应用程序,当一个应用程序关闭再打开后,端口号不变

3> 引入端口号后,网络通信的两个重要因素就集结完毕:ip 地址 + 端口号

ip地址可以在网络中,唯一确定对端的主机地址,通过端口号能够找到该主机中指定的对端应 用程序

4> 端口号的分类 1、0~1023 :众所周知的 "VIP"端口号:被特殊的应用程序已经占用了的。

可以查看 / etc /services 中的文件内容,该文件中记录了特殊的端口号

TCP和UDP分别使用不同的一套标准 注意:

有时使用这些特殊的端口号时,需要使用管理员权限

cpp 复制代码
echo 7/tcp
echo 7/udp
ssh 22/tcp # The Secure Shell (SSH)
Protocol
ssh 22/udp # The Secure Shell (SSH)
Protocol
ftp 21/tcp
ftp 21/udp fsp fspd
telnet 23/tcp
telnet 23/udp
tftp 69/tcp
tftp 69/udp
http 80/tcp www www-http # WorldWideWeb HTTP
http 80/udp www www-http # HyperText Transfer Protocol
http 80/sctp # HyperText Transfer Protocol
https 443/tcp # http protocol over TLS/SSL
https 443/udp # http protocol over TLS/SSL
https 443/sctp # http protocol over TLS/SSL

2、1024 ~ 49151:用户可分配的端口号

3、49152~65535:动态分配或系统自动分配的端口号

5.2网络通信基础

1、套接字(socket)的概念

1> 在网络通信过程中,需要创建一个信息的载体来进行数据的通信,这个载体我们可以称之为套 接字

2> 我们可以调用函数:socket(),创建一个用于通信的套接字端点,并返回该端点对应的文件 描述符

3> 在通信端点中,有两个缓冲区,分别对应发送缓冲区和接受缓冲区

4> 原理如下:

5> socket函数

cpp 复制代码
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:为通信创建一个端点,并返回该端点对应的文件描述符,文件描述符的使用原则是最小未分配原则
参数1:协议族,常用的协议族如下
Name Purpose Man page
AF_UNIX, AF_LOCAL 本地通信,同一主机的多进程通信 具体内容查看 man 7 unix
AF_INET 提供IPv4的相关通信方式 具体内容查看 man 7 ip
AF_INET6 提供IPv6的相关通信方式 具体内容查看 man 7 ipv6
参数2:通信类型,指定通信语义,常用的通信类型如下
SOCK_STREAM 支持TCP面向连接的通信协议
SOCK_DGRAM 支持UDP面向无连接的通信协议
参数3:通信协议,当参数2中明确指定特定协议时,参数3可以设置为0,但是有多个协议共同使用
时,需要用参数3指定当前套接字确定的协议
返回值:成功返回创建的端点对应的文件描述符,失败返回-1并置位错误码

2、基于TCP面向连接的通信方式

在网络通信过程中,有两种通信方式,分别是基于BS模型的,即浏览器服务器模型(第六章讲 解),和基于CS模型,即客户端服务器模型,该章节中,使用的是基于CS模型

1> 通信原理

2> bind函数

cpp 复制代码
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:为套接字分配名称,给套接字绑定ip地址和端口号
参数1:要被绑定的套接字文件描述符
参数2:通用地址信息结构体,对于不同的通信域而言,使用的实际结构体是不同的,该结构体的目
的是为了强制类型转换,防止警告
通信域为:AF_INET而言,ipv4的通信方式
struct sockaddr_in {
sa_family_t sin_family; /* 地址族: AF_INET */
in_port_t sin_port; /* 端口号的网络字节序 */
struct in_addr sin_addr; /* 网络地址 */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* ip地址的网络字节序 */
};
通信域为:AF_UNIX而言,本地通信
struct sockaddr_un {
sa_family_t sun_family; /* 通信域:AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* 通信使用的文件 */
};
参数3:参数2的大小
返回值:成功返回0,失败返回-1并置位错误码

3> listen函数

cpp 复制代码
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:将套接字设置成被动监听状态
参数1:套接字文件描述符
参数2:挂起队列能够增长的最大长度,一般为128
返回值:成功返回0,失败返回-1并置位错误码

4> accept函数

cpp 复制代码
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:阻塞等待客户端的连接请求,如果已连接队列中有客户端,则从连接队列中拿取第一个,并创
建一个用于通信的套接字
参数1:服务器套接字文件描述符
参数2:通用地址信息结构体,用于接受已连接的客户端套接字地址信息的
参数3:接收参数2的大小
返回值:成功发那会一个新的用于通信的套接字文件描述符,失败返回-1并置位错误码

5> recv、send数据收发

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:从套接字中读取消息放入到buf中
参数1:通信的套接字文件描述符
参数2:要存放数据的起始地址
参数3:读取的数据的大小
参数4:读取标识位,是否阻塞读取
0:表示阻塞等待
MSG_DONTWAIT:非阻塞
返回值:可以是大于0:表示成功读取的字节个数
可以是等于0:表示对端已经下线(针对于TCP通信)
-1:失败,并置位错误码
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:向套接字文件描述符中将buf这个容器中的内容写入
参数1:通信的套接字文件描述符
参数2:要发送的数据的起始地址
参数3:发送的数据的大小
参数4:读取标识位,是否阻塞读取
0:表示阻塞等待
MSG_DONTWAIT:非阻塞
返回值:成功返回发送字节的个数
-1:失败,并置位错误码

6> close关闭套接字

cpp 复制代码
#include <unistd.h>
int close(int fd);
功能:关闭套接字文件描述符
参数:要关闭的套接字文件描述符
返回值:成功返回0,失败返回-1并置位错误码

7> connect连接函数

cpp 复制代码
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:将指定的套接字,连接到给定的地址上
参数1:要连接的套接字文件描述符
参数2:通用地址信息结构体
参数3:参数2的大小
返回值:成功返回0,失败返回-1并置位错误码

3、TCP服务器端和客户端实现

1> 服务器端代码实现

cpp 复制代码
#include<myhead.h>
#define SER_PORT 8888 //服务器端口号
#define SER_IP "192.168.11.53" //服务器IP地址

int main(int argc, const char *argv[]){
    //1、创建用于连接的套接字文件描述符
    int sfd = socket(AF_INET, SOCK_STERAM, 0);
    //参数1:AF_INET表示使用的是ipv4的通信协议
    //参数2:SOCK_STREAM表示使用的是tcp通信
    //参数3:由于参数2指定了协议,参数3填0即可
    if(sfd == -1){
        perror("sokcet error");
        return -1;
    }
    printf("sokcet success sfd = %d\n", sfd);//3

    //2、绑定ip地址和端口号
    //2.1填充要绑定的ip地址和端口号结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;//通信域
    sin.sin_port = htons(SER_PORT);//端口号
    sin.sin_addr.s_addr = inet_addr(SER_IP);//ip地址

    //2.2绑定工作
    //参数1:要被绑定的套接字文件描述符
    //参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
    //参数3:参数2的大小
    if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) == -1){
        perror("bind error");
        return -1;
    }
    printf("bind success\n");

    //3、启动监听
    //参数1:要启动监听的文件描述符
    //参数2:挂起队列的长度
    if(listen(sfd, 128) == -1){
        perror("listen error");
        return -1;
    }
    
    //4、阻塞等待客户端的连接请求
    //定义变量,用于接受客户端地址信息结构体
    struct sockaddr_in cin;//用于接收地址信息结构体的
    socklen_t socklen = sizeof(cin);//用于接收地址信息的长度

    int newfd = accept(sfd, (struct sockaddr *)&cin, &socklen);
    //参数1:服务器套接字文件描述符
    //参数2:用于接收客户端地址信息结构体的容器,如果不接收,也可以填NULL
    //参数3:接收参数2的大小,如果参数2为NULL,则参数3也是NULL
    if(newfd == -1){
        perror("accept error");
        return -1;
    }
    printf("[%s:%d]:已连接成功!!!!\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));

    //5、数据收发
    char rbuf[128] = "";//数据容器
    while(1){
        //清空容器中的内容
        bzero(rbuf, sizeof(rbuf));

        //从套接字中读取信息
        int res = recv(newfd, rbuf, sizeof(rbuf), 0);
        ir(res == 0){
            printf("对端已经下线\n");
            break;
        }
        printf("[%s:%d]:%s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port),rbuf);

        //对收到的数据处理一下,回给客户端
        strcat(rbuf, "*_*");
        //将消息发送给客户端
        if(send(newfd, rbuf, strlen(rbuf), 0) == -1){
            perror("send error");
            return -1;
        }
        printf("发送成功\n");
    }
    //6、关闭套接字
    close(newfd);
    close(sfd);

    std::cout << "Hello, world" << std::endl;

    return 0;
}

2> 客户端实现

cpp 复制代码
#include<myhead.h>
#define SER_PORT 8888 //服务器端口号
#define SER_IP "192.168.150.103" //服务器IP地址
#define CLI_PORT 9999 //客户端端口号
#define CLI_IP "192.168.150.103" //客户端ip地址

int main(int argc, const char *argv[]){
    //1、创建用于通信的客户端套接字文件描述符
    int cfd = socket(AF_INET, SOCK_STREAM, 0);
    if(cfd == -1){
        perror("socket error");
        return -1;
    }
    printf("socket success cfd = &d\n", cfd);

    //2、绑定IP地址和端口号(可选)
    //2.1填充要绑定的地址信息结构体
    struct sockaddr_in cin;
    cin.sin_family = AF_INET;
    cin.sin_port = htons(CLI_PORT);
    cin.sin_addr.s_addr = inet_addr(CLI_IP);
    //2.2绑定工作
    if(bind(cfd, (struct sockaddr*)&cin, sizeof(cin)) == -1){
        perror("bind error");
        return -1;
    }
    printf("bind success\n");

    //3、连接服务器
    //3.1 填充要连接的服务器地址信息结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;//通信域
    sin.sin_port = htons(SER_PORT);//端口号
    sin.sin_addr.s_addr = inet_addr(SER_IP);//服务器ip地址
    //3.2连接工作
    if(connect(cfd, (struct sockaddr*)&sin, sizoef(sin)) == -1){
        perror("connect error");
        return -1;
    }
    printf("连接服务器成功\n");
    //4、数据收发
    char wbuf[128] = "";
    while(1){
        //清空容器
        bzero(wbuf, sizeof(wbuf));

        //从终端获取数据
        fgets(wbuf, sizeof(wbuf), stdin);
        wbuf[strlen(wbuf) - 1] = 0;//将换行转换成'\0'

        //将数据发送给服务器
        if(send(cfd, wbuf, sizeof(wbuf), 0) == 1){
            perror("send error");
            return -1;
        }
        
        //接受服务器发送过来的消息
        if(recv(cfd, wbuf, sizeof(wbuf), 0) == 0){
            printf("对端已经下线\n");
            break;
        }
        printf("收到服务器信息为:%s\n", wbuf);
    }

    //5、关闭套接字
    close(cfd);
    std::cout << "Hello, World!" << std::endl;

    return 0;
}

4、UDP通信模型讲解

1> udp通信是面向无连接的,不可靠的,尽最大努力传输的通心方式,传输过程中,可能会出现数据的丢失、重复、失序、乱序等现象

2> 通信模型

3> 相关函数

cpp 复制代码
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr
*src_addr, socklen_t *addrlen);
功能:从套接字中读取消息放入到buf中,并接受对端的地址信息结构体
参数1:套接字文件描述符
参数2:存放数据的容器起始地址
参数3:读取的数据大小
参数4:是否阻塞,0表示阻塞 , MSG_DONTWAIT表示非阻塞
参数5:接收对端地址信息结构体的容器
参数6:参数5的大小
返回值:成功返回读取字节的个数,失败返回-1并置位错误码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct
sockaddr *dest_addr, socklen_t addrlen);
功能:向套接字中发送消息,并且指定对方的地址信息结构体
参数1:套接字文件描述符
参数2:要发送的数据的起始地址
参数3:要发送的数据大小
参数4:是否阻塞,0表示阻塞 , MSG_DONTWAIT表示非阻塞
参数5:要发送的对端地址信息结构体
参数6:参数5的大小
返回值:成功返回发送的字节的个数,失败返回-1并置位错误码

5、UDP服务器端和客户端实现

1> UDP服务器端程序代码实现

cpp 复制代码
#include<myhead.h>
#define SER_PORT 8888 //服务器端口号
#define SER_IP "192.168.150.103"//服务器IP地址

int main(int argc, const char *argv[]){
    //1.创建用于通信的套接字文件描述符
    int sfd = socket(AF_INET, SOCK_DGRAM, 0);
    //SOCK_DGRAM表示基于udp通信方式
    if(sfd == -1){
        perror("socket error");
        return -1;
    }
    printf("socket success sfd = %d\n", sfd);

    //2.绑定ip地址和端口号
    //2.1填充要绑定的ip地址和端口号结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;//通信域
    sin.sin_port = htons(SER_PORT);//端口号
    sin.sin_addr.s_addr = inet_addr(SER_IP);//ip地址

    //2.2绑定工作
    //参数1:要绑定的套接字文件描述符
    //参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
    //参数3:参数2的大小
    if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) == -1){
        perror("bind error");
        return -1;
    }
    printf("bind success\n");

    //3.数据收发
    char rbuf[128] = "";

    //定义容器接收对端的地址信息结构体
    struct sockaddr_in cin;
    socklen_t socklen = sizeof(cin);

    while(1){
        //清空容器
        bzero(rbuf, sizeof(rbuf));

        //从客户端中读取信息
        if(recvfrom(sfd, rbuf, sizeof(rbuf), 0, (struct sockaddr*)&cin, &socklen) == -1){
            perror("recvfrom error");
            return -1;
        }
        printf("[%s:%d]:%s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port),rbuf);

        //加个笑脸发给客户端
        strcat(rbuf, "*_*");

        //将数据发送给客户端
        sendto(sfd, rbuf, strlen(rbuf), 0, (struct sockaddr*)&cin, sizeof(cin));
        printf("发送成功\n");
    }
    //4、关闭套接字
    close(sfd);
    std::cout << "Hello, world" << std::endl;
    return 0;
}

2> UDP客户端程序代码实现

cpp 复制代码
#include<myhead.h>
#define SER_PORT 8888 //服务器端口号
#define SER_IP "192.168.150.103" //服务器IP地址
#define CLI_PORT 9999 //客户端端口号
#define CLI_IP "192.168.150.103" //客户端IP地址

int main(int argc, const char*argv[]){
    //1.创建用于通信的客户端套接字文件描述符
    int cfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(cfd == -1){
        perror("socket error");
        return -1;
    }
    printf("socket success cfd = &d\n", cfd);

    //2、绑定ip地址和端口号(可选)
    //2.1 填充要绑定的地址信息结构体
    struct sockaddr_in cin;
    cin.sin_family = AF_INET;
    cin.sin_port = htons(CLI_PORT);
    cin.sin_addr.s_addr = inet_addr(CLI_IP);
    //2.2 绑定工作
    if(bind(cfd, (struct sockaddr*)&cin, sizeof(cin)) == -1){
        perror("bing error");
        return -1;
    }
    printf("bind success\n");

    //数据收发
    char wbuf[128] = "";
    //填充服务器的地址信息结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;//通信域
    sin.sin_port = htons(SER_PORT);//端口号
    sin.sin_addr.s_addr = inet_addr(SER_IP);//ip地址

    while(1){
        //清空容器
        bzero(wbuf, sizeof(wbuf));
        //从终端读取数据
        fgets(wbuf, sizeof(wbuf), stdin);
        wbuf[strlen(wbuf) - 1] = 0;

        //将数据发送给服务器
        sendto(cfd, wbuf, strlen(wbuf), 0, (struct sockaddr*)&sin, sizeof(sin));

        //接收服务器发来的数据
        recvfrom(cfd, wbuf, sizeof(wbuf), 0, NULL, NULL);
        printf("服务器发来的消息为:%s\n", wbuf);
    }
    //4、关闭套接字
    close(cfd);
    std::cout << "Hello, world" << std::endl;

    return 0;
}

5.3TCP并发服务器

1、多进程实现并发服务器

对于tcp通信方式而言,目前的通信方式,只能实现一个服务器对应一个服务器一个客户端,不能实现一个服务器对应多个客户端,为了完成该操作,我们需要引入并发操作

1> 能够实现一个服务器端对应多个客户端的操作:循环服务器

2> 循环服务器模型

cpp 复制代码
sfd = socket(); //创建用于链接的套接字文件描述符
bind(); //绑定ip地址和端口号
listen(); //启动被动监听状态
while(1)
{
    newfd = accept(); //阻塞接受客户端连接请求
    //跟客户端进行数据收发
    recv();
    send();
    close(newfd);
}
close(sfd);

3> 通过上述案例,我们可以发现,accept函数和recv函数都是阻塞函数,但是,我们想让这阻塞任务同时执行,互不干扰,此时,我们就要引入并发机制:多线程、多进程,IO多路复用

2、多进程实现并发服务器

1> 原理:主进程可以用于完成对客户端的连接请求,子进程可以完成对客户端通信操作

2> 原理实现

3> 总结模型

cpp 复制代码
void handler(int signo)
    {
        while (waitpid() > 0)
            ; // 以非阻塞形式回收僵尸进程
    }
    signal(SIGCHLD, handler); // 将信号与信号处理函数连接起来
    sfd = socket();           // 创建用于链接的套接字文件描述符
    bind();                   // 绑定ip地址和端口号
    listen();                 // 启动被动监听状态
    while (1)
    {
        newfd = accept(); // 阻塞接受客户端连接请求
        pid = fork();     // 创建子进程用于跟客户端进行通信
        if (pid > 0)
        {
            3.3 多线程实现并发服务器 1 > 原理:主线程用于接收客户端的连接请求, 分支线程用于跟客户端进行通信 2 > 代码实现
                                                                                                                     // 关闭newfd
                                                                                                                     close(newfd);
        }
        else if (pid == 0)
        {
            // 子进程
            close(sfd);
            // 跟客户端进行数据收发
            recv();
            send();
            close(newfd);
            exit(EXIT_SUCCESS); // 退出子进程
        }
    }
    close(sfd);

3、多线程实现并发服务器

1> 原理:主线程用于接收客户端的连接请求, 分支线程用于跟客户端进行通信

2> 代码实现

3> 通信模型

cpp 复制代码
// 定义分支线程
    void *deal_cli_msg(void *arg)
    {
        // 跟客户端进行数据收发
        recv();
        send();
        close(newfd);
        // 退出线程
        pthread_exit(NULL);
    }
    // 主线程内容
    sfd = socket(); // 创建用于链接的套接字文件描述符
    bind();         // 绑定ip地址和端口号
    listen();       // 启动被动监听状态
    while (1)
    {
        newfd = accept();                               // 阻塞接受客户端连接请求
        pthread_create(&tid, NULL, deal_cli_msg, &buf); // 创建分支线程
        // 将线程设置成分离态
        pthread_detach(tid);
    }
    close(sfd);

4、IO多路复用

1> 引入目的:当主机没有操作系统时,或者说程序不能使用多进程或多线程完成任务的并发操作 时,我们可以引入IO多路复用的技术,完成多任务并发执行的操作

2> 事件和函数的关系:比如,scanf是一个阻塞函数,该函数完成的是输入事件

当函数先于事件发生时,函数会阻塞等待事件的到来

当函数后于事件发生时,函数就不会阻塞,直接执行

cpp 复制代码
#include <myhead.h>
    int main(int argc, const char *argv[])
    {
        int num = 0, key; // 定义一个整型变量
        printf("请输入num的值:");
        scanf("%d", &num); // 提示并输入第一个值
        printf("num = %d\n", num);
        从上述例子中的第二个输入可以看出,如果事件先于函数发生,当程序再执行到函数时,函数会直
        接运行,不会阻塞了
        3 > IO多路复用原理图 4 > IO多路复用相关函数有两个:select、poll 5 > select函数:如果man手册中没有select函数,执行指令 sudo yum install man - pages
                                                                                                                                                         printf("请输入key的值:");
        scanf("%d", &key); // 提示并输入第二个值
        printf("num = %d\n", key);
        std::cout << "Hello, World!" << std::endl;
        return 0;
    }

从上述例子中的第二个输入可以看出,如果事件先于函数发生,当程序再执行到函数时,函数会直 接运行,不会阻塞了

3> IO多路复用原理图

4> IO多路复用相关函数有两个:select、poll

5> select函数:如果man手册中没有select函数,执行指令 sudo yum install man-pages

cpp 复制代码
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds,
struct timeval *timeout);
功能:阻塞等待文件描述符集合中是否有事件产生,如果有事件产生,则解除阻塞
参数1:文件描述符集合中,最大的文件描述符 加1
6> select函数的基本使用方式
参数2、参数3、参数4:分别表示读集合、写集合、异常处理集合的起始地址
由于对于写操作而言,我们也可以转换读操作,所以,只需要使用一个集合
就行
对于不使用的集合而言,直接填NULL即可
参数5:超时时间,如果填NULL表示永久等待,如果想要设置时间,需要定义一个如下结构体类型
的变量,并将地址传递进去
struct timeval {
long tv_sec; /* 秒数 */
long tv_usec; /* 微秒 */
};
and
struct timespec {
long tv_sec; /* 秒数 */
long tv_nsec; /* 纳秒 */
};
返回值:
>0:成功返回解除本次阻塞的文件描述符的个数
=0:表示设置的超时时间,时间已经到达,但是没有事件事件产生
=-1:表示失败,置位错误码
注意:当该函数解除阻塞时,文件描述符集合中,就只剩下本次触发事件的文件描述符,其余的文
件描述符就被删除了
//专门针对于文件描述符集合提供的函数
void FD_CLR(int fd, fd_set *set); //将fd文件描述符从容器set中删除
int FD_ISSET(int fd, fd_set *set); //判断fd文件描述符,是否存在于set容器
中
void FD_SET(int fd, fd_set *set); //将fd文件描述符,放入到set容器中
void FD_ZERO(fd_set *set); //清空set容器

6> select函数的基本使用方式

cpp 复制代码
#include <myhead.h>
#define SER_PORT 8888            // 服务器端口号
#define SER_IP "192.168.150.103" // 服务器IP地址

int main(int argc, const char *argv[])
{
    // 1、创建用于连接的套接字文件描述符
    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    // 参数1:AF_INET表示使用的是ipv4的通信协议
    // 参数2:SOCK_STREAM表示的是ipv4的通信协议
    // 参数3:由于参数2指定了协议,参数3填0即可
    if (sfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("socket success sfd = %d\n", sfd); // 3

    // 2、绑定ip地址和端口号
    // 2.1 填充要绑定的ip地址和端口号结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;                // 通信域
    sin.sin_port = htons(SER_PORT);          // 端口号
    sin.sin_addr.s_addr = inet_addr(SER_IP); // ip地址
    // 2.2 绑定工作
    // 参数1:要被绑定的套接字文件描述符
    // 参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
    // 参数3:参数2的大小
    if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
    {
        perror("bind error");
        return -1;
    }
    printf("bind success\n");

    // 3、启动监听
    // 参数1:要启动监听的文件描述符
    // 参数2:挂起队列的长度
    if (listen(sfd, 128) == -1)
    {
        perror("listen error");
        return -1;
    }
    printf("listen success\n");

    // 4、阻塞等待客户端的连接请求
    // 定义变量,用于接受客户端地址信息结构体
    struct sockaddr_in cin;          // 用于接收地址信息结构体的
    socklen_t socklen = sizeof(cin); // 用于接收地址信息的长度
    // 定义文件描述符集合
    fd_set readfds, tempfds; // 读文件描述符集合
    // 将该文件描述符集合清空
    FD_ZERO(&readfds);
    // 将0号文件描述符以及sfd文件描述符放入到集合中
    FD_SET(0, &readfds);
    FD_SET(sfd, &readfds);

    while (1)
    {
        // 将reafds备份一份放入tempfds中
        tempfds = readfds;

        // 调用阻塞函数,完成对文件描述符集合的管理工作
        int res = select(sfd + 1, &tempfds, NULL, NULL, NULL);
        if (res == -1)
        {
            perror("select error");
            return -1;
        }
        else if (res == 0)
        {
            printf("time out !!!\n");
            return -1;
        }

        // 程序执行至此,表示一定有其中至少一个文件描述符产生了事件,只需要判断哪个文件描述符在集合中
        // 就说明文件描述符产生了事件
        // 表示sfd文件描述符触发了事件
        if (FD_ISSET(sfd, &tempfds))
        {
            int newfd = accept(sfd, (struct sockaddr *)&cin, &socklen);
            // 参数1:服务器套接字文件描述符
            // 参数2:用于接收客户端地址信息结构体的容器,如果不接收,也可以填NULL
            // 参数3:接收参数2的大小,如果参数2为NULL,则参数3也是NULL
            if (newfd == -1)
            {
                perror("accept error");
                return -1;
            }
            printf("[%s:%d]:已连接成功,newfd = %d!!!!\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd);
        }
        // 判断0号文件描述符是否产生了事件
        if (FD_ISSET(0, &tempfds))
        {
            char wbuf[128] = "";              // 字符数组
            fgets(wbuf, sizeof(wbuf), stdin); // 从终端读取数据,阻塞函数
            printf("触发了键盘输入事件:%s\n", wbuf);
        }
    }
    close(sfd);
    std::cout << "Hello, world!" << std::endl;
    return 0;
}

7> select 实现TCP并发服务器

cpp 复制代码
#include <myhead.h>
#define SER_PORT 8888            // 服务器端口号
#define SER_IP "192.168.174.128" // 服务器IP地址
int main(int argc, const char *argv[])
{
    // 1、创建用于连接的套接字文件描述符
    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    // 参数1:AF_INET表示使用的是ipv4的通信协议
    // 参数2:SOCK_STREAM表示使用的是tcp通信
    // 参数3:由于参数2指定了协议,参数3填0即可
    if (sfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("socket success sfd = %d\n", sfd); // 3
    // 2、绑定ip地址和端口号
    // 2.1 填充要绑定的ip地址和端口号结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;                // 通信域
    sin.sin_port = htons(SER_PORT);          // 端口号
    sin.sin_addr.s_addr = inet_addr(SER_IP); // ip地址
    // 2.2 绑定工作
    // 参数1:要被绑定的套接字文件描述符
    // 参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
    // 参数3:参数2的大小
    if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
    {
        perror("bind error");
        return -1;
    }
    printf("bind success\n");
    // 3、启动监听
    // 参数1:要启动监听的文件描述符
    // 参数2:挂起队列的长度
    if (listen(sfd, 128) == -1)
    {
        perror("listen error");
        return -1;
    }
    printf("listen success\n");
    // 4、阻塞等待客户端的连接请求
    // 定义变量,用于接受客户端地址信息结构体
    struct sockaddr_in cin;          // 用于接收地址信息结构体的
    socklen_t socklen = sizeof(cin); // 用于接收地址信息的长度
    // 定义文件描述符集合
    fd_set readfds, tempfds; // 读文件描述符集合
    // 将该文件描述符集合清空
    FD_ZERO(&readfds);
    // 将0号文件描述符以及sfd文件描述符放入到集合中
    FD_SET(0, &readfds);
    FD_SET(sfd, &readfds);
    // 定义一个变量,用于存储容器中的最大文件描述符
    int maxfd = sfd;
    int newfd = -1; // 接收客户端连接请求后,创建的通信套接字文件描述符
    // 定义一个地址信息结构体数组来存储客户端对应的地址信息
    struct sockaddr_in cin_arr[1024];

    while (1)
    {
        // 将reafds备份一份放入tempfds中
        tempfds = readfds;

        // 调用阻塞函数,完成对文件描述符集合的管理工作
        int res = select(maxfd + 1, &tempfds, NULL, NULL, NULL);
        if (res == -1)
        {
            perror("select error");
            return -1;
        }
        else if (res == 0)
        {
            printf("time out !!!\n");
            return -1;
        }
        // 程序执行至此,表示一定有其中至少一个文件描述符产生了事件,只需要判断哪个文件描述符
        // 就说明该文件描述符产生了事件
        // 表示sfd文件描述符触发了事件
        if (FD_ISSET(sfd, &tempfds))
        {
            newfd = accept(sfd, (struct sockaddr *)&cin, &socklen);
            // 参数1:服务器套接字文件描述符
            // 参数2:用于接收客户端地址信息结构体的容器,如果不接收,也可以填NULL
            // 参数3:接收参数2的大小,如果参数2为NULL,则参数3也是NULL
            if (newfd == -1)
            {
                perror("accept error");
                return -1;
            }
            printf("[%s:%d]:已连接成功,newfd = %d!!!!\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd);
            
            //将客户端对应的套接字地址信息结构体放入数组对应的位置上
            cin_arr[newfd] = cin;//newfd文件描述符对应的地址信息结构体

            //将当前的newfd放入到检测文件描述符集合中,以便于检测使用
            FD_SET(newfd, &readfds);//加入到tempfds中
            //更新maxfd,如何更新?
            if(maxfd < newfd){
                //判断最新的文件描述符是否比当前容器中最大的文件描述符大
                maxfd = newfd;
            }
        }
        //判断0号文件描述符是否产生了事件
        if(FD_ISSET(0, &tempfds)){
            char wbuf[128] = "";//字符数组
            fgets(wbuf, sizeof(wbuf), stdin);//从终端读取数据,阻塞函数
            printf("触发了键盘输入事件:%s\n", wbuf);
            //能不能将输入的数据,全部发送给所有客户端
            for(int i = 4; i <= maxfd; i++){
                send(i, wbuf, strlen(wbuf), 0);//将数据发送给所有客户端
            }
        }
        //判断是否是newfd产生了事件
        //循环将所有客户端文件描述符遍历一遍,如果还存在于tempfds中的客户端,表示有数据接收过来
        for(int i = 4; i <= maxfd; i++){
            if(FD_ISSET(i, &tempfds)){
                //5、数据转发
                char rbuf[128] = "";//数据容器

                //清空容器中的内容
                bzero(rbuf, sizeof(rbuf));

                //从套接字中读取信息
                int res = recv(i, rbuf, sizeof(rbuf), 0);
                if(res == 0){
                    printf("对端已经下线\n");
                    //将文件描述符进行关闭
                    close(i);
                    //需要将该文件描述符从readfds中删除
                    FD_CLR(i, &readfds);
                    //更新maxfd
                    for(int k = maxfd; k >= 0; k--){
                        if(FD_ISSET(k , &readfds)){
                            maxfd = k;
                            break;//结束向下进行的循环
                        }
                    }
                    continue;//本次循环结束,继续下一次的select的阻塞
                }
                printf("[%s:%d]:%s\n", inet_ntoa(cin_arr[i].sin_addr),ntohs(cin_arr[i].sin_port), rbuf);
                //对收到的数据处理一下,回给客户端
                strcat(rbuf, "*_*");

                //将信息发送给客户端
                if(send(i, rbuf, strlen(rbuf), 0) == -1){
                    perror("send error");
                    return -1;
                }
                printf("发送成功\n");
            }
        }
    }
    close(sfd);
    std::cout << "Hello, World!" << std::endl;
}

8> select 实现TCP并发服务器的模型

cpp 复制代码
sfd = socket();          // 创建用于连接的套接字文件描述符
    bind();                  // 绑定ip和端口号
    listen();                // 监听
    fd_set readfds, tempfds; // 定义文件描述符集合
    FD_ZERO();               // 清空容器
    FD_SET();                // 将文件描述符放入容器
    maxfd = sfd;             // 记录最大的文件描述符
    while (1)
    {
        tempfds = readfds;                         // 备份一份容器
        select(maxfd, &readfds, NULL, NULL, NULL); // 阻塞等待集合中是否有事件产生
        // 判断相关文件描述符是否在集合中
        if (FD_ISSET(sfd, &tempfds))
        {
            newfd = accept();        // 接收客户端请求
            FD_SET(newfd, &readfds); // 将新文件描述符放入集合
            // 更新maxfd
        }
        // 判断是否是客户端发来数据
        for (i = 4; i <= maxfd; i++)
        {
            send();
            recv();
            close(i); // 退出客户端
            FD_CLR(i, &readfds);
            // 更新maxfd
        }
    }
    // 关闭监听
    close(sfd);

9> 使用poll实现IO多路复用

cpp 复制代码
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:阻塞等待文件描述符集合中是否有事件产生,如果有,则解除阻塞,返回本次触发事件的文件
描述符个数
参数1:文件描述符集合容器的起始地址,是一个结构体数组,结构体类型如下
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 要等待的事件:由用户填写 */
short revents; /* 实际发生的事件 :调用函数结束后,内核会自动设置*/
10> poll的使用实例:poll完成TCP客户端中发送数据和读取数据的并发
};
关于事件对应的位:
POLLIN:读事件
POLLOUT:写事件
参数2:集合中文件描述符的个数
参数3:超时时间,负数表示永久等待,0表示非阻塞
返回值:
>0:表示触发本次解除阻塞事件的文件描述符的个数
=0:表示超时
=-1:出错,置位错误码

10> poll的使用实例:poll完成TCP客户端中发送数据和读取数据的并发

cpp 复制代码
#include <myhead.h>
#define SER_PORT 8888            // 服务器端口号
#define SER_IP "192.168.150.103" // 服务器IP地址
#define CLI_PORT 9999            // 客户端端口号
#define CLI_IP "192.168.150.103" // 客户端ip地址

int main(int argc, const char *argv[])
{
    // 1、创建用于通信的客户端套接字文件描述符
    int cfd = socket(AF_INET, SOCK_STREAM, 0);
    if (cfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("socket success cfd = %d\n", cfd); // 3
    // 2、绑定ip地址和端口号(可选)
    // 2.1 填充要绑定的地址信息结构体
    struct sockaddr_in cin;
    cin.sin_family = AF_INET;
    cin.sin_port = htons(CLI_PORT);
    cin.sin_addr.s_addr = inet_addr(CLI_IP);
    // 2.2 绑定工作
    if (bind(cfd, (struct sockaddr *)&cin, sizeof(cin)) == -1)
    {
        perror("bind error");
        return -1;
    }
    printf("bind success\n");
    // 3、连接服务器
    // 3.1 填充要连接的服务器地址信息结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;                // 通信域
    sin.sin_port = htons(SER_PORT);          // 端口号
    sin.sin_addr.s_addr = inet_addr(SER_IP); // 服务器ip地址
    // 3.2 连接工作
    if (connect(cfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
    {
        perror("connect error");
        return -1;
    }
    printf("连接服务器成功\n");

    //使用poll完成终端输入和套接字接受数据的并行执行
    struct pollfd pfds[2];//pfds[0] pfds[1]
    //分别给数组中两个文件描述符成员赋值
    pfds[0].fd = 0;//表示检测0号
    pfds[0].events = POLLIN;//表示检测的是读事件

    pfds[1].fd = cfd;//检测cfd文件描述符
    pfds[1].events = POLLIN;//检测读事件

    //4、数据收发
    char wbuf[128] = "";
    while(1){
        int res = poll(pfds, 2, -1);
        //功能:阻塞等待文件描述符集合中是否有事件产生
        //参数1:文件描述符集合起始地址
        //参数2:文件描述符个数
        //参数3:表示永久等待
        if(res == -1){
            perror("poll error");
            return -1;
        }
        //程序执行至此,表示文件描述符容器中,有事件产生
        //表示0号文件描述符的事件
        if(pfds[0].revents == POLLIN){
            //清空容器
            bzero(wbuf, sizeof(wbuf));

            //从终端获取数据
            fgets(wbuf, sizeof(wbuf), stdin);//0
            wbuf[strlen(wbuf) - 1] = 0;//将换行改成'\0'
            //将数据发送给服务器
            if(send(cfd, wbuf, sizeof(wbuf), 0)== -1){
                perror("send error");
                return -1;
            }
        }
        //表示有客户端发来消息
        if(pfds[1].revents == POLLIN){
            //接受服务器发送来的消息
            if(recv(cfd, wbuf, sizeof(wbuf), 0) == 0){
                printf("对端已经下线\n");
                break;
            }
            printf("收到服务器消息为:%s\n", wbuf);
        }
    }
    //5、关闭套接字
    close(cfd);

    std::cout << "Hello, World!" << std::endl;


    return 0;
}

11> 总结select和poll的区别

1、select管理三个文件描述符集合,poll只管理一个,但是,可以操作很多事件

2、select管理的文件描述符有上限,一般是1024个,而poll管理文件描述符没有这个限制

3、对于效率而言,poll的效率比select的略高

5、IO多路复用之epoll

1> epoll全称为eventpoll,是内核实现IO多路复用的一种实现方式。在其中一个或多个事件得到满足时,可以接触阻塞。

epoll是select和poll的升级版,相比于select和poll而言,epoll改进了工作方式,效率更高。

bash 复制代码
1、select和poll都是基于线性结构进行检测集合,而epoll是基于树形结构(红黑树)完成管理检测集合的
2、select和poll检测时,随着集合的增大,效率会越来越低。epoll使用的是函数回调机制,效率较高。处理文件描述符的效率也不会随着文件秒数的增大而降低。
3、select和poll在工作过程中,不断的在内核空间与用户空间频繁拷贝文件描述符的数据。epoll在主场新的文件描述符或者修改文件描述符时,只需一次,能够有效减少数据在用户空间和内核空间之间的切换
4、和poll一样,epoll没有最大文件描述符的限制,仅仅收到程序能够打开的最大文件描述符数量限制
5、对于select和poll而言,需要对返回的文件描述符集合进行判断后才知道是哪些文件描述符就绪了,而epoll可以直接得到已经就绪的文件描述符,无需再次检测
6、当多路复用比较频发进行、IO流量频繁的时候,一般不使用select和poll,使用epoll比较合适
7、epoll只适用于linux平台,不能跨平台创作

2> epoll相关API函数:epoll提供了三个函数,分别处理不同的操作

cpp 复制代码
#include<sys/epoll.h>
int epoll_create(int size);
功能:创建一个epoll实例,并返回该实例的句柄,是一个文件描述符
参数1:epoll实例中能够容纳的最大节点个数,自从linux2.6.8版本后,size可以忽略,但是必须要是一个大于0的数字
返回值:成功返回控制epoll实例的文件描述符,失败返回-1并置位错误码
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:完成对epoll实例的控制
参数1:通过epoll_create创建的epoll实例文件描述符
参数2:op表示要进行的操作
    EPOLL_CTL_ADD:向epoll树上添加新的要检测的文件描述符
    EPOLL_CTL_MOD:改变epoll树上的文件描述符检测的事件
    EPOLL_CTL_DEL:删除epoll树上的要检测的文件描述符,此时参数3可以忽略填NULL
参数3:要检测的文件描述符
参数4:要检测的事件,是一个结构体变量地址,属于输入变量
typedef union epoll_data{
    void *ptr;//提供的解释性数据
    int fd;//文件描述符
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
struct epoll_event{
    uint32_t events;//要检测的事件
    epoll_data_t data;//用户有效数据,是一个共用体
};
要检测的事件
EPOLLIN:读事件
EPOLLOUT:写事件
EPOLLERR:异常事件
EPOLLTE:表示设置epoll的模式为边沿触发模式
#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents; int timeout);
功能:阻塞检测epoll实例中是否有文件描述符准备就绪,如果准备就绪了,就解除阻塞
参数1:epoll实例对于的文件描述符
参数2:文件描述符集合,当有文件描述符产生事件时,将所有事件的文件描述符,放入到该集合中
参数3:参数2的大小
参数4:超时时间,以毫秒为单位的超时时间,如果填-1表示永久阻塞
返回值:> 0:表示接触本次操作的触发的文件描述符个数
        = 0:表示没有文件描述符产生事件
        =-2:失败,置位错误码

3> epoll实现TCP并发服务器的模型

cpp 复制代码
1、sfd = socket();  // 创建用于连接的文件描述符
2、bind();       // 绑定ip地址和端口号
    3、listen(); // 将套接字设置成被动监听状态
    4、创建一个epoll实例,并将用于连接的文件描述符放入到epoll树中 struct epoll_event ev;
ev.events = EPOLLIN; // 检测读事件
ev.data.fd = sfd;    // 要检测的文件描述符
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
5、检测文件描述符是否就绪(有客户端发来连接请求) int num = epoll_wait(epfd, evs, size, -1);
6、如果检测到文件描述符就绪,则建立连接,将newfd放入到集合中 newfd = accept(); // 接收客户端连接请求
struct epoll_event ev;
ev.event = EPOLLIN;
ev.data.fd = newfd;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &ev);
7、如果检测的是客户端文件描述符,则进行通信,如果客户端下线,则将文件描述符进行删除 int len = recv();
if (len == 0)
{
    epoll_ctl(epfd, EPOLL_CTL_DEL, newfd, NULL); // 将newfd从集合中删除
    close(newfd);
}
else if (len > 0)
{
    send();
}

4> epoll 实现一个TCP数据处理服务器

cpp 复制代码
#include <myhead.h>
#define SER_PORT 8888
#define SER_IP "172.168.150.103" // 服务器IP地址

int main(int argc, const char *argv[])
{
    // 1、创建用于连接的套接字文件描述符
    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    // 参数1:AF_INET表示使用的是ipv4的通信协议
    // 参数2:SOCK_STREAM表示使用的是tcp通信
    // 参数3:由于参数2指定了协议,参数3填0即可
    if (sfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("socket success sfd = &d\n", sfd); // 3

    // 2、绑定ip地址和端口号
    // 2.1 填充要绑定的ip地址和端口号结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;                // 通信域
    sin.sin_port = htons(SER_PORT);          // 端口号
    sin.sin_addr.s_addr = inet_addr(SER_IP); // ip地址

    // 2.2绑定工作
    //  参数1:要被绑定的套接字文件描述符
    // 参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
    // 参数3:参数2的大小
    if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
    {
        perror("bind error");
        return -1;
    }
    printf("bind success\n");
    // 3、启动监听
    // 参数1:要启动监听的文件描述符
    // 参数2:挂起队列的长度
    if (listen(sfd, 128) == -1)
    {
        perror("listen error");
        return -1;
    }
    printf("listen success\n");

    // 4、阻塞等待客户端的连接请求
    // 定义变量,用于接受客户端地址信息结构体
    struct sockaddr_in cin;          // 用于接收地址信息结构体
    socklen_t socklen = sizeof(cin); // 用于接收地址信息的长度

    // 创建epoll实例,用于检测文件描述符
    int epfd = epoll_create(1);
    if (epfd == -1)
    {
        perror("epoll_create error");
        return -1;
    }

    // 将sfd放入到检测集合中
    struct epoll_event ev;
    ev.events = EPOLLIN; // 要检测的是读事件
    ev.data.fd = sfd;    // 要检测的是文件描述符信息
    epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
    // 功能:将sfd放入到检测集合中
    // 参数1:epoll实例的文件描述符
    // 参数2:epoll操作,表现要添加文件描述符
    // 参数3:要检测的文件描述符的值
    // 参数4:要检测的文件

    // 定义接收返回的事件集合
    struct epoll_event evs[1024];
    int size = sizeof(evs) / sizeof(evs[0]); // 数组大小

    while (1)
    {
        // 阻塞检测文件描述符集合中是否有事件产生
        int num = epoll_wait(epfd, evs, size, -1);
        // 参数1:epoll实例的文件描述符
        // 参数2:返回触发事件的文件事件集合
        // 参数3:集合的大小
        // 参数4:是否阻塞
        printf("num = %d\n", num); // 输出本次触发的文件描述符个数
        // 循环遍历集合,判断是哪个文件描述符就绪
        for (int i = 0; i < num; i++)
        {
            int fd = evs[i].data.fd; // 获取本次解除阻塞的文件描述符
            // 判断是否为sfd文件描述符就绪
            if (fd == sfd)
            {
                // 说明有新的客户端发来连接请求
                int newfd = accept(sfd, (struct sockaddr *)&cin, &socklen);
                // 参数1:服务器套接字文件描述符
                // 参数2:用于接收客户端地址信息结构体的容器,如果不接收,也可以填NULL
                // 参数3:接收参数2的大小,如果参数2为NULL,则参数3也是NULL
                if (newfd == -1)
                {
                    perror("accept error");
                    return -1;
                }
                printf("[%s:%d]:已连接成功,newfd = %d!!!!\n",
                       inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd);
                // 将客户端文件描述符放入到epoll检测集合中
                struct epoll_event ev;
                ev.events = EPOLLIN; // 要检测的是读事件
                ev.data.fd = newfd;  // 要检测的文件描述符信息
                epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &ev);
            }
            else
            {
                // 表示客户端文件描述符就绪,也就是说客户端有数据发来
                // 5、数据收发
                char rbuf[128] = ""; // 数据容器
                // 清空容器中的内容
                bzero(rbuf, sizeof(rbuf));
                // 从套接字中读取消息
                int res = recv(fd, rbuf, sizeof(rbuf), 0);
                if (res == 0)
                {
                    printf("对端已经下线\n");
                    // 将客户端从epoll树中删除
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    // 关闭套接字
                    close(fd);
                    break;
                }
                printf("收到数据:%s\n", rbuf);
                // 对收到的数据处理一下,回给客户端
                strcat(rbuf, "*_*");
                // 将消息发送给客户端
                if (send(fd, rbuf, strlen(rbuf), 0) == -1)
                {
                    perror("send error");
                    return -1;
                }
                printf("发送成功\n");
            }
        }
    }
    close(sfd);  // 关闭监听
    close(epfd); // 关闭epoll实例
    std::cout << "Hello, World!" << std::endl;

    return 0;
}

5> epoll的工作模式:epoll的工作模式有两种,分别是水平触发和边沿触发

bash 复制代码
1、水平模式:
    简称LT模式(level trigered),是默认的一种初始模式,并且该模式支持阻塞和非阻塞的形式
    在这种模式下,当文件描述符准备就绪后,内核会通知使用者哪些文件描述符就绪了。
    使用者可以对就绪的文件描述符进行操作。
    如果使用者不做任何操作或者没有全部处理完该文件描述符的信息,内核会继续通知使用者数据
特点:
    对于读事件,如果该文件描述符中的数据没有被读取完毕,则内核会继续解除阻塞,让用户继续处理,知道缓冲区中数据可以处理为止。
    后面的解除阻塞,是自动完成的,无需用户进行对文件描述符的后续操作。
    对于写事件,检测文件描述符缓冲区是否可以,如果可用通过则解除阻塞,一般写文件描述符的缓冲区都是可以的,一般不对文件描述符进行检测
2、边沿模式
    检测ET模式(edge trigered),需要手动设置该模式,并且该模式一般支持非阻塞形式
    在该模式下,当文件描述符准备就绪后,内核会通知使用者哪些文件描述符就绪了,但是仅仅只通知一次   
    使用者可以对就绪的文件描述符进行操作
    如果使用者不做任何操作,或者没有全部处理该文件描述符的信息,内核不会通知使用者再次处理数据,知道下一次该文件描述符的事件产生
特点:对于读事件,如果文件描述符中的缓冲区中的数据没有被读取完毕,则内核不会继续解除阻塞,直到下一次的该文件描述符事件产生,但是下一次的文件描述符的事件,读取的是上一次没有读取完毕的内容
    对于写事件,检测文件描述符缓冲区是否可以,如果可用则解除阻塞,一般写文件描述符的缓冲区都是可以的,一般不对写文件描述符进行检测
    边沿触发模式的处理效率会更高一些,要求用户必须一次性处理文件描述符中的数据
3、如何设置边沿触发:在将文件描述符放入到epoll树中时,需要加一个属性
    struct epoll_event ev;
    ev.event = EPOLLIN|EPOLLEV;

验证水平模式

cpp 复制代码
struct epoll_event ev;
ev.events = EPOLLIN; // 要检测的是读事件

边沿触发的测试

cpp 复制代码
// 将sfd放入到检测集合中
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 要检测的是读事件 EPOLLET表示该文件描述符检测是
使用边沿触发模式

5.4广播和组播

1、网络属性

1> 网络套接字在不同层中,有不同的设置,分别有应用层(套接字层),传输层、网络层、以太网层

2> 对套接字属性进行设置,可以使用setsockopt和getsockopt函数完成

cpp 复制代码
#include<sys/types.h>
#include<sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
功能:设置或者获取套接字文件描述符的数据
参数1:套接字文件描述符
参数2:表示操作的套接字层
SOL_SOCKET:表示应用层或者套接字层,通过man 7 socket进行查找
该层中常用的属性是:SO_REUSEADDR 地址快速重用
                   SO_BROADCAST 允许广播
                   SO_RCVTIMEO and SO_SNDTIMEO:发送或接受超时时间
IPPROTO_TCP:表示传输层的基于TCP的协议
IPPROTO_UDP:表示传输层中基于UDP的协议
IPPROTO_IP:表示网络层
该层常用的属性:IP_ADD_MEMBERSHIP,加入多播组
参数3:对应层中属性的名称
参数4:参数3属性的值,一般为int类型,其他类型,会给出
参数5:参数4的大小
返回值:成功返回0,失败返回-1并置位错误码

3> 验证网络属性

cpp 复制代码
// 1、创建用于连接的套接字文件描述符
int sfd = socket(AF_INET, SOCK_STREAM, 0);
// 参数1:AF_INET表示使用的是ipv4的通信协议
// 参数2:SOCK_STREAM表示使用的是tcp通信
// 参数3:由于参数2指定了协议,参数3填0即可
if (sfd == -1)
{
    perror("socket error");
    return -1;
}
printf("socket success sfd = %d\n", sfd); // 3
// 测试套接字地址默认是否能快速重用
int reuse = -1; // 接收获取下来的数据,有可能操作系统差异,如果运行失败修改成char类
型
    socklen_t reuselen; // 接收大小
if (getsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, &reuselen) == -1)
{
    perror("getscokopt error");
    return -1;
}
printf("reuse = %d\n", reuse); // 0表示默认不可用 1表示默认可用
// 设置端口号快速重用
int reu = 1;
if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reu, sizeof(reu)) == -1)
{
    perror("setsockopt error");
    return -1;
}
// 验证
if (getsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, &reuselen) == -1)
{
    perror("getscokopt error");
    return -1;
}
printf("reuse = %d\n", reuse); // 1表示默认可用

2、广播

1> 主机之间是一对多的通信方式,网络对其中的每一台主机都会将消息进行转发

2> 在当前网络下的所有主机都会收到该消息,无论是否愿意收到消息

3> 基于无连接的通信方式完成,UDP通信

4> 广播分为发送端和接收端

5> 对于发送端而言,需要设置允许广播,并且向广播地址发送数据

6> 广播地址:网络号+全为1的主机号,可以通过指令ifconfig查看

7> 广播消息不能穿过路由器到骨干路由上

1、广播发送到流程------>类似于UDP的客户端

bash 复制代码
1、socket:创建于发送数据的套接字文件描述符
2、setsockopt:设置套接字属性,允许广播 SOL_SOCKET层 中SO_BROADCAST
3、bind:可以绑定也可以不绑定
4、数据收发,对端套接字地址信息结构体
    IP:广播地址
    PORT:与接收端保持一致
5、sendto/recvfrom:数据收发
6、close:关闭套接字
cpp 复制代码
#include <myhead.h>
int main(int argc, const char *argv[])
{
    // 1、创建用于通信的客户端套接字文件描述符
    int sndfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sndfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("socket success cfd = %d\n", sndfd);

    // 设置允许广播
    int broad = 1; // 要设置的值
    if (setsockopt(sndfd, SOL_SOCKET, SO_BROADCAST, &broad, sizeof(broad)) == -1)
    {
        perror("setsocketopt error");
        return -1;
    }
    printf("成功设置广播\n");

    // 2、可以绑定也可以不绑定
    // 3、数据收发
    char wbuf[128] = "";
    // 填充接收端的地址信息结构体
    struct sockaddr_in rin;
    rin.sin_family = AF_INET;                           // 通信域
    rin.sin_port = htons(8888);                         // 端口号
    rin.sin_addr.s_addr = inet_addr("192.168.150.255"); // 广播IP地址

    while (1)
    {
        // 清空容器
        bzero(wbuf, sizeof(wbuf));

        // 从终端读取数据
        fgets(wbuf, sizeof(wbuf), stdin);
        wbuf[strlen(wbuf) - 1] = 0;

        // 将数据发送给服务器
        sendto(sndfd, wbuf, strlen(wbuf), 0, (struct sockaddr *)&rin, sizeof(rin));
        printf("发送成功\n");
    }

    // 关闭套接字
    close(sndfd);
    std::cout << "Hello,world" << std::endl;

    return 0;
}

2、广播的接收端流程--->类似于UDP的服务端

bash 复制代码
1、socket:创建用于接收数据的套接字文件描述符
2、bind:必须绑定
    ip:广播地址
    POET:与发送端一致
4、sendto/recvfrom:数据收发
5、close:关闭套接字
cpp 复制代码
#include <myhead.h>
int main(int argc, const char *argv[])
{
    // 1、创建用于通信的套接字文件描述符
    int recvfd = socket(AF_INET, SOCK_DGRAM, 0);
    // SOCK_DGRAM表示基于udp通信方式
    if (recvfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("socket success sfd = &d\n", recvfd);

    // 2、绑定ip地址和端口号
    // 2.1填充要绑定的ip地址和端口号结构体
    struct sockaddr_in rin;
    rin.sin_family = AF_INET;                           // 通信域
    rin.sin_port = htons(8888);                         // 端口号
    rin.sin_addr.s_addr = inet_addr("192.168.150.255"); // 广播ip地址

    // 2.2绑定工作
    // 参数1:要被绑定的套接字文件描述符
    // 参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
    // 参数3:参数2的大小
    if (bind(recvfd, (struct sockaddr *)&rin, sizeof(rin)) == -1)
    {
        perror("bind error");
        return -1;
    }
    printf("bind success\n");

    // 3、数据收发
    char rbuf[128] = "";
    // 定义容器接收对端的地址信息结构体
    struct sockaddr_in cin;
    socklen_t socklen = sizeof(cin);
    while (1)
    {
        // 清空容器
        bzero(rbuf, sizeof(rbuf));
        // 从客户端中读取信息
        if (recvfrom(recvfd, rbuf, sizeof(rbuf), 0, (struct sockaddr *)&cin, &socklen) == -1)
        {
            perror("recvfrom error");
            return -1;
        }
        printf("[%s:%d]:%s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf);
    }
    // 4、关闭套接字
    close(recvfd);
    std::cout << "Hello, world!" << std::endl;

    return 0;
}

3、组播

1> 广播是给同一个网络下的所有主机发送消息,会占用大量的网络带宽,影响正常网络通信,造成网络拥塞

2> 组播也是实现一对多的通信机制,也就是说,加入同一个多播组的成员都能收到组播消息

3> 组播也就是使用UDP实现的,发送者发送的消息,无论接收者愿不愿意,都会收到消息

4> 组播地址:D类(224.0.0.0 --- 239.255.255.255)

1、组播的发送端流程------>类似于UDP客户端

cpp 复制代码
1、socket:创建用于消息通信的套接字文件描述符
2、bind:非必须
3、填充要发送的地址信息结构体
IP:D类网络
PORT:与接收端保持一致
4、sendto:发送数据
5、close:关闭套接字
cpp 复制代码
#include<myhead.h>
int main(int argc, const char *argv[]){
    //1、用于通信的客户端套接字文件描述符
    int sendfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sendfd == -1){
        perror("socket error");
        return -1;
    }
    printf("socket success cfd = %d\n", sendfd);

    //2、绑定ip地址和端口号(可选)

    //3、数据收发
    char wbuf[128] = "";
    //填充接收端的地址信息结构体
    struct sockaddr_in rin;
    rin.sin_family = AF_INET;//通信域
    rin.sin_port = htons(8888);//端口号
    rin.sin_addr.s_addr = inet_addr("224.1.2.3");//组播ip地址

    while(1){
        //清空容器
        bzero(wbuf, sizeof(wbuf));

        //从终端读取数据
        fgets(wbuf, sizeof(wbuf), stdin);
        wbuf[strlen(wbuf) - 1] = 0;

        //将数据发送给服务器r
        sendto(sendfd, wbuf, strlen(wbuf), 0, (struct sockaddr*)&rin, sizeof(rin));
    }
    //4.关闭套接字
    close(sendfd);
    std::cout << "Hello,World!" << std::endl;

    return 0;
}
cpp 复制代码
1、socket:创建于通信的套接字文件描述符
2、setsocketopt:设置套接字加入多播组 在IPPROTO_IP网络层 IP_ADD_MEMBERSHIP属性
    属性为结构体类型
struct ip_mreqn{
    struct in_addr imr_multiaddr;/*组播地址的网络字节序*/
    struct in_addr imr_address;/*接收端的主机地址*/
    int imr_ifindex;/*网卡设备编号 一般为2 可以通过指令 ip ad 查看*/
3、bind:绑定地址信息结构体
4、recv、recvfrom、read:接收数据
5、close:关闭套接字    
cpp 复制代码
#include <myhead.h>
int main(int argc, const char *argv[])
{
    // 1、创建用于通信的套接字文件描述符
    int recvfd = socket(AF_INET, SOCK_DGRAM, 0);
    // SOCK_DGRAM表示基于udp通信方式
    if (recvfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("socket success sfd = %d\n", recvfd);

    // 将套接字加入多播组
    struct ip_mreqn imr;                                   // 多播组属性结构体
    imr.imr_multiaddr.s_addr = inet_addr("224.1.2.3");     // 组播地址
    imr.imr_address.s_addr = inet_addr("192.168.150.103"); // 本机ip地址
    imr.imr_ifindex = 2;
    imr.imr_ifindex = 2; // 网卡编号
    // 调用设置网络属性
    if (setsockopt(recvfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr)) == -1)
    {
        perror("setsockopt error");
        return -1;
    }
    printf("成功加入多播组\n");

    // 2.1 填充要绑定的ip地址和端口号结构体
    struct sockaddr_in rin;
    rin.sin_family = AF_INET;                     // 通信域
    rin.sin_port = htons(8888);                   // 端口号
    rin.sin_addr.s_addr = inet_addr("224.1.2.3"); // 组播ip地址

    // 2.2绑定工作
    // 参数1:要绑定的套接字文件描述符
    // 参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
    // 参数3:参数2的大小
    if (bind(recvfd, (struct sockaddr *)&rin, sizeof(rin)) == -1)
    {
        perror("bind error");
        return -1;
    }
    printf("bind success\n");
    // 3、数据收发
    char rbuf[128] = "";
    while (1)
    {
        // 清空容器
        bzero(rbuf, sizeof(rbuf));

        // 从客户端中读取信息
        if (recvfrom(recvfd, rbuf, sizeof(rbuf), 0, NULL, NULL) == -1)
        {
            perror("recvfrom error");
            return -1;
        }
        printf("[读取的消息为]%s\n", rbuf);
    }
    // 4、关闭套接字
    close(recvfd);
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

5.5域套接字

1、域套接字相关内容

1> 域套接字就是原始的套接字通信方式,跟IPC通信一样,属于同一主机之间的多个进程间通信方式

2> 由于不需要进行跨主机通信了,也就不需要使用ip地址和端口号了

3> 域套接字通信载体为套接字文件

linux中文件类型:bcd-lsp 其中s类型的文件就是套接字文件

4> 通信本质:使用内核空间完成数据的传输

5> 域套接字通信也分为流式域套接字和报式域套接字

6> 跟跨主机网络通信区别的相关函数

cpp 复制代码
#include<sys/types.h> 
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
功能:为通信创建一个端点,并返回该端点对应的文件描述符,文件描述符的使用原则是最小未分配原则
参数1:协议族,常用的协议族如下
Name Purpose Man page
AF_UNIX, AF_LOCAL 本地通信,同一主机的多进程通信 具体内容查看 man 7 unix
AF_INET 提供IPv4的相关通信方式 具体内容查看 man 7 ip
AF_INET6 提供IPv6的相关通信方式 具体内容查看 man 7 ipv6
参数2:通信类型,指定通信语义,常用的通信类型如下
    SOCK_STREAM 支持TCP面向连接的通信协议
    SOCK_DGRAM 支持UDP面向无连接的通信协议
参数3:通信协议,当参数2中明确指定特定协议时,参数3可以设置为0,但是有多个协议共同使用
    时,需要用参数3指定当前套接字确定的协议
返回值:成功返回创建的端点对应的文件描述符,失败返回-1并置位错误码
include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:为套接字分配名称,给套接字绑定ip地址和端口号
参数1:要被绑定的套接字文件描述符
参数2:通用地址信息结构体,对于不同的通信域而言,使用的实际结构体是不同的,该结构体的目
        的是为了强制类型转换,防止警告
通信域为:AF_UNIX而言,本地通信
struct sockaddr_un {
    sa_family_t sun_family; /* 通信域:AF_UNIX */
    char sun_path[UNIX_PATH_MAX]; /* 通信使用的文件 */ 要求套接字文件不存在
};
参数3:参数2的大小
返回值:成功返回0,失败返回-1并置位错误码

7> 如何判断一个文件是否存在于文件系统

cpp 复制代码
#include <unistd.h>
int access(const char *pathname, int mode);
功能:判断给定的文件是否具有给的的权限
参数1:要被判断的文件描述符
参数2:要被判断的权限
    R_OK:读权限
    W_OK:写权限
    X_OK:执行权限
    F_OK:是否存在
返回值:如果要被判断的权限都存在,则返回0,否则返回-1并置位错误码

8> 如何删除一个文件系统中的文件

cpp 复制代码
#include <unistd.h>
int unlink(const char *path);
功能:删除指定的文件
参数:要删除的文件路径
返回值:成功删除返回0,失败返回-1并置位错误码

2、流式域套接字的实现

1> 服务器端实现

cpp 复制代码
#include <myhead.h>
int main(int argc, const char *argv[])
{
    // 1、创建用于连接的套接字文件描述符
    int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    // 参数1:AF_UNIX表示使用的是ipv4的通信协议
    // 参数2:SOCK_STREAM表示使用的是tcp通信
    // 参数3:由于参数2指定了协议,参数3填0即可
    if (sfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("socket success sfd = %d\n", sfd); // 3
    // 判断套接字文件是否存在
    if (access("./unix", F_OK) == 0)
    {
        // 说明文件已经存在,需要将其进行删除操作
        if (unlink("./unix") == -1)
        {
            perror("unlink error");
            return -1;
        }
    }
    // 2、绑定套接字文件描述符
    // 2.1 填充要绑定的ip地址和端口号结构体
    struct sockaddr_un sun;
    sun.sun_family = AF_UNIX; // 通信域
    // sun.sun_path = "./unix"; //?不可以,因为字符串赋值需要使用 strcpy
    strcpy(sun.sun_path, "./unix");
    // 2.2 绑定工作
    // 参数1:要被绑定的套接字文件描述符
    // 参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
    // 参数3:参数2的大小
    if (bind(sfd, (struct sockaddr *)&sun, sizeof(sun)) == -1)
    {
        perror("bind error");
        return -1;
    }
    printf("bind success\n");
    // 3、启动监听
    // 参数1:要启动监听的文件描述符
    // 参数2:挂起队列的长度
    if (listen(sfd, 128) == -1)
    {
        perror("listen error");
        return -1;
    }
    printf("listen success\n");
    // 4、阻塞等待客户端的连接请求
    // 定义变量,用于接受客户端地址信息结构体
    struct sockaddr_un cun;          // 用于接收地址信息结构体的
    socklen_t socklen = sizeof(cun); // 用于接收地址信息的长度
    int newfd = accept(sfd, (struct sockaddr *)&cun, &socklen);
    // 参数1:服务器套接字文件描述符
    // 参数2:用于接收客户端地址信息结构体的容器,如果不接收,也可以填NULL
    // 参数3:接收参数2的大小,如果参数2为NULL,则参数3也是NULL
    if (newfd == -1)
    {
        perror("accept error");
        return -1;
    }
    printf("[%s]:已连接成功!!!!\n", cun.sun_path); // 输出套接字文件
    // 5、数据收发
    char rbuf[128] = ""; // 数据容器
    while (1)
    {
        // 清空容器中的内容
        bzero(rbuf, sizeof(rbuf));
        // 从套接字中读取消息
        int res = recv(newfd, rbuf, sizeof(rbuf), 0);
        if (res == 0)
        {
            printf("对端已经下线\n");
            break;
        }
        printf("[%s]:%s\n", cun.sun_path, rbuf);
        // 对收到的数据处理一下,回给客户端
        strcat(rbuf, "*_*");
        // 将消息发送给客户端
        if (send(newfd, rbuf, strlen(rbuf), 0) == -1)
        {
            perror("send error");
            return -1;
        }
        printf("发送成功\n");
    }
    // 6、关闭套接字
    close(newfd);
    close(sfd);
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

2> 客户端实现

cpp 复制代码
#include <myhead.h>
int main(int argc, const char *argv[])
{
    // 1、创建用于通信的客户端套接字文件描述符
    int cfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (cfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("socket success cfd = %d\n", cfd); // 3
    // 2、绑定ip地址和端口号(可选) 如果没有绑定,系统也不会自动绑定,会给的一个随机的文件
    // 判断套接字文件是否存在
    if (access("./linux", F_OK) == 0)
    {
        // 说明文件已经存在,需要将其进行删除操作
        if (unlink("./linux") == -1)
        {
            perror("unlink error");
            return -1;
        }
    }
    // 2.1 填充要绑定的地址信息结构体
    struct sockaddr_un cun;
    cun.sun_family = AF_UNIX;
    strcpy(cun.sun_path, "./linux");
    // 2.2 绑定工作
    if (bind(cfd, (struct sockaddr *)&cun, sizeof(cun)) == -1)
    {
        perror("bind error");
        return -1;
    }
    printf("bind success\n");
    // 3、连接服务器
    // 3.1 填充要连接的服务器地址信息结构体
    struct sockaddr_un sun;
    sun.sun_family = AF_UNIX;       // 通信域
    strcpy(sun.sun_path, "./unix"); // 服务器套接字文件
    // 3.2 连接工作
    if (connect(cfd, (struct sockaddr *)&sun, sizeof(sun)) == -1)
    {
        perror("connect error");
        return -1;
    }
    printf("连接服务器成功\n");
    // 4、数据收发
    char wbuf[128] = "";
    while (1)
    {
        // 清空容器
        bzero(wbuf, sizeof(wbuf));
        // 从终端获取数据
        fgets(wbuf, sizeof(wbuf), stdin); // 0
        wbuf[strlen(wbuf) - 1] = 0;       // 将换行改成 '\0'
        // 将数据发送给服务器
        if (send(cfd, wbuf, sizeof(wbuf), 0) == -1)
        {
            perror("send error");
            return -1;
        }
        // 接受服务器发送过来的消息
        if (recv(cfd, wbuf, sizeof(wbuf), 0) == 0)
        {
            printf("对端已经下线\n");
            break;
        }
        printf("收到服务器消息为:%s\n", wbuf); // 5、关闭套接字
        close(cfd);
        std::cout << "Hello, World!" << std::endl;
        return 0;
    }
}

3> 注意:

cpp 复制代码
1、跟网络通信比较,不需要使用ip地址和端口号了,需要使用的是套接字文件
2、与网络通信比较,注意有两个不同:通信域为AF_UNIX, 地址信息结构体为 struct scokaddr_un
3、绑定时用到的套接字文件,必须是不存在的
4、对于客户端而言,如果没有绑定地址信息结构体,系统不会自动帮其绑定一个随机文件的,文件名是一个乱码

3、报式域套接字的实现

1> 服务器端实现

cpp 复制代码
#include <myhead.h>
int main(int argc, const char *argv[])
{
    // 1、创建用于通信的套接字文件描述符
    int sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    // SOCK_DGRAM表示基于udp通信方式
    if (sfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("socket success sfd = %d\n", sfd); // 3
    // 2、绑定套接字文件
    // 判断套接字文件是否存在
    if (access("./unix", F_OK) == 0)
    {
        // 说明文件已经存在,需要将其进行删除操作
        if (unlink("./unix") == -1)
        {
            perror("unlink error");
            return -1;
        }
    }
    // 2.1 填充要绑定的ip地址和端口号结构体
    struct sockaddr_un sun;
    sun.sun_family = AF_UNIX; // 通信域
    strcpy(sun.sun_path, "./unix"); // 套接字文件
    
    // 2.2 绑定工作
    // 参数1:要被绑定的套接字文件描述符
    // 参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
    // 参数3:参数2的大小
    if (bind(sfd, (struct sockaddr *)&sun, sizeof(sun)) == -1)
    {
        perror("bind error");
        return -1;
    }
    printf("bind success\n");
    // 3、数据收发
    char rbuf[128] = "";
    // 定义容器接收对端的地址信息结构体
    struct sockaddr_un cun;
    socklen_t socklen = sizeof(cun);
    while (1)
    {
        // 清空容器
        bzero(rbuf, sizeof(rbuf));
        // 从客户端中读取消息
        if (recvfrom(sfd, rbuf, sizeof(rbuf), 0, (struct sockaddr *)&cun,
                     &socklen) == -1)
        {
            perror("recvfrom error");
            return -1;
        }
        printf("[%s]:%s\n", cun.sun_path, rbuf);
        // 加个笑脸发给客户端
        strcat(rbuf, "*_*");
        // 将数据发送给客户端
        sendto(sfd, rbuf, strlen(rbuf), 0, (struct sockaddr *)&cun, sizeof(cun));
        printf("发送成功\n");
    }
    // 4、关闭套接字
    close(sfd);
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

2> 客户端端实现

cpp 复制代码
#include <myhead.h>
int main(int argc, const char *argv[])
{
    // 1、创建用于通信的客户端套接字文件描述符
    int cfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (cfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("socket success cfd = %d\n", cfd); // 3
    // 2、绑定ip地址和端口号(可选)
    // 判断套接字文件是否存在
    if (access("./linux", F_OK) == 0)
    {
        // 说明文件已经存在,需要将其进行删除操作
        if (unlink("./linux") == -1)
        {
            perror("unlink error");
            return -1;
        }
    }
    // 2.1 填充要绑定的地址信息结构体
    struct sockaddr_un cun;
    cun.sun_family = AF_UNIX;
    strcpy(cun.sun_path, "./linux"); // 绑定套接字文件
    // 2.2 绑定工作
    if (bind(cfd, (struct sockaddr *)&cun, sizeof(cun)) == -1)
    {
        perror("bind error");
        return -1;
    }
    printf("bind success\n");
    // 3、数据收发
    char wbuf[128] = "";
    // 填充服务器的地址信息结构体
    struct sockaddr_un sun;
    sun.sun_family = AF_UNIX;       // 通信域
    strcpy(sun.sun_path, "./unix"); // 套接字文件
    while (1)
    {
        // 清空容器
        bzero(wbuf, sizeof(wbuf));
        // 从终端读取数据
        fgets(wbuf, sizeof(wbuf), stdin);
        wbuf[strlen(wbuf) - 1] = 0;
        // 将数据发送给服务器
        sendto(cfd, wbuf, strlen(wbuf), 0, (struct sockaddr *)&sun, sizeof(sun));
        // 接收服务器发来的数据
        recvfrom(cfd, wbuf, sizeof(wbuf), 0, NULL, NULL);
        printf("服务器发来的消息为:%s\n", wbuf);
    }
    // 4、关闭套接字
    close(cfd);
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

六、Http服务器

一、基本相关概念

1、万维网www

1> 万维网(World Wide Web):是一个大规模的、联机式的信息储藏所,简称web。能够实现使用链接的方式方便的从一个站点访问到另一个站点,从而主动的按需获取丰富的信息。

2> 超文本:包含了指向其他文档的链接的文本文档成为超文本,能够被浏览器所识别。一个超文本是由多个信息源组成,而这些信息源可以分布在世界各地,并且数量不受限制。超文本是万维网的基础。

4> 万维网使用的是BS模型,即浏览器服务器模型。浏览器向服务器发送请求,服务器响应浏览器并发送客户端所需要的超文本

URL由四部分组成:协议://主机名:端口号/路径。例如:http//www/baidu.com.cn

协议:指出使用的何种协议来获取万维网的文档,常用的协议就是http(超文本传输协议),://是规定格式,必须写上。

主机名:是万维网文档所在的主机域名(ip),通常以www开头,但也不是硬性要求,也可以直接使用主机的ip地址来代替

端口号:就是该协议对应的端口号,对应合同谈判协议而言,默认端口号是80

路径:较长的字符串,超文本文档所在的路径

2、超文本传输协议http

1> 概念:HTTP定义了浏览器是怎样从万维网服务器中请求超文本,以及服务器如何把文档传递给浏览器的。

2> 从层次角度来说:HTTP是面向事务的应用层协议,能够实现的可靠的文件交换

事务:指的是一系列的信息交换(建立链接、发送请求、响应请求、展示信息并断开请求),而这一系列的信息脚后是一个不可分割的整体,要执行就全部执行结束,要不执行就全部不执行。

可靠:该协议是应用层协议,其传输层使用的是TCP协议进行的可靠传输

3> HTTP传输永远是客户端发起请求,服务器端进行响应,这样就限制了HTTP协议使用时,无法实现在客户端没有发起请求时,服务器主动向客户端发送消息。

4> HTTP协议时无状态的协议,同一个客户端的本次连接服务器和下一次的连接服务器没有任何联系。服务器没有记忆功能。

5> HTTP协议本身是无连接的,也就是说限制每个连接只能处理一个请求,服务器处理完客户端的请求并收到客户端的应答后就会立即断开连接。

3、超文本标记语言HTML

3.1HTML的基本内容

1> HTML(HyperText Markup Language):超文本标记语言,时用来完成超文本文档的编写的。html并不是协议,而是万维网浏览器能够识别的一种语言

2> 该语言中定义了需要使用的排版命令,这些命令我们称之为"标签"

3> 使用html语言编写的文档称为超文本,后缀为.html或者.htm

4> 浏览器主要解析的就是该语言编写出来的超文本,能将相关标签解析成界面

5> html编译器可以是任意编译器,如文本编辑、vscode、sublime等待

3.2常用标签

1> 标签格式:使用双尖括号包裹着关键字,通常成对出现

2> 标签分类:

html 复制代码
1、单标签:也称空标签 <标签名/> 例如:<br/>
2、双标签:成对出现 <标签名> 内容 </标签名>

3> 常用的标签

html 复制代码
h1--h6:标题标签
p:段落标签
div:盒子标签
input:输入标签

www.runoob.com/html/html-tutorial.html 参考菜鸟教程

3.3第一个HTML文件

1> vscode中创建一个后缀为.html的文档

2> 输入html:5 或者 !后按tab键补齐

3> 在拓展部分,加一个live server的扩展

二、HTTP版本

2.1、http1.0版本

从上世纪90年代开始,浏览器诞生后的第一个标准,是一个短连接的版本。 该版本的很大的一个问题就是每发起一个请求,都要建立一次TCP的连接,而且是串行请求,做了 很多无谓的TCP连接和断开,增加了开销,并且一般请求时使用的是明文模式,容易被攻击,安全性能 比较低

2.2、http1.1版本(正在使用的)

1> 在http1.0版本的基础上增加了持续连接的功能。服务器在发送响应后的一段时间内仍然保持这条连接,使同一个客户端和服务器可以在这条连接上继续后续的请求和响应工作。直到某一段主动断开连接或者长时间不响应时也会断开连接

2> http1.1版本支持两种工作模式

非流水线方式:客户端在收到前一个响应之后才能发出下一个请求

流水线方式:客户端在收到前一个响应之前就能直接发送下一个请求

3> 特点:简单(报文首部和报文内容)、灵活易于拓展(请求方法和状态码没有固定死)

4> 不足:依然是无状态、铭文传输不安全

5> http1.1版本所作的优化

cpp 复制代码
1、使用长连接的方式,解决了1.0版本的短连接的性能问题:只要任意一端没有明确提出断开连接,那么则一
直保持TCP的连接状态
2、使用管道网络传输:允许在同一个TCP连接中,建立多个管道,客户端可以发送多个请求,不需要等到客户
端响应后才发送另一个请求。这样就提高了整体的响应时间。解决了请求的队头阻塞
不足是:服务器端必须按照接收请求的书匈奴发送这些请求的响应,如果服务器在处理请求A花费了
很长时间,那么后续的请求的处理就都会被阻塞。这成为响应队头阻塞
3、实际上HTTP1.1的管道化技术并不是默认开启的,而且很多浏览器技术都不支持。所以,大家只需要知道
http1.1版本提供了该功能,但是并没有得到广泛应用

6> http1.1的瓶颈

bash 复制代码
1、头部巨大且重复:http头部会含有很多固定的字段,并且加起来会有几百字节甚至上千字节,未经压缩就
直接发送。
大量的请求和响应报文中会有很多重复的字段;
http协议是无状态的,想要记录之前的操作,在首部外加了一个cookie的技术,也会
使得首部比较巨大
2、http1.1版本传输请求和响应的过程使用的是ASCII编码完成,而不是二进制编码
3、并发连接数是有限的,以谷歌为例,最大的并发连接数是6个,而且每个连接都要经过TCP的三次握手,以
及TCP的慢启动过程
4、响应对头阻塞问题没有解决
5、不支持服务器推送过程,只能客户端请求,服务器响应

2.3、http2.0版本

1> 2015年推出了http2.0版本,目的是为了改善http的性能,并且兼容了http1.1版本

2> http2.0版本只在应用层做了改变,传输层还是使用的TCP传输协议。HTTP2.0把http分成【语 义】和【语法】两部分,其中语义层不做改动,跟http1.1保持一致。但是在语法部分做了很多改动, 基本上改变了HTTP的报文传输格式

3> HTTP2.0完成了头部压缩,HTTP2.0使用了HPACK的算法,发送的是压缩后的内容,这样就可 以节约带宽,解决了http1.1版本的头部巨大且重复问题。HPACK算法将传输过程中进行的同一个TCP传 输的内容进行合并,将重复的内容不需要进行传输了。提高传输效率

4> http2.0能够实现并发传输:引入了stream的概念,可以实现并发发送stream,只需要建立一 个TCP的连接,节约了多次建立连接过程中握手机制的事件

5> http2.0实现了数据推送功能:服务器端和客户端双方都会建立一个stream,并且使用stream ID进行区分。客户端建立的是奇数号,服务器建立的是偶数号。服务器推送数据时,会先发送 PUSH_PROMISS帧,告诉客户端接下来哪个stream发送资源。

6> HTTP2.0的不足

bash 复制代码
1、也存在响应队头阻塞问题
2、TCP的握手延时问题
3、网络迁移时需要进行重新连接:服务器和客户端建立连接的四要素:源ip地址、源端口号、目的ip地址、
目的端口号
总结:以上问题主要的原因是应用层使用的是TCP服务,无论http层再怎么设计都无法逃避。如果想要从根本
上解决问题,那么就需要彻底改变传输层的相关协议,将传输层的TCP协议改成UDP协议,这就是http3.0的
重要改革部分

2.4、http3.0版本

1> http3.0是最新推出的传输协议

2> http3.0将传输层的TCP协议更改成了UDP协议,使用的是基于UDP的QUIC的协议 UDP:面向无连接的,不关心是否丢包问题,所以,解决了队头阻塞问题

3> QUIC协议:基于UDP实现的买那些无连接的协议,也不许进行三次握手和四次挥手。再协议 中实现了类似于TCP的连接管理、拥塞控制、流量控制等相关操作。相当于将UDP的不可靠传输改成可 靠的传输协议了

4> QUIC吸引中也有一个stream的概念,保证数据的可靠性,每个sream数据报都有一个唯一的 ID标识。当一个数据报一旦丢失后,该数据会被重传,不会影响其他数据包的传输。

5> 能实现更快的连接建立:QUIC协议也需要进行握手连接,相当雨将TCP的握手机制结合在一 起,基于一个ID进行连接,而不是基于源ip地址、源端口号、目的ip地址、目的端口号

6> 不是基于四元组进行连接的,是通过一个stream ID完成,那么就可以实现在不同网络下的迁移

2.5、https版本

1> 为了传输的安全性,引入了HTTPS,加密的超文本传输协议

2> 在http层和TCP层之间加入了TLS协议,该协议提供了信息加密、校验机制以及身份证数组证 书

3> TLS协议需要进行四次握手机制

cpp 复制代码
1、客户端向服务器发送随机数C、TLS协议版本号、密钥套件系列列表
2、服务器收到客户端信息后回复一个ACK
3、服务器在发送一个随机数S、切人TLS版本号、使用的密钥套件
4、客户端回复一个ACK

4> 注意:TLS的四次握手,是发生在TCP的三次握手建立连接之后执行的安全性检查。是一个耗 时操作,但是确保了信息的安全性。

2.6、各个模型

三、HTTP报文格式

1> http报文分为两类

请求报文:从客户端向服务器发送的请求报文

响应报文:从服务器向客户端发送的应答

2> 报文由三部分组成:开始行、首部行和实体主体

bash 复制代码
1、开始行:用于区分是请求报文还是响应报文
请求报文中,开始行也成为请求行
响应报文中,开始行也成为状态行
2、首部行:用来说明浏览器、服务器以及报文主体的一些信息
首部行可以由很多行,也可以没有
在每一个首部行都有一个首部的字段名和它的值,中间使用冒号隔开,每一行的首部行中间使用回
车换行隔开
整个首部行结束后,还有一个空行将首部行和后面的实体主体进行分割
3、实体主体:在请求报文中,一般不用。有时响应报文也可以没有

3.1客户端请求报文格式

1> 请求行由三部分组成,分别是请求方法、URL和版本,中间使用空格隔开

2> 请求方式:就是对所谓的对象进行的操作,因此那些方法实际上也就是一些操作指令。因此,请求报文的类型是由它所指定的方法来决定的

bash 复制代码
常用的请求方法:
1、GET:用来请求URL的资源,并返回实体主体
2、POST:向指定的资源提交数据并进行处理(例如提交的是表单或者上传文件)。数据被包含在实体主体部
分。POST请求可能会导致新的资源的建立或者对已有资源的修改
3、HEAD:类似于GET请求,但是返回的是响应中的首部,没有内容主体
4、PUT:从客户端向服务器传送的是指定的文档内容
5、DELETE:请求服务器去删除某些页面
6、OPTION:请求一些选项的信息
7、TRACE:用来进行环回测试使用的报文请求
8、CONNECT:HTTP/1.1版本中预留给能够将连接改为管道方式的代理服务

3> URL:统一资源定位符,就是要申请的资源路径

4> 版本:HTTP版本,目前常用的使用HTTP1.1版本

5> CR和LF:回车和换行

6> 报文请求的实例

3.2响应报文格式

1> 响应报文的首部也成为状态行:也由三部分组成,分别是版本、状态码和解释状态码的短语, 中间也是有空格隔开

2> 版本:就是HTTP版本

3> 状态码:状态码是由三位数字组成,第一个数字定义了响应的类别,共分为5类

bash 复制代码
1XX:指示信息---表示请求已经接收,继续处理
2XX:成功---表示请求已经被成功接收、理解以及处理。如200表示成功
3XX:重定向---表示要完成请求必须要进行更进一步的操作。如304
4XX:客户端错误---请求的语法错误或者请求无法实现
432:HTTP错误
400:客户端请求有语法错误,不能被服务器所解析
401:认证失败,请求未被接受
403:服务器收到请求,但是拒绝提供服务
404:请求的资源不存在,或者输入的URL有误
5XX:服务器错误---表示服务器未能实现合法的请求
500:服务器发生了不可预测的错误
503:服务器不能处理当前客户端的请求,一段时间后可能恢复正常

4> 实体主体:可以有可以无,如果服务器需向客户端发送数据,则将数据放入到该处

5> 响应报文实例:

七、基于UDP的TFTP文件传输项目

1、TFTP

基于UDP通信的TFTP(Trivial File Transfer Protocal,简单文件传输协议)模型是一种用于在网络上进行文件传输发简单协议。TFTP设计简单,易于实现,通常用于传输小文件或在不支持复杂协议的环境中使用

1.1协议概述

TFTP使用UDP(User Datagram Protocol,用户数据协议)作为传输层协议,默认端口号为69。TFTP协议设计简单,仅支持五种类型的报文,分别是:

RPQ(Read Request):客户端请求读取文件

WRQ(Write Request):客户端请求写入文件

DATA:数据传输报文

ACK:确认报文

ERROR:错误报文

1.2通信流程

TFTP的通信流程通常分为两个阶段:请求阶段和传输阶段

请求阶段:

1、客户端发送RRQ或WRQ:客户端向服务器发送RRQ或WRQ报文,请求读取或写入文件。RRQ 报文包含文件名和传输模式(通常是"netascii"或"octet"),WRQ报文类似。

2、服务器响应:服务器接收到RRQ或WRQ后,会分配一个新的UDP端口用于数据传输,并发送 ACK(对于WRQ)或直接发送DATA(对于RRQ)作为响应。

传输阶段:

1、数据传输:在RRQ请求中,服务器开始发送DATA报文,每个DATA报文包含一个块编号和数据 块。客户端接收到DATA报文后,发送ACK报文确认接收到的块编号。在WRQ请求中,客户端 发送DATA报文,服务器发送ACK报文确认。

2、每个DATA报文和ACK报文都包含一个16位的块编号,从1开始递增。块编号用于确保 数据的有序传输和确认。

3、当传输的文件大小小于512字节时,DATA报文的数据部分小于512字节,表示传输 结束。对于WRQ请求,客户端发送一个小于512字节的DATA报文表示传输结束。

1.3报文格式

bash 复制代码
有关错误码对应的信息:
0 未定义,差错错误信息
1 File Not Found
2 访问违规。
3 磁盘满或分配超出。
4 非法操作。
5 未知的传输ID。
6 文件已存在。
7 没有该用户。

传输模型

2.1客户端流程图

上传流程:

下载流程:

2.2客户端实现

1>头文件

cpp 复制代码
#ifndef TFTP_CLIENT_H
#define TFTP_CLIENT_H

//引入头文件
#include<myhead.h>
#include<errno.h>

//封装一个客户端类
class TFTPClient
{
    private:
    static const int PORT = 69;//服务器端口号
    static const int BUFFER_SIZE = 516;//协议包的大小

    int sfd; //客户端套接字
    struct sockaddr_in server_addr;//服务器地址信息结构体

    //该客户端提供的私有的成员函数
    int doDownload();//下载函数
    int doUpload();//上传函数
    void clearScream();//清屏函数
    void waitForInput();//等待输入函数
    void showMenu(); //展示菜单函数

    public:
    TFTPClient(const string &serverIP);//构造函数
    ~TFTPClient();//析构函数
    
    void run();//客户端执行函数
};

#endif

2>源文件

cpp 复制代码
#include "tftp.client.h"

// 用户打印错误信息的宏
#define ERR_LOG(msg)                                                    \
    do                                                                  \
    {                                                                   \
        perror(msg);                                                    \
        cout << __LINE__ << " " << __func__ << " " << __FILE__ << endl; \
    } while (0)

// 构造函数的定义
TFTPClient::TFTPClient(const string &serverIP)
{
    // 创建udp套接字
    this->sfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sfd < 0)
    {
        ERR_LOG("sock error");
        return;
    }

    // 设置服务器地址信息结构体
    server_addr.sin_family = AF_INET;                          // 通信域
    server_addr.sin_port = htons(PORT);                        // 服务器端口号
    server_addr.sin_addr.s_addr = inet_addr(serverIP.c_str()); // 服务器ip地址
}

// 析构函数的定义
TFTPClient::~TFTPClient()
{
    if (sfd > 0)
    {
        close(sfd); // 如果套接字没有被关闭,则关闭套接字
    }
}

// 定义运行函数
void TFTPClient::run()
{
    while (true)
    {
        // 展示菜单
        showMenu();

        char choice;    // 菜单选项
        cin >> choice;  // 输入选项
        waitForInput(); // 吸收垃圾字符

        switch (choice)
        {
        case '1':
            doDownload(); // 执行下载请求
            break;
        case '2':
            doUpload(); // 执行上传请求
            break;
        case '3':
            return;
        default:
            cout << "输入的功能有误,请重新输入" << endl;
            break;
        }
        clearScream(); // 清屏
    }
}
// 下载函数
int TFTPClient::doDownload()
{
    string filename; // 存储下载的文件名
    cout << "请输入要下载的文件名称:";
    // cin >> filename;//输入下载的文件名(只能输入没有空格的,不建议)
    getline(cin, filename); // 输入下载的文件名(可以输入有空格的)

    // 封装下载请求
    char buf[BUFFER_SIZE] = "";
    int size = sprintf(buf, "%c%c%s%c%s%c", 0, 1, filename.c_str(), 0, "octet", 0); // 封装请求包
    // 向服务器发送请求
    if (sendto(sfd, buf, size, 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        ERR_LOG("sendto error");
        return -1;
    }
    // 循环接受服务器发送过来的数据包
    size_t recv_len;
    unsigned short num = 1;                  // 记录接受的数据块编号
    socklen_t addrlen = sizeof(server_addr); // 存储服务器地址信息结构体的大小
    // 启动循环

    // 定义有关文件操作的遍历
    int flag = 0; // 标识文件是否被打开
    int fd;       // 文件描述符

    while (true)
    {
        // 清空消息容器
        bzero(buf, BUFFER_SIZE);

        // 读取服务器发送过来的数据包
        recv_len = recvfrom(sfd, buf, BUFFER_SIZE, 0, (struct sockaddr *)&server_addr, &addrlen);
        if (recv_len < 0)
        {
            ERR_LOG("recvfrom error");
            return -1;
        }

        // 判断数据包的类型
        // 判断是否为数据包
        if (3 == buf[1])
        {
            // 读取数据操作
            // 打开文件操作
            if (flag == 0) // 表示文件未打开
            {
                fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0664);
                if (fd < 0)
                {
                    ERR_LOG("open error");
                    return -1;
                }
                // 更改标识位
                flag = 1; // 表示文件已经被打开
            }
            // 判断数据
            if (htons(num) == *(unsigned short *)(buf + 2)) // 表示是传过来的数据包
            {
                // 将数据包中的数据写入文件
                if (write(fd, buf + 4, recv_len - 4) < 0)
                {
                    cout << "fd = " << fd << "   recv_len = " << recv_len << endl;
                    ERR_LOG("write error");
                    close(fd);
                    break;
                }
            }
            // 发送确认包ACK
            // 组装ACK包
            buf[1] = 4; // ACK包
            if (sendto(sfd, buf, 4, 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
            {
                ERR_LOG("sendto error");
                close(fd);
                return -1;
            }
            // 判断收取的数据长度是否小于容器长度
            if (recv_len < BUFFER_SIZE) // 表示该数据包是最后一个数据包
            {
                cout << "文件下载完毕" << endl;
                close(fd);
                break;
            }
            num++; // 块编号自增
        }
        else if (5 == buf[1])
        {
            // 说明有错误信息
            cout << "____error:" << (buf + 4) << "____" << endl;
            if (flag == 1) // 表示文件已经被打开,出现错误也需要关闭
            {
                close(fd); // 关闭文件
            }
            break;
        }
    }
    return 0;
}
// 上传函数
int TFTPClient::doUpload()
{
    string filename; // 存储上传的文件名
    cout << "请输入要上传的文件名:";
    getline(cin, filename); // 输入上传的文件名(可以输入有空格的)

    // 检查文件是否存在
    int fd = open(filename.c_str(), O_RDONLY); // 以只读打开,不存在就报错
    if (fd < 0)
    {
        if (errno == ENOENT)
        {
            cout << "文件不存在" << endl;
            return -2;
        }
        ERR_LOG("open error");
        return -1;
    }
    // 程序执行至此表示文件存在

    // 构造上传请求
    char buf[BUFFER_SIZE] = "";
    int size = sprintf(buf, "%c%c%s%c%s%c", 0, 2, filename.c_str(), 0, "octet", 0);
    if (sendto(sfd, buf, size, 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        ERR_LOG("sendto error");
        close(fd);
        return -1;
    }
    // 循环接受服务器发来的消息
    int recv_len;           // 接受发的消息长度
    unsigned short num = 0; // 消息块
    socklen_t addrlen = sizeof(server_addr);
    int res; // 读取文件的返回值

    while (true)
    {
        // 清空容器
        bzero(buf, BUFFER_SIZE);
        // 读取服务器发来的消息
        recv_len = recvfrom(sfd, buf, BUFFER_SIZE, 0, (struct sockaddr *)&server_addr, &addrlen);
        if (recv_len < 0)
        {
            ERR_LOG("recvfrom error");
            close(fd);
            return -1;
        }
        // 判断消息类型
        if (4 == buf[1])
        {
            // 表示收到的是服务器发来的ACK包
            // 判断块编号
            if (num == ntohs(*(signed short *)(buf + 2)))
            {
                buf[1] = 3;                                // 修改成数据包
                num++;                                     // 块编号自增
                *(unsigned short *)(buf + 2) = htons(num); // 修改块编号
            }
            // 组装下一个数据包
            // 从文件中读取数据
            res = read(fd, buf + 4, BUFFER_SIZE - 4);
            if (res < 0)
            {
                ERR_LOG("read error");
                close(fd);
                break;
            }
            else if (res == 0)
            {
                // 表示文件已经读取完毕
                cout << "*******文件上传完毕********" << endl;
                break;
            }
            // 发送数据包
            if (sendto(sfd, buf, res + 4, 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
            {
                ERR_LOG("sendto error");
                close(fd);
                return -1;
            }
        }
        else if (5 == buf[1])
        {
            // 表示收到的是服务器发来的错误包
            cout << "********文件上传失败, 请检查网络********" << endl;
            break;
        }
    }

    return 0;
}
// 清屏函数
void TFTPClient::clearScream()
{
    cout << "请输入任意字符进行清屏" << endl;
    while (getchar() != '\n')
        ; // 吸收垃圾字符
}
// 等待输入函数//吸收垃圾字符
void TFTPClient::waitForInput()
{
    while (getchar() != '\n')
        ; // 吸收垃圾字符
}
// 展示菜单函数
void TFTPClient::showMenu()
{
    // 清屏
    system("clear");
    cout << "**********基于UDP的TFTP文件传输客户端**********" << endl;
    cout << "******************1.下载文件******************" << endl;
    cout << "******************2.上传文件******************" << endl;
    cout << "******************3.退出程序******************" << endl;
    cout << "*********************************************" << endl;
}

主程序

cpp 复制代码
#include"tftp.client.h"
#include<myhead.h>

int main(int argc, const char *argv[])
{
    //对输入的服务器ip地址进行判断
    if(argc < 2)
    {
        cout << "请输入ip地址" << endl;
        return -1;
    }

    //实例化对象
    try
    {
        TFTPClient client(argv[1]);//通过有参构造,构造一个客户端
        client.run();//执行客户端
    }
    catch(const std::exception& e)
    {
        std::cerr << e.what() << '\n';
        return -1;
    }

    return 0;
}

4> CMakeLists.txt

cpp 复制代码
cmake_minimum_required(VERSION 2.8)

project(tftp_client)

#设置c++标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 添加可执行文件
add_executable(${PROJECT_NAME}
    src/main.cpp
    src/tftp.client.cpp
)

#添加相关路径
target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/header)

2.3服务器端流程图

1> 主程序流程图

2> 下载请求流程图

3> 上传请求流程图

2.4服务器端实现

1> 头文件

cpp 复制代码
#ifndef TFTP_SERVER_H
#define TFTP_SERVER_H
#include<myhead.h>

//创建服务器类
class TFTPServer
{
    private:
    static const int PORT = 6666;//服务器端口号
    static const int BUFFER_SIZE = 516;//数据包最大容量

    int sfd; //服务器套接字
    struct sockaddr_in server_addr;//服务器地址信息结构体
    string root_dir;//文件服务器的根目录

    //处理客户端请求的函数
    //const char*filename:要发送的客户端文件名
    //client_addr:客户端地址信息结构体
    //addr_len:客户端地址信息结构体的长度
    void handleReadRequest(const char *filename, struct sockaddr_in &client_addr, socklen_t addr_len);//处理读请求
    
    //char *filename:将客户端发来的数据存储到的文件
    //client_addr:客户端地址信息结构体
    //addr_len:客户端地址信息结构体的长度
    void handleWriteRequest(const char *filename, struct sockaddr_in &client_addr, socklen_t addr_len);//处理写请求
    void sendError(const char *msg, struct sockaddr_in &client_addr);

    public:
    TFTPServer(const string &root = ".");//构造函数
    ~TFTPServer();//析构函数
    void run();//启动服务器执行函数

};
#endif

2> 源文件

cpp 复制代码
#include "tftp_server.h"

// 用户打印错误信息的宏
#define ERR_LOG(msg)                                                     \
    do                                                                   \
    {                                                                    \
        perror(msg);                                                     \
        cout << __LINE__ << "  " << __func__ << " " << __FILE__ << endl; \
    } while (0)

// 构造函数的定义
TFTPServer::TFTPServer(const string &root) : root_dir(root)
{
    // 创建UDP套接字
    sfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sfd < 0)
    {
        ERR_LOG("socket error");
        return;
    }

    // 填充地址信息结构体
    server_addr.sin_family = AF_INET;                // 通信域
    server_addr.sin_port = htons(PORT);              // 端口号
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 任何一个主机都可以向我发信息

    //设置端口号,快速重用
    int reuse = 1;
    if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
    {
        ERR_LOG("setsockopt error");
        return;
    }

    // 绑定工作
    if (bind(sfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        ERR_LOG("bind error");
        return;
    }
}
// 析构函数的定义
TFTPServer::~TFTPServer()
{
    if (sfd >= 0)
    {
        close(sfd);
    }
}
// 定义运行函数
void TFTPServer::run()
{
    // 输出提示信息
    cout << "TFTP Server started on port:" << PORT << endl;
    cout << "Serving file from: " << root_dir << endl;

    char buf[BUFFER_SIZE];                    // 定义缓冲区存储数据包
    struct sockaddr_in client_addr;           // 用来存储客户端地址结构体
    socklen_t addr_len = sizeof(client_addr); // 客户端地址结构体的大小

    // 循环服务器
    while (true)
    {
        // 接收客户端的请求包
        int n = recvfrom(sfd, buf, BUFFER_SIZE, 0, (struct sockaddr *)&client_addr, &addr_len);
        if (n < 0)
        {
            ERR_LOG("recvfrom error");
            return;
        }
        // 检查操作码
        if (buf[0] != 0)
            continue; // 说明协议错误

        // 解析请求包
        char *filename = buf + 2;                     // 文件名从buf[2]开始,解析请求文件名
        const char *mode = filename + strlen(filename) + 1; // 得到协议模式

        // 检查传输模式
        if (strcmp(mode, "octet") != 0)
        {
            sendError("Only binary mode is supported", client_addr); // 发送错误信息
            continue;
        }

        // 处理正常请求
        switch (buf[1])
        {
        case 1: // 读请求
            cout << "read request for : " << filename << endl;
            handleReadRequest(filename, client_addr, addr_len);
            break;
        case 2: // 写请求
            cout << "write request for : " << filename << endl;
            handleWriteRequest(filename, client_addr, addr_len);
            break;
        default:
            sendError("Unknown request", client_addr); // 处理请求异常
            break;
        }
    }
}

//处理读请求的函数
void TFTPServer::handleReadRequest(const char *filename, struct sockaddr_in &client_addr, socklen_t addr_len)
{
    //找到要下载的文件路径
    string full_path = root_dir + "/" + filename;//c++中字符串可以是+拼接
    //以只读的形式打开文件
    int fd = open(full_path.c_str(), O_RDONLY);
    if (fd < 0)
    {
        //要下载的文件在服务器不存在
        sendError("File not found", client_addr);
        return;
    }
    //执行至此,表示要执行的文件存在,开始发送数据包
    char buf[BUFFER_SIZE] = "";//数据包
    unsigned short block_num = 1;//块编号

    //循环发送数据包
    while(true)
    {
        //准备数据包
        buf[0] = 0;
        buf[1] = 3;//数据包的操作码为3
        *(unsigned short *)(buf + 2) = htons(block_num);//块编号封装到数据包

        //从头文件读取数据到buf + 4之后
        int n = read(fd, buf + 4, BUFFER_SIZE - 4);
        if(n < 0)
        {
            sendError("Read file error", client_addr);
            close(fd);
            return;
        }
        //执行至此,表示数据包已经封装好
        if(sendto(sfd, buf, n + 4, 0, (struct sockaddr *)&client_addr, addr_len) < 0)
        {
            ERR_LOG("sendto error");
            close(fd);
            return;
        }
        //程序执行至此,表示服务器成功发送一个数据包给客户端
        //等待客户端的ACK
        do
        {
            //收取客户端发来的ACK包
            if(recvfrom(sfd, buf, BUFFER_SIZE, 0, (struct sockaddr *)&client_addr, &addr_len) < 0)
            {
                ERR_LOG("recvfrom error");
                close(fd);
                return;
            }
        }while(buf[1] != 4 || ntohs(*(unsigned short *)(buf + 2)) != block_num);
        //判断当前的文件是否读取接受
        if(n < BUFFER_SIZE - 4)
        {
            //表示文件读取完毕
            break;
        }
        //执行至此,表示文件还没有读取完毕,继续发送下一个数据
        block_num++;//块编号加1
        //关闭文件描述符
        close(fd);
    }
}
//处理写请求的函数
void TFTPServer::handleWriteRequest(const char *filename, struct sockaddr_in &client_addr, socklen_t addr_len)
{
    //找到要上传的文件路径
    string full_path = root_dir + "/" + filename;//c++中字符串可以是+拼接
    //以只写的形式打开文件
    int fd = open(full_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0664);
    if (fd < 0)
    {
        sendError("File create error", client_addr);
        return;
    }
    //执行至此,表示文件创建成功
    //发送ACK
    char buf[BUFFER_SIZE] = "";//数据包
    unsigned short block_num = 0;//块编号
    buf[0] = 0;
    buf[1] = 4;//ACK包的操作码为4
    *(unsigned short *)(buf + 2) = htons(block_num);//块编号

    //发送ACK
    if(sendto(sfd, buf, 4, 0, (struct sockaddr*)&client_addr, addr_len) < 0)
    {
        ERR_LOG("sendto error");
        return;
    }

    //循环接受数据包
    while(true)
    {
        //读取数据包
        int n = recvfrom(sfd, buf, BUFFER_SIZE, 0, (struct sockaddr*)&client_addr, &addr_len);
        if(n < 0)
        {
            ERR_LOG("recvfrom error");
            close(fd);
            return;
        }
        //判断是否为数据包
        if(buf[1] == 3 && ntohs(*(unsigned short*)(buf + 2)) == block_num + 1)
        {
            //说明是数据包,将数据包中的数据,写入文件中
            if(write(fd, buf + 4, n - 4) < 0)
            {
                sendError("write error", client_addr);
                close(fd);
                return;
            }

            //向客户端发送一ACK
            block_num++;
            buf[0] = 0;
            buf[1] = 4;//说明是ACK包
            *(unsigned short *)(buf + 2) = htons(block_num);//块编号
            if(sendto(sfd, buf, 4, 0, (struct sockaddr *)&client_addr, addr_len) < 0)
            {
                ERR_LOG("sendto error");
                return;
            }
            //判断读取的数据长度和缓冲区长度大小
            if(n < BUFFER_SIZE)
            {
                break;//表示上传完成
            }
        }
    }
    close(fd);//关闭文件
}
void TFTPServer::sendError(const char *msg, struct sockaddr_in &client_addr)
{
    //封装错误包
    char buf[BUFFER_SIZE] = "";
    buf[0] = 0;
    buf[1] = 5; //错误码为5
    buf[2] = 0; //错误码为0,表示未定义的错误
    buf[3] = 1;//错误码
    strcpy(buf + 4, msg); //错误信息

    //将信息发送给客户端
    if(sendto(sfd, buf, strlen(msg) + 5, 0, (struct sockaddr *)&client_addr, sizeof(client_addr)) < 0)
    {
        ERR_LOG("sendto error");
        return;
    }
}

3> 主程序

cpp 复制代码
#include<myhead.h>
#include"tftp_server.h"//自定义类的头文件

int main(int argc, const char *argv[])
{

    try
    {
        //如果外部传入的要操作的文件路径,则使用操作的路径,否则使用课执行程序
        //所在的路径作为要操作的路径。
        string root_dir = (argc > 1) ? argv[1] : ".";
        //构造一个服务器
        TFTPServer server(root_dir);

        //运行服务器
        server.run();
    }
    catch(const std::exception& e)
    {
        std::cerr << e.what() << '\n';
        return -1;
    }
}

4> CMakeLists.txt

cpp 复制代码
cmake_minimum_required(VERSION 2.8)

project(tftp_server)

#设置c++标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 添加可执行文件
add_executable(${PROJECT_NAME}
    src/main.cpp
    src/tftp_server.cpp
)

#添加相关路径
target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/header)

2、线程池

一、什么是线程池,有什么作用

线程池是一种预先创建一组线程的机制,这些线程在应用程序启动时,就以及创建好了,等待执行 线程。当有新的任务需要执行时,线程池会从线程集合中,分配一个空闲的线程来执行该任务,而不是每 一个任务都重新创建一个线程

二、为什么要使用线程池

2.1 提高性能

创建和销毁线程时开销较大的操作,通过重用现有的线程,线程池减少了这种开销,进而提高程序 的性能

2.2 控制并发量

线程池运行你限制并发线程的数量,防止系统因创建过多的线程二出现资源耗尽的情况(如CPU过 载、内存不足)

2.3 简化线程管理

使用线程池可以避免手动管理线程的声明周期,减少代码的复杂性。线程池通常提供了任务排队和 任务调度的功能,使得多线程编程更加简化

2.4 使用场景

1> 服务器应用:对于套接字通信或者Web服务器通信

2> 高性能的计算:在需要进行高性能的计算时,线程池可以帮助分配和管理计算任务

3> 异步任务处理:在需要并行处理多个任务之间没有依赖关系时,线程池是一个理想的解决方案

三、线程池原理图

四、使用C++封装线程池的操作

1> 头文件

cpp 复制代码
#ifndef THREADPOOL_H
#define THREADPOOL_H

#include<vector>//用于存储多个线程
#include<thread>//线程的创建及其管理
#include<queue> //用于存储多个任务的队列
#include<condition_variable>//条件变量头文件
#include<mutex> //互斥锁头文件
#include<functional>//用于使用函数类型
#include<myhead.h>

class threadPool
{
private:
    //线程池相关的私有成员
    vector<thread> workers;//存储工作的线程容量
    queue<function<void()>> tasks;//存储任务的队列
    mutex task_mutex;//互斥锁
    condition_variable task_cv;//用于通知线程有新任务的条件变量
    bool stop;//线程池的标志,判断线程池是否停止

    //启动线程池
    void startThreadPool(size_t numThreads);
public:
    threadPool(int threadPoolSize);//构造函数声明
    ~threadPool();//析构函数声明

    void addTask(function<void()> task);//添加任务队列的线程池
};




#endif

2> 源文件

cpp 复制代码
#include "threadPool.h"

//构造函数定义
threadPool::threadPool(int ThreadPoolSize):stop(false)
{
    startThreadPool(ThreadPoolSize);//启动线程池
}
//析构函数定义
threadPool::~threadPool()
{
    {
        unique_lock<mutex> lock(task_mutex);//获取锁资源,用于保护stop变量
        stop = true;
    }
    //将所有的等待线程唤醒
    task_cv.notify_all();

    //将所有的工作线程回收
    for(auto &worker : workers)
    {
        worker.join();
    }

}
//将任务加到线程池中
void threadPool::addTask(function<void()> task)
{
    {
        unique_lock<mutex> lock(task_mutex);//获取锁资源,保护条件变量
        tasks.push(task);//将任务添加到任务队列
    }
    //唤醒一个等待的线程开始工作
    task_cv.notify_one();//唤醒一个线程去工作
}

//启动线程池函数的定义
void threadPool::startThreadPool(size_t numThreads)
{
    //循环创建多个线程
    for(int i = 0; i < numThreads; i++)
    {
        //创建一个任务并且将线程放入到线程的容器中
        workers.emplace_back([this]//使用lambda表达式
        {
            while(true)
            {
                function<void()> task;//创建一个任务
                {
                    unique_lock<mutex> lock(task_mutex);//加锁,保护任务队列
                    task_cv.wait(lock, [this]{//等待条件变量通知
                        return stop || !tasks.empty();//当线程停止工作或者任务队列不为空
                    });

                    if(stop || tasks.empty())
                    {
                        return;//当线程池停止或者任务队列为空,退出线程
                    }
                    task = move(tasks.front());//从任务队列中取出一个任务
                    tasks.pop();//将任务从任务队列中移除
                }
                task();//执行任务
            }
        });

    }
}

3> 主程序

cpp 复制代码
#include<myhead.h>
#include"threadPool.h"

int main(int argc, const char *argv[])
{
    threadPool mypool(4);//一个线程池初始化4个线程
    for(int i = 0; i < 20; i++)//添加20个任务到线程池
    {
        mypool.addTask([i](){
            //使用lambda表达式当做线程体函数
            cout << "任务:" << i << "正在执行,其tid:" << this_thread::get_id() << endl;
            //模拟任务执行时间,表示该任务执行100毫秒
            this_thread::sleep_for(std::chrono::milliseconds(500));
        });
    }
    //等待所有任务完成
    this_thread::sleep_for(std::chrono::seconds(5));

    return 0;
}

3、基于TCP的网络聊天室

一、原理图

二、服务器端实现逻辑

1> 头文件

cpp 复制代码
#ifndef CHATSERVER_H
#define CHATSERVER_H

#include<vector>//用于存储多个线程
#include<thread>//线程的创建及其管理
#include<queue> //用于存储多个任务的队列
#include<condition_variable>//条件变量头文件
#include<mutex> //互斥锁头文件
#include<functional>//用于使用函数类型
#include<myhead.h>

#define N 128 //消息缓冲区的大小
#define LOGIN 1 //表示登录消息类型
#define CHAT 2  //表示聊天消息类型
#define QUIT 3  //表示退出消息类型

//自定义服务器类
class chatServer
{
public:
    //定义消息结构体
    struct MSG{
        int type;//消息类型
        char name[20];//客户端名称
        char text[N];//消息内容

        //序列化内容,将消息结构体转为二进制数据,便于传输
        string serialize() const
        {
            string data;//临时变量存储要转换的字符串
            data.append(reinterpret_cast<const char*>(&type), sizeof(type));//将消息放入字符串
            data.append(name, sizeof(name));//将消息中的客户端名称放入字符串
            data.append(text, sizeof(text));//将消息正文放入字符串

            return data;//将拼接的字符串返回
        }
        //反序列化函数,将二进制数据转为结构体
        void deserialize(const string &data)
        {
            size_t offset = 0;//获取字符串是的偏移量

            memcpy(&type, data.c_str() + offset, sizeof(type));//将字符串的消息类型解析出来
            offset += sizeof(type);//偏移量向后偏移

            memcpy(name, data.c_str() + offset, sizeof(name));//将字符串的客户端名字解析出来
            offset += sizeof(name);

            memcpy(text, data.c_str() + offset, sizeof(text));//将字符串的消息正文解析出来
        }
    };
    //客户端结构体类型
    struct Client
    {
        int fd;//客户端套接字
        struct sockaddr_in cin;//客户端地址信息结构体
    };
private:
    int sfd;//服务器套接字
    vector<Client> clients;//在线的客户端列表
    mutex client_mutex;//保护clients客户端列表互斥锁
    //线程池相关变量
    vector<thread> workers;//存储工作的线程容量
    queue<function<void()>> tasks;//存储任务的队列
    mutex task_mutex;//互斥锁
    condition_variable task_cv;//用于通知线程有新任务的条件变量
    bool stop;//线程池的标志,判断线程池是否停止

    void errLog(const char *msg);//错误信息日志函数
    void startThreadPool(size_t numThreads);//启动线程池函数
    void addTask(function<void()> task);//将任务添加到线程池中

public:
    chatServer(const char *ip, int port, size_t threadPoolSize = 4);//构造函数声明
    ~chatServer();//析构函数声明
    void run();//运行服务器函数
    void handleClient(int client_fd, struct sockaddr_in cin);//处理客户端连接函数
    void broadcast(const MSG& msg, int exclude_fd = -1);//广播消息函数
};





#endif

2> 源文件

cpp 复制代码
#include "chatServer.h"

// 构造函数的定义,初始化服务器,创建套接字,绑定地址信息结构体,启动监听
chatServer::chatServer(const char *ip, int port, size_t threadPoolSize) : stop(false)
{
    // 创建服务器套接字
    sfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sfd < 0)
    {
        perror("socket error");
        return;
    }

    // 端口号快速复用
    int reuse = 1;
    if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
    {
        perror("setsockopt error");
        return;
    }

    // 填充地址信息结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;            // 通信域IPv4
    sin.sin_port = htons(port);          // 端口号
    sin.sin_addr.s_addr = inet_addr(ip); // ip地址

    // 绑定地址信息结构体
    if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
    {
        perror("bind error");
        return;
    }

    // 启动监听
    if (listen(sfd, 10) < 0)
    {
        perror("listen error");
        return;
    }

    // 启动线程池
    startThreadPool(threadPoolSize);
}

chatServer::~chatServer()
{
    stop = true;          // 设置停止标志,表示停止线程池
    task_cv.notify_all(); // 唤醒所有任务
    for (auto &worker : workers)
    {
        worker.join(); // 等待回收所有线程
    }
    close(sfd); // 关闭服务器套接字
}

// 启动线程池函数的定义
void chatServer::startThreadPool(size_t numThreads)
{
    // 循环创建多个线程
    for (int i = 0; i < numThreads; i++)
    {
        // 创建一个任务并且将线程放入到线程的容器中
        workers.emplace_back([this] // 使用lambda表达式
                             {
            while(true)
            {
                function<void()> task;//创建一个任务
                {
                    unique_lock<mutex> lock(task_mutex);//加锁,保护任务队列
                    task_cv.wait(lock, [this]{//等待条件变量通知
                        return stop || !tasks.empty();//当线程停止工作或者任务队列不为空
                    });

                    if(stop || tasks.empty())
                    {
                        return;//当线程池停止或者任务队列为空,退出线程
                    }
                    task = move(tasks.front());//从任务队列中取出一个任务
                    tasks.pop();//将任务从任务队列中移除
                }
                task();//执行任务
            } });
    }
}

// 将任务加到线程池中
void chatServer::addTask(function<void()> task)
{
    {
        unique_lock<mutex> lock(task_mutex); // 获取锁资源,保护条件变量
        tasks.push(task);                    // 将任务添加到任务队列
    }
    // 唤醒一个等待的线程开始工作
    task_cv.notify_one(); // 唤醒一个线程去工作
}

// 打印错误日志函数的定义
void chatServer::errLog(const char *msg)
{
    cerr << __FILE__ << " " << __func__ << " " << __FILE__ << endl;
    perror(msg);
}

// 启动服务器的函数的定义
void chatServer::run()
{
    // 循环等待客户端连接请求
    while (true)
    {
        struct sockaddr_in cin;       // 用于接收客户端的地址信息结构体
        socklen_t clen = sizeof(cin); // 客户端地址信息的长度

        // 阻塞接收客户端连接请求
        int client_fd = accept(sfd, (struct sockaddr *)&cin, &clen);
        if (client_fd < 0)
        {
            errLog("accept error");
            continue; // 继续接收下一个
        }

        // 将处理客户端的信息的任务,添加到线程池中
        addTask([this, client_fd, cin]
                {
            //函数体就是处理客户端的功能函数
            this->handleClient(client_fd, cin); });
    }
}

// 处理客户端消息的函数定义
void chatServer::handleClient(int client_fd, struct sockaddr_in cin)
{
    // 定义一个消息类型的变量, 用于接收客户端消息
    MSG msg;                  // 处理后的消息形态
    char buffer[sizeof(MSG)]; // 刚接收到的消息形态

    // 循环接收客户端消息
    while (true)
    {
        // 读取客户端消息
        int recv_len = recv(client_fd, buffer, sizeof(buffer), 0);
        // 当客户端下线或者recv函数错误
        if (recv_len <= 0)
        {
            // 表示客户端下线
            // 变量客户端容器
            auto it = clients.begin(); // 变量客户端容器的迭代器
            while (it != clients.end())
            {
                if (it->fd == client_fd) // 在容器中找到当前套接字
                {
                    clients.erase(it); // 将当前套接字从列表移除
                    break;
                }
                ++it; // 指针向后偏移
            }
            close(client_fd); // 关闭套接字
            break;
        }
        // 程序执行至此,表示收到某个客户端消息,需要进行反序列化
        msg.deserialize(string(buffer, recv_len));

        // 对消息类型进行判断
        switch (ntohl(msg.type))
        {
        case LOGIN:
        {
            // 表示客户端发的是登录消息
            // 定义一个互斥锁,保护客户端容器
            unique_lock<mutex> lock(client_mutex);
            Client new_client;
            // 将客户端的相关信息获取下来
            new_client.fd = client_fd;
            new_client.cin = cin;

            // 将新客户端放入客户端容器中
            clients.push_back(new_client);

            // 组装一条信息,广播所有客户端
            sprintf(msg.text, "--------%s 登录成功--------", msg.name);
            broadcast(msg); // 告诉所有用户,包括自己,该用户登录成功
            break;
        }
        case CHAT:
        {
            // 表示客户端发的是登录消息
            // 定义一个互斥锁,保护客户端容器
            unique_lock<mutex> lock(client_mutex);
            broadcast(msg, client_fd); // 除了自己,将消息转发给其他所有人
            break;
        }
        case QUIT:
        {
            // 表示客户端发的是登录消息
            // 定义一个互斥锁,保护客户端容器
            unique_lock<mutex> lock(client_mutex);
            auto it = clients.begin(); // 定义迭代器,遍历客户端容器
            while (it != clients.end())
            {
                if (it->fd == client_fd)
                {
                    clients.erase(it); // 将当前用户清除干净
                    break;
                }
                ++it; // 继续遍历下一个
            }
            sprintf(msg.text, "----------%s 退出聊天室----------", msg.name);
            // 将消息广播给所有人
            broadcast(msg);
            close(client_fd); // 关闭客户端套接字
            break;
        }
        default:
        {
            cout << "消息类型有误" << endl;
            break;
        }
        }
    }
}

// 定义广播的函数
void chatServer::broadcast(const MSG &msg, int exclude_fd)
{
    // 将要广播的消息进行序列号操作,转换成字符串
    string data = msg.serialize();

    // 将字符串转发给所有客户端
    for (const auto &client : clients)
    {
        if (client.fd != exclude_fd)
        {
            if (send(client.fd, data.c_str(), data.size(), 0) < 0)
            {
                perror("send error");
                continue;
            }
        }
    }
}

3> 主程序

cpp 复制代码
#include<myhead.h>
#include "chatServer.h"

int main(int argc, const char *argv[])
{
    //判断传参是否正确
    if(argc < 3)
    {
        cout << "请输入ip地址和端口号" << endl;
        return -1;
    }
    //传参正确,构造一个服务器
    chatServer server(argv[1], atoi(argv[2]));

    //启动服务器
    server.run();

    return 0;
}

4> CMakeLists.txt

cpp 复制代码
cmake_minimum_required(VERSION 2.8)

project(ChatServer)#可执行程序项目名

#设置C++标准
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")

#添加可执行文件
add_executable(${PROJECT_NAME}
    src/main.cpp
    src/chatServer.cpp
)

#添加文件路径
target_include_directories(${PROJECT_NAME}
    PRIVATE
    ${PROJECT_SOURCE_DIR}/header
)

#添加远程支持库
target_link_libraries(ChatServer pthread)

三、客户端实现

1> 头文件

cpp 复制代码
#ifndef CHATCLIENT_H
#define CHATCLIENT_H
#include <myhead.h>
#include <thread> //线程库

#define N 128   // 消息缓冲区的大小
#define LOGIN 1 // 表示登录消息类型
#define CHAT 2  // 表示聊天消息类型
#define QUIT 3  // 表示退出消息类型

//定义客户端类
class ChatClient
{
    // 定义一些公共类型
public:
    // 定义消息结构体
    struct MSG
    {
        int type;      // 消息类型
        char name[20]; // 客户端名称
        char text[N];  // 消息内容

        // 序列化内容,将消息结构体转为二进制数据,便于传输
        string serialize() const
        {
            string data;                                                      // 临时变量存储要转换的字符串
            data.append(reinterpret_cast<const char *>(&type), sizeof(type)); // 将消息放入字符串
            data.append(name, sizeof(name));                                  // 将消息中的客户端名称放入字符串
            data.append(text, sizeof(text));                                  // 将消息正文放入字符串

            return data; // 将拼接的字符串返回
        }
        // 反序列化函数,将二进制数据转为结构体
        void deserialize(const string &data)
        {
            size_t offset = 0; // 获取字符串是的偏移量

            memcpy(&type, data.c_str() + offset, sizeof(type)); // 将字符串的消息类型解析出来
            offset += sizeof(type);                             // 偏移量向后偏移

            memcpy(name, data.c_str() + offset, sizeof(name)); // 将字符串的客户端名字解析出来
            offset += sizeof(name);

            memcpy(text, data.c_str() + offset, sizeof(text)); // 将字符串的消息正文解析出来
        }
    };
    // 客户端结构体类型
    struct Client
    {
        int fd;                 // 客户端套接字
        struct sockaddr_in cin; // 客户端地址信息结构体
    };

    //类中私有成员
private:
    int cfd;//客户端套接字
    string name;//客户端名称
    bool running;//判断客户端是否允许的标志

    void errLog(const char *msg);//错误日志函数
    void sendMsg(int type, const string & text = "");//发送消息函数
    void recvMsg();//接收消息函数
public:
    ChatClient(const char *ip, int port, const string &name);//有参构造
    ~ChatClient();//析构函数

    void run();//客户端允许函数
};



#endif

2> 源文件

cpp 复制代码
#include "../header/chatClient.h"

// 构造函数的定义
ChatClient::ChatClient(const char *ip, int port, const string &name) : name(name), running(true)
{
    // 创建客户端套接字
    cfd = socket(AF_INET, SOCK_STREAM, 0);
    if (cfd < 0)
    {
        perror("socket error");
        return;
    }

    // 连接服务器
    // 1、初始化地址信息结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;            // 通信域
    sin.sin_port = htons(port);          // 端口号
    sin.sin_addr.s_addr = inet_addr(ip); // 服务器ip

    // 连接服务
    if (connect(cfd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
    {
        perror("connect error");
        return;
    }
    // 程序执行至此,表示连接服务器成功
    // 顺便向服务器发送登录信息
    sendMsg(LOGIN);
}

// 析构函数的定义
ChatClient::~ChatClient()
{
    sendMsg(QUIT); // 向服务器发送退出信息
    close(cfd);    // 关闭套接字
}
// 定义客户端
void ChatClient::run()
{
    // 创建一个分支线程,主要用于接收服务器的数据
    thread recvThread(&ChatClient::recvMsg, this);

    // 主线程向服务器发送信息
    string text; // 存储要发送的消息
    while (running)
    {
        // 从终端读用户要发送的消息
        getline(cin, text);

        // 判断是否为退出消息
        if (text == "quit")
        {
            // 标识客户端的状态为退出状态
            running = false;
            break;
        }

        // 调用发送消息函数
        sendMsg(CHAT, text);
    }
    recvThread.join(); // 回收线程资源
}

// 错误日志函数
void ChatClient::errLog(const char *msg)
{
    // 输出错误的文件、功能、对应的错误行
    cerr << __FILE__ << " " << __func__ << __FILE__ << endl;
    perror(msg);
}
// 发送消息函数
void ChatClient::sendMsg(int type, const string &text)
{
    MSG msg;                                           // 要发送的结构体
    msg.type = htonl(type);                            // 消息类型
    strncpy(msg.name, name.c_str(), sizeof(msg.name)); // 设置客户端的用户名
    strncpy(msg.text, text.c_str(), sizeof(msg.text)); // 消息正文

    // 将要发送的消息进行序列化处理
    string data = msg.serialize();

    // 将转换后的数据发送给服务器
    if (send(cfd, data.c_str(), data.size(), 0) < 0)
    {
        errLog("send error");
        return;
    }
    cout << "消息发送成功" << endl;

    // 判断是否为退出信息
    //  if(type == "quit")
    //  {
    //      running = false;//客户端退出状态
    //      exit(EXIT_SUCCESS);//退出进程
    //  }
}
// 接收消息函数
void ChatClient::recvMsg()
{
    // 用于接收消息
    char buffer[sizeof(MSG)];
    while (running)
    {
        // 非阻塞接收客户端信息
        int recv_len = recv(cfd, buffer, sizeof(buffer), MSG_DONTWAIT);
        if (recv_len < 0)
        {
            if (errno == EAGAIN)
            {
                // 休眠100ms
                usleep(100000);
            }
            else
            {
                errLog("recv error");
                running = false;
                break;
            }
        }
        else if (recv_len == 0)
        {
            // 错误
            errLog("recv error");
            running = false;
            break; // 连接关闭
        }
        else
        {
            // 将消息进行解析
            MSG msg;
            msg.deserialize(string(buffer, recv_len)); // 反序列化
            cout << msg.name << ":" << msg.text << endl;
        }
    }
}

3> 主程序

cpp 复制代码
#include"../header/chatClient.h"
#include<myhead.h>

int main(int argc, const char *argv[])
{  
    //确保执行时,外部传入了ip地址,端口号,用户名
    if(argc < 4)
    {
        cout << "请输入 端口号 IP 用户名" << endl;
        return -1;
    }

    //实例化一个客户端
    ChatClient client(argv[1], atoi(argv[2]), argv[3]);

    //运行客户端
    client.run();




    return 0;
}

4> 工程管理文件

cpp 复制代码
cmake_minimum_required(VERSION 2.8)

project(ChatClient)#可执行程序项目名

#设置C++标准
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")

#添加可执行文件
add_executable(${PROJECT_NAME}
    src/main.cpp
    src/chatClient.cpp
)

#添加文件路径
target_include_directories(${PROJECT_NAME}
    PRIVATE
    ${PROJECT_SOURCE_DIR}/header
)

#添加远程支持库
target_link_libraries(ChatClient pthread)

4、基于TCP的网络词典

一、sqlite3数据库

1.1数据库概念

1> 数据:能够被计算机识别、存储、处理的用于描述客观信息的符号就称为数据

2> 数据库:是在数据库管理系统管理和控制之下,存放在存储介质上的数据的集合称为数据库 对于程序而言,数据库本质上可以理解成外部文件操作

3> 记录:能够描述一条数据的完整信息称为一条记录,例如一个学生的完整信息

4> 字段:能够描述数据的不可分割的最小单位,例如学生信息中的性别、身份证号

5> 常用的数据库:

1、Oracle数据库:用于大型的数据库管理

2、mysql数据库:用于小型的关系数据库管理

3、sqlserver:也是用于小型的关系数据库

4、selite:主要用于嵌入式linux开发的轻量级的数据库

1.2sqlite3数据库基础

1>官网:https://sqlite.org/

2>一般linux系统是支持sqlite3数据库的,但是不一定安装头文件库了,想要安装sqlite3的头文 件库

bash 复制代码
sudo yum install sqlite-devel

验证是否装了sqlite3数据的头文件: ls /usr/include/sqlite3.h 如果不显示没有该文件,则说明,头文件库已经安装好了

1.3数据库的常规操作

学习资料网址:https://www.runoob.com/sqlite/sqlite-tutorial.html

1> 相关指令:在终端上直接可以执行的

1、创建数据库指令:sqlite3 数据库名字 即可创建/打开一个数据库

2、操作数据库分为两种:点命令和sql语句,点命令是以 点(.)开头的,没有分号结尾的, sql语句是没有点开头,必须以分号结尾

3、 点命令

常用点命令:

bash 复制代码
.table :能够展示当前数据库中有哪些数据表
.quit : 退出数据库操作的命令行
.shema :查看数据表的结构
.header on:展示数据信息时加表头
.mode column: 以列的形式左对齐

2> sql语句:专门用于对数据库进行增删改查操作的语法结构称为sql语句,所有的sql语句以分号 结束

1、创建数据表

sql 复制代码
1、 create table 表名 (字段名1 类型1,字段名2 类型2, 。。。 ,字段名n 类型n); //创建
一个数据表
2、 CREATE TABLE IF NOT EXISTS 表名 (字段名1 类型1,字段名2 类型2, 。。。 ,字段名n 类
型n); //即使创建同名的表不会报错

2、删除数据表

sql 复制代码
DROP TABLE database_name.table_name;

3、插入记录:有两种操作

sql 复制代码
1、 INSERT INTO TABLE_NAME [(column1, column2, column3,...columnN)] VALUES
(value1, value2,value3,...valueN); //可以选择指定字段添加
2、 INSERT INTO TABLE_NAME VALUES (value1,value2,value3,...valueN); //全部字段
添加

4、查询记录:

sql 复制代码
SELECT column1, column2, columnN FROM table_name; //通过指定字段查找记录
SELECT * FROM table_name; //查询所有信息

5、修改记录

sql 复制代码
UPDATE table_name SET column1 = value1, column2 = value2...., columnN = valueN
WHERE [condition];

6、删除记录

sql 复制代码
DELETE FROM table_name WHERE [condition];

1.4 sqlite3数据库编程

在程序中对数据库操作,无外乎三种:打开数据库、执行sql语句、关闭数据库,想要使用这些函 数,需要链接sqlite3库, -lsqlite3

1> 打开数据库函数

cpp 复制代码
int sqlite3_open(
const char *filename, /* Database filename (UTF-8) */
sqlite3 **ppDb /* OUT: SQLite db handle */
);
/**
功能:打开或者创建一个数据库并打开
参数1:要打开的数据库名称,是一个字符串
参数2:返回打开的数据库句柄,是数据库的指针。如果没有内存分配sqlite3对象,那么会返回NULL
返回值:成功创建或打开数据库返回SQLITE_OK(0),失败返回错误码(注意不是置位错误码)
*/

2> 处理错误信息的函数

cpp 复制代码
int sqlite3_errcode(sqlite3 *db); //返回数据库句柄操作中错误码的值
const char *sqlite3_errmsg(sqlite3*); //返回数据库句柄操作中的错误信息

3> 关闭数据库

cpp 复制代码
int sqlite3_close(sqlite3*);
/**
功能:关闭数据库句柄指针操作的数据库
参数:数据库句柄指针
返回值:成功返回SQLITE_OK,失败返回错误码
*/

4> 执行sql语句的函数

cpp 复制代码
int sqlite3_exec(
sqlite3*, /* 已经打开的数据库句柄指针 */
const char *sql, /* 要被执行的sql语句 */
int (*callback)(void*,int,char**,char**), /* 回调函数 */
void *, /* 回调函数的第一个参数 */
char **errmsg /* 错误信息 */
);
/**
功能:在指定的数据库中执行给顶的sql语句
返回值:成功返回SQLITE_OK失败返回错误码
有关该函数的第三个参数:是一个函数指针,需要传递一个功能函数
如果sql语句是执行数据库的增删改操作,那么第三个参数填NULL即可
如果sql语句是进行查找操作,那么必须传递一个功能参数,查找的结果集中的每
个结果都会执行该函数
*/

使用C语言实现一个简单的学生管理系统,用于练习上述函数

二、网络电子词典服务器端实现

2.1流程设计

2.2 主要模块

1> 数据库模块:两个数据库,用户数据库、单词数据库 单词数据库:存储四级单词词汇表 用户数据库:两个表:

1、存储用户名和用户的密码

2、用户查询历史记录表

2> 服务器模块:创建服务器套接字、绑定IP和端口号、启动监听、接收连接请求、分线程管理客 户端

3> 数据模块:

1、传输数据类型: 用户注册类型、用户登录类型、用户退出类型、单词查询类型、查询历史记录类型

2、数据类型: 该数据传输的类型 用户名 传输文本 注意:也要进行数据转换:网络字节序和主机字节序的转换

2.3 代码实现

项目结构:

1> database_manager.cpp

cpp 复制代码
#include "database_manager.hpp" //包含自定义的头文件
#include <fstream>              //包含io操作
#include <ctime>                //时间的头文件
#include <stdexcept>            //异常处理头文件

#define DICT_PATH "./dict.txt" // 单词文本的头文件

/*
    构造函数:打开数据库连接
    参数1:用户数据库的头文件路径
    参数2:词典数据库的文件路径
    返回值:如果数据库打开失败,则返回异常处理:runtime_error
*/

DatabaseManager::DatabaseManager(const string &usr_db_path, const string &dict_db_path)
{
    // 打开或创建用户数据库
    if (sqlite3_open(usr_db_path.c_str(), &usr_db_) != SQLITE_OK)
    {
        throw runtime_error("用户数据库打开失败: " + string(sqlite3_errmsg(usr_db_))); // 抛出异常
    }

    // 打开或创建单词数据库
    if (sqlite3_open(dict_db_path.c_str(), &dict_db_) != SQLITE_OK)
    {
        throw runtime_error("单词数据库打开失败: " + string(sqlite3_errmsg(dict_db_))); // 抛出异常
    }
}

/*
    析构函数:关闭数据库连接
*/
DatabaseManager::~DatabaseManager()
{
    sqlite3_close(usr_db_);  // 关闭用户数据库
    sqlite3_close(dict_db_); // 关闭单词数据库
}

/*
    初始化数据库:创建相关数据库,并且导入单词库
*/
bool DatabaseManager::initializeDatabase()
{
    return initializeUserDB() && initializeDictDB(); // 用户表和单词表都创建成功后,表示数据库初始化成功
}

/*
    初始化用户表:创建一个表
*/
bool DatabaseManager::initializeUserDB()
{
    // 使用互斥锁锁定数据库的操作,防止出现多线程概念
    lock_guard<mutex> lock(usr_mutex_);

    // 准备sql语句,创建用户表和历史记录表的sql语句
    const char *sql = "create table if not exists usr("     // 创建用户表
                      "name text primary key,"              // 用户名(主键)
                      "passwd int,"                         // 密码
                      "stage int);"                         // 用户状态:0表示离线 1表示在线
                      "create table if not exists history(" // 创建历史记录表
                      "name text,"                          // 用户名
                      "word text,"                          // 查询的单词
                      "mean text,"                          // 单词意思
                      "time text);";                        // 查询时间
    // 执行sql语句
    if (!executeSQL(usr_db_, sql))
    {
        cerr << "用户数据表初始化失败" << endl;
        return false;
    }

    // 重置所有的客户端都为离线状态
    sql = "update usr set stage=0;";
    if (!executeSQL(usr_db_, sql))
    {
        cerr << "用户状态重置失败" << endl;
        return false;
    }
    return true; // 表示两个数据表创建成功,并且将所有客户端设置成离线状态
}

/*
    初始化单词数据库的表结构,并且进行单词的导入
*/
bool DatabaseManager::initializeDictDB()
{
    // 创建互斥锁,保护单词表
    lock_guard<mutex> lock(dict_mutex_);
    // 准备创建单词数据库的sql语句
    const char *sql = "create table if not exists dict(word text, mean text);";
    if (!executeSQL(dict_db_, sql))
    {
        cerr << "单词表创建失败" << endl;
        return false;
    }
    // 检查词典数据库是否为空
    sql = "select count(*) from dict;"; // 查询数据库中的记录个数
    sqlite3_stmt *stmt;                 // 接收预处理sql的结果
    // 将上面的sql预编译一下
    // 函数介绍:将sql语句进行预编译一遍,用于后面的执行
    // 参数1:数据库句柄
    // 参数2:要编译的sql语句
    // 参数3:-1表示sql是一个字符串
    // 参数4:编译后的结果存放的位置
    // 参数5:接收没有处理的剩余的sql语句
    if (sqlite3_prepare_v2(dict_db_, sql, -1, &stmt, NULL) != SQLITE_OK)
    {
        cerr << "sql准备失败" << sqlite3_errmsg(dict_db_) << endl;
        return false;
    }
    bool need_import = false; // 标识是否需要导入数据
    // 执行sql语句
    if (sqlite3_step(stmt) == SQLITE_ROW) // 表示成功执行
    {
        if (sqlite3_column_int(stmt, 0) == 0) // 表示数据为空
        {
            // 表示表为空,需要导入数据
            need_import = true;
        }
    }
    // 释放stmt空间
    // 功能:是否预编译sql语句的空间
    sqlite3_finalize(stmt);
    // 如果需要导入,返回导入函数的结果,如果已经有了数据表内容,则直接返回
    return need_import ? dictToDatabase() : true;
}
/*
    将单词文件中的信息导入到数据库中
*/
bool DatabaseManager::dictToDatabase()
{
    // 使用文件操作,打开文件dict.txt
    ifstream file(DICT_PATH);
    if (!file)
    {
        cerr << "无法打开词典文件:" << DICT_PATH << endl;
        return false;
    }
    string line;                // 用于读取文件中的一行数据
    while (getline(file, line)) // 循环从文件中读取一行信息放入到line中
    {
        // 跳过空格
        if (line.empty())
            continue;
        // 分别单词和意思(格式,单词 意思)
        size_t pos = line.find(' '); // 定位到空格所在的位置
        if (pos == string::npos)     // 表示没有该位置,也就是说改行没有空格
        {
            cerr << "无效的词典条目(缺少空格分隔)" << line << endl;
            continue;
        }
        // 将单词和含义分别存储
        string word = line.substr(0, pos);  // 将当前行中的单词截取处理
        string mean = line.substr(pos + 1); // 将当前行中的单词含义截取出来

        // 准备sql语句
        const char *sql = "insert into dict values(?,?)";
        // 预编译sql语句
        sqlite3_stmt *stmt = NULL;
        if (sqlite3_prepare_v2(dict_db_, sql, -1, &stmt, NULL) != SQLITE_OK)
        {
            cerr << "sql准备失败" << sqlite3_errmsg(dict_db_) << endl;
            file.close();
            return false;
        }
        // 预编译成功,可以给已经预编译成功的内容绑定参数
        sqlite3_bind_text(stmt, 1, word.c_str(), -1, SQLITE_STATIC);
        sqlite3_bind_text(stmt, 2, mean.c_str(), -1, SQLITE_STATIC);
        // 执行插入语句
        if (sqlite3_step(stmt) != SQLITE_DONE)
        {
            cerr << "数据插入失败" << sqlite3_errmsg(dict_db_) << endl;
            sqlite3_finalize(stmt);
            file.close();
            return false;
        }
        // 程序执行至此,表示该单词已经导入到数据库了
        // 是否stmt的资源
        sqlite3_finalize(stmt);
    }
    // 程序执行至此,表示,单词表导入成功
    file.close();
    cout << "单词库导入成功" << endl;
    return true;
}

// 执行sql语句函数
bool DatabaseManager::executeSQL(sqlite3 *db, const string &sql, char **errmsg)
{
    if (sqlite3_exec(db, sql.c_str(), NULL, NULL, errmsg) != SQLITE_OK)
    {
        cerr << "sql执行失败:" << *errmsg << endl;
        // 释放错误信息空间
        sqlite3_free(errmsg);
        return false;
    }
    return true; // 表示正常执行了sql语句
}

/*
    注册功能:注册时,将用户输入的账号和密码存放到usr数据表中,如果该用户名已经存在,则给出报错信息
    参数1:用户名
    参数2:密码
*/
bool DatabaseManager::registerUser(const string &name, const string &password)
{
    // 使用互斥锁锁定数据库操作,防止出现多线程竞态
    lock_guard<mutex> lock(usr_mutex_);

    // 准备sql语句 : ?表示通配符,用于后面的绑定,0表示默认注册时,所有用户都是离线状态
    const char *sql = "insert into usr values(?,?,0);";
    // 预编译sql语句
    sqlite3_stmt *stmt = NULL;
    if (sqlite3_prepare_v2(usr_db_, sql, -1, &stmt, NULL) != SQLITE_OK)
    {
        cerr << "sql准备失败" << sqlite3_errmsg(dict_db_) << endl;
        return false;
    }
    // 预编译成功,可以给已经预编译成功的内容绑定参数
    sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 2, password.c_str(), -1, SQLITE_STATIC);

    // 执行插入操作
    int result = sqlite3_step(stmt);
    sqlite3_finalize(stmt);
    
    //对执行结果进行判断:该用户名已存在(失败)用户名不存在(注册成功)
    if (result == SQLITE_CONSTRAINT)
    {
        cerr << "该用户名已经存在:" << name << endl;
        return false; // 违反了主键唯一性
    }
    // 程序执行至此,表示注册成功
    return result == SQLITE_DONE;
}
/*
    用户登录功能
    参数1:用户名
    参数2:登录密码
    参数3:表示是否已经在线,如果已经在线,则登录失败,防止重复登录
*/
bool DatabaseManager::loginUser(const string &name, const string &password, bool &is_online)
{
    // 使用互斥锁锁定数据库的操作,防止出现多线程竞态
    lock_guard<mutex> lock(usr_mutex_);
    // 准备sql语句
    const char *sql = "select stage from usr where name=? and passwd=?;";
    // 预编译sql语句
    sqlite3_stmt *stmt = NULL;
    if (sqlite3_prepare_v2(usr_db_, sql, -1, &stmt, NULL) != SQLITE_OK)
    {
        cerr << "sql准备失败" << sqlite3_errmsg(dict_db_) << endl;
        return false;
    }
    // 预编译成功,可以给已经预编译成功的内容绑定参数
    sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 2, password.c_str(), -1, SQLITE_STATIC);
    // 执行查询操作
    int result = sqlite3_step(stmt);
    // 对执行结果进行判断
    if (result == SQLITE_ROW)
    {
        // 获取用户状态:0表示离线,1表示在线
        is_online = sqlite3_column_int(stmt, 0) == 1;
        // 如果当前用户不在线,更新状态为在线
        if (!is_online)
        {
            sqlite3_finalize(stmt); // 先释放空间
            // 准备sql语句
            const char *update_sql = "update usr set stage=1 where name=?;";
            if (sqlite3_prepare_v2(usr_db_, update_sql, -1, &stmt, NULL) != SQLITE_OK)
            {
                cerr << "sql准备失败:" << sqlite3_errmsg(usr_db_) << endl;
                return false;
            }
            // 绑定信息
            sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);
            result = sqlite3_step(stmt);
        }
    }

    // 释放空间
    sqlite3_finalize(stmt);
    return result == SQLITE_DONE;
}
/*
    用户退出功能:将该用户对对应的状态调整成0即可
    参数:用户名
*/
bool DatabaseManager::logoutUser(const string &name)
{
    // 使用互斥锁锁定数据库的操作,防止出现多线程竞态
    lock_guard<mutex> lock(usr_mutex_);
    // 准备sql语句
    const char *sql = "update usr set stage=0 where name=?;";
    // 预编译sql语句
    sqlite3_stmt *stmt = NULL;
    if (sqlite3_prepare_v2(usr_db_, sql, -1, &stmt, NULL) != SQLITE_OK)
    {
        cerr << "sql准备失败" << sqlite3_errmsg(dict_db_) << endl;
        return false;
    }
    // 绑定信息
    sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);
    // 执行更新操作
    int result = sqlite3_step(stmt);
    sqlite3_finalize(stmt);
    return result == SQLITE_DONE;
}
/*
    查单词功能
    参数1:要查找的单词
    参数2:单词的含义
*/
bool DatabaseManager::querryWord(const string &word, string &meaning)
{
    // 使用互斥锁锁定数据库的操作,防止出现多线程竞态
    lock_guard<mutex> lock(dict_mutex_);
    // 准备sql语句
    const char *sql = "select mean from dict where word=?;";
    // 预编译sql语句
    sqlite3_stmt *stmt = NULL;
    if (sqlite3_prepare_v2(dict_db_, sql, -1, &stmt, NULL) != SQLITE_OK)
    {
        cerr << "sql准备失败" << sqlite3_errmsg(dict_db_) << endl;
        return false;
    }
    // 绑定信息
    sqlite3_bind_text(stmt, 1, word.c_str(), -1, SQLITE_STATIC);
    // 执行查询
    bool found = false; // 标识是否查询到
    if (sqlite3_step(stmt) == SQLITE_ROW)
    {
        // 获取查询结果
        const unsigned char *result = sqlite3_column_text(stmt, 0);
        if (result)
        {
            meaning = reinterpret_cast<const char *>(result); // 转换数据类型
            found = true;                                     // 标识找到了
        }
    }
    // 释放空间
    sqlite3_finalize(stmt);
    return found;
}

/*
    历史记录函数:将给定的信息插入到历史数据库中
    参数1:用户名
    参数2:单词
    参数3:含义
    参数4:查询时间
*/
bool DatabaseManager::recordHistory(const string &name, const string &word, const string &meaning, const string &time)
{
    // 使用互斥锁锁定数据库的操作,防止出现多线程竞态
    lock_guard<mutex> lock(usr_mutex_);
    // 准备失去了语句
    const char *sql = "insert into history values(?,?,?,?);";
    // 预编译sql语句
    sqlite3_stmt *stmt = NULL;
    if (sqlite3_prepare_v2(usr_db_, sql, -1, &stmt, NULL) != SQLITE_OK)
    {
        cerr << "sql准备失败" << sqlite3_errmsg(usr_db_) << endl;
        return false;
    }
    // 绑定信息
    sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 2, word.c_str(), -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 3, meaning.c_str(), -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 4, time.c_str(), -1, SQLITE_STATIC);
    // 执行插入操作
    int result = sqlite3_step(stmt);
    sqlite3_finalize(stmt);
    return result == SQLITE_DONE;
}
/*
    获取查单词的历史记录
    参数1:用户名
    参数2:查询的结果字符串
*/
bool DatabaseManager::getHistory(const string name, string &history)
{
    // 使用互斥锁锁定数据库的操作,防止出现多线程竞态
    lock_guard<mutex> lock(usr_mutex_);
    // 准备sql语句
    const char *sql = "select word,mean,time from history where name=? order by time DESC;";
    // 预编译sql语句
    sqlite3_stmt *stmt = NULL;
    if (sqlite3_prepare_v2(usr_db_, sql, -1, &stmt, NULL) != SQLITE_OK)
    {
        cerr << "sql准备失败" << sqlite3_errmsg(usr_db_) << endl;
        return false;
    }
    // 绑定信息
    sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);
    // 执行查询语句
    history.clear(); // 防止之前的数据干扰
    while (sqlite3_step(stmt) == SQLITE_ROW)
    {
        // 获取该信息的每一列
        const unsigned char *word = sqlite3_column_text(stmt, 0); // 获取查询的单词
        const unsigned char *mean = sqlite3_column_text(stmt, 1); // 获取查询的含义
        const unsigned char *time = sqlite3_column_text(stmt, 2); // 获取查询的时间
        // 将数据组成字符串
        if (word && mean && time)
        {
            history += string(reinterpret_cast<const char *>(word)) + "\t";
            history += string(reinterpret_cast<const char *>(mean)) + "\t";
            history += string(reinterpret_cast<const char *>(time)) + "\n";
        }
    }
    sqlite3_finalize(stmt);
    return true;
}

2> database_manager.hpp

cpp 复制代码
#ifndef DATABASE_MANAGER_H
#define DATABASE_MANAGER_H

#include<myhead.h>
#include<sqlite3.h>//数据库头文件
#include<string>//字符串头文件
#include<mutex> //互斥锁头文件
#include<memory>//智能指针头文件

class DatabaseManager
{
private:
    sqlite3 *usr_db_;//用户数据库指针
    sqlite3 *dict_db_;//单词数据库指针
    mutex usr_mutex_;//用户库互斥锁
    mutex dict_mutex_;//单词库互斥锁

    //定义私有函数
    bool initializeUserDB();//初始化用户表
    bool initializeDictDB();//初始化单词表
    bool dictToDatabase();//导入词典数据

    //执行sql语句的函数
    bool executeSQL(sqlite3 *db, const string &sql, char ** errmsg = NULL);

public:
    //构造函数:需要外部传入用户库的路径以及单词库的路径
    DatabaseManager(const string &usr_db_path, const string &dict_db_path);
    ~DatabaseManager();

    //初始化数据表
    bool initializeDatabase();

    //用户相关操作
    bool registerUser(const string &name, const string &password);//用户注册操作
    bool loginUser(const string &name, const string &password, bool &is_online);//用户登录操作
    bool logoutUser(const string &name);//用户退出操作

    //单词表操作
    bool querryWord(const string &word, string &meaning);//查询单词
    bool recordHistory(const string &name, const string &word, const string &meading, const string &time);//历史记录
    bool getHistory(const string name, string &history);

};





#endif

3> main.cpp

cpp 复制代码
#include <myhead.h>
#include"server.hpp"
#include "database_manager.hpp"

int main(int argc, const char *argv[])
{
    // 判断外部是否传入的三个参数
    if (argc != 3)
    {
        cerr << "用法:" << argv[0] << "<IP> <port>" << endl;
        return -1;
    }
    try
    {
        // 构造一个数据库管理对象,用于管理两个数据库
        auto db_manager = make_shared<DatabaseManager>("usr.db", "dict.db");
        // 初始化数据库
        if (!db_manager->initializeDatabase())
        {
            cerr << "数据库初始化失败,请检查:" << endl;
            cerr << "1、dict.txt文件是否存在" << endl;
            cerr << "2、当前目录是否具有写权限" << endl;
            return -1;
        }
        // 构造一个服务器
        Server server(db_manager, argv[1], atoi(argv[2]));
        // 启动服务器
        server.start();
    }
    catch (const std::exception &e)
    {
        std::cerr << e.what() << endl;
        return -1;
    }
    std::cout << "Hello, World!" << std::endl;

    return 0;
}

4> message.hpp

cpp 复制代码
#ifndef MESSAGE_HPP
#define MESSAGE_HPP

#include<myhead.h>

//操作类型的定义
#define R 1 //用户注册
#define L 2 //用户登录
#define Q 3 //用户退出
#define S 4 //单词查询
#define H 5 //历史记录

//定义客户端与服务器进行数据通信的协议
struct Msg
{
    int type; //操作类型
    char name[20];//用户名
    char text[128]; //文本内容 (密码/单词)

    //定义相关字节序的转换函数
    //主机字节序转网络字节序
    void networkByteOrder()
    {
        type = htonl(type);
    }

    //网络字节序转主机字节序
    void hostByteOrder()
    {
        type = ntohl(type);
    }
};



#endif

5> server.cpp

cpp 复制代码
#include "server.hpp"  //服务器头文件
#include "message.hpp" //消息协议头文件
#include <myhead.h>
#include <ctime>  //有关事件的头文件
#include <thread> //C++线程支持库

Server::Server(shared_ptr<DatabaseManager> db_manager, const string ip, int port)
    : db_manager_(db_manager), ip_(ip), port_(port), running_(false) // 初始化列表
{
    // 创建用于连接的套接字相关文件描述符
    sfd_ = socket(AF_INET, SOCK_STREAM, 0);
    if (sfd_ < 0)
    {
        throw runtime_error("socket error"); // 抛出异常
    }

    // 设置端口号快速重用
    int opt = 1;
    if (setsockopt(sfd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
    {
        close(sfd_);
        throw runtime_error("setsockopen error");
    }

    // 绑定ip地址和端口号
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(port);                  // 端口号
    sin.sin_addr.s_addr = inet_addr(ip.c_str()); // ip地址

    if (bind(sfd_, (sockaddr *)&sin, sizeof(sin)) < 0)
    {
        close(sfd_);
        throw runtime_error("bind error");
    }
}

/*
    析构函数:停止服务器运行
*/
Server::~Server()
{
    stop();
}

// 启动服务器函数
bool Server::start()
{
    // 启动监听
    if (listen(sfd_, 5) < 0)
    {
        cerr << "listen error" << endl;
        return false;
    }

    // 设置运行状态
    running_ = true; // 标识可以接收客户端请求了
    cout << "Server started on " << ip_ << ":" << port_ << endl;

    // 等待客户端的连接请求:循环完成
    while (running_)
    {
        sockaddr_in client_addr;                  // 接收客户端地址信息结构体
        socklen_t addr_len = sizeof(client_addr); // 接收客户端地址结构体的大小

        // 接收客户端请求连接操作
        int cfd = accept(sfd_, (struct sockaddr *)&client_addr, &addr_len);
        if (cfd < 0)
        {
            if (running_)
            {
                cerr << "accept error" << endl;
            }
            continue;
        }
        // 创建一个分支线程,在分支线程中,进行跟客户端的通信
        thread([this, cfd, client_addr]
               {
            cout << "Client connected : " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << endl;

            //调用客户端处理函数
            handleClient(cfd, client_addr); })
            .detach(); // 将线程设置或分离态,资源有系统回放
    }
    return true;
}

/*
停止服务器函数
//关闭套接字文件描述符
*/
bool Server::stop()
{
    if (running_)
    {
        running_ = false; // 非运行
        close(sfd_);      // 关闭服务器套接字
        cout << "Server stopped" << endl;
        return true;
    }
    return false;
}

/*
    处理客户端请求,分为不同的请求模式,接收客户端消息,根据不同类型进行不同的处理
    参数1:客户端套接字文件描述符
    参数2:客户端地址信息结构体
*/
void Server::handleClient(int cfd, sockaddr_in client_addr)
{
    Msg msg; // 用于存储收发的数据
    while (true)
    {
        // 接收客户端消息
        ssize_t recv_len = recv(cfd, &msg, sizeof(msg), 0);
        if (recv_len <= 0)
        {
            if (recv_len == 0) // 当前客户端已经下线
            {
                cout << "Client disconnect :" << inet_ntoa(client_addr.sin_addr) << endl;
            }
            else
            {
                cerr << "recv error" << endl;
            }
            break;
        }
        // 程序执行至此,表示正常读取到客户端的消息
        // 转换字节序
        msg.hostByteOrder();
        // 准备响应消息
        Msg response;                                            // 用于回复消息的容器
        strncpy(response.name, msg.name, sizeof(response.name)); // 表示要给谁传递消息
        response.type = msg.type; // 回复的消息类型
        // 根据不同的消息请求处理不同的响应
        switch (msg.type)
        {
        case R: // 注册
        {
            // 执行注册操作
            int success = db_manager_->registerUser(msg.name, msg.text);
            strcpy(response.text, success ? "**OK**" : "**EXISTS**");
            // 如果成功注册回复OK,否则回复EXISTS
            break;
        }
        case L: // 登录
        {
            // 执行登录操作
            bool is_online = false; // 返回是否已经在线
            bool success = db_manager_->loginUser(msg.name, msg.text, is_online); // 执行登录函数
            if (success)
            {
                strcpy(response.text, is_online ? "**EXISTS**" : "**OK**");
                // 根据是否已经在线,进行回复
            }
            else
            {
                strcpy(response.text, "**FAIL**"); // 表示登录失败
            }
            break;
        }
        case Q: // 退出
        {
            // 执行退出操作
            db_manager_->logoutUser(msg.name);
            close(cfd);
            break;
        }
        case S: // 查询
        {
            // 执行查单词操作
            string meaning; // 报错单词含义
            if (db_manager_->querryWord(msg.text, meaning))
            {
                // 成功找到,将单词和含义一起回复给客户端
                snprintf(response.text, sizeof(response.text), "%s %s", msg.text, meaning.c_str());
            }
            else
            {
                strcpy(response.text, "Not Found"); // 表示没找到
            }
            // 保存查单词记录
            time_t now = time(NULL);     // 获取当前系统时间对应的秒数
            tm *local = localtime(&now); // 将以秒数为单位的时间转变成时间的结构体 
            char time_str[20] = "";   // 保存当前系统时间的字符串
            strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", local); // 将结构体中的年月日时分秒转变成格式串
            // 调用历史查询记录
            db_manager_->recordHistory(msg.name, msg.text, meaning, time_str);
            break;
        }
        case H: // 历史记录
        {
            // 执行查看历史记录操作
            string history; // 保存得到的历史记录
            if (db_manager_->getHistory(msg.name, history))
            {
                // 表示执行成功,history中就有了历史记录
                strncpy(response.text, history.c_str(), sizeof(response.text)); // 将历史记录发送给客户端
            }
            else
            {
                strcpy(response.text, "No history"); // 当前用户没有进行查单词操作
            }
            break;
        }
        default:
            strcpy(response.text, "Ivalid Command"); // 表示请求非法
        }
        // 发送请求
        response.networkByteOrder(); //转变成网络字节序
        if (send(cfd, &response, sizeof(response), 0) < 0)
        {
            cerr << "send error" << endl;
            break;
        }
        // 判断如果是查看历史记录,那么最后还要给出一个历史查询结束的情况
        if (msg.type == H)
        {
            strcpy(response.text, "**OVER**");
            send(cfd, &response, sizeof(response), 0);
        }
    }
    close(cfd); // 关闭客户端套接字
}
// 获取当前系统时间的静态成员函数
string Server::getCurrentTime()
{
    // 保存查单词记录
    time_t now = time(NULL);                                          // 获取当前系统时间对应的秒数
    tm *local = localtime(&now);                                      // 将以秒数为单位的时间转变成时间的结构体
    char time_str[20] = "";                                           // 保存当前系统时间的字符串
    strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", local); // 将结构体中的年月日时分秒转变成格式串
    return string(time_str);
}

6> server.hpp

cpp 复制代码
#ifndef SERVER_HPP
#define SERVER_HPP

#include"database_manager.hpp"//数据库相关头文件
#include<memory>//智能指针头文件
#include<myhead.h>

//定义电子词典服务器类
class Server
{
private:
    int sfd_;//服务器套接字文件描述符
    int port_;//端口号
    string ip_;//ip地址
    bool running_;//运行标识
    shared_ptr<DatabaseManager> db_manager_;//数据库管理器

    //处理客户端函数
    void handleClient(int sfd, sockaddr_in client_addr);

    //提供一个获取当前系统时间的函数
    static string getCurrentTime();

public:
    //构造函数:需要使用数据库管理对象、ip地址、端口号进行构造一个服务器
    Server(shared_ptr<DatabaseManager> db_manager, const string ip, int port);
    ~Server();//析构函数

    //启动服务器
    bool start();
    //停止服务器
    bool stop();

};







#endif

7> CMakeLists.txt

cpp 复制代码
cmake_minimum_required(VERSION 3.12)
project(dict_server CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(SQLite3 REQUIRED)
find_package(Threads REQUIRED)

add_executable(server
    src/main.cpp
    src/server.cpp
    src/database_manager.cpp
    )

target_include_directories(server PRIVATE ${CMAKE_SOURCE_DIR}/src)
target_link_libraries(server PRIVATE SQLite::SQLite3 Threads::Threads)

三、网络电子词典客户端实现

3.1 流程设计

3.2代码实现项目逻辑

2> client.cpp

cpp 复制代码
#include "client.hpp"

// 主程序循环
void DictClient::run()
{
    while (true)
    {
        showMainMenu(); // 展示主菜单
        int choice;     // 选择功能
        cin >> choice;
        cin.ignore(); // 清除缓冲区

        // 对选择内容进行分支
        switch (choice)
        {
        case R:           // 注册功能
            doRegister(); // 调用注册函数
            break;
        case L:            // 登录功能
            if (doLogin()) // 调用登录函数
            {
                showUserMenu(); // 调用用户展示函数
            }
            break;
        case Q:       // 退出功能
            doQuit(); // 调用退出函数
            cout << "再见!" << endl;
            break;
        default:
            cout << "无效的选择" << endl;
            break;
        }
        cout << "按回车继续....";
        cin.get(); // 吸收回车
    }
}

// 主菜单函数
void DictClient::showMainMenu()
{
    system("clear"); // 用于清屏
    cout << "***************************" << endl;
    cout << "*********1、注册***********" << endl;
    cout << "*********2、登录***********" << endl;
    cout << "*********3、退出***********" << endl;
    cout << "***************************" << endl;
    cout << "请选择:";
}
// 显示用户菜单
void DictClient::showUserMenu()
{
    while (is_logged_in_)
    {
        system("clear"); // 清屏
        cout << "当前用户:" << username_ << endl;
        cout << "*********************************" << endl;
        cout << "**********1、查单词***************" << endl;
        cout << "**********2、历史记录***************" << endl;
        cout << "**********3、返回上一级***************" << endl;
        cout << "*********************************" << endl;
        cout << "请选择:";
        int choice;
        cin >> choice;
        cin.ignore();
        // 对选择内容进行多分枝
        switch (choice)
        {
        case 1:
            doQuerry(); // 调用查单词函数
            break;
        case 2:
            doHistory(); // 调用查单词函数
            break;
        case 3:
            doQuit(); // 调用查单词函数
            break;
        default:
            cout << "无效的选择" << endl;
            break;
        }
        cout << "按回车继续...." << endl;
        cin.get();
    }
}

// 用户注册函数
bool DictClient::doRegister()
{
    // 定义相关数据信息
    Msg msg;
    msg.type = R; // 表示是注册类型
    // 提示并输入用户名和密码
    cout << "请输入用户名称(最长19个字符):";
    cin.getline(msg.name, sizeof(msg.name));
    cout << "请输入密码(最长127个字符):";
    cin.getline(msg.text, sizeof(msg.text));
    // 将上面的信息转换成网络字节序
    msg.networkByteOrder();
    // 发送给服务器
    if (send(sockfd_, &msg, sizeof(msg), 0) < 0)
    {
        perror("注册请求失败");
        return false;
    }
    // 程序执行至此,表示注册成功
    if (recv(sockfd_, &msg, sizeof(msg), 0) <= 0)
    {
        perror("接收注册信息响应失败");
        return false;
    }
    // 说明收到服务器的消息
    msg.hostByteOrder(); // 将收到的服务器消息转换为本机字节序
    // 处理服务器给出的响应
    if (strcmp(msg.text, "**OK**") == 0)
    {
        cout << "注册成功" << endl;
        return true;
    }
    else if (strcmp(msg.text, "**EXISTS**") == 0)
    {
        cout << "注册失败,用户名已经存在" << endl;
    }
    else
    {
        cout << "注册失败,错误未知" << endl;
    }
    return false;
}

// 定义登录函数
bool DictClient::doLogin()
{
    // 定义相关数据信息
    Msg msg;
    msg.type = L; // 表示是登录类型
    cout << "请输入用户名:";
    cin.getline(msg.name, sizeof(msg.name));
    cout << "请输入密码:";
    cin.getline(msg.text, sizeof(msg.text));
    // 转换为网络字节序
    msg.networkByteOrder();
    // 发送给服务器
    if (send(sockfd_, &msg, sizeof(msg), 0) < 0)
    {
        perror("发送登录请求失败");
        return false;
    }
    // 程序执行至此,表示向服务器发送数据成功,接收服务器反馈的结果
    if (recv(sockfd_, &msg, sizeof(msg), 0) <= 0)
    {
        perror("接收登录响应失败");
        return false;
    } // 将接收的反馈信息转换为本机字节序
    msg.hostByteOrder();
    // 对接收的结果进行处理
    if (strcmp(msg.text, "**OK**") == 0)
    {
        cout << "登录成功" << endl;
        username_ = msg.name;
        is_logged_in_ = true; // 表示在线
        return true;
    }
    else if (strcmp(msg.text, "**EXISTS**") == 0)
    {
        cout << "登录失败,用户已经在线" << endl;
    }
    else
    {
        cout << "登录失败,原因未知" << endl;
    }
    return false;
}

// 查单词功能
void DictClient::doQuerry()
{
    Msg msg;                             // 信息传递体
    msg.type = S;                        // 表示查询功能
    strcpy(msg.name, username_.c_str()); // 要查单词的用户名
    // 循环进行查询单词
    while (true)
    {
        cout << "请输入要查询的单词(输入#后结束查询):";
        cin.getline(msg.text, sizeof(msg.text));
        // 判断是否结束查询
        if (strcmp(msg.text, "#") == 0)
        {
            cout << "*********";
            break;
        }
        // 将消息发送给服务器,进行检索
        msg.networkByteOrder();
        if (send(sockfd_, &msg, sizeof(msg), 0) < 0)
        {
            perror("发送查询消息失败");
            return;
        }
        // 函数执行至此,表示插叙成功,并收到服务器反馈的结果
        if (recv(sockfd_, &msg, sizeof(msg), 0) <= 0)
        {
            perror("接收查询结果失败");
            return;
        }
        msg.hostByteOrder(); // 转化为主机字节序
        cout << "释义:" << msg.text << endl;
    }
}

// 查看历史记录
void DictClient::doHistory()
{
    Msg msg;                             // 信息传递体
    msg.type = H;                        // 表示查询功能
    strcpy(msg.name, username_.c_str()); // 要查单词的用户名
    // 转换为网络字节序
    msg.networkByteOrder();
    if (send(sockfd_, &msg, sizeof(msg), 0) < 0)
    {
        perror("发送历史记录请求失败");
        return;
    }
    cout << "查询历史记录:" << endl;
    while (true)
    {
        if (recv(sockfd_, &msg, sizeof(msg), 0) <= 0)
        {
            perror("接收历史数据失败");
            return;
        }
        msg.hostByteOrder(); // 转换为主机字节序
        cout << "[DEBUG] type=" << msg.type << " text=" << msg.text << endl; // 加这一行
        // 判断是否查询完毕
        if (strcmp(msg.text, "**OVER**") == 0)
        {
            break;
        }
        // 将消息展示出来
        cout << msg.text << endl;
    }
}

// 退出登录功能
void DictClient::doQuit()
{
    if (is_logged_in_) // 判断是否在线
    {
        Msg msg;                             // 信息传递体
        msg.type = Q;                        // 表示查询功能
        strcpy(msg.name, username_.c_str()); // 要查单词的用户名
        // 转换为网络字节序
        msg.networkByteOrder();
        if (send(sockfd_, &msg, sizeof(msg), 0) < 0)
        {
            perror("发送突出请求失败");
            return;
        }
        else
        {
            is_logged_in_ = false; // 表示下线
            username_.clear();
            cout << "用户已经退出" << endl;
        }
    }
}

3> client.hpp

cpp 复制代码
#ifndef CLIENT_H
#define CLIENT_H

#include "message.hpp" //消息协议头文件

class DictClient
{
private:
    int sockfd_;         // 客户端套接字头文件
    string username_;   // 用户名
    bool is_logged_in_; // 判断是否已经登录

    // 网络连接函数
    bool connectToServer(const string &ip, int port);

    // 业务功能函数
    void showMainMenu(); // 展示主菜单
    void showUserMenu(); // 展示业务菜单
    bool doRegister();   // 注册功能
    bool doLogin();      // 登录功能
    void doQuerry();     // 查询功能
    void doHistory();    // 获取历史记录功能
    void doQuit();       // 退出功能
public:
    DictClient(const string &ip, int port); // 构造函数
    ~DictClient();                          // 析构函数
    void run();                             // 主程序运行函数
};



#endif

3> main.cpp

cpp 复制代码
#ifndef CLIENT_H
#define CLIENT_H

#include "message.hpp" //消息协议头文件

class DictClient
{
private:
    int sockfd_;         // 客户端套接字头文件
    string username_;   // 用户名
    bool is_logged_in_; // 判断是否已经登录

    // 网络连接函数
    bool connectToServer(const string &ip, int port);

    // 业务功能函数
    void showMainMenu(); // 展示主菜单
    void showUserMenu(); // 展示业务菜单
    bool doRegister();   // 注册功能
    bool doLogin();      // 登录功能
    void doQuerry();     // 查询功能
    void doHistory();    // 获取历史记录功能
    void doQuit();       // 退出功能
public:
    DictClient(const string &ip, int port); // 构造函数
    ~DictClient();                          // 析构函数
    void run();                             // 主程序运行函数
};



#endif

3> main.cpp

cpp 复制代码
#include"client.hpp"
#include<myhead.h>

int main(int argc, const char *argv[])
{
    //判断传入的数据是否足够
    if(argc != 3)
    {
        cerr<< "用法:" << argv[0] << " <服务器ip> <端口号>" << endl;
        return -1;
    }

    //实例化客户端对象
    try
    {
        DictClient client(argv[1], atoi(argv[2]));//实例化客户端对象
        client.run();//运行主要逻辑
    }
    catch(const std::exception& e)
    {
        std::cerr << e.what() << '\n';
    }
    
    cout << "Hello World!" << endl;
    return 0;
}

4> message.hpp

cpp 复制代码
#ifndef CLIENT_H
#define CLIENT_H

#include "message.hpp" //消息协议头文件

class DictClient
{
private:
    int sockfd_;         // 客户端套接字头文件
    string username_;   // 用户名
    bool is_logged_in_; // 判断是否已经登录

    // 网络连接函数
    bool connectToServer(const string &ip, int port);

    // 业务功能函数
    void showMainMenu(); // 展示主菜单
    void showUserMenu(); // 展示业务菜单
    bool doRegister();   // 注册功能
    bool doLogin();      // 登录功能
    void doQuerry();     // 查询功能
    void doHistory();    // 获取历史记录功能
    void doQuit();       // 退出功能
public:
    DictClient(const string &ip, int port); // 构造函数
    ~DictClient();                          // 析构函数
    void run();                             // 主程序运行函数
};



#endif

5> network.cpp

6> CMakeLists.txt

cpp 复制代码
cmake_minimum_required(VERSION 2.8)
project(dict_client)

#设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

#查找 SQLite3(客户端可能不需要,保留给未来扩展)
find_package(SQLite3)

#查找线程库
find_package(Threads REQUIRED)

#添加可执行文件(直接引用当前目录的源文件)
add_executable(${PROJECT_NAME}
    client.cpp
    network.cpp
    main.cpp
)

#包含当前目录
target_include_directories(${PROJECT_NAME} PRIVATE 
    ${CMAKE_CURRENT_SOURCE_DIR})

#连接库
target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads)

#设置调试编译选项
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -g)
endif()

完结散花

相关推荐
来一碗刘肉面2 小时前
C++中的引用语法
c++·算法
杜子不疼.2 小时前
【Qt初识】工程起步:项目创建、代码解读与对象树
数据库·c++·qt
知无不研3 小时前
算法:Fizz Buzz解题思路
c++·算法·leetcode
汉克老师3 小时前
GESP2026年6月认证C++八级( 第二部分判断题(1-10))精讲
c++·归并排序·dijkstra·二分·互斥·gesp8级·可重复组合
nianniannnn4 小时前
c++复习自存--流、异常处理、多线程与C++工程规范
开发语言·c++
知无不研5 小时前
缺省参数与缺省函数的区别
c++·语法
从零开始的代码生活_6 小时前
C++ 类和对象中篇:默认成员函数、拷贝控制与运算符重载
服务器·开发语言·c++
csdndenglu7 小时前
Easy3D:一个轻量级、易用、高效的C++库,用于处理和渲染3D数据,无复杂要求时可替代VTK
开发语言·c++·图形渲染·3d渲染
^yi8 小时前
【C++】String 类
开发语言·c++