alin的学习之路(Linux网络编程:六)(线程池、UDP的C/S模型)

alin的学习之路(Linux网络编程:六)(线程池、UDP的C\S模型)

1. 线程池解析

1. 原理图

在这里插入图片描述

2. 结构体

typedef struct {
    void *(*function)(void *);      /* 函数指针,回调函数 */
    void *arg;              /* 上面函数的参数 */
} threadpool_task_t;           /* 各子线程任务结构体 */
/* 描述线程池相关信息 */

struct threadpool_t {
    pthread_mutex_t lock;        /* 用于锁住本结构体 */  
    pthread_mutex_t thread_counter;   /* 记录忙状态线程个数de琐 -- busy_thr_num */
    pthread_cond_t queue_not_full;   /* 任务队列满时,添加任务线程阻塞,等待此条件变量 */
    pthread_cond_t queue_not_empty;   /* 任务队列里不为空时,通知等待任务的线程 */
    pthread_t *threads;         /* 存放线程池中每个线程的tid。数组 */
    pthread_t adjust_tid;        /* 存管理线程tid */
    threadpool_task_t *task_queue;    /* 任务队列 */
    int min_thr_num;           /* 线程池最小线程数 */
    int max_thr_num;           /* 线程池最大线程数 */
    int live_thr_num;          /* 当前存活线程个数 */
    int busy_thr_num;          /* 忙状态线程个数 */
    int wait_exit_thr_num;        /* 要销毁的线程个数 */
    int queue_front;           /* task_queue队头下标 */
    int queue_rear;           /* task_queue队尾下标 */
    int queue_size;           /* task_queue队中实际任务数 */
    int queue_max_size;         /* task_queue队列可容纳任务数上限 */
    int shutdown;            /* 标志位,线程池使用状态,true或false */
};

3. main函数

  1. 创建线程池
  2. 由server创建任务添加到线程池中,模拟服务器利用线程池处理业务
  3. 等待子线程完成任务
  4. 销毁线程池

4. threadpool_create 创建线程池

  1. 创建线程池结构体指针 pool ,创建在堆上,使用 malloc
  2. 初始化线程池的属性
  3. 为线程池中的线程数组 pool->threads 开辟空间,大小为最大线程个数 ,使用 malloc
  4. 开辟任务队列空间 pool->task_queue,大小为最大任务个数
  5. 初始化锁和条件变量
  6. 创建 min_thr_num 个子线程等待处理业务
  7. 启动管理者线程 pool->adjust_tid
  8. 如果前面代码调用失败,调用 threadpool_free 释放空间

5. threadpool_thread 线程池中的工作线程(回调函数)

  1. 接收参数传递来的线程池指针 pool
  2. 对线程池加锁
  3. 任务队列中没有任务的时候阻塞在 pthread_cond_wait(&(pool->queue_not_empty), &(pool->lock));,此时会有隐含的解锁
  4. 当条件满足,收到pthread_cond_signal/broadcast 时,阻塞解除,并重新加锁
  5. 从任务队列中获取任务,完成任务队列的出队操作, pool->queue_front 保存队头的元素+1,即出队。使用 (pool->queue_front + 1) % pool->queue_max_size; 实现数字逻辑上的循环队列
  6. 任务队列大小-1
  7. 一个任务结束后可以通知有新的任务添加进来
  8. 加锁,忙任务数+1,解锁。
  9. 调用回调函数,启动任务。 (*(task.function))(task.arg); 或 task.function(task.arg);。
  10. 加锁,忙任务数-1,解锁。

6. adjust_thread 管理者线程(回调函数)

  1. 循环10s执行一次
  2. 加锁,获取 任务队列当前大小queue_size、存活线程数live_thr_num、当前忙的线程数 busy_thr_num ,解锁
  3. 判断线程扩容和瘦身条件是否达成
    1. 达成扩容条件:创建 wait_exit_thr_num 个添加到线程池中
    2. 达成瘦身条件:销毁 wait_exit_thr_num 个线程池中的线程:使用方法:给空闲的线程发送 pthread_cond_signal(&(pool->queue_not_empty)); ,给条件变量上的线程 发送 “假信号”,使空闲的线程开始执行工作,在工作线程的执行代码中判断 wait_exit_thr_num ,如果wait_exit_thr_num 大于0,则直接pthread_exit() 退出,实现结束线程

7. threadpool_add 向线程池中添加任务

  1. 对线程池加锁
  2. 队列已满时 pthread_cond_wait(&(pool->queue_not_full), &(pool->lock)); 阻塞,并解锁
  3. 当条件满足时队列不满,pthread_cond_wait 返回并重新加锁
  4. 清空任务队列对应下标的内容
  5. 添加任务到任务队列,任务队列尾+1 ,pool->queue_rear = (pool->queue_rear + 1) % pool->queue_max_size;
  6. 任务队列大小+1
  7. 唤醒等待任务的线程 pthread_cond_signal(&(pool->queue_not_empty));
  8. 解锁

8. threadpool_destroy 销毁线程池

  1. pool->shutdown 置为 true
  2. 销毁管理者线程
  3. 通知所有的存活线程 pthread_cond_broadcast(&(pool->queue_not_empty)); ,即工作线程会经由判断退出,工作线程自己结束
  4. 回收结束的线程
  5. 调用 threadpool_free() 函数释放线程池 pool

2. UDP

1. TCP通信和UDP通信各自特性

TCP UDP
特性 面向连接的,可靠的数据包传递。
对于不稳定的网络层。采取完全弥补的通信方式。 丢包重传。
无连接的,不可靠的数据报传递。
对于不稳定的网络层。采取完全不弥补的通信方式。 默认还原网络真实状态。
优点 数据流量稳定。速度稳定。顺序稳定。 传输速度快,相对高。开销小。
缺点 传输速度慢。通信效率低。开销大。 数据流量不稳定。速度不稳定。顺序不稳定。
使用场景 对数据完整性,要求较高, 不追求效率。—— 大数据传输。文件传参…… 对数据时效性要求较高,稳定性其次。—— 视频会议、视频电话、游戏……

2. UDP 实现 C/S 模型

1.原理图

在这里插入图片描述

2.server端实现流程

  1. socket() 函数创建套接字 sockfd,指定 参数type 为 SOCK_DGRAM
  2. 设置服务器的地址结构sockaddr_in
  3. bind() 绑定地址结构
  4. while(1){
    1. recvfrom() 函数接收客户端发送来的信息 ----- 涵盖 accept 函数的作用。 传出对端的地址结构
    2. 小写转大写 处理字符串,写到屏幕
    3. sendto() 函数将处理后的字符串发送给客户端 ---- 替换 write()
  5. }
  6. close(sockfd);

3.client端实现流程

  1. socket() 函数创建套接字 sockfd,指定 参数type 为 SOCK_DGRAM
  2. 设置服务器端的sockaddr_in ,作为sendto() 函数的传参使用
  3. sendto() 函数将数据发送给服务器 ----- 相当于替换 connect(sockfd, “服务端的地址结构”, 地址结构大小);
  4. 写到屏幕
  5. recvfrom() 函数接收客户端发送来的信息 ---- 替换 read()
  6. close(sockfd);

3. UDP 的相关函数

recvfrom() 函数

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
            struct sockaddr *src_addr, socklen_t *addrlen);
    sockfd: 套接字
    buf:缓冲区地址
    len:缓冲区大小
    flags:0
	src_addr:(struct sockaddr *)&src_addr  传出对端的地址结构。——对应accpet理解。
	addrlen:传入传出。——对应accpet理解。
	返回:
		成功:> 0 接收数据字节数。
		0:对端关闭。
		失败:-1, errno

sendto() 函数

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
           const struct sockaddr *dest_addr, socklen_t addrlen);
    sockfd: 套接字
    buf:存储着数据的缓冲区地址
    len:数据长度
    flags:0
    dest_addr:(struct sockaddr *)&src_addr  传入对端地址结构。——对应connect理解。
    addrlen:地址结构长度。——对应accpet理解。
    返回:
    	成功:实际写出的字节数。
    	失败:-1, errno

4. UDP 特性

  • udp 的服务器,不需要与 client 建立连接。 默认支持并发。
  • netstat -apn命令, 查询的网络通信状态,专用于 “TCP ” 通信。 UDP 不适用!!!

5. 代码实现

server

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

#define SRV_PORT 8066

int main()
{
    char buf[BUFSIZ] = {0};
    char cltip[16] = {0};
    struct sockaddr_in clt_addr,srv_addr;
    socklen_t clt_addr_len;

    bzero(&srv_addr,sizeof(srv_addr));
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(SRV_PORT);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd == -1)
    {
        perror("socket error");
        exit(1);
    }

    int ret = bind(sockfd,(struct sockaddr*)&srv_addr,sizeof(srv_addr));
    if(-1 == ret)
    {
        perror("bind error");
        exit(1);
    }

    while(1)
    {

        clt_addr_len = sizeof(clt_addr);
        ret = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&clt_addr,&clt_addr_len);
        if(-1 == ret)
        {
            perror("recvfrom error");
            exit(1);
        }

        printf("client ip:%s,port:%d\n",inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,cltip,sizeof(cltip)),ntohs(clt_addr.sin_port));

        for(int i=0 ;i<ret ;++i)
        {
            buf[i] = toupper(buf[i]);
        }
        write(STDOUT_FILENO,buf,ret);
        printf("\n");

        ret = sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&clt_addr,clt_addr_len);
        if(-1 == ret)
        {
            perror("sendto error");
            exit(0);
        }

    }
    close(sockfd);

    return 0;
}

client

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

#define SRV_PORT 8066

int main()
{
    char buf[BUFSIZ] = {0};
    struct sockaddr_in srv_addr;
    socklen_t srv_addr_len = sizeof(srv_addr);

    bzero(&srv_addr,sizeof(srv_addr));
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(SRV_PORT);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd == -1)
    {
        perror("socket error");
        exit(1);
    }

    int ret,i = 0;
    while(1)
    {
        char str[32] = {0};
        sprintf(str,"%s %d","hello udp",i++);
        printf("%s\n",str);
        ret = sendto(sockfd,str,strlen(str),0,(struct sockaddr*)&srv_addr,srv_addr_len);
        if(-1 == ret)
        {
            perror("sendto error");
            exit(0);
        }

        ret = recvfrom(sockfd,buf,sizeof(buf),0,NULL,NULL);
        if(-1 == ret)
        {
            perror("recvfrom error");
            exit(1);
        }
        sleep(1);
    }
    close(sockfd);

    return 0;
}

注意:可以通过 nc 命令充当UDP的客户端时,要加上 -u 选项

3. some small point

  1. ps -Lf 进程号 :显示该进程创建的所有线程
  2. 条件变量的静态初始化使用时机:当全局仅有该一份全局变量时,使用静态初始化。其他情况使用动态初始化。

猜你喜欢

转载自blog.csdn.net/qq_41775886/article/details/107823397