基于线程池和使用openssl加密的TCP文件传输
主函数部分
在主函数完成线程池的初始化,socket初始化,以及SSL加密算法库的初始化和加密算法的加载。在完成这些任务的初始化后,在while()循环里等待客户端的接入,一旦有客户端接入就将对接客户端所需要的参数和处理函数加入消费者任务链表,让消费者线程去处理客户端的任务请求。而后回来再次等待下一个客户端的接入,并再次将相关参数加入任务链表。
代码演示
#include "pthreadpool.h"
#include "tcp_function.h"
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <strings.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
//#define DEBUG
int main(int argc,char**argv)
{
struct timeval tm;
SSL_CTX *ctx;
fd_set set;
int ret=-1;
thread_pool *pool = (thread_pool *)malloc(sizeof(thread_pool));
threadpool_init(pool, 10000,100); //初始化线程池,并设置线程池容量
/* SSL 库初始化 */
SSL_library_init();
/* 载入所有 SSL 算法 */
OpenSSL_add_all_algorithms();
/* 载入所有 SSL 错误消息 */
SSL_load_error_strings();
/* 以 SSL V2 和 V3 标准兼容方式产生一个 SSL_CTX ,即 SSL Content Text */
ctx = SSL_CTX_new(SSLv23_server_method());
/* 也可以用 SSLv2_server_method() 或 SSLv3_server_method() 单独表示 V2 或 V3标准 */
if (ctx == NULL)
{
ERR_print_errors_fp(stdout);
exit(1);
}
/* 载入用户的数字证书, 此证书用来发送给客户端。 证书里包含有公钥 */
if (SSL_CTX_use_certificate_file(ctx,argv[1], SSL_FILETYPE_PEM) <= 0)
{
ERR_print_errors_fp(stdout);
exit(1);
}
/* 载入用户私钥 */
if (SSL_CTX_use_PrivateKey_file(ctx,argv[2], SSL_FILETYPE_PEM) <= 0)
{
ERR_print_errors_fp(stdout);
exit(1);
}
/* 检查用户私钥是否正确 */
if (!SSL_CTX_check_private_key(ctx)) {
ERR_print_errors_fp(stdout);
exit(1);
}
int socketfd=tcp_init(&tm);//初始化socket
if (socketfd<0)
{
printf("socket 初始化错误\n");
}
struct sockaddr_in caddr;
socklen_t caddrlen=sizeof(caddr);
FD_ZERO(&set);
FD_SET(socketfd,&set);
while (1)
{
ret=select(socketfd+1,&set,NULL,NULL,NULL);
if (ret<0)
{
perror("why");
}
else if (ret==0)
{
printf("等待超时\n");
}
else
{
if (FD_ISSET(socketfd,&set))
{
int accept_fd=accept(socketfd,(struct sockaddr*)&caddr,&caddrlen);
sock_for_thread* sockfd_ctx;
sockfd_ctx=(sock_for_thread*)malloc(sizeof(sock_for_thread));
sockfd_ctx->sockfd=accept_fd;
sockfd_ctx->ctx=ctx;
if (accept_fd<0)
{
printf("TCP accept 失败\n");
perror("why");
}else //如果有端口接入,则往链表添加任务,让阻塞的线程去处理客户端的接入
{
threadpool_add_runner(pool, dealwith_client_connect, sockfd_ctx);
}
}
}
}
/* 释放 CTX */
SSL_CTX_free(ctx);
threadpool_destroy(&pool); //销毁前程池
return 0;
}
部分代码讲解
阅读上面的主函数代码可能会好奇在accept检测客户端接入之前先使用select检测socket套接字是否发生,再调用accept对接客户端,看上有点多此一举,但是其实不然,应为我在初始化socket初始化的时候设置了socket的接收超时参数,但是我在调试的过程中发现设置socket接收超时参数会导致accept()由原先的阻塞等待客户端的接入变成了非阻塞,不管有没有客户端接入,accept都会返回,这就导致了主函数的while循环一直在跑,所以就在accept()之前先使用select()监听socket套接字有没有事件发生,如果select返回就可以说明监听的句柄有事件发生了,即为有新的客户端接入,此时再调用accept对接。至于为什么要将socket设置成非阻塞的我们下面再讨论。
线程池部分
线程池部分由线程池初始化、线程体函数(消费者),任务添加函数、杀死线程函数、服务器退出信号处理函数、线程池管理者、线程池销毁等函数组成。线程池通过使用条件变量和互斥锁来约束消费者去任务和主线程添加任务。
代码演示
.c文件
#include "pthreadpool.h"
#include "tcp_function.h"
#include <signal.h>
thread_pool* exit_pool=NULL;
//#define DEBUG
/**
* 函数名称:void threadpool_init(thread_pool* pool, int max_thread_size,int min_thread_size)
* 函数作用:初始化线程池。
* 参数:
* pool:指向线程池结构有效地址的动态指针。
* max_thread_size:需要创建最大的线程数。
*/
void threadpool_init(thread_pool* pool, int max_thread_size,int min_thread_size)
{
// 初始化互斥量
pthread_mutex_init(&(pool->mutex), NULL);
// 初始化条件变量
pthread_cond_init(&(pool->cond), NULL);
pool->runner_head = NULL;
pool->runner_tail = NULL;
pool->max_thread_size = max_thread_size;
pool->min_thread_size=min_thread_size;
pool->shutdown = 0;
pool->pthread_num=0;
pool->task_num=0;
pool->pthread_busy_num=0;
pool->kill_customer_num=0;
//创建所有分离态线程(即创建线程池)
pool->threads = (pthread_t *)malloc(max_thread_size * sizeof(pthread_t));
pool->manager=(pthread_t*)malloc(sizeof(pthread_t));
int i = 0;
pthread_mutex_lock(&(pool->mutex));
for (i = 0; i < min_thread_size; i++)
{
pthread_attr_t attr;
pthread_attr_init(&attr); //创建线程属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //设置以分离状态启动线程
pthread_create(&(pool->threads[i]), &attr, (void*)run, (void*)pool);
}
pool->pthread_num=min_thread_size;
pthread_mutex_unlock(&(pool->mutex));
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
pthread_create(pool->manager,&attr,manager_trurn,(void*)pool);
#ifdef DEBUG
printf("threadpool_init-> create %d detached thread\n", max_thread_size);
#endif
}
/*******************************
*函数名称:void run(void *arg)
*函数作用:线程体(消费者线程)从任务链表中取出任务,并调用任务处理函数处理客户端任务。
*函数参数:void*arg,就是指向线程池结构体指针。
*函数返回值:无。
*******************************/
void run(void *arg)
{
thread_pool* pool = (thread_pool*)arg;
while (1)
{
// 加锁
pthread_mutex_lock(&(pool->mutex));
#ifdef DEBUG
printf("run-> locked\n");
#endif
// 如果等待队列为0并且线程池未销毁,则处于阻塞状态(即等待任务的唤醒)
while (pool->runner_head == NULL && !pool->shutdown)
{
pthread_cond_wait(&(pool->cond),&(pool->mutex));
if ((pool->kill_customer_num>0)&&(pool->pthread_num>pool->min_thread_size))
{
pool->kill_customer_num--;
pool->pthread_num--;
killpthread(pool);
}
}
//如果线程池已经销毁
if (pool->shutdown)
{
// 解锁
pthread_mutex_unlock(&(pool->mutex));
#ifdef DEBUG
printf("run-> unlocked and thread exit\n");
#endif
pthread_exit(NULL);
}
//取出链表中的头元素
thread_runner *runner = pool->runner_head;
pool->runner_head = runner->next;
pool->task_num=pool->task_num-1; //从链表中取出一个任务,当前任务数被消费掉了减一
pool->pthread_busy_num=pool->pthread_busy_num+1;
// 解锁
pthread_mutex_unlock(&(pool->mutex));
#ifdef DEBUG
printf("run-> unlocked\n");
#endif
// 调用回调函数,执行任务
(runner->callback)(runner->sockfd_ctx);
free(runner);
runner = NULL;
pthread_mutex_lock(&(pool->mutex));
pool->pthread_busy_num=pool->pthread_busy_num-1;
pthread_mutex_unlock(&(pool->mutex));
#ifdef DEBUG
printf("run-> runned and free runner\n");
#endif
}
}
/********************************
* 函数名称:void threadpool_add_runner(thread_pool* pool, void(*callback)(sock_for_thread *sockfd_ctx), sock_for_thread *sockfd_ctx)
* 函数作用:向线程池任务链表加入任务
* 参数:
* pool:指向线程池结构有效地址的动态指针
* callback:消费者线程对接客户端任务处理函数指针(将该函数指针加入任务链表让消费者线程取出任务函数并执行对接客户端请求)
* sockfd_ctx:储存callback函数参数的结构体指针变量
********************************/
void threadpool_add_runner(thread_pool* pool, void(*callback)(sock_for_thread *sockfd_ctx), sock_for_thread *sockfd_ctx)
{
// 构造一个新任务
thread_runner *newrunner = (thread_runner *)malloc(sizeof(thread_runner));
newrunner->callback = callback;
newrunner->sockfd_ctx = sockfd_ctx;
newrunner->next = NULL;
// 加锁
pthread_mutex_lock(&(pool->mutex));
#ifdef DEBUG
printf("threadpool_add_runner-> locked\n");
#endif
// 将任务加入到等待队列中
if (pool->runner_head != NULL)
{
pool->runner_tail->next = newrunner;
// 尾指针指到最后一个任务
pool->runner_tail = newrunner;
}
else
{
pool->runner_head = newrunner;
pool->runner_tail = newrunner;
}
pool->task_num=pool->task_num+1;
// 解锁
pthread_mutex_unlock(&(pool->mutex));
#ifdef DEBUG
printf("threadpool_add_runner-> unlocked\n");
#endif
//唤醒一个等待线程处理任务
pthread_cond_signal(&(pool->cond));
#ifdef DEBUG
printf("threadpool_add_runner-> add a runner and wakeup a waiting thread\n");
#endif
}
/********************************
* 函数名称:void killpthread(thread_pool*pool)
* 函数作用:当线程池的消费者数量过多时杀死消费者调整线程池大小节省性能开销
* 参数:
* pool:指向线程池结构有效地址的动态指针
********************************/
void killpthread(thread_pool*pool)
{
if (pool==NULL)
{
exit(0);
}
for (int i = 0; i < pool->max_thread_size; i++)
{
if (pool->threads[i]==pthread_self())
{
pool->threads[i]=0;
break;
}
}
printf("工作者%ld死亡\n",pthread_self());
pthread_mutex_unlock(&pool->mutex);
pthread_exit(NULL);
}
/********************************
* 函数名称:void exit_function(int sig)
* 函数作用:在服务器进程接收到信号后会执行该函数用于停止运行服务器进程(相当于中断处理程序)
* 参数:
* pool:接收到的SIGINT信号
********************************/
void exit_function(int sig)
{
if (sig!=SIGINT)
{
return;
}
printf("服务器准备停止运行\n");
threadpool_destroy(&exit_pool);
_exit(0377);
}
/********************************
* 函数名称:void *manager_trurn(void*arg)
* 函数作用:线程池管理者,监视线程池中消费者与任务数量,并对线程池的大小做出动态调整
在客户端接入高峰时创建更多的消费者对接客户的任务请求
*函数参数:void*arg,就是指向线程池结构体指针。
********************************/
void *manager_trurn(void*arg)
{
thread_pool* pool=(thread_pool*)arg;
int pthread_busy_num; //用于暂时保持忙碌工作者线程数
int pthread_num; //当前工作者线程数
int task_num; //当前任务数
int shutdown;
int free_pthread_num; //空闲的线程数
int min_thread_size;
exit_pool=pool; //将维护线程池的结构体指针做一份拷贝,用于线程池销毁
signal(SIGINT,exit_function); //初始化捕获信号,和信号处理函数
while (1)
{
sleep(1);
pthread_mutex_lock(&pool->mutex);
pthread_busy_num=pool->pthread_busy_num;
pthread_num=pool->pthread_num;
task_num=pool->task_num;
min_thread_size=pool->min_thread_size;
pthread_mutex_unlock(&pool->mutex);
printf("当前线程数%d\n",pthread_num);
printf("当前任务数%d\n",task_num);
if (shutdown==1)
{
killpthread(NULL); //退出管理者线程
}
//消费者数量控制算法
free_pthread_num=pthread_num-pthread_busy_num;
if ((free_pthread_num)<(5*task_num))
{
pthread_mutex_lock(&pool->mutex);
for (int i = 0; i < pool->max_thread_size; i++)
{
if ((pool->threads[i]==0)&&(pool->shutdown!=1))
{
for (int i = 0; i <((task_num)-free_pthread_num); i++)
{
if (pool->pthread_num>=pool->max_thread_size)
{
break;
}
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //设置以分离状态启动线程
pthread_create(&pool->threads[i],&attr,(void*)run,(void*)pool);
pool->pthread_num++;
}
break;
}
}
pthread_mutex_unlock(&pool->mutex);
}
//查看是否满足条件,杀死线程
if ((pthread_num>(task_num*3))&&(pthread_num>min_thread_size))
{
pthread_mutex_lock(&pool->mutex);
pool->kill_customer_num=50;
pthread_cond_broadcast(&(pool->cond));
pthread_mutex_unlock(&pool->mutex);
}
}
}
/********************************
* 函数名称:void threadpool_destroy(thread_pool** ppool)
* 函数作用:在服务器程序退出前,销毁线程池
* 函数参数:指向线程池结构体的函数指针的指针
********************************/
void threadpool_destroy(thread_pool** ppool)
{
thread_pool *pool = *ppool;
// 防止2次销毁
if (!pool->shutdown)
{
pool->shutdown = 1;
// 唤醒所有等待线程,线程池要销毁了
pthread_cond_broadcast(&(pool->cond));
// 等待所有线程中止
sleep(1);
#ifdef DEBUG
printf("threadpool_destroy-> wakeup all waiting threads\n");
#endif
// 回收空间
free(pool->threads);
// 销毁等待队列
thread_runner *head = NULL;
while (pool->runner_head != NULL)
{
head = pool->runner_head;
pool->runner_head = pool->runner_head->next;
free(head);
}
#ifdef DEBUG
printf("threadpool_destroy-> all runners freed\n");
#endif
// 条件变量和互斥量也别忘了销毁
pthread_mutex_destroy(&(pool->mutex));
pthread_cond_destroy(&(pool->cond));
#ifdef DEBUG
printf("threadpool_destroy-> mutex and cond destoryed\n");
#endif
free(pool);
(pool) = NULL;
#ifdef DEBUG
printf("threadpool_destroy-> pool freed\n");
#endif
}
}
.h文件
#ifndef THREADPOOL_H
#define THREADPOOL_H
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include "tcp_function.h"
/************************
* 线程体任务链表数据结构
************************/
typedef struct runner
{
void(*callback)(sock_for_thread* sockfd_ctx); // 回调函数指针
void* arg; // 回调函数的参数
struct runner* next;
sock_for_thread* sockfd_ctx;
} thread_runner;
/***********************
* 线程池数据结构
***********************/
typedef struct
{
pthread_mutex_t mutex; // 互斥量
pthread_cond_t cond; // 条件变量
thread_runner* runner_head;// 线程池中所有等待任务的头指针
thread_runner* runner_tail;// 线程池所有等待任务的尾指针
int shutdown; // 线程池是否销毁
int kill_customer_num; //1表示线程池的消费者过多需要杀死
pthread_t* threads; // 所有线程
pthread_t* manager; //线程管理者
int max_thread_size; // 线程池中允许的活动线程数目
int min_thread_size;
int pthread_num; //当前工作者线程数
int task_num; //当前任务数
int pthread_busy_num; //忙碌线程数
} thread_pool;
void run(void *arg);
void threadpool_init(thread_pool* pool, int max_thread_size,int min_thread_size);
void threadpool_add_runner(thread_pool* pool, void(*callback)(sock_for_thread *sockfd_ctx), sock_for_thread *sockfd_ctx);
void threadpool_destroy(thread_pool** ppool);
void *manager_trurn(void*arg);
void killpthread(thread_pool*pool);
void finishserve(int sig);
#endif
部分代码讲解
对接客户端代码(消费者处理接客户端请求)
此部分的代码主要由处理客户端请求的功能函数组成,当然还有一个socket初始化函数。最主要的还是void dealwith_client_connect(sock_for_thread *sockfd_ctx)函数,在主函数里当有客户端接入是就将此函数的地址和它所需要的运行参数放入到任务链表中,这样完成了任务的添加,并通知消费者去消费任务,消费者就是通过此函数的一些辅助功能函数完成对客户端的请求。
代码演示
.c文件
#include "tcp_function.h"
#include "pthreadpool.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define IP_ADDR "192.168.2.224"
/*****************
函数名称:int tcp_init(struct timeval *tm)
作用:用于socket的初始化
参数:struct timeval *tm时间结构体变量指针
******************/
int tcp_init(struct timeval *tm)
{
int error_num=0;
int socketfd=socket(AF_INET,SOCK_STREAM,0);
tm->tv_sec=1;
tm->tv_usec=0;
setsockopt(socketfd, SOL_SOCKET,SO_RCVTIMEO,(const void*)tm, sizeof(*tm));
if (socketfd==-1)
{
error_num=-1;
return error_num;
}
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(8999);
inet_aton(IP_ADDR,&addr.sin_addr);
int ret=bind(socketfd,(struct sockaddr*)&addr,(socklen_t)sizeof(addr));
if (ret!=0)
{
error_num=-2;
return error_num;
}
ret=listen(socketfd,30);
if (ret!=0)
{
error_num=-3;
return error_num;
}
return socketfd;
}
/**********
作用:对从socket接收到的数据进行分析
参数:struct SockStruct_t *sock_container为用于装载信息的结构体
返回值:无
*********/
void data_analysis(SockStruct_t *sock_container)
{
sock_container->cmd=0;
printf("from client cmd->%d\n",sock_container->cmd);
if(!strcmp("sls",sock_container->rbuf)) sock_container->cmd=LS;
if(!strstr(sock_container->rbuf,"scd")) sock_container->cmd=CD;
if(!strstr(sock_container->rbuf,"get")) sock_container->cmd=GET;
if(!strcmp("spwd",sock_container->rbuf)) sock_container->cmd=PWD;
}
/*****************
* 函数名称:void sls_function_cmd(SSL*ssl,SockStruct_t*sock_container)
作用:列出服务器当前目录的所有文件名称,并发送到客户端
参数:struct SockStruct_t *sock_container为用于装载信息的结构体指针
SSL*ssl加密传输的描述符
返回值:无
*****************/
void sls_function_cmd(SSL*ssl,SockStruct_t*sock_container)
{
FILE *fp=NULL;
char buf[12800];
char sbuf[10240];
size_t written=0;
bzero(buf,sizeof(buf));
bzero(sbuf,sizeof(sbuf));
fp = popen("ls ","r");
if( !fp )
{
printf("popen FILE失败\n");
return;
}
while (fgets (buf,sizeof(buf),fp) != NULL) //从流中获取数据
{
strcat(sock_container->sbuf,buf);
}
pclose(fp);
memcpy(sbuf,sock_container,sizeof(*sock_container));
int ret=SSL_write_ex(ssl, (const void *)sbuf, sizeof(sbuf), &written);
memset(sock_container->sbuf,0,strlen(sock_container->sbuf));
if (ret==0)
{
printf("send 发送数据失败\n");
}
}
/*****************
* 函数名称:void pwd_function_cmd(SSL*ssl,SockStruct_t*sock_container)
作用:用于列出服务器当前的所在路径,并发送到客户端
参数:struct SockStruct_t *sock_container为用于装载信息的结构体指针
SSL*ssl加密传输的描述符
返回值:无
*****************/
void pwd_function_cmd(SSL*ssl,SockStruct_t*sock_container)
{
FILE *fp=NULL;
char buf[12800];
char sbuf[10240];
int ret=0;
size_t written=0;
bzero(buf,sizeof(buf));
bzero(sbuf,sizeof(sbuf));
fp = popen("pwd ","r");
if( !fp )
{
printf("popen FILE失败\n");
return;
}
while (fgets (buf,sizeof(buf),fp) != NULL)
{
strcat(sock_container->sbuf,buf);
}
pclose(fp);
memcpy(sbuf,sock_container,sizeof(*sock_container));
//ret=send(accept_fd,&sbuf,sizeof(sbuf),MSG_NOSIGNAL);
ret=SSL_write_ex(ssl, (const void*)&sbuf, sizeof(sbuf), &written);
memset(sock_container->sbuf,0,strlen(sock_container->sbuf));
if (ret==0)
{
printf("send 发送数据失败\n");
}
}
/*****************
* 函数名称:void put_function_cmd(SSL*ssl,SockStruct_t*sock_container)
作用:接收客户端上传的文件数据并保存
参数:SSL*ssl加密传输的描述符
struct SockStruct_t *sock_container为用于装载信息的结构体指针
返回值:无
*****************/
void put_function_cmd(SSL*ssl,SockStruct_t*sock_container)
{
char interim_buf[1024];
size_t readbytes=0;
int ret=-1;
bzero(interim_buf,sizeof(interim_buf));
int filename_fd=open(sock_container->file_name,O_RDWR|O_CREAT,0666);
if (filename_fd<=0)
{
perror("why");
printf("save file to open error\n");
}
else
{
bzero(interim_buf,sizeof(interim_buf));
bzero(sock_container->file_name,sizeof(*(sock_container->file_name)));
bzero(interim_buf,sizeof(interim_buf));
sleep(1);
while (1)
{
ret=SSL_read_ex(ssl, (void*)&interim_buf, sizeof(interim_buf),&readbytes);
if (ret==0) break;
if (readbytes<=0)
{
printf("break put while\n");
break;
}
else if ((write(filename_fd,(void*)&interim_buf,readbytes))<0)
{
perror("why");
printf("save file to write error\n");
}
bzero(&interim_buf,sizeof(interim_buf));
}
}
printf("read file ok\n");
close(filename_fd);
}
/*****************
* 函数名称:void get_function_cmd(SSL *ssl,SockStruct_t *sock_container)
作用:将服务器的文件数据发送到服务端
参数:SSL*ssl加密传输的描述符
struct SockStruct_t *sock_container为用于装载信息的结构体指针
返回值:无
*****************/
void get_function_cmd(SSL *ssl,SockStruct_t *sock_container)
{
ssize_t file_bytes_number=0;
size_t written=0;
char interim_buf[1024];
if (access(sock_container->file_name,F_OK)==-1)
{
return;
}
else
{
int filename_fd=open(sock_container->file_name,O_RDONLY);
if (filename_fd==-1)
{
printf("open file error\n");
perror("why");
}
while((file_bytes_number=read(filename_fd,interim_buf,sizeof(interim_buf)))>0)
{
//send(accept_fd,(void*)&interim_buf,file_bytes_number,MSG_NOSIGNAL);
int ret=SSL_write_ex(ssl,(const void*)&interim_buf, file_bytes_number, &written);
if(ret==0) printf("写入数据失败\n");
memset(interim_buf,0,sizeof(interim_buf));
}
close(filename_fd);
}
}
/*****************
* 函数名称:void dealwith_client_connect(sock_for_thread *sockfd_ctx)
作用:对接客户端请求的任务处理函数,当有客户端接入时,就会将此函数添加到任务链表,通知消费者消费任务。
参数:sock_for_thread *sockfd_ct储存者socket描述符和openssl的ctx结构体指针
返回值:无
*****************/
void dealwith_client_connect(sock_for_thread *sockfd_ctx)
{
int accept_fd=sockfd_ctx->sockfd;
SSL_CTX *ctx=sockfd_ctx->ctx;
free(sockfd_ctx);
sockfd_ctx=NULL;
SockStruct_t sock_container;
char rbuf[10240];
int ret=0;
SSL *ssl;
size_t readbytes;
/* 基于 ctx 产生一个新的 SSL */
ssl = SSL_new(ctx);
/* 将连接用户的 socket 加入到 SSL */
SSL_set_fd(ssl, accept_fd);
/* 建立 SSL 连接 */
if (SSL_accept(ssl) == -1)
{
perror("accept");
close(accept_fd);
return;
}
memset(sock_container.rbuf,'0',sizeof(sock_container.rbuf));
memset(sock_container.sbuf,'0',sizeof(sock_container.sbuf));
sock_container.cmd=-1;
#ifdef DEBUG
#else
while (1)
{
memset(&rbuf,0,sizeof(rbuf));
SSL_read_ex(ssl, rbuf, sizeof(rbuf), &readbytes);
ret=SSL_get_error(ssl,0);
if (ret==SSL_ERROR_ZERO_RETURN)
{
printf("客户端断开链接\n");
break;
}
memset(&sock_container,0,sizeof(sock_container));
memcpy(&sock_container,rbuf,sizeof(sock_container));
if (ret==-1) printf("Failed receive data\n");
else
{
switch (sock_container.cmd)
{
case LS: //开发完成
sls_function_cmd(ssl,&sock_container);
break;
case CD: //功能待开发中
printf("此功能暂未开启\n");
break;
case GET://完成
get_function_cmd(ssl,&sock_container);
break;
case PWD://完成
pwd_function_cmd(ssl,&sock_container);
break;
case PUT: //完成
put_function_cmd(ssl,&sock_container);
break;
case RM:
break;
default:
break;
}
sock_container.cmd=-1;
}
}
/* 关闭 SSL 连接 */
SSL_shutdown(ssl);
/* 释放 SSL */
SSL_free(ssl);
/* 关闭 socket */
close(accept_fd);
#endif
}
.h文件
部分代码调试问题
(1) 在最初编写代码的时候没有注意到send()函数的使用,在此函数的最后一个参数int flags参数并没有对他进行设置,导致当客户端断开连接的时候服务器程序莫名的结束运行,原来是由于我没有把int flags参数设置成MSG_NOSIGNAL,导致在写无连接的套接字时产生SIGIPE信号,SIGPIPE 信号管道断裂,向已关闭的管道写操作 进程终止,导致线程池崩溃。如果是使用多进程的方式对接客户端请求,每个进程对接一个客户端请求,就不会发生这样的事情。
因为因为进程是线程运行的容器,进程是内存单元分配的基本单位,而线程是cpu运行的基本单位。不同的线程共享处在同一个进程下的所有数据段和代码段,以及堆栈和全局变量。一旦有一个线程导致进程退出,操作系统会回收为进程分配的内存,从而会导致其他线程也随之崩溃,服务器进程退出这在多线程任务程序中显然是不能被允许的。
(2) 在完成put功能的数据传输的时候cls功能可以正常使用,但是sls用能就会卡死无法使用。这是由于在while循环里面不断地recv读取客户端发送过来的数据recv设置为了MSG_WAITALL没有数据来就一直等待数据到来并读取的选项,但是客户端的数据已经发送完成了,但是服务器对接客户端的线程还在一直阻塞等待数据到来读取,没有一个合理的读取完数据退出while循环的机制,所以把recv()函数int flag选项设置为非阻塞MSG_DONTWAIT所以此时如果没有读到数据就会不在等待继续往下执行但是我们可以从man手册可以查到有关recv()函数返回值的描述“These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error.”所以recv()函数返回的是读取到的字节数,如果错误将会返回-1,所以如果读取到-1或者o就可以break;退出while循环了;但是这样又带来了一个问题,那就是在读取客户端发送的数据之前首先时recv读取文件名(文件名和文件数据是分开发送的)所以在客户端线程在读取到文件名后就会open()O_CREAT这个文件然后就会马上进入while()循环读取来自客户端发送的文件数据,但是由于服务器线程创建文件后到进入while循环这个时间端很短很快就会,但是由于客户端传输的数据收到网络传输速度的影响并不是马上完成传输的,这就导致了客户端还没有发送完一批数据,服务器就提前进入了while()循环读取文件数据但是因为我们此前设置了MSG_DONTWAIT选项,recv()函数并不会等待数据,而是直接返回一个错误导致还没有结束到文件数据
服务器就提前结束数据接收,所以我们需要让客户端的数据发送完成才能让服务器线程进入while循环读取文件数据,我们在while循环前加上一个sleep(1);等待客户端发送数据再进入接收就可以解决这个问题,等待一秒后,此时可以保证客户端一直有数据传输过来,知道发送完成,然后recv()就从网络读取不到数据了,就会退出while循环结束一次完整的接收任务。同理get函数也是一样的,接收数据的一方要等待数据发送才能开始接收。不然的话就会导致while已经退出数据接收了,但是网络中还存在大批的客户端发送的数据,这样就会影响服务器对客户端数据接收的下一步操作,导致服务器在主循环里面一直在读取此前客户端发送的数据,这样服务器就跑飞了。
(3) 在使用openssl库进行文件加密传输,编写get函数的功能的时候,客户端在接收大文件的时候,当文件接收完成
由于SSL_read_ex()函数时阻塞等待数据的所以当文件数据接收完成后就会阻塞等待数据的到来,所以就会一直卡在
SSL_read_ex()无法退出while循环,在前采用了slect()函数IO检查是否有IO时间发生但是效果并不理想,虽然能够让SSL_read_ex()函数退出while()循环,但是并不能读取到数据,创建并读取的测试文件大小为0字节。其次就是通过SSL_set_mode(ssl,SSL_MODE_AUTO_RETRY);函数和SSL_CTX_set_mode(ctx,SSL_MODE_AUTO_RETRY);
函数企图设置BIO为non-blocking但是并没有效果,可能是自己对openssl库并不了解。最后最后一种解决方法可以解决SSL_read_ex()函数的阻塞问题感谢大佬的文章给我提供了解决问题的参考方向附带上大佬的文章链接。就像大佬说的那样那就是 SSL_read 的阻塞其实是 socket 造成的,原因嘛,我们前面不是说过了 ssl/tls 不过是封装了加密的 socket 吗?而这种封装都集中在连接过程中,数据通讯过程完全就是 socket 过程,只是数据是加密过的而已。所以 SSL_read 的阻塞解决起来是很简单的,只要设置 socket 超时参数就可以了,网友说 openssl 文档中说可以用 select 函数 … 严格来说这个做法不全对,因为 socket 设置超时的做法是很多的。我们使用setsockopt()函数就可以设置socket()的读取数据超时时间。当文件数据已经去读完成了的时候,SSL_read_ex()
函数就会阻塞,但是我们此前设置了接收超时参数,当接收超时的时候就会break退出文件接收,并关门文件描述符,这样文件
接收就完成。
(4) 在服务器端使用由于也要使用到put上传文件所以就需要服务器进程需要一个while循环去读取来自客户端发送的文件数据也需要一个合理的退出机制所以服务器的soket()也要像客户端那样使用setsockopt()函数设置socket的接收超时参数但是客户端设置了超时参数时在服务器端已经运行的情况下的,所以在设置了客户端的超时参数后调用connect()函数就可以连接到服务器,但是服务器设置了socket超时参数就会导致accept()函数有原先的阻塞等待客户端的接入变为,变为非阻塞状态(设置了socket的接收超时参数后不管是recv(),还是SSL_read_ex()函数都会变为超时返回,同时accept()函数也会变为没有客户端接入的情况下变为非阻塞状态,而且客户端连接上了但是无法正常进行数据传输 )不管客户端是否接入都不断地检测和返回失败accept(),导致客户端无法正常接入进行数据传输,本次项目我采用的解决方法时使用select()函数对使socket套接字绑定到监听范围,对socket()函数的返回值套接字进行监听,这样如果有新的客户端接入select()函数就会返回,还有就是我在使用select()函数时并没有设置超时时间,这样只要监听的套接字描述符时间没有发生,就会一直阻塞等待事件的发生并返回,对其返回值进行判断然后在调用FD_ISSET()宏判对socket
套接字描述符的事件是否发生如果发生就可以使用accept()函数链接客户端了,这样一来就相当于恢复了accept()函数的无连接
阻塞状态。
(5) 还有就是当客户端接入的时候消费者线程执行线程回调函数,在线程回调函数中需要一个while()循环不断地SSL_read_ex()读取客户端发送的指令然后调用各种指令函数完成消费者线程与客户端的交互。但是由于此前设置了socket()的超时参数所以就导致了无法通过SSL_read_ex()的返回值判断客户端是否断开链接,因为SSL_read_ex()函数超时了的返回值是0,断开链接的返回值也是0,但是此前recv()函数是可以直接通过判断返回值来判断客户端是否已经断开了链接,如果客户端断开了链接recv()函数就会返回-1,当客户端还在链接就会返回去读到的字节数目。所以就可以判断recv函数的返回值来执行是否break;当前while()循环。但是在使用openssl库进行加密通信的时候这种方法时行不通的,非常幸运openssl为我们提供了一个非常好用的错误检测函数SSL_get_error(const SSL *ssl, int ret)从man手册中我们可以查到关于此函数的描述和关于SSL_ERROR_WANT_READ宏和SSL_ERROR_ZERO_RETURN宏这两个宏的描述,其中SSL_ERROR_ZERO_RETURN描述如下:The TLS/SSL peer has closed the connection for writing by sending the close_notify alert. No more data can be read. Note that
SSL_ERROR_ZERO_RETURN does not necessarily indicate that the underlying transport has been closed. 说白了就是说当检测到与ssl绑定的套接字端口断开链接的时候SSL_get_error()函数就会
返回这个宏;关于SSL_ERROR_WANT_READ宏的描述如下:SSL_ERROR_WANT_READ is returned when the
last operation was a read operation from a non-blocking BIO. It means that not enough data was available at this time to complete the operation. If at a later time the underlying BIO has data available for reading the same function can be called again.也会是当设置了非阻塞的时候,没有读取到足够有效的就会返回这个宏。我们通过检测SSL_get_error()所返回的宏就可以判断客户端是否断开了链接。
截至到这里,以上就是服务端的代码,线程池,和对接客户端的功能函数共同组成了TCP服务端。
客户端部分
代码演示
.c文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <fcntl.h>
#include <sys/time.h>
#define NU 0
#define CLS 1
#define SLS 2
#define CCD 3
#define Superior 4
#define SCD 5
#define GET 6
#define CPWD 7
#define SPWD 8
#define PUT 9
#define Q 10
#define RM 11
#define IP_ADDR "192.168.2.224"
typedef struct SockStruct
{
int cmd;
char check[4];
char rbuf[1024];
char sbuf[1024];
char file_name[1024];
}SockStruct_t;
char sbuf[10240];
char rbuf[10240];
char *keyboard_cmd=NULL;
char*pathname;
char*filename;
int filename_fd;
char interim_buf[1024];
int file_size;
SockStruct_t sockbuf;
int sockfd;
/**********
函数名:void ShowCerts (SSL* ssl)
函数作用:打印加密证书等相关信息
函数参数:SSL*ssl加密传输的描述符
函数返回值:无
*********/
void ShowCerts (SSL* ssl)
{
X509 *cert;
char *line;
cert=SSL_get_peer_certificate(ssl);
if(cert !=NULL){
printf("数字证书信息:\n");
line=X509_NAME_oneline(X509_get_subject_name(cert),0,0);
printf("证书:%s\n",line);
free(line);
line=X509_NAME_oneline(X509_get_issuer_name(cert),0,0);
printf("颁发者:%s\n",line);
free(line);
X509_free(cert);
}else
printf("无证书信息!\n");
}
/**********
函数名:int data_analysis(char*buf)
函数作用:对从键盘输入的字符串命令进行分析
函数参数:struct SockStruct_t *sock_container为用于装载信息的结构体
函数返回值:返回用于装载信息的结构体
*********/
int data_analysis(char*buf)
{
if(!strcmp("sls",buf)) return SLS;
if(!strcmp("cls",buf)) return CLS;
if(strstr(buf,"ccd")!=NULL) return CCD;
if(strstr(buf,"scd")!=NULL) return SCD;
if(strstr(buf,"get")!=NULL) return GET;
if(!strcmp("cpwd",buf)) return CPWD;
if(!strcmp("cd../",buf)) return Superior;
if(!strcmp("spwd",buf)) return SPWD;
if(strstr(buf,"put")!=NULL) return PUT;
if(!strcmp("Q",buf)) return Q;
if(strstr(buf,"crm")!=NULL) return RM;
else return NU;
}
/*************************
函数名:char *string_split(char*cmsg)
函数作用:用于字符串分割
函数参数:需要分割的字符串
返回值:以空格为分割接线被分割出来的空格后的字符串
*************************/
char *string_split(char*cmsg) // 字符串分割
{
char *p;
char *splite_buf;
p=strtok_r(cmsg," ",&splite_buf);
p=strtok_r(NULL,"\0",&splite_buf);
return p;
}
/*************************************
函数名:void Sls_function_cmd(void)
函数作用:给服务器发送ls命令列出服务器当前文件夹的所以文件,并接收文件夹的文件名称输出到终端
函数参数:SSL*ssl加密传输的描述符
函数返回值:无
**************************************/
void Sls_function_cmd(SSL *ssl)
{
size_t written=0;
size_t readbytes=0;
memset(&sockbuf,0,sizeof(sockbuf));
sockbuf.cmd=1;
memcpy(&sbuf,&sockbuf,sizeof(sockbuf));
int ret=SSL_write_ex(ssl,&sbuf,sizeof(sbuf),&written);
if (ret==0) perror("why");
memset(&rbuf,0,sizeof(rbuf));
SSL_read_ex(ssl,(void *)&rbuf, sizeof(rbuf), &readbytes);
memset(&sockbuf,0,sizeof(sockbuf));
memcpy(&sockbuf,rbuf,sizeof(sockbuf));
printf("%s\n",sockbuf.sbuf);
printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
}
/*************************************
函数名:void spwd_function_cmd(void)
函数作用:给服务器发送pwd命令列出服务器当前文件夹的所在路径,
并接收服务器文件夹的的所在路径并输出到终端
函数参数:SSL*ssl加密传输的描述符
函数返回值:无
**************************************/
void spwd_function_cmd(SSL *ssl)
{
size_t written=0;
size_t readbytes=0;
memset(&sockbuf,0,sizeof(sockbuf));
sockbuf.cmd=4;
memcpy(&sbuf,&sockbuf,sizeof(sockbuf));
int ret=SSL_write_ex(ssl,&sbuf, sizeof(sbuf), &written);
if (ret==0) printf("SSL_write_ex data failure\n");
memset(&rbuf,0,sizeof(rbuf));
SSL_read_ex(ssl,&rbuf,sizeof(rbuf), &readbytes);
memset(&sockbuf,0,sizeof(sockbuf));
memcpy(&sockbuf,rbuf,sizeof(sockbuf));
printf("%s\n",sockbuf.sbuf);
printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
}
/*************************************
函数名:void put_function_cmd(void)
函数作用:上传文件到服务器
函数参数:SSL*ssl加密传输的描述符
函数返回值:无
**************************************/
void put_function_cmd(SSL*ssl)
{
size_t written=0;
filename=string_split(keyboard_cmd);
if (access(filename,F_OK)==-1)
{
printf("No such file or directory");
}
else
{
filename_fd=open(filename,O_RDONLY,0666);
memset(&sockbuf,0,sizeof(sockbuf));
memset(&sbuf,0,sizeof(sbuf));
sockbuf.cmd=5;
strcpy(sockbuf.file_name,filename);
memcpy(&sbuf,&sockbuf,sizeof(sockbuf));
SSL_write_ex(ssl, (const void *)&sbuf, sizeof(sbuf), &written);
memset(interim_buf,0,sizeof(interim_buf));
memset(&sockbuf,0,sizeof(sockbuf));
bzero(&file_size,sizeof(file_size));
while ((file_size=read(filename_fd,interim_buf,sizeof(interim_buf)))>0)
{
SSL_write_ex(ssl, (const void *)&interim_buf,file_size, &written);
bzero(&interim_buf,sizeof(interim_buf));
}
close(filename_fd);
printf("put file ok\n");
printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
}
}
/*************************************
函数名:void put_function_cmd(void)
函数作用:从服务器下载文件
函数参数:SSL*ssl加密传输的描述符
函数返回值:无
**************************************/
void get_function_cmd(SSL*ssl)
{
size_t written=0;
size_t readbytes=0;
int ret=-2;
filename=string_split(keyboard_cmd);
memset(&sockbuf,0,sizeof(sockbuf));
sockbuf.cmd=3;
strcpy(sockbuf.file_name,filename);
memcpy(&sbuf,&sockbuf,sizeof(sockbuf));
SSL_write_ex(ssl, (const void *)sbuf, sizeof(sbuf), &written);
memset(&rbuf,0,sizeof(rbuf));
filename_fd=open(filename,O_RDWR|O_CREAT,0666);
sleep(1);
if (filename_fd<0)
{
printf("open file to save failur\n");
return;
}
else
{
while (1)
{
ret=SSL_read_ex(ssl, (void *)&interim_buf,sizeof(interim_buf),&readbytes);
write(filename_fd,(void*)&interim_buf,readbytes);
memset(&interim_buf,0,sizeof(interim_buf));
if (ret==0)
{
printf("文件传输完成\n");
printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
break;
}
}
}
close(filename_fd);
}
/*************************************
函数名:int main(void)
函数作用:连接服务器,并调用各个命令函数完成任务
函数参数:无
函数返回值:(主函数的返回值被返回到终端)
**************************************/
int main()
{
SSL_CTX *ctx;
SSL *ssl;
struct timeval tm;
SSL_library_init();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
ctx=SSL_CTX_new(SSLv23_client_method());
keyboard_cmd=(char*)malloc(sizeof(keyboard_cmd));
if(ctx==NULL)
{
ERR_print_errors_fp(stdout);// 将错误打印到FILE中
exit(1);
}
sockfd=socket(AF_INET,SOCK_STREAM,0);
tm.tv_sec=1;
tm.tv_usec=0;
setsockopt(sockfd, SOL_SOCKET,SO_RCVTIMEO,(const void*)&tm, sizeof(tm));
if (sockfd==-1)
{
printf("creat socket error\n");
perror("why");
}
struct sockaddr_in addr;
inet_aton(IP_ADDR,&addr.sin_addr);
addr.sin_family=AF_INET;
addr.sin_port=htons(8999);
int ret=connect(sockfd,(struct sockaddr *)&addr,(socklen_t)sizeof(addr));
if (ret==-1)
{
printf("connect error\n");
perror("why");
}
//基于ctx产生一个新的ssl,建立SSL连接
ssl=SSL_new(ctx);
SSL_set_fd(ssl,sockfd);
if(SSL_connect(ssl)==-1) ERR_print_errors_fp(stderr);
else
{
printf("connect with %s encryption\n",SSL_get_cipher(ssl));
ShowCerts(ssl);
}
while(1)
{
read(0,keyboard_cmd,100); //从标准输入读取数据
if(strcmp("\n",keyboard_cmd)!=0) //防止只输入回车,导致程序发生段错误
{
keyboard_cmd=strtok(keyboard_cmd,"\n"); //剔除字符串中的回车字符,以免影响下一步操作
}
printf(">>>>>>>>>>>\n");
switch (data_analysis(keyboard_cmd))
{
case SLS:
Sls_function_cmd(ssl);
break;
case CLS:
system("ls");
printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
break;
case CCD:
pathname=string_split(keyboard_cmd);
chdir(pathname);
break;
case Superior:
chdir("..");
break;
case SCD:
printf("此功能暂未开启\n");
break;
case GET:
get_function_cmd(ssl);
break;
case CPWD:
system("pwd");
break;
case SPWD:
spwd_function_cmd(ssl);
break;
case PUT:
put_function_cmd(ssl);
break;
case RM:
pathname=string_split(keyboard_cmd);
remove(pathname);
break;
case Q:
/* 关闭 SSL 连接 */
SSL_shutdown(ssl);
/* 释放 SSL */
SSL_free(ssl);
//4关闭套接字
close(sockfd);
return 0;
break;
default:
printf("Invalid data entered\n");
break;
}
memset(keyboard_cmd,0,sizeof(*keyboard_cmd));
memset(keyboard_cmd,0,sizeof(pathname));
}
}
部分代码调试问题
(1) 在调试tcpclient.c客户端代码put功能的过程中遇到了一个有趣的问题就是当读取到需要上传文件的数据时,使用strcpy()函数将文件数据拷贝到结构体变量sockbuf.sbuf时只要当文件的内容足够大就会影响到sockbuf.file_name用于储存文件名的变量,并将文件名数据覆盖掉,导致文件名变量也存储了一部分的文件数据但是sockbuf.file_name变量的文件数据的前半部分被截断,只剩下后半部分.这是由于strcpy(sockbuf.sbuf,interim_buf);中interim_buf储存的文件数据比sockbuf.sbuf变量的空间大导致strcpy()函数拷贝内容时sockbuf.sbuf的空间爆满,又因为结构体的内存空间时连续的sockbuf.sbuf爆满的文件数据就会溢出到sockbuf.file_name变量的内存空间中去,并将事先储存的文件名覆盖掉。可是使用memcpy(&sockbuf.sbuf,interim_buf,sizeof(sockbuf.sbuf));限制拷贝
的数据大小,防止数据溢出到sockbuf.file_name中去。
(2)在编写put函数传输文件时服务端拿到的数据已经不是原始文件数据,这是由于此前在编写客户端读取文件大小的时候read()函数会返回实际读到的文件大小,不是我们自己在read()函数中设置的读取文件数据大小,read的函数能读到多少数据会进行返回,这个才是真是的数据大小同样在recv()函数读取网络数据的时候也会返回读取到的数据大小,而不真的时我们设置的数据读取量。同样在将读取到的数据写入文件的时候write()函数设置数据的写入大小就是recv()函数返回的读取到的数据大小。
程序编译和证书,密钥的生成
工程编译的sh脚本:
#!/bin/sh
gcc -g tcp_serve_main.c tcp_function.c pthreadpool.c -pthread -lssl -lcrypto -ldl -o ftpserve
gcc -g tcpclient.c -lssl -lcrypto -ldl -o client
数字证书与私钥生成sh脚本:
#!/bin/sh
openssl genrsa -out privkey.pem 2048
openssl req -new -x509 -key privkey.pem -out cacert.pem -days 1095
代码运行演示
运行服务端:
服务器端运行成功
运行客户端:
客户端成功与服务器连接并获取了服务器端的所有文件名。
站在巨人的肩膀上少加班,参考大佬的博客:
参考大佬大佬的openssl加密
程序源码已经上传同学们下载直接./gcc.sh执行编译脚本就可以编译出可执行文件了,但是要提醒一下程序的运行需要openssl库的支持,不然程序运行找不到动态库。