diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cdf233d --- /dev/null +++ b/Makefile @@ -0,0 +1,68 @@ +# 注意一定要在书写命令时,必须在命令开头敲一个Tab键,而不能使用4个空格(space)来代替Tab +# 这个模板,以后只需要更改文件名和链接文件夹即可!!! +# 惨痛教训,链接的时候要加上库:@g++ $(OBJS_C) $(OBJS_CPP) -o $(TARGET) -lcurl + +# 根路径 +ROOT := $(shell pwd) + +# --------------------------修改区---------------------- +# 添加链接编译文件的文件夹,有.cpp文件 +SUBDIR := $(ROOT) +SUBDIR += $(ROOT)/func +SUBDIR += $(ROOT)/data + +# 生成可执行程序的文件名 +TARGET := main +# TARGET := test1 + +# 保存生成文件的文件夹 +OUTPUT := ./output + +# JSON 头文件路径 +JSON_INCLUDE := /home/lb/mylib/json-develop/include/ # 将路径替换为你的 json.hpp 所在位置 + +# ----------------------------------------------------- + +# 编译文件夹 +INCS := $(foreach dir,$(SUBDIR),-I$(dir)) +INCS += -I$(JSON_INCLUDE) # 添加 JSON 头文件路径 + +# 筛选出c文件名,链接需要 +SRCS_C := $(foreach dir,$(SUBDIR),$(wildcard $(dir)/*.c)) +SRCS_CPP := $(foreach dir,$(SUBDIR),$(wildcard $(dir)/*.cpp)) +# 筛选出c++文件名,链接需要 +OBJS_C := $(patsubst $(ROOT)/%.c,$(OUTPUT)/%.o,$(SRCS_C)) +OBJS_CPP := $(patsubst $(ROOT)/%.cpp,$(OUTPUT)/%.o,$(SRCS_CPP)) + + +# g++ 提供了强大的自动生成依赖,记录有依赖关系的 *.d 文件————不用手动为每个源文件添加头文件依赖 +DEPS := $(patsubst %.o,%.d,$(OBJS_C)) +DEPS += $(patsubst %.o,%.d,$(OBJS_CPP)) + +# 开始链接 +main : $(OBJS_C) $(OBJS_CPP) + @echo linking... + @g++ $(OBJS_C) $(OBJS_CPP) -o $(TARGET) -lcurl -lopencv_core -lopencv_highgui -lopencv_imgproc -lopencv_legacy + @echo complete! + +# c编译 +$(OUTPUT)/%.o : %.c + @echo C compile $<... + @mkdir -p $(dir $@) + @gcc -MMD -MP -c $(INCS) $< -o $@ -lsocket -lnetwork -lcurl + +# C++编译 +$(OUTPUT)/%.o : %.cpp + @echo C++ compile $<... + @mkdir -p $(dir $@) + @g++ -std=c++11 -MMD -MP -c $(INCS) $< -o $@ -lsocket -lnetwork -lcurl -lopencv_core -lopencv_highgui -lopencv_imgproc -lopencv_legacy + +# 删除过程文件 +.PHONY : clean + +clean: + @echo try to clean... + @rm -r $(OUTPUT) + @echo complete! + +-include $(DEPS) diff --git a/func/UpDownFile.cpp b/func/UpDownFile.cpp new file mode 100644 index 0000000..8005c62 --- /dev/null +++ b/func/UpDownFile.cpp @@ -0,0 +1,168 @@ +#include "../include/myhead.h" + +/** + * 上传文件 服务器 + * //开始写入 服务器检测STF标志位 + * //结束写入 服务器检测EOF标志位 + * 直接写入 + * + * + * 解决粘包 + * 粘包 + * 发生端发送2个小数据包,接收端会一次性接收到这两个字符串,并将它们合并成一个较大的数据包 + * 解决方法 + * 消息前缀是一个4字节的整数,用于表示消息的长度 + * 每次取数据步骤 + * 1.先取4字节的消息前缀 + * 2.按照消息前缀去取数据,根据长度来准确切割消息 + * + */ +int UpLoadFile(int clientSocket, char *strFileName) +{ + printf("客户端%d 上传任务\n", clientSocket); + // 参数判断 + if (clientSocket < 0 || NULL == strFileName || strlen(strFileName) <= 0) + { + return -1; + } + + // 从传输开始标志位STF起写入文件 + // 连接-绝对路径 + char str_file[512]; + const char *base_path = DATA_PATH; + strcpy(str_file, base_path); + strcat(str_file, strFileName); + printf("客户端%d 服务器保存位置:%s\n", clientSocket, str_file); + + // 打开本地文件以保存下载内容 + FILE *file = fopen(str_file, "wb"); + if (!file) + { + printf("客户端%d ", clientSocket); + perror("打开本地文件失败"); + return -1; + } + + // recv and fwrite + int iRet = -1; + char *buf = new char[BUF_SIZE_VIDEO](); + memset(buf, 0, BUF_SIZE_VIDEO); + while ((iRet = recv(clientSocket, buf, BUF_SIZE_VIDEO, 0)) > 0) + { + fwrite(buf, 1, iRet, file); + memset(buf, 0, iRet); + } + + + // 检测 + if (iRet==0) + { + printf("客户端%d 文件上传成功\n", clientSocket); + } + else + { + printf("客户端%d 接收数据失败\n",clientSocket); + fclose(file); + delete[] buf; + return -1; + } + + // 关闭资源 + fclose(file); + delete[] buf; + return 0; +} + +/** + * 下载文件 服务器 + * 开始传输 服务器发送STF标志位 + * 结束传输 服务器发送EOF标志位 + */ +int DownLoadFile(int clientSocket, char *strFileName) +{ + const char *stfFlag = "STF"; // 定义文件开始传输标志位 + const char *eofFlag = "EOF"; // 定义文件结束传输标志位 + + printf("客户端%d 下载任务\n", clientSocket); + if (clientSocket < 0 || NULL == strFileName || strlen(strFileName) <= 0) + { + perror("非法参数\n"); + return -1; + } + // send file size to client + + // 查找并删除末尾的换行符--客户端处理 + size_t len = strlen(strFileName); + if (len > 0 && strFileName[len - 1] == '\n') + { + strFileName[len - 1] = '\0'; // 将换行符替换为字符串终止符 + } + + // 绝对路径 + char str_file[512]; + const char *base_path = DATA_PATH; + strcpy(str_file, base_path); + strcat(str_file, strFileName); + + struct stat stBuf; + memset(&stBuf, 0, sizeof(struct stat)); + if (-1 == stat(str_file, &stBuf)) + { + perror("错误"); + printf("客户端%d 数据检测失败:%s\n", clientSocket, str_file); + return -1; + } + + char *buf = new char[BUF_SIZE_VIDEO](); + memset(buf, 0, BUF_SIZE_VIDEO); + + // 发送一个传输开始标志位STF + if (send(clientSocket, stfFlag, strlen(stfFlag), 0) == -1) + { + perror("发送 STF 标志位失败"); + // 处理发送失败的情况——断开连接 + delete[] buf; + return -1; + } + printf("客户端%d 发送传输标志位 %s\n", clientSocket, stfFlag); + + // 打开文件 + FILE *file = fopen(str_file, "rb"); + if (!file) + { + printf("客户端%d 打开 %s 文件失败\n", clientSocket, str_file); + fclose(file); + delete[] buf; + return -1; + } + // read and send + int iRet = -1; + printf("客户端%d 开始发送...\n", clientSocket); + + while ((iRet = fread(buf, 1, BUF_SIZE_VIDEO, file)) > 0) + { + // send + if (-1 == send(clientSocket, buf, iRet, 0)) + { + perror("Error sending data"); + break; + } + // printf("客户端%d:发送数据 %s\n", clientSocket, buf); + memset(buf, 0, BUF_SIZE_VIDEO); + } + printf("客户端%d 发送成功\n", clientSocket); + + // 发送一个传输结束标志位EOF + if (send(clientSocket, eofFlag, strlen(eofFlag), 0) == -1) + { + perror("发送 EOF 标志位失败"); + // 处理发送失败的情况——断开连接 + fclose(file); + delete[] buf; + return -1; + } + + fclose(file); + delete[] buf; + return 0; +} diff --git a/func/VideoHandle.cpp b/func/VideoHandle.cpp new file mode 100644 index 0000000..1c12647 --- /dev/null +++ b/func/VideoHandle.cpp @@ -0,0 +1,410 @@ +#include "../include/myhead.h" + +using namespace std; +using json = nlohmann::json; + +/* + 视频处理模块 +*/ + +// 编写hls的index.m3u8文件---等待编写 +void write_m3u8(const std::string &directoryPath) +{ + printf("子进程%d write_m3u8:编写index.m3u8文件,文件保存路径 %s\n", getpid(), directoryPath.c_str()); +} + +// 创建文件夹 +bool createDirRecs(const std::string &path) +{ + // 如果路径为空或已经存在,返回 true + // 0644:对于所有者具有读和写权限,对于组和其他用户,只有读权限 + int ret = mkdir(path.c_str(), 0755); + if (ret == 0) + { + // 如果创建成功或目录已经存在,返回 true + return true; + } + else + { + // 已存在 + if (errno == EEXIST) + { + return true; + } + // 创建失败 + return false; + } +} + +// 去除文件后缀 +std::string removeFileExtension(const std::string &fileName) +{ + size_t lastDotPos = fileName.rfind('.'); + if (lastDotPos != std::string::npos) + { + return fileName.substr(0, lastDotPos); + } + else + { + return fileName; // 如果没有找到点,返回原始文件名 + } +} + +// 函数用于获取目录下所有的.ts文件 +std::vector getTsFiles(const std::string &dirPath) +{ + std::vector tsFiles; + DIR *dp = opendir(dirPath.c_str()); // 打开目录 + + if (dp) + { + struct dirent *entry; + + // 遍历目录中的文件和子目录 + while ((entry = readdir(dp))) + { + std::string filename = entry->d_name; + + // 检查文件扩展名是否是 .ts + if (filename.length() >= 3 && filename.substr(filename.length() - 3) == ".ts") + { + tsFiles.push_back(filename); + } + } + + closedir(dp); // 关闭目录 + } + + return tsFiles; +} +// curl回调函数,用于处理接收到的响应数据 +size_t WriteCallback(void *contents, size_t size, size_t nmemb, std::string *output) +{ + size_t totalSize = size * nmemb; + output->append(static_cast(contents), totalSize); + return totalSize; +} + +int curl_request_file(CURL *curl, const std::string &input_dir, std::string &response) +{ + // 设置要上传的文件 + const char *file_path = input_dir.c_str(); + // 创建一个文件读取句柄 + FILE *file = fopen(file_path, "rb"); + if (!file) + { + std::cerr << "Failed to open file: " << file_path << std::endl; + curl_easy_cleanup(curl); + return 1; + } + curl_easy_setopt(curl, CURLOPT_READDATA, file); + // 执行HTTP请求 + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) + { + std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl; + return -1; + } + return 0; +} + +// 处理视频并添加目标框 +int processVideoBox(const string &inputVideoPath, const string &outputVideoPath, const json &targetData) +{ + cv::VideoCapture videoCapture(inputVideoPath); + if (!videoCapture.isOpened()) + { + cerr << "Error: Failed to open input video." << endl; + return -1; + } + + double fps = videoCapture.get(CV_CAP_PROP_FPS); + int frameWidth = static_cast(videoCapture.get(CV_CAP_PROP_FRAME_WIDTH)); + int frameHeight = static_cast(videoCapture.get(CV_CAP_PROP_FRAME_HEIGHT)); + + cv::VideoWriter videoWriter(outputVideoPath, CV_FOURCC('X', 'V', 'I', 'D'), fps, cv::Size(frameWidth, frameHeight)); + + if (!videoWriter.isOpened()) + { + cerr << "Error: Failed to open output video for writing." << endl; + return -1; + } + + json::const_iterator emotionData = targetData.begin(); + cv::Mat frame; + + int index = 0; + while (true) + { + videoCapture >> frame; + if (frame.empty()) + { + break; + } + const string &emotion = (*emotionData)[0].get(); + const json &boundingBoxes = (*emotionData)[1]; + // const auto &boundingBox = boundingBoxes[index]; + const auto &boundingBox = boundingBoxes.at(index); + + // 如果已经处理完所有数据,不画框但写入 + if (index >= boundingBoxes.size()) + { + videoWriter.write(frame); + continue; + } + + vector bbox = boundingBox.get>(); + int left = bbox[0]; + int right = bbox[1]; + int top = bbox[2]; + int bottom = bbox[3]; + + cv::rectangle(frame, cv::Point(left, top), cv::Point(right, bottom), cv::Scalar(0, 255, 0), 2); + cv::putText(frame, emotion, cv::Point(left + 120, top - 10), cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 255, 0), 2); + + index++; + + videoWriter.write(frame); + } + + videoCapture.release(); + videoWriter.release(); + + return 0; +} + +// 完全拷贝 +int cp_file(const std::string& old_file, const std::string& new_file) { + std::ifstream source(old_file, std::ios::binary); + if (!source) { + std::cerr << "无法打开源文件:" << old_file << std::endl; + return -1; + } + + std::ofstream destination(new_file, std::ios::binary); + if (!destination) { + std::cerr << "无法创建目标文件:" << new_file << std::endl; + return -1; + } + + destination << source.rdbuf(); + + if (source.fail() || destination.fail()) { + std::cerr << "文件拷贝失败" << std::endl; + return -1; + } + + source.close(); + destination.close(); + + std::cout << "文件拷贝成功" << std::endl; + return 0; +} + +// 发送API+合成视频---等待编写 +int curl_API_all(const std::string &input_dir, const std::string &output_dir) +{ + printf("子进程%d curl_API_all:发送API与合成视频\n", getpid()); + printf("子进程%d curl_API_all:input_dir:%s\n", getpid(), input_dir.c_str()); + printf("子进程%d curl_API_all:output_dir:%s\n", getpid(), output_dir.c_str()); + + // 创建合成视频文件夹 + if (createDirRecs(output_dir)) + { + std::cout << "子进程" << getpid() << " 存放合成视频文件夹创建成功或已存在:" << output_dir.c_str() << std::endl; + } + else + { + std::cerr << "子进程" << getpid() << " 创建合成视频文件夹失败" << std::endl; + return -1; + } + + // 获取源文件夹下所有ts文件组成一个列表 + std::vector tsFiles = getTsFiles(input_dir); + + // 打印所有找到的.ts文件 + std::cout << "子进程" << getpid() << " 生成的ts数量:" << tsFiles.size() << std::endl; + + /* + API + */ + // 初始化libcurl + curl_global_init(CURL_GLOBAL_DEFAULT); // 启用线程安全(支持多线程环境)和设置一些库的全局状态 + CURL *curl = curl_easy_init(); + + if (!curl) + { + std::cout << "子进程" << getpid() << " 初始化libcurl失败" << std::endl; + curl_global_cleanup(); + return -1; + } + // 设置要访问的URL + // const char *url = "http://192.168.12.41:30088/predictions/IDB-emo-video"; + const char *url = "http://192.168.3.108:30088/predictions/IDB-emo-video"; + + // 获取的响应 + std::string response; + + // 设置libcurl选项 + curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(curl, CURLOPT_URL, url); + // 设置写回调函数,用于接收响应数据 + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + std::string old_ts_file; + std::string new_ts_file; + + // 循环作为参数发送 + for (const std::string &tsFile : tsFiles) + { + old_ts_file = input_dir + tsFile; + new_ts_file = output_dir + tsFile; + + int ret = curl_request_file(curl, old_ts_file, response); + + if (ret == 0) + { + std::cout << "子进程" << getpid() << " " << tsFile << " 请求成功" << std::endl; + } + else + { + std::cout << "子进程" << getpid() << " " << tsFile << " 请求失败" << std::endl; + } + + // 解析 JSON 字符串 + json data = json::parse(response); + + // 打印解析结果 + // std::cout << "Emotion: " << data[0][0].get() << std::endl; + // std::cout << "Coordinates:" << std::endl; + // for (const auto &item : data[0][1]) + // { + // std::cout << " [" << item[0] << ", " << item[1] << ", " << item[2] << ", " << item[3] << "]" << std::endl; + // } + + // 生成视频 + try + { + int result = processVideoBox(old_ts_file, new_ts_file, data); + if (result == 0) + { + std::cout << "子进程" << getpid() << " " << tsFile << " 合成视频成功 " + << "新视频路径:" << new_ts_file.c_str() << std::endl; + } + else + { + std::cout << "子进程" << getpid() << " " << tsFile << " 合成视频失败" << std::endl; + } + + // 清空响应数据以准备下一次请求 + response.clear(); + } + catch (const std::exception &e) + { + int ret = cp_file(old_ts_file, new_ts_file);//完全拷贝 + std::cout << "子进程" << getpid() << " " << tsFile << " 合成视频失败,完全拷贝成功" << std::endl; + + } + + // 先只执行一个 + // break; + } + + // 清理和关闭 CURL + curl_easy_cleanup(curl); + curl_global_cleanup(); + + return 0; +} + +/** + * 当前模块主函数 + */ +int handle_video(char *strFileName, char *file_m3u8_path) +{ + printf("系统:开始处理视频\n"); + + // if (strFileName == NULL || file_m3u8_path == NULL || strlen(strFileName) == 0 || strlen(file_m3u8_path) == 0) + if (strFileName == NULL || strlen(strFileName) == 0) + { + // 一个或多个参数不能为空 或 空字符串 + printf("系统:视频处理模块:无效参数\n"); + return -1; + } + + // 文件的绝对路径 + std::string data_path = DATA_PATH; + std::string fileName(strFileName); + std::string input_file_path = data_path + fileName; // 输入文件路径 + std::string new_dir = data_path + removeFileExtension(fileName) + "/"; + std::string output_file_path = new_dir + "index.m3u8"; // 输出文件路径 + + // 创建新文件夹 + if (createDirRecs(new_dir)) + { + std::cout << "系统:存放m3u8文件夹创建成功或已存在:" << new_dir.c_str() << std::endl; + } + else + { + std::cerr << "系统:创建文件夹失败" << std::endl; + return -1; + } + + printf("系统:待处理视频路径 %s\n", input_file_path.c_str()); + printf("系统:输出原视频m3u8文件路径 %s\n", output_file_path.c_str()); + + // return 0; + + // 创建子线程,用execlp()调用ffmpeg处理视频 + pid_t child_pid = fork(); + if (child_pid == 0) + { + // 子进程 + printf("子进程%d:开始视频转HLS流文件...\n", getpid()); + + // 获取程序开始执行的时间点 + auto start_time = std::chrono::high_resolution_clock::now(); + + // 需要linux的FFmpeg环境 + // execlp("ffmpeg", "ffmpeg", "-i", input_file_path.c_str(), + // "-force_key_frames", "expr:gte(t,n_forced*1)", "-strict", "-1", + // "-c:a", "aac", "-c:v", "libx264", "-hls_time", "1", + // "-f", "hls", output_file_path.c_str(), NULL); + sleep(3); + + printf("子进程%d:转化完成\n", getpid()); + + // 生成index.m3u8文件 + write_m3u8(output_file_path); + + // 使用循环发送API获取识别结果 + std::string output_dir = DATA_PATH; // 合成视频的结果目录 + output_dir.append("output_video/"); + createDirRecs(output_dir); + output_dir += removeFileExtension(fileName) + "/"; + + // API + curl_API_all(new_dir, output_dir); + + printf("子进程%d 输出识别后视频m3u8文件路径 %s\n", getpid(), output_dir.c_str()); + strcpy(file_m3u8_path, output_dir.c_str()); + + // 子进程执行完命令后,退出,并发送给父进程回收信号 + printf("子进程%d 已退出,发送回收信号\n", getpid()); + kill(getppid(), SIGUSR1); + + // 获取程序执行完毕的时间点 + auto end_time = std::chrono::high_resolution_clock::now(); + + // 计算时间差 + auto duration = std::chrono::duration_cast(end_time - start_time); + + // 输出程序执行时间(以秒为单位) + std::cout << "子进程 " << getpid() << " 程序执行时间: " << duration.count() << " 秒" << std::endl; + + exit(0); + } + + return 0; +} \ No newline at end of file diff --git a/include/UpDownFile.h b/include/UpDownFile.h new file mode 100644 index 0000000..3ae74e3 --- /dev/null +++ b/include/UpDownFile.h @@ -0,0 +1,13 @@ +#ifndef _UPDOWNFILE_H_ +#define _UPDOWNFILE_H_ + +/* + 上传下载模块 +*/ + + +extern int UpLoadFile(int iClient, char *strFileName); +extern int DownLoadFile(int iClient, char *strFileName); + + +#endif \ No newline at end of file diff --git a/include/VideoHandle.h b/include/VideoHandle.h new file mode 100644 index 0000000..00ca9e7 --- /dev/null +++ b/include/VideoHandle.h @@ -0,0 +1,11 @@ +#ifndef _VIDEOHANDLE_H_ +#define _VIDEOHANDLE_H_ + +/* + 视频处理模块 +*/ + + +extern int handle_video(char *strFileName,char *file_m3u8_path); + +#endif \ No newline at end of file diff --git a/include/myhead.h b/include/myhead.h new file mode 100644 index 0000000..1167992 --- /dev/null +++ b/include/myhead.h @@ -0,0 +1,61 @@ +#ifndef _MYHEAD_H_ +#define _MYHEAD_H_ + +// 错误码 +enum ErrorCode +{ + ERROR = -1, + OK, + +}; + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" +{ + #include +} +#endif + + +#include "UpDownFile.h" +#include "VideoHandle.h" + +// #define BUF_SIZE_VIDEO (1024 * 20) +#define BUF_SIZE_VIDEO (1024) +#define BUF_SIZE (500) + +//同时检测客户端数量 +#define MAX_EVENTS 4090 + + +// 统一使用末尾加/,前面不加/ +#define DATA_PATH ("data/") + +#endif diff --git a/main b/main new file mode 100644 index 0000000..e7544d0 Binary files /dev/null and b/main differ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..63be741 --- /dev/null +++ b/main.cpp @@ -0,0 +1,262 @@ +#include "./include/myhead.h" + +/** + * 设置回收信号处理函数 + * 进程间信号触发设置——异步通信 + */ +void signal_handler(int signal_num) +{ + if (signal_num == SIGUSR1) + { + printf("系统:检测到回收信号,开始回收子进程...\n"); + int status; + // 等待与当前进程组ID相同的任何子进程退出并回收资源 + // int ret = waitpid(0, &status, WNOHANG); // WNOHANG:非阻塞形式 + int ret = waitpid(0, &status, 0); // 0:阻塞形式 + if (ret > 0) + { + printf("系统:回收子进程,进程ID为 %d,退出状态为 %d,已回收.\n", ret, WEXITSTATUS(status)); + } + else if (0 == ret) + { + // 子进程状态发生改变,但未退出 + printf("系统:没有子进程退出\n"); + } + else + { + perror("系统:waitpid发送错误"); + } + } +} + +/* + 服务器入口 +*/ +int main(int argc, char const *argv[]) +{ + /* + IO多路复用并发服务器 + */ + int ret; + char buf[BUF_SIZE] = {0}; + + // 注册回收信号并绑定回调函数 + signal(SIGUSR1, signal_handler); // 用于子传父 + + // 1 socket 创建流式套接字 + int fd_Server = socket(AF_INET, SOCK_STREAM, 0); // 返回服务端文件描述符 + if (-1 == fd_Server) + { + perror("socket error!\r\n"); + return -1; + } + + // 2 bind 绑定协议端口IP + struct sockaddr_in stServer; // 网络配置结构体 + memset(&stServer, 0, sizeof(stServer)); + + stServer.sin_family = AF_INET; // 指定要使用 IPv4 地址族 + stServer.sin_port = htons(8888); // 指定端口 + // stServer.sin_addr.s_addr = inet_addr("192.168.12.111"); // 绑定ip + // stServer.sin_addr.s_addr = inet_addr("192.168.10.61"); // 绑定ip + stServer.sin_addr.s_addr = inet_addr("0.0.0.0"); // 绑定ip + + ret = bind(fd_Server, (const struct sockaddr *)&stServer, sizeof(const struct sockaddr)); + if (-1 == ret) + { + perror("bind error!\r\n"); + return -1; + } + + // 3 listen + ret = listen(fd_Server, 10); + if (-1 == ret) + { + perror("listen error!\r\n"); + return -1; + } + + printf("系统:服务器启动\n"); + + struct sockaddr_in stClient; // 用于存放客户端的容器 + socklen_t len = sizeof(struct sockaddr_in); + /** + * 使用epoll检测请求 + */ + // 创建epoll实例 + int epoll_fd = epoll_create(1); + if (epoll_fd == -1) + { + perror("epoll_create"); + exit(EXIT_FAILURE); + } + struct epoll_event event; + event.events = EPOLLIN; + event.data.fd = fd_Server; + + // 将监听套接字添加到epoll实例中 + if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd_Server, &event) == -1) + { + perror("epoll_ctl"); + exit(EXIT_FAILURE); + } + + struct epoll_event events[MAX_EVENTS]; + + int max = fd_Server; // 设置当前最大检测文件符--动态更改 + + // 开始检测 + while (1) + { + int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); + if (num_events == -1) + { + perror("epoll_wait"); + continue; + } + + for (int i = 0; i < num_events; i++) + { + int fd = events[i].data.fd; + + // 服务器IO活跃,表示新客户端连接,接入连接,加入检测表,更新max + if (fd == fd_Server) + { + memset(&stClient, 0, sizeof(struct sockaddr_in)); + int fd_newCli = accept(fd_Server, (struct sockaddr *)&stClient, &len); + if (-1 == fd_newCli) + { + continue; + } + // 加入检测表 + event.events = EPOLLIN; + event.data.fd = fd_newCli; + + // 将新连接的套接字添加到epoll实例中 + if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd_newCli, &event) == -1) + { + perror("epoll_ctl"); + continue; + } + + printf("系统:新客户端连接 分配fd号:%d\n", fd_newCli); + } + else + { + /* + bug1: + 非阻塞IO,第二次接收到的是传输内容,没有命令,会当成传输命令处理 + 开始和结束标志位都由发送文件方发出,不需要等待,因为是TCP连接,没有丢包,只需要发数据和拿数据 + 原因是由于服务器接受错误退出,但客户端还在上传引发的问题 + 解决办法:直接关闭 + bug2: + 出现粘包---2个小数据包被合并成一个数据包 + 解决方法---消息前缀 + */ + memset(buf, 0, BUF_SIZE); + + // 客户端IO活跃,接入连接,检测指令,阻塞接收 + + // 消息前缀解决粘包 + int message_length = 0; + int bytes_received = recv(fd, &message_length, sizeof(int), 0); + printf("客户端%d 获取指令的消息前缀 %d\n", fd, message_length); + if (bytes_received != sizeof(int)) + { + printf("客户端%d 非法指令,断开连接\n", fd); + // 关闭文件描述符并停止检测该IO + close(fd); + epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); + + printf("客户端%d 关闭 fd=%d\n\n", fd, fd); + continue; + } + + ret = recv(fd, buf, message_length, 0); + if (ret > 0) + { + printf("客户端%d 指令:%s\n", fd, buf); + // 解析指令 + if (0 == strncmp(buf, "put", 3)) + { + // 上传:put test.txt + ret = UpLoadFile(fd, buf + 4); + if (0 == ret) + { + if ((strstr(buf, ".mp4") != NULL) || (strstr(buf, ".avi") != NULL) || (strstr(buf, ".ts") != NULL)) + { + // 处理视频,返回处理后的m3u8文件名 + printf("系统:检测到视频文件 进入API分析模块\n"); + char file_m3u8_path[100] = {0}; + ret = handle_video(buf + 4, file_m3u8_path); + } + printf("客户端%d 上传成功\n", fd); + // 关闭文件描述符并停止检测该IO + close(fd); + epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); + printf("客户端%d 关闭 fd=%d\n\n", fd, fd); + continue; + } + else + { + printf("客户端%d 上传失败\n", fd); + + // 关闭文件描述符并停止检测该IO + close(fd); + epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); + printf("客户端%d 关闭 fd=%d\n\n", fd, fd); + continue; + } + } + else if (0 == strncmp(buf, "get", 3)) + { + // 下载:get test.txt + ret = DownLoadFile(fd, buf + 4); + if (ret < 0) + { + printf("客户端%d 下载失败\n", fd); + + // 关闭文件描述符并停止检测该IO + close(fd); + epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); + + printf("客户端%d 关闭 fd=%d\n\n", fd, fd); + continue; + } + else + { + printf("客户端%d 下载成功\n", fd); + // 关闭文件描述符并停止检测该IO + close(fd); + epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); + + printf("客户端%d 关闭 fd=%d\n\n", fd, fd); + continue; + } + } + else + { + printf("客户端%d 非法指令,断开连接\n", fd); + // 关闭文件描述符并停止检测该IO + close(fd); + epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); + + printf("客户端%d 关闭 fd=%d\n\n", fd, fd); + continue; + } + } + else + { + // 关闭文件描述符并停止检测该IO + close(fd); + epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); + + printf("系统:关闭 fd=%d\n\n", fd); + continue; + } + } + } + } + + return 0; +} \ No newline at end of file diff --git "a/\350\257\264\346\230\216\346\226\207\346\241\243.txt" "b/\350\257\264\346\230\216\346\226\207\346\241\243.txt" new file mode 100644 index 0000000..a189a54 --- /dev/null +++ "b/\350\257\264\346\230\216\346\226\207\346\241\243.txt" @@ -0,0 +1,25 @@ +1.0 实现功能: + 1.指令上传:put test.txt + 2.指令下载: get test.txt + 3.IO多路复用 + +2.0 实现功能: + 1.处理视频 + 2.单次视频分块ts流 + 3.发送API请求 + 4.响应API请求 + 5.opencv处理ts视频 + + +4.0 实现功能: + 1. 指令上传下载 + 2. IO多路复用(select),高性能并发 + 3. 使用消息前缀(int),防止粘包 + 4. 创建子进程处理视频 + 5. 子进程处理完任务发信号通知父进程回收资源,防止僵尸进程 + 6. 将上传的视频处理成hls流媒体文件 + 7. 循环发送API请求,携带ts文件 + 8. json+opencv库处理API返回结果,生成新的hls流媒体文件 + +4.1 实现功能: + 1.更改为用epoll实现IO多路复用 \ No newline at end of file