目录
更多文章和源码在我的个人博客:首页 (niuniu65.top)
如需咨询请添加个人微信:a15135158368
欢迎叨扰,多多交流
一、较与上版本的优化
(如需查看,请移步到本人上篇文章)
1、服务器和客户端加入了多线程,实现多并发处理数据传输;
2、更加健壮的代码程序
3、标准输出加入了高亮文本,更方便调试
二、代码编写思路
服务器
主函数
信号处理:使 'sigaction'注册了'SIGINT'
服务器初始化:创建服务器套接字,并设置 SO_REUSEADDR 选项,允许服务器在关闭后立即重新绑定到相同的地址和端口。
绑定与监听:将服务器套接字绑定到指定的 IP 地址和端口,并开始监听客户端的连接请求。
客户端连接处理:进入一个无限循环,持续等待客户端的连接请求。每次接收到新的客户端连接时,创建一个新线程处理客户端的请求。线程由 do_client_request 函数实现,负责处理具体的客户端请求。
2.客户端请求处理 (do_client_request
)
从参数中获取客户端套接字,并通过
handle_client
函数处理客户端的文件上传、下载和查询操作。在处理完请求后,关闭客户端套接字并释放资源。
3.处理与客户端的通信 (handle_client
):
接收操作码:从客户端接收操作码,表示客户端希望执行的操作类型。支持的操作码有:
0x01
:文件上传。
0x02
:文件下载。
0x03
:文件查询。
接收文件名:从客户端接收文件名,并根据操作码执行相应操作。
上传操作 (
0x01
):接收文件大小,并将客户端发送的文件数据保存到服务器端的文件中。
下载操作 (
0x02
):检查文件是否存在,并将文件内容发送给客户端。
查询操作 (
0x03
):检查文件是否存在,并将查询结果发送给客户端。
4.信号处理函数 (handle_sigint
):
在接收到
SIGINT
信号时,关闭服务器套接字并退出程序。
客户端
主函数:
信号处理:使用
sigaction
注册信号处理函数handle_sigint
服务器连接:在主循环中,客户端通过套接字与服务器进行连接。
用户输入:
用户输入文件名和操作码(1: 上传, 2: 下载, 3: 查询, 0: 退出)。
对输入进行基本验证。
线程创建:根据用户选择的操作码,动态分配线程数据结构,将套接字和操作参数传递给新创建的线程。每个线程独立处理一个用户操作。
线程处理逻辑 (
handle_operation
函数):上传文件 (
upload_image
):将本地文件发送到服务器。下载文件 (
download_image
):从服务器下载文件到本地。查询文件 (
query_image
):检查服务器上是否存在指定文件。线程执行完相应的操作后关闭套接字,并释放动态分配的内存。
文件操作
上传文件:
打开本地文件并读取其内容,将文件名、文件大小和内容发送到服务器。
下载文件:
从服务器接收文件内容并将其写入本地文件。先接收文件大小,然后循环接收文件数据,直到完整下载文件。
查询文件:
发送文件名到服务器,接收服务器返回的文件存在状态(存在或不存在)。
三、代码优缺点
服务器
优点:
多线程并发
信号处理
基本的文件操作支持
缺点:
缺乏心跳机制:无法实时监测客户端的状态
缺乏文件操作的并发控制:加入文件锁等同步机制
硬编码配置:ip地址和端口号等配置被硬编码在代码中,缺乏灵活性。
客户端
优点
多线程并发处理:
并发性:采用多线程处理用户操作,使得客户端可以同时执行多个文件操作(上传、下载、查询),提高了程序的并发性能和响应速度。
资源管理:每个操作都在独立线程中执行,使用
pthread_detach
实现了线程分离,避免了线程阻塞主线程,也免去了主线程需要手动管理线程生命周期的负担。代码结构清晰:
模块化设计:代码通过函数划分不同的功能模块(如上传、下载、查询、信号处理等),使得代码逻辑清晰易懂,便于维护和扩展。
动态内存管理:使用动态内存分配 (
malloc
和free
) 来管理线程数据,避免了不必要的全局变量,提高了程序的可扩展性和灵活性。
缺点
线程安全问题:
潜在的竞争条件:每个操作都在独立线程中执行,但如果多个线程试图同时访问或修改共享资源(如全局变量或文件),可能会引发竞争条件。
虽然使用了独立的套接字连接,但如果程序扩展涉及共享数据(如计数器、日志文件等),则需注意线程同步问题。
客户端套接字管理:
重复连接:每次用户输入操作后,客户端都会重新创建一个新的套接字并连接到服务器。虽然简化了代码逻辑,但频繁的连接/断开操作可能会带来一定的性能开销。
用户交互阻塞:
主线程阻塞:主线程在每次并启动线程后,使用
sleep(0.5)
来等待一段时间,会使主线程在这段时间内阻塞,影响程序的实时性。
四、源代码
服务器
/** ****************************************************************************** * @file : tcp_server.c * @author : niuniu * @brief : 该程序实现了一个简单的多线程并发TCP服务器,能够处理客户端的文件上传、下载和查询操作 * @attention : * @date : 2024/8/13 ****************************************************************************** */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <fcntl.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <signal.h> #include <pthread.h> // 定义用于终端文本高亮显示的颜色代码 #define BRIGHT_RED_TEXT "\033[91m" // 红色高亮文本 #define BRIGHT_GREEN_TEXT "\033[92m" // 绿色高亮文本 #define BRIGHT_YELLOW_TEXT "\033[93m" // 黄色高亮文本 #define BRIGHT_BLUE_TEXT "\033[94m" // 蓝色高亮文本 #define RESET_COLOR "\033[0m" // 重置颜色设置 #define PORT 8080 // 服务器监听的端口号 #define BUFFER_SIZE 1024 // 缓冲区大小 #define SERVER_IP "192.168.216.149" // 服务器IP地址 int server_socket; // 服务器套接字 struct sockaddr_in server_addr, client_addr; // 存储服务器和客户端地址信息的结构体 //@brief 处理客户端请求的函数 int handle_client(int client_socket); //@brief 处理SIGINT信号的函数(Ctrl+C) void handle_sigint(int sig); /** * @brief 客户端请求处理线程 * @param argv 客户端套接字的指针 * @return void* 线程返回值 */ void *do_client_request(void* argv) { int client_socket = *((int *)argv); // 从参数中获取客户端套接字 free(argv); // 释放传递进来的指针内存 printf(BRIGHT_GREEN_TEXT"等待指定操作中...\n"RESET_COLOR); // 处理客户端请求 int handle_ret = handle_client(client_socket); if(handle_ret != EXIT_SUCCESS) { perror("handle_client"); printf(BRIGHT_RED_TEXT"\n未能正确实现功能\n"RESET_COLOR); } // 关闭客户端套接字,释放资源 printf(BRIGHT_GREEN_TEXT"释放client_socket:%d 资源\n\n"RESET_COLOR, client_socket); usleep(100000); // 延迟100毫秒 close(client_socket); return NULL; } /** * @brief 服务器主函数,初始化并启动服务器,持续监听客户端的连接请求 * @return int 程序退出状态码 */ int main(void) { socklen_t client_addr_size; // 客户端地址结构体的大小 // 注册信号处理函数,用于处理 SIGINT (Ctrl+C) struct sigaction sa; sa.sa_handler = handle_sigint; // 设置处理函数 sa.sa_flags = 0; // 默认标志 sigemptyset(&sa.sa_mask); // 清空信号屏蔽字 if (sigaction(SIGINT, &sa, NULL) == -1) { perror("sigaction"); exit(EXIT_FAILURE); } // 创建服务器套接字 server_socket = socket(PF_INET, SOCK_STREAM, 0); if (server_socket == -1) { perror("socket"); exit(EXIT_FAILURE); } // 允许在套接字关闭后立即重新绑定到相同的地址和端口 int opt = 1; if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) { perror("setsockopt"); exit(EXIT_FAILURE); } // 初始化服务器地址结构体 memset(&server_addr, 0, sizeof(server_addr)); // 清空结构体 server_addr.sin_family = AF_INET; // 使用IPv4协议 server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 绑定服务器IP地址 server_addr.sin_port = htons(PORT); // 绑定端口号 // 绑定服务器套接字到指定IP和端口 if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("bind"); exit(EXIT_FAILURE); } // 监听端口,设置最大等待队列长度为5 if (listen(server_socket, 5) == -1) { perror("listen"); exit(EXIT_FAILURE); } printf(BRIGHT_YELLOW_TEXT"服务器正在监听【%d】号端口号\n\n"RESET_COLOR, PORT); while (1) // 无限循环,持续接收客户端请求 { // 初始化客户端结构体大小 client_addr_size = sizeof(client_addr); int *client_socket = malloc(sizeof(int)); // 动态分配内存存储客户端套接字 if (client_socket == NULL) { perror("malloc"); continue; } // 接受客户端连接 *client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_size); if(*client_socket == -1) { perror("accept"); free(client_socket); // 释放内存,避免内存泄漏 continue; } printf(BRIGHT_GREEN_TEXT"成功接收了客户端的连接\n"RESET_COLOR); printf(BRIGHT_GREEN_TEXT"新连接到的客户端为{%s : %d}\n\n"RESET_COLOR, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 创建线程处理客户端请求 pthread_t pid; int pid_ret = pthread_create(&pid, NULL, do_client_request, (void*)client_socket); if (pid_ret != 0) // 线程创建失败 { perror("pthread_create"); close(*client_socket); // 关闭连接 free(client_socket); continue; } pthread_detach(pid); // 设置线程为 detached 状态,自动回收资源 printf(BRIGHT_GREEN_TEXT"创建子线程并处理为detached状态\n\n"RESET_COLOR); } close(server_socket); // 关闭服务器套接字 return 0; } /** * @brief 处理SIGINT信号的函数(Ctrl+C) * @param sig 信号值 */ void handle_sigint(int sig) { printf(BRIGHT_RED_TEXT"\n接收到信号 %d,正在关闭服务器...\n"RESET_COLOR, sig); printf(BRIGHT_RED_TEXT"释放资源\n"RESET_COLOR); close(server_socket); exit(0); } /** * @brief 处理与客户端的通信,执行上传、下载、查询操作 * * @param client_socket 与客户端通信的套接字 */ int handle_client(int client_socket) { char buffer[BUFFER_SIZE]; // 用于存储接收和发送的数据的缓冲区 int name_len; // 文件名长度 char file_name[256] = {0}; // 用于存储文件名的字符数组 unsigned int file_size; // 文件大小(字节数) // 接收操作码,表示客户端希望执行的操作类型 if (recv(client_socket, buffer, 1, 0) <= 0) // 接收操作码失败则退出循环 { perror("recv_opcode"); return EXIT_FAILURE; } char opcode = buffer[0]; // 操作码,0x01上传,0x02下载,0x03查询 // 接收文件名长度 if (recv(client_socket, buffer, 1, 0) <= 0) // 接收文件名长度失败则退出循环 { perror("recv_len"); return EXIT_FAILURE; } // 获取文件名长度 name_len = buffer[0]; // 接收文件名 if (recv(client_socket, file_name, name_len, 0) <= 0) // 接收文件名失败则退出循环 { perror("recv_name"); return EXIT_FAILURE; } file_name[name_len] = '\0'; // 文件名字符串的末尾添加结束符 switch (opcode) { case 0x01: { int file_fd; // 接收文件大小 if (recv(client_socket, buffer, 4, 0) <= 0) // 接收文件大小失败则退出循环 { perror("recv4"); return EXIT_FAILURE; } file_size = *((unsigned int *)buffer); // 将接收的文件大小转换为整数 // 打开或者创建一个新文件,用于保存上传的图片 file_fd = open(file_name, O_WRONLY | O_CREAT | O_TRUNC, 0666); if (file_fd < 0) // 文件打开失败则退出循环 { perror("open"); return EXIT_FAILURE; } int received = 0; // 已接收的数据大小 // 循环接收文件内容,直到接收完整个文件 while (received < file_size) { int len = recv(client_socket, buffer, BUFFER_SIZE, 0); if (len <= 0) // 接收数据失败则退出循环 { perror("recv_content"); close(file_fd); return EXIT_FAILURE; } received += len; // 更新已接收的字节数 write(file_fd, buffer, len); // 将接收的数据写入文件 } close(file_fd); // 关闭文件描述符 printf(BRIGHT_YELLOW_TEXT"已将文件【%s】上传\n"RESET_COLOR, file_name); // 打印成功接收的文件名 return EXIT_SUCCESS; } case 0x02: { int file_fd; file_fd = open(file_name, O_RDONLY); // 打开要下载的文件 if (file_fd < 0) // 文件不存在,发送文件大小为0 { file_size = 0; send(client_socket, &file_size, sizeof(file_size), 0); perror("open failed"); close(file_fd); return EXIT_FAILURE; } else { // 计算文件大小 (通过 lseek 的偏移量返回值) file_size = lseek(file_fd, 0, SEEK_END); lseek(file_fd, 0, SEEK_SET); // 发送文件大小给客户端 send(client_socket, &file_size, sizeof(file_size), 0); // 循环发送文件内容 int read_len = 0; while ((read_len = read(file_fd, buffer, BUFFER_SIZE)) > 0) { send(client_socket, buffer, read_len, 0); } printf(BRIGHT_YELLOW_TEXT"已将文件【%s】发送下载成功\n"RESET_COLOR, file_name); } close(file_fd); // 关闭文件描述符 return EXIT_SUCCESS; } case 0x03: { // 使用 access 函数判断文件是否存在,存在返回1,不存在返回0 int exists = access(file_name, F_OK) != -1; send(client_socket, &exists, sizeof(exists), 0); printf(BRIGHT_YELLOW_TEXT"已查询文件【%s】是否存在\n"RESET_COLOR, file_name); return EXIT_SUCCESS; } } return EXIT_SUCCESS; }
客户端
/** ****************************************************************************** * @file : tcp_client.c * @author : niuniu * @brief : 采用多线程并发实现客户端程序,用于连接服务器并执行上传、下载、查询操作 * @attention : None * @date : 2024/8/13 ****************************************************************************** */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <fcntl.h> #include <sys/types.h> #include <sys/socket.h> #include <pthread.h> #include <signal.h> #define BRIGHT_RED_TEXT "\033[91m" //红色高亮文本 #define BRIGHT_GREEN_TEXT "\033[92m" //绿色高亮文本 #define BRIGHT_YELLOW_TEXT "\033[93m" //黄色高亮文本 #define BRIGHT_BLUE_TEXT "\033[94m" //蓝色高亮文本 #define RESET_COLOR "\033[0m" //重置颜色设置 #define SERVER_IP "192.168.216.149" // 服务器的IP地址 #define PORT 8080 // 服务器的端口号 #define BUFFER_SIZE 1024 // 缓冲区大小 #define FILE_SIZE 256 // 文件名大小 void upload_image(int client_socket, const char *file_path); //上传图片 void download_image(int client_socket, const char *file_name); //下载图片 void query_image(int client_socket, const char *file_name); //查找图片是否存在 void handle_sigint(int sig); //信号处理函数 int client_socket = -1; // 定义一个结构体来存储线程所需的数据 typedef struct { int client_socket; int opcode; char file_name[FILE_SIZE]; } thread_data_t; // 线程处理函数 void *handle_operation(void *arg) { // 将传入的 void* 类型参数转换为 thread_data_t* 类型 // 这样我们就可以访问传递给线程的数据,包括操作码、文件名和客户端套接字 thread_data_t *data = (thread_data_t *)arg; // 根据操作码执行相应的文件操作 switch (data->opcode) { case 1: // 如果操作码为 1,则执行上传操作 printf(BRIGHT_GREEN_TEXT"上传文件: [%s]\n\n"RESET_COLOR, data->file_name); // 调用上传函数,将文件上传到服务器 upload_image(data->client_socket, data->file_name); break; case 2: // 如果操作码为 2,则执行下载操作 printf(BRIGHT_GREEN_TEXT"下载文件: [%s]\n\n"RESET_COLOR, data->file_name); // 调用下载函数,从服务器下载文件 download_image(data->client_socket, data->file_name); break; case 3: // 如果操作码为 3,则执行查询操作 printf(BRIGHT_GREEN_TEXT"查询文件: [%s]\n\n"RESET_COLOR, data->file_name); // 调用查询函数,查询服务器上是否存在指定文件 query_image(data->client_socket, data->file_name); break; default: // 如果操作码不在预期范围内,打印错误信息 fprintf(stderr, BRIGHT_RED_TEXT"无效的操作码\n"RESET_COLOR); break; } // 关闭客户端套接字,以释放与服务器的连接资源 close(data->client_socket); // 释放线程数据结构所占用的内存 // 该内存是在主线程中为每个线程动态分配的 free(data); // 线程执行完毕后返回 NULL return NULL; } int main(void) { // 注册信号处理函数,用于处理 SIGINT (Ctrl+C) struct sigaction sa; sa.sa_handler = handle_sigint; sa.sa_flags = 0; sigemptyset(&sa.sa_mask); if (sigaction(SIGINT, &sa, NULL) == -1) { perror("sigaction"); exit(EXIT_FAILURE); } struct sockaddr_in server_addr; // 清零 server_addr 结构体 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; // 设置地址族为 IPv4 server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 设置服务器 IP 地址 server_addr.sin_port = htons(PORT); // 设置端口号,使用网络字节序 while (1) { // 创建套接字 client_socket = socket(PF_INET, SOCK_STREAM, 0); if (client_socket == -1) { perror("socket"); // 打印错误信息 exit(EXIT_FAILURE); // 退出程序 } // 尝试连接到服务器 if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("connect"); // 打印错误信息 close(client_socket); // 关闭套接字 exit(EXIT_FAILURE); // 退出程序 } printf(BRIGHT_BLUE_TEXT"已连接到服务器\n"RESET_COLOR); char file_name[FILE_SIZE]; int opcode; // 提示用户输入文件名 printf(BRIGHT_BLUE_TEXT"请输入文件名(最大为 %d 字符)\n"RESET_COLOR, FILE_SIZE); if (fgets(file_name, FILE_SIZE, stdin) == NULL) { fprintf(stderr, BRIGHT_RED_TEXT"读取文件名失败\n"RESET_COLOR); close(client_socket); continue; } // 去除文件名末尾的换行符 size_t len = strlen(file_name); if (len > 0 && file_name[len - 1] == '\n') { file_name[len - 1] = '\0'; } // 提示用户输入操作码 printf(BRIGHT_BLUE_TEXT"请输入操作码 (1: 上传, 2: 下载, 3: 查询, 0: 退出): "RESET_COLOR); if (scanf("%d", &opcode) != 1) { fprintf(stderr, BRIGHT_RED_TEXT"无效的操作码\n"RESET_COLOR); while (getchar() != '\n'); // 清空输入缓冲区 close(client_socket); continue; } while (getchar() != '\n'); // 清空输入缓冲区 if (opcode == 0) { printf(BRIGHT_GREEN_TEXT"即将退出程序...\n"RESET_COLOR); close(client_socket); // 关闭套接字 break; } if (opcode < 1 || opcode > 3) { printf(BRIGHT_RED_TEXT"无效的操作码\n"RESET_COLOR); close(client_socket); // 关闭套接字 continue; } // 动态分配线程数据结构的内存,并初始化 thread_data_t *data = (thread_data_t *)malloc(sizeof(thread_data_t)); if (data == NULL) { perror("malloc"); close(client_socket); continue; } data->client_socket = client_socket; data->opcode = opcode; strncpy(data->file_name, file_name, FILE_SIZE); // 创建线程并处理操作 pthread_t thread_id; if (pthread_create(&thread_id, NULL, handle_operation, (void *)data) != 0) { perror("pthread_create"); free(data); close(client_socket); continue; } // 分离线程,以便线程资源在完成时自动释放 pthread_detach(thread_id); sleep(0.5); } return 0; } // 上传图片函数 void upload_image(int client_socket, const char *file_path) { char buffer[BUFFER_SIZE]; char file_name[FILE_SIZE]; unsigned int file_size; int file_fd = open(file_path, O_RDONLY); if (file_fd < 0) { perror("open"); return; } // 获取文件名 snprintf(file_name, sizeof(file_name), "%s", strrchr(file_path, '/') ? strrchr(file_path, '/') + 1 : file_path); buffer[0] = 0x01; send(client_socket, buffer, 1, 0); buffer[0] = strlen(file_name); send(client_socket, buffer, 1, 0); send(client_socket, file_name, strlen(file_name), 0); file_size = lseek(file_fd, 0, SEEK_END); lseek(file_fd, 0, SEEK_SET); send(client_socket, &file_size, sizeof(file_size), 0); int read_len; while ((read_len = read(file_fd, buffer, BUFFER_SIZE)) > 0) { send(client_socket, buffer, read_len, 0); } close(file_fd); printf(BRIGHT_RED_TEXT"成功上传【%s】文件\n"RESET_COLOR, file_name); } // 下载图片函数 void download_image(int client_socket, const char *file_name) { char buffer[BUFFER_SIZE]; unsigned int file_size; buffer[0] = 0x02; send(client_socket, buffer, 1, 0); buffer[0] = strlen(file_name); send(client_socket, buffer, 1, 0); send(client_socket, file_name, strlen(file_name) + 1, 0); recv(client_socket, &file_size, sizeof(file_size), 0); if (file_size > 0) { int file_fd = open(file_name, O_WRONLY | O_CREAT | O_TRUNC, 0666); if (file_fd < 0) { perror("open"); return; } int received = 0; while (received < file_size) { int len = recv(client_socket, buffer, BUFFER_SIZE, 0); if (len < 0) { perror("recv"); close(file_fd); return; } else if (len == 0) { fprintf(stderr, "服务器意外关闭连接\n"); close(file_fd); return; } if (write(file_fd, buffer, len) != len) { perror("write"); close(file_fd); return; } received += len; } close(file_fd); printf(BRIGHT_RED_TEXT"下载文件【%s】成功\n"RESET_COLOR, file_name); } else { printf(BRIGHT_RED_TEXT"未找到【%s】文件\n"RESET_COLOR, file_name); } } // 查询图片是否存在函数 void query_image(int client_socket, const char *file_name) { char buffer[BUFFER_SIZE]; int exists; buffer[0] = 0x03; send(client_socket, buffer, 1, 0); buffer[0] = strlen(file_name); send(client_socket, buffer, 1, 0); send(client_socket, file_name, strlen(file_name), 0); recv(client_socket, &exists, sizeof(exists), 0); if (exists) { printf(BRIGHT_RED_TEXT"文件【%s】存在\n"RESET_COLOR, file_name); } else { printf(BRIGHT_RED_TEXT"文件【%s】不存在\n"RESET_COLOR, file_name); } } // 信号处理函数,处理 SIGINT 信号 void handle_sigint(int sig) { printf("\n接收到中断信号(SIGINT),程序即将退出...\n"); // 在这里可以添加程序退出前的清理代码,例如关闭打开的文件、套接字等 if (client_socket != -1) { close(client_socket); } exit(EXIT_SUCCESS); // 退出程序 }