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函数
- 创建线程池
- 由server创建任务添加到线程池中,模拟服务器利用线程池处理业务
- 等待子线程完成任务
- 销毁线程池
4. threadpool_create 创建线程池
- 创建线程池结构体指针 pool ,创建在堆上,使用 malloc
- 初始化线程池的属性
- 为线程池中的线程数组 pool->threads 开辟空间,大小为最大线程个数 ,使用 malloc
- 开辟任务队列空间 pool->task_queue,大小为最大任务个数
- 初始化锁和条件变量
- 创建 min_thr_num 个子线程等待处理业务
- 启动管理者线程 pool->adjust_tid
- 如果前面代码调用失败,调用 threadpool_free 释放空间
5. threadpool_thread 线程池中的工作线程(回调函数)
- 接收参数传递来的线程池指针 pool
- 对线程池加锁
- 任务队列中没有任务的时候阻塞在 pthread_cond_wait(&(pool->queue_not_empty), &(pool->lock));,此时会有隐含的解锁
- 当条件满足,收到pthread_cond_signal/broadcast 时,阻塞解除,并重新加锁
- 从任务队列中获取任务,完成任务队列的出队操作, pool->queue_front 保存队头的元素+1,即出队。使用 (pool->queue_front + 1) % pool->queue_max_size; 实现数字逻辑上的循环队列
- 任务队列大小-1
- 一个任务结束后可以通知有新的任务添加进来
- 加锁,忙任务数+1,解锁。
- 调用回调函数,启动任务。 (*(task.function))(task.arg); 或 task.function(task.arg);。
- 加锁,忙任务数-1,解锁。
6. adjust_thread 管理者线程(回调函数)
- 循环10s执行一次
- 加锁,获取 任务队列当前大小queue_size、存活线程数live_thr_num、当前忙的线程数 busy_thr_num ,解锁
- 判断线程扩容和瘦身条件是否达成
- 达成扩容条件:创建 wait_exit_thr_num 个添加到线程池中
- 达成瘦身条件:销毁 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 向线程池中添加任务
- 对线程池加锁
- 队列已满时 pthread_cond_wait(&(pool->queue_not_full), &(pool->lock)); 阻塞,并解锁
- 当条件满足时队列不满,pthread_cond_wait 返回并重新加锁
- 清空任务队列对应下标的内容
- 添加任务到任务队列,任务队列尾+1 ,pool->queue_rear = (pool->queue_rear + 1) % pool->queue_max_size;
- 任务队列大小+1
- 唤醒等待任务的线程 pthread_cond_signal(&(pool->queue_not_empty));
- 解锁
8. threadpool_destroy 销毁线程池
- pool->shutdown 置为 true
- 销毁管理者线程
- 通知所有的存活线程 pthread_cond_broadcast(&(pool->queue_not_empty)); ,即工作线程会经由判断退出,工作线程自己结束
- 回收结束的线程
- 调用 threadpool_free() 函数释放线程池 pool
2. UDP
1. TCP通信和UDP通信各自特性
TCP | UDP | |
---|---|---|
特性 | 面向连接的,可靠的数据包传递。 对于不稳定的网络层。采取完全弥补的通信方式。 丢包重传。 |
无连接的,不可靠的数据报传递。 对于不稳定的网络层。采取完全不弥补的通信方式。 默认还原网络真实状态。 |
优点 | 数据流量稳定。速度稳定。顺序稳定。 | 传输速度快,相对高。开销小。 |
缺点 | 传输速度慢。通信效率低。开销大。 | 数据流量不稳定。速度不稳定。顺序不稳定。 |
使用场景 | 对数据完整性,要求较高, 不追求效率。—— 大数据传输。文件传参…… | 对数据时效性要求较高,稳定性其次。—— 视频会议、视频电话、游戏…… |
2. UDP 实现 C/S 模型
1.原理图
2.server端实现流程
- socket() 函数创建套接字 sockfd,指定 参数type 为 SOCK_DGRAM
- 设置服务器的地址结构sockaddr_in
- bind() 绑定地址结构
- while(1){
- recvfrom() 函数接收客户端发送来的信息 ----- 涵盖 accept 函数的作用。 传出对端的地址结构
- 小写转大写 处理字符串,写到屏幕
- sendto() 函数将处理后的字符串发送给客户端 ---- 替换 write()
- }
- close(sockfd);
3.client端实现流程
- socket() 函数创建套接字 sockfd,指定 参数type 为 SOCK_DGRAM
- 设置服务器端的sockaddr_in ,作为sendto() 函数的传参使用
- sendto() 函数将数据发送给服务器 ----- 相当于替换 connect(sockfd, “服务端的地址结构”, 地址结构大小);
- 写到屏幕
- recvfrom() 函数接收客户端发送来的信息 ---- 替换 read()
- 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
- ps -Lf 进程号 :显示该进程创建的所有线程
- 条件变量的静态初始化使用时机:当全局仅有该一份全局变量时,使用静态初始化。其他情况使用动态初始化。