c++ http高并发服务器笔记

第二章 Linux多进程开发

2.9 waitpid函数

在这里插入图片描述
由于子进程一直没有结束,所以 这里是非阻塞的,父进程会一直输出语句
非阻塞会一直往下面运行程序,不会一直停在waitpid
在这里插入图片描述

默认阻塞就把opition那里改成0,默认阻塞的情况父进程只会输出一条语句,子进程会一直输出
阻塞情况下会一直运行waitpid
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

把子进程kill掉后
在这里插入图片描述
在这里插入图片描述

2.11 匿名管道

在这里插入图片描述

【Linux】linux下wc -l 命令

2.12

父进程和子进程可以互相给对方发送数据和读取数据,但是顺序一定得是相反的,因为read是阻塞的
在这里插入图片描述

2.13

先创建管道再创建子进程才可以实现通信

2.14管道的读写特点和管道设置为非阻塞

非阻塞条件下就会一直read,就算没有数据父进程也会一直read,read不会阻塞在那里等待数据传过来

在这里插入图片描述

2.15 有名管道的介绍和使用

1.echo
linux命令echo的实现,Linux echo命令的使用及三种实现方式

2、有名管道只开启写端是不会开始写数据的,因为此时读端还没有开启,只有读端开启了管道才开始通信,才会开始写数据读数据

2.20 kill、raise、abort函数

1、kill
在这里插入图片描述
父进程杀死子进程的例子:
在这里插入图片描述

2、raise
在这里插入图片描述
3、abort
在这里插入图片描述

2.21 alarm函数

在这里插入图片描述
被遮住的地方:
不进行倒计时,不发送信号

比如第一次调用alarm(10) ,SIGALARM返回0,过了一秒后调用alarm(5),因为之前有定时器倒计时10秒,所以返回9
在这里插入图片描述

下面这个例子里面alarm(2)相当于重新设置倒计时为2s,所以过了2s后程序会结束
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2.22 setitimer函数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
eg:定时器开始了一运行就打印出来了(因为是非阻塞的),3s之后就结束了程序
这里的定时并没有体现,因为还没用到捕捉
在这里插入图片描述

2.23 signal信号捕捉函数

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

eg:信号捕捉设置设置定时器之前,还是上一节的例子,这里设置的是忽略信号,所以这个进程一直都不会结束
在这里插入图片描述
eg:设置回调函数
在这里插入图片描述
输出:
打印”定时器开始了…”之后再过三秒第一次打印捕捉到的信号,后面每过2s打印一次
在这里插入图片描述

在这里插入图片描述
因为SIGKILL是杀死进程,如果被捕捉了进程在一直孕行的过程中都不能杀死进程,如果写了一个这样的病毒危害性是很大的

2.24 信号集及相关函数

在这里插入图片描述

1、sigemptyset

在这里插入图片描述

2、sigfillset

在这里插入图片描述

3、sigaddset、sigdelset

在这里插入图片描述

4、sigismember

在这里插入图片描述

2.25 sigprocmask

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在控制台输出的时候可以设置前台控制还是后台控制?可以通过&符号来进行控制?

为什么这里设置非阻塞之后就可以结束进程了?
在这里插入图片描述
在这里插入图片描述
因为只要有信号是阻塞的,循环里面就会一直打印阻塞信号,但是设置了UNBLOCK之后应该是将所有信号都设置为非阻塞的,所以会结束
在这里插入图片描述

2.26 sigaction

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

说明:要避免使用signal,因为不同的机器可能版本不一样

未决信号集和阻塞信号集不能排队,它的标志位只能是0或者1,不能说来了多少个信号就记录多少个数字,所以不能排队

2.27 SIGCHLD信号

该信号可用于解决僵尸进程,可以捕捉SIGCHLD信号,捕捉到后父进程再去调用wait或者waitpid函数
在这里插入图片描述

比如要有20个子进程,要回收3个子进程,都是17号,在第一个子进程结束发送SIGCHLD的时候,17号被记为1,其他两个也结束了,但是此时未决信号集只能记录一个,它还是1,所以这些信号被忽略了
在这里插入图片描述
处理方法:可以在回调函数里面用一个循环不停去回收,但是这样会死循环,父进程就不能做其他事了
在这里插入图片描述
继续改进:
waitpid的-1代表回收所有子进程,
当调用waitpid函数时,它会去搜索是否存在已经终止的子进程,找到了那就处理。这里while循环,如果存在终止子进程,waitpid就一定会去处理。因为ret等于0代表此时并没有终止的子进程,而ret等于-1代表没有子进程。总结,只要存在终止子进程,while就会循环多一次,有多少个就循环多少次。
在这里插入图片描述
但是也会存在一个问题,多运行几次发生段存储错误
因为在用sigaction还没注册好时子进程就已经结束了,导致捕捉不到SIGCHLD信号
在这里插入图片描述
解决方法:提前设置好阻塞信号集
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
所以这样设置阻塞后能够保证信号捕捉注册好以后再进行信号捕捉,即使这时候所有的子进程都已经结束了也能进入回调函数,回收子进程

在这里插入图片描述
在这里插入图片描述
未决信号集对应的位置被设置1,说明发生了该信号但是还没有被处理,如果此时没有捕捉到的话,那么父进程就会忽略此条SIGCHLD信号,但是由于前面将阻塞信号集相应位置设置为了1,所以该信号还不能被处理,只有解除阻塞之后该信号才可以被处理,此时捕捉函数就可以捕捉到SIGCHLD信号

2.28 共享内存

1、shmgt函数

在这里插入图片描述
在这里插入图片描述

2、shmat函数

在这里插入图片描述

3、shmdt函数

在这里插入图片描述

4、shmctl函数

在这里插入图片描述

在这里插入图片描述

5、ftok函数

在这里插入图片描述

这里的ipcrm第一个用法只是标记删除,实际的连接数还是没变,相当于只是把key给删除?连接数只能程序执行后取消关联才会减少
在这里插入图片描述

在这里插入图片描述

——————————————————
在这里插入图片描述
最后一句话是也不能进行关联

在这里插入图片描述
4.数据安全
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.31 守护进程

调用setsid开启新会话是没有控制终端的
因此内核就不能通过发送一些信号去杀死进程
在这里插入图片描述

对于第一点在子进程中进行操作的原因:
首先组id和父进程是一样的,如果利用父进程新创建一个会话,那么这个会话的组id也是100,相当于两个会话id一样,有冲突
在这里插入图片描述

关闭文件描述符:为了防止守护进程往终端里面读写数据

第六点重定向后会丢弃设备

守护进程是孤儿进程
kill是发送信号的shell命令,可以给指定进程发送指定信号。不能被控制终端的Ctrl+C或者Ctrl+\结束,因为它没有自己的控制终端,是新的会话没有绑定控制终端,但它还是一个进程,可以接收信号。

代码:
在回调函数里面如果将数据输出到终端里面就不会显示出来,因为这个守护进程的标准输出已经重定向了,但是将数据输出到文件中还是可以输出的

/*
    写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。
*/

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>

void work(int num) {
    
    
    // 捕捉到信号之后,获取系统时间,写入磁盘文件
    time_t tm = time(NULL);
    struct tm * loc = localtime(&tm);
    // char buf[1024];

    // sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
    // ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);

    // printf("%s\n", buf);

    char * str = asctime(loc);
    int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
    write(fd ,str, strlen(str));
    close(fd);
}

int main() {
    
    

    // 1.创建子进程,退出父进程
    pid_t pid = fork();

    if(pid > 0) {
    
    
        exit(0);
    }

    // 2.将子进程重新创建一个会话
    setsid();

    // 3.设置掩码
    umask(022);

    // 4.更改工作目录
    chdir("/home/nowcoder/");

    // 5. 关闭、重定向文件描述符
    int fd = open("/dev/null", O_RDWR);
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);

    // 6.业务逻辑

    // 捕捉定时信号
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    struct itimerval val;
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;

    // 创建定时器
    setitimer(ITIMER_REAL, &val, NULL);

    // 不让进程结束
    while(1) {
    
    
        sleep(10);
    }

    return 0;
}

第三章 Linux多线程开发

3.1 线程概述

如果创建线程的话
相当于栈空间分成了三份,每一份放一个进程
text空间也是这样
其他的区域都是共享的,所以可以节约很多时间,不需要像创建进程那样使用写时复制技术
在这里插入图片描述

3.2 创建线程

2进行应该是2进程
在这里插入图片描述

pthread_create函数

在这里插入图片描述

在这里插入图片描述

3.3 结束线程

pthread_exit函数

在这里插入图片描述

pthread_t

在这里插入图片描述
后面的printf不会再执行了
在这里插入图片描述

pthread_equal

在这里插入图片描述

3.4 连接已结束的线程

pthread_join

因为是阻塞的,所以函数会一直等到要回收的线程结束后才会往下执行
在这里插入图片描述
pthread_exit函数中可以返回一个指针,所以要对这个返回的指针进行操作的话这里的参数列表应该用二级指针

/*
    #include <pthread.h>
    int pthread_join(pthread_t thread, void **retval);
        - 功能:和一个已经终止的线程进行连接
                回收子线程的资源
                这个函数是阻塞函数,调用一次只能回收一个子线程
                一般在主线程中使用
        - 参数:
            - thread:需要回收的子线程的ID
            - retval: 接收子线程退出时的返回值
        - 返回值:
            0 : 成功
            非0 : 失败,返回的错误号
*/

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

int value = 10;

void * callback(void * arg) {
    
    
    printf("child thread id : %ld\n", pthread_self());
    // sleep(3);
    // return NULL; 
    // int value = 10; // 局部变量
    pthread_exit((void *)&value);   // return (void *)&value;
} 

int main() {
    
    

    // 创建一个子线程
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, callback, NULL);

    if(ret != 0) {
    
    
        char * errstr = strerror(ret);
        printf("error : %s\n", errstr);
    }

    // 主线程
    for(int i = 0; i < 5; i++) {
    
    
        printf("%d\n", i);
    }

    printf("tid : %ld, main thread id : %ld\n", tid ,pthread_self());

    // 主线程调用pthread_join()回收子线程的资源
    int * thread_retval;
    ret = pthread_join(tid, (void **)&thread_retval);

    if(ret != 0) {
    
    
        char * errstr = strerror(ret);
        printf("error : %s\n", errstr);
    }

    printf("exit data : %d\n", *thread_retval);

    printf("回收子线程资源成功!\n");

    // 让主线程退出,当主线程退出时,不会影响其他正常运行的线程。
    pthread_exit(NULL);

    return 0; 
}

3.5 线程的分离

pthread_detach

在这里插入图片描述

3.6 线程取消

pthread_cancel

在这里插入图片描述

在这里插入图片描述

3.7线程属性

pthread_attr_init\pthread_attr_destroy\pthread_attr_getdetachstate\pthread_attr_setdetachstate

/*
    int pthread_attr_init(pthread_attr_t *attr);
        - 初始化线程属性变量

    int pthread_attr_destroy(pthread_attr_t *attr);
        - 释放线程属性的资源

    int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
        - 获取线程分离的状态属性

    int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
        - 设置线程分离的状态属性
*/     

3.9 互斥锁

pthread_mutex_init

限制下面的举例表示不可以用另一个指针来操作这个互斥锁
在这里插入图片描述

pthread_mutex_destroy

pthread_mutex_lock

pthread_mutex_trylock

pthread_mutex_unlock

/*
    互斥量的类型 pthread_mutex_t
    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
        - 初始化互斥量
        - 参数 :
            - mutex : 需要初始化的互斥量变量
            - attr : 互斥量相关的属性,NULL
        - restrict : C语言的修饰符,被修饰的指针,不能由另外的一个指针进行操作。
            pthread_mutex_t *restrict mutex = xxx;
            pthread_mutex_t * mutex1 = mutex;

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
        - 释放互斥量的资源

    int pthread_mutex_lock(pthread_mutex_t *mutex);
        - 加锁,阻塞的,如果有一个线程加锁了,那么其他的线程只能阻塞等待

    int pthread_mutex_trylock(pthread_mutex_t *mutex);
        - 尝试加锁,如果加锁失败,不会阻塞,会直接返回。

    int pthread_mutex_unlock(pthread_mutex_t *mutex);
        - 解锁
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 全局变量,所有的线程都共享这一份资源。
int tickets = 1000;

// 创建一个互斥量
pthread_mutex_t mutex;

void * sellticket(void * arg) {
    
    

    // 卖票
    while(1) {
    
    

        // 加锁
        pthread_mutex_lock(&mutex);

        if(tickets > 0) {
    
    
            usleep(6000);
            printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
            tickets--;
        }else {
    
    
            // 解锁
            pthread_mutex_unlock(&mutex);
            break;
        }

        // 解锁
        pthread_mutex_unlock(&mutex);
    }

    

    return NULL;
}

int main() {
    
    

    // 初始化互斥量
    pthread_mutex_init(&mutex, NULL);

    // 创建3个子线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, sellticket, NULL);
    pthread_create(&tid2, NULL, sellticket, NULL);
    pthread_create(&tid3, NULL, sellticket, NULL);

    // 回收子线程的资源,阻塞
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    pthread_exit(NULL); // 退出主线程
    
    // 释放互斥量资源
    pthread_mutex_destroy(&mutex); //放到最后是为了不让它这么快被释放,其实这句代码不会被实现?


    return 0;
}

3.13 条件变量

当缓冲区为空,不用条件变量时,消费者处于无意义的循环,此时只执行加锁和解锁,直到时间片完才发生线程切换,浪费cpu资源。而加入条件变量,一旦发现缓存区是空的,会立刻从运行态进入阻塞态,此时一定会发生线程切换,把cpu资源让出。互斥锁和条件变量都可以实现线程同步,但使用条件变量效率更高。

/*
    条件变量的 类型 pthread_cond_t
    int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
    int pthread_cond_destroy(pthread_cond_t *cond);
    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
        - 等待,调用了该函数,线程会阻塞。
    int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
        - 等待多长时间,调用了这个函数,线程会阻塞,直到指定的时间结束。
    int pthread_cond_signal(pthread_cond_t *cond);
        - 唤醒一个或者多个等待的线程
    int pthread_cond_broadcast(pthread_cond_t *cond);
        - 唤醒所有的等待的线程
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

// 创建一个互斥量
pthread_mutex_t mutex;
// 创建条件变量
pthread_cond_t cond;

struct Node{
    
    
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;

void * producer(void * arg) {
    
    

    // 不断的创建新的节点,添加到链表中
    while(1) {
    
    
        pthread_mutex_lock(&mutex); //加互斥锁
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        
        // 只要生产了一个,就通知消费者消费
        pthread_cond_signal(&cond);

        pthread_mutex_unlock(&mutex);
        usleep(100);
    }

    return NULL;
}

void * customer(void * arg) {
    
    

    while(1) {
    
    
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;
        // 判断是否有数据
        if(head != NULL) {
    
    
            // 有数据
            head = head->next;
            printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);
            usleep(100);
        } else {
    
    
            // 没有数据,需要等待
            // 当这个函数调用阻塞的时候,会对互斥锁进行解锁,当不阻塞的,继续向下执行,会重新加锁。
            pthread_cond_wait(&cond, &mutex);
            pthread_mutex_unlock(&mutex);
        }
    }
    return  NULL;
}

int main() {
    
    

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    for(int i = 0; i < 5; i++) {
    
    
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }

    for(int i = 0; i < 5; i++) {
    
    
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    while(1) {
    
    
        sleep(10);
    }

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    pthread_exit(NULL);

    return 0;
}

3.14 信号量

/*
    信号量的类型 sem_t
    int sem_init(sem_t *sem, int pshared, unsigned int value);
        - 初始化信号量
        - 参数:
            - sem : 信号量变量的地址
            - pshared : 0 用在线程间 ,非0 用在进程间
            - value : 信号量中的值

    int sem_destroy(sem_t *sem);
        - 释放资源

    int sem_wait(sem_t *sem);
        - 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞

    int sem_trywait(sem_t *sem);

    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
    int sem_post(sem_t *sem);
        - 对信号量解锁,调用一次对信号量的值+1

    int sem_getvalue(sem_t *sem, int *sval);

    sem_t psem; //生产者消费变量
    sem_t csem; //消费者信号变量
    init(psem, 0, 8);
    init(csem, 0, 0);

    producer() {
        sem_wait(&psem);
        sem_post(&csem)
    }

    customer() {
        sem_wait(&csem);
        sem_post(&psem)
    }

*/

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>

// 创建一个互斥量
pthread_mutex_t mutex;
// 创建两个信号量
sem_t psem;
sem_t csem;

struct Node{
    
    
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;

void * producer(void * arg) {
    
    

    // 不断的创建新的节点,添加到链表中
    while(1) {
    
    
        sem_wait(&psem);
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        pthread_mutex_unlock(&mutex);
        sem_post(&csem);//告诉消费者有一个空位可以进行消费了
    }

    return NULL;
}

void * customer(void * arg) {
    
    

    while(1) {
    
    
        sem_wait(&csem);
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;
        head = head->next;
        printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
        free(tmp);
        pthread_mutex_unlock(&mutex);
        sem_post(&psem); //告诉生产者有一个空位可以进行生产了
       
    }
    return  NULL;
}

int main() {
    
    

    pthread_mutex_init(&mutex, NULL);
    sem_init(&psem, 0, 8);
    sem_init(&csem, 0, 0);

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    for(int i = 0; i < 5; i++) {
    
    
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }

    for(int i = 0; i < 5; i++) {
    
    
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    while(1) {
    
    
        sleep(10);
    }

    pthread_mutex_destroy(&mutex);

    pthread_exit(NULL);

    return 0;
}

第四章 linux网络编程

4.12 IP地址转换

把int类型中的四个字节分别传给char类型指针
将点分十进制的IP地址转换成网络字节序的整数
在这里插入图片描述

输出如下:
因为转换成网络字节序是大端序,高地址存放低字节数,p是地址,p+3是高地址
在这里插入图片描述

在这里插入图片描述
printf (“%s”,str ); / / 从首地址str一直输出,一直到 \0结束,没有\0就会越界。

在这里插入图片描述

4.17 TCP三次握手

在这里插入图片描述

4.19 TCP四次挥手

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

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

服务器在发送的时候并没有发送\n
所以在发送数据的时候,在strlen后面加1,把\n发送过去。或者可以初始化char数组的时候放一个0值,但是初始化会产生一个问题,如果两次发送的数据长度不一样,比如第二次发送的数据比第一次发送的短,那么char数组的后面一部分就不会被覆盖,发送过去的数据是会存在问题的
在这里插入图片描述

服务器的accept在捕获到sigchld信号之后产生软中断,会导致这个accept函数产生一个错误,并且不阻塞,所以会执行下面if里面的代码
在这里插入图片描述
解决办法:
在这里插入图片描述

4.22 多线程实现并发服务器

在创建结构体保存要传入子线程的参数时,这是一个局部变量,当下一次while循环时,它就消失了,所以可以使用malloc将其创建在堆中,但是在子线程中还需要释放掉。但是这样做资源消耗是非常大的,当来了很多个子线程时,要申请很多个堆区内存
在这里插入图片描述
解决办法:创建一个结构体数组sockinfos,然后在main函数里面对结构体数组进行初始化,文件描述符需要初始化为-1,代表这是可用的结构体,在创建子线程之前先判断哪一个结构体是可用的,如果128个客户端都被用了,则睡眠、i-1,直到有可用的文件描述符

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

struct sockInfo {
    
    
    int fd; // 通信的文件描述符
    struct sockaddr_in addr;
    pthread_t tid;  // 线程号
};

struct sockInfo sockinfos[128]; //最多允许128个客户端进行连接

void * working(void * arg) {
    
    
    // 子线程和客户端通信   cfd 客户端的信息 线程号
    // 获取客户端的信息
    struct sockInfo * pinfo = (struct sockInfo *)arg;

    char cliIp[16];
    inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(pinfo->addr.sin_port);
    printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

    // 接收客户端发来的数据
    char recvBuf[1024];
    while(1) {
    
    
        int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

        if(len == -1) {
    
    
            perror("read");
            exit(-1);
        }else if(len > 0) {
    
    
            printf("recv client : %s\n", recvBuf);
        } else if(len == 0) {
    
    
            printf("client closed....\n");
            break;
        }
        write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
    }
    close(pinfo->fd);
    return NULL;
}

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
    
    
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
    
    
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
    
    
        perror("listen");
        exit(-1);
    }

    // 初始化数据
    int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
    for(int i = 0; i < max; i++) {
    
    
        bzero(&sockinfos[i], sizeof(sockinfos[i]));
        sockinfos[i].fd = -1;
        sockinfos[i].tid = -1;
    }

    // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
    while(1) {
    
    

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

        struct sockInfo * pinfo;
        for(int i = 0; i < max; i++) {
    
    
            // 从这个数组中找到一个可以用的sockInfo元素
            if(sockinfos[i].fd == -1) {
    
    
                pinfo = &sockinfos[i];
                break;
            }
            if(i == max - 1) {
    
    
                sleep(1);
                i--;
            }
        }

        pinfo->fd = cfd;
        memcpy(&pinfo->addr, &cliaddr, len);

        // 创建子线程
        pthread_create(&pinfo->tid, NULL, working, pinfo);

        pthread_detach(pinfo->tid); //注意这里不能用join,因为join是阻塞的
    }

    close(lfd);
    return 0;
}

4.25 IO多路复用

IO指的是文件和内存之间进行写入和读出
在这里插入图片描述
这里的IO是指对缓冲区的操作
在这里插入图片描述

4.27 select

select每次调用的时候都需要重新设置
在while循环里面调用的其实是副本,因为内核处理过后可能会把本来是1的位置设置为0,但其实这些位还是需要检查的
在这里插入图片描述

4.28 poll

poll里面使用了结构体,内核每次修改的其实是结构体的revents,不会修改fd,这样就会比select好
不过poll还是需要从内核态转到用户态,用户态转到内核态,并且进行循环遍历

这里就是既检测读又检测写
在这里插入图片描述

poll相当于把select的第三点和第四个缺点给去掉了
在这里插入图片描述

————————————————————————
注意结构体0号是存放了监听文件描述符,所以后面的循环都是从1号开始循环的

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>


int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 初始化检测的文件描述符数组
    struct pollfd fds[1024];
    for(int i = 0; i < 1024; i++) {
    
    
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;//监听文件描述符
    int nfds = 0;

    while(1) {
    
    

        // 调用poll系统函数,让内核帮检测哪些文件描述符有数据
        int ret = poll(fds, nfds + 1, -1);
        if(ret == -1) {
    
    
            perror("poll");
            exit(-1);
        } else if(ret == 0) {
    
    
            continue;
        } else if(ret > 0) {
    
    
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            if(fds[0].revents & POLLIN) {
    
    //这里是fds[0]
                // 表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 将新的文件描述符加入到集合中
                for(int i = 1; i < 1024; i++) {
    
    
                    if(fds[i].fd == -1) {
    
    
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                        break;
                    }
                }

                // 更新最大的文件描述符的索引
                nfds = nfds > cfd ? nfds : cfd;
            }

            for(int i = 1; i <= nfds; i++) {
    
    
                if(fds[i].revents & POLLIN) {
    
    
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {
    
    0};
                    int len = read(fds[i].fd, buf, sizeof(buf));
                    if(len == -1) {
    
    
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
    
    
                        printf("client closed...\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    } else if(len > 0) {
    
    
                        printf("read buf = %s\n", buf);
                        write(fds[i].fd, buf, strlen(buf) + 1);
                    }
                }
            }

        }

    }
    close(lfd);
    return 0;
}


4.30 epoll代码编写

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {
    
    

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
    
    
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

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

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
    
    
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
    
    
                if(epevs[i].events & EPOLLOUT) {
    
    //如果在前面添加了很多事件时一定要在后面记得判断一下
                    continue;
                }   
                // 有数据到达,需要通信
                char buf[1024] = {
    
    0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
    
    
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
    
    
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
    
    
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}

4.31 epoll的两种工作模式

注意:EAGAIN只有设置为非阻塞的情况下才会产生
Linux下EAGAIN宏的含义:
从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。例如,以O_NONBLOCK的标志打开file/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。

又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。

ET模式下的代码
epoll_et.c

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {
    
    

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
    
    
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

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

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
    
    
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 设置cfd属性非阻塞
                int flag = fcntl(cfd, F_GETFL);//获取cfd的属性
                flag |= O_NONBLOCK; //将flag设为非阻塞
                fcntl(cfd, F_SETFL, flag); //将cfd设为非阻塞

                epev.events = EPOLLIN | EPOLLET;    // 设置边沿触发
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
    
    
                if(epevs[i].events & EPOLLOUT) {
    
    
                    continue;
                }  

                // 循环读取出所有数据
                char buf[5];
                int len = 0;
                while( (len = read(curfd, buf, sizeof(buf))) > 0) {
    
    
                    // 打印数据
                    // printf("recv data : %s\n", buf);//printf是行缓冲,要加换行,或者缓冲区满了才刷新缓冲区
                    write(STDOUT_FILENO, buf, len);
                    write(curfd, buf, len);
                }
                if(len == 0) {
    
    
                    printf("client closed....");
                }else if(len == -1) {
    
    
                    if(errno == EAGAIN) {
    
    
                        //表示数据已经读完了,此时不退出
                        printf("data over.....");
                    }else {
    
    
                        perror("read");
                        exit(-1);
                    }
                    
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}

第五章 实战

5.1 阻塞和非阻塞、同步和异步

有些错误并不是程序当中的错误
注意非阻塞的size==-1的出错情况,或产生EAGIN或EWOULDBLOCK
阻塞情况下产生EINTR
在这里插入图片描述

同步就是recv函数阻塞在那里,必须等到内核中的数据全部输入到buf中后才可以执行下面的代码
异步是把这些事情交给内核去做,然后去做自己的事情
在这里插入图片描述
异步一般是和非阻塞搭配使用

异步程序非常复杂,而且容易出错,建议使用IO复用和非阻塞

5.7 解析http请求报文

在这里插入图片描述
根据主状态机的信息去判断要解析哪一部分,是解析头还是解析什么

猜你喜欢

转载自blog.csdn.net/qq_43050258/article/details/129760160