目录
更多文章和源码在我的个人博客:首页 (niuniu65.top)
如有需要请添加个人微信:a15135158368
一、主要思路
客户端思路
1.程序启动:按照命令行参数要求输入文件名作为参数
2.程序初始化:创建套接字,设置服务器地址,连接到服务器
3.用户输入操作码: 0:退出;
1: 上传;
2:下载;
3:查询;
4.上传函数流程 (`upload_image` 函数)
打开文件;
从文件路径中提取文件名,发送到服务器;
依次发送操作码(0x01)、文件名、文件大小、文件内容;
关闭文件。
5.下载函数流程(`download_image` 函数)
依次发送操作码(0x02)、文件名、文件大小
循环接收文件内容
关闭文件
6.查询函数 (`query_image` 函数)
提取文件名
发送操作码
发送文件名
接收查询结果
服务器思路
1.程序初始化:
创建套接字
配置服务器地址
绑定套接字
监听连接
2.接受客户端连接
接受连接(accept)
处理客户端请求(handle_client)
3.处理客户端请求
操作码 0x01(上传文件)
操作码 0x02(下载文件)
操作码 0x03(查询文件是否存在)
4. 关闭连接
5. 信号处理
捕捉sigint信号(CTRL+c)(类似于中断)
二、优缺点
服务器优点
1. 简单易懂
2. 基础功能完备
3. 错误处理
4. 信号退出程序
服务器缺点
1. 阻塞模式:只能处理一个客户端请求,其他客户端必须等待,导致并发性差
2. 缺乏并发性处理:没有使用多线程或多进程处理多个客户端的连接
3. 数据安全性不足:对传输的数据没有加密或安全处理,建议加入TLS/SSL等安全措施
4. 缺乏完整性校验:没有对文件完整性的校验,可能导致数据丢失
5. 硬编码IP地址:需手动修改IP地址,建议使用“INADDR_ANY”绑定所有可用端口,增强代码适应性
6. 错误处理机制不完善:建议加入恢复或重试机制。
客户端优点
1. 结构清晰
2. 错误处理
3. 动态文件名获取
4. 简单的用户交互
客户端缺点
1. 没有并发支持:客户端是单线程的,不能同时进行多个操作或处理多个文件。操作需要依次执行,这在需要处理大量文件或同时执行多个操作时效率低下。
2. 文件路径处理:在获取文件名时,代码中对文件路径进行了一些硬编码的处理。
3. 没有连接超时处理:在连接服务器或进行网络操作时,代码没有设置连接超时或操作超时,这可能导致客户端在网络问题或服务器不可用时长时间挂起。
4. 硬编码的服务器信息:客户端的服务器IP和端口号是硬编码的,如果要连接不同的服务器,需要修改代码。
可以考虑通过配置文件或命令行参数动态设置服务器信息。
5. 缺乏文件完整性校验:在上传和下载文件时,客户端没有进行文件完整性检查,如计算和验证文件哈希值。
6. 错误处理不完善:虽然有基本的错误处理,但在实际应用中,可能需要更详细的错误处理和恢复机制。例如,在网络操作或文件操作失败后,可以考虑重试机制或更详细的错误日志记录。
完整代码
服务器代码
/**
******************************************************************************
* @file : tcp_server.c
* @author : niuniu
* @brief : 该程序实现了一个简单的TCP服务器,能够处理客户端的文件上传、下载和查询操作
* @attention : 服务器使用阻塞模式,多个客户端同时连接时,需考虑并发处理
* @date : 2024/8/11
******************************************************************************
*/
#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>
#define PORT 8080 // 服务器监听的端口号
#define BUFFER_SIZE 1024 // 缓冲区大小
#define SERVER_IP "192.168.216.149" // 服务器IP地址
void handle_sigint(int sig);
/**
* @brief 处理与客户端的通信,执行上传、下载、查询操作
*
* @param client_socket 与客户端通信的套接字
*/
void handle_client(int client_socket)
{
char buffer[BUFFER_SIZE]; // 用于存储接收和发送的数据的缓冲区
int name_len; // 文件名长度
char file_name[256] = {0}; // 用于存储文件名的字符数组
unsigned int file_size; // 文件大小(字节数)
while (1) // 无限循环,持续监听和处理客户端的请求
{
// 接收操作码,表示客户端希望执行的操作类型
if (recv(client_socket, buffer, 1, 0) == 0) // 接收操作码失败则退出循环
{
perror("recv");
break;
}
char opcode = buffer[0]; // 操作码,0x01上传,0x02下载,0x03查询
// 接收文件名长度
if (recv(client_socket, buffer, 1, 0) <= 0) // 接收文件名长度失败则退出循环
{
perror("recv2");
break;
}
// 获取文件名长度
name_len = buffer[0];
// 接收文件名
if (recv(client_socket, file_name, name_len, 0) <= 0) // 接收文件名失败则退出循环
{
perror("recv3");
break;
}
file_name[name_len] = '\0'; // 文件名字符串的末尾添加结束符
// 判断为上传图片操作
if (opcode == 0x01)
{
// 接收文件大小
if (recv(client_socket, buffer, 4, 0) <= 0) // 接收文件大小失败则退出循环
{
perror("recv4");
break;
}
file_size = *((unsigned int *)buffer); // 将接收的文件大小转换为整数
// 打开或者创建一个新文件,用于保存上传的图片
int file_fd = open(file_name, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (file_fd < 0) // 文件打开失败则退出循环
{
perror("open");
break;
}
int received = 0; // 已接收的数据大小
// 循环接收文件内容,直到接收完整个文件
while (received < file_size)
{
int len = recv(client_socket, buffer, BUFFER_SIZE, 0);
if (len <= 0) // 接收数据失败则退出循环
{
perror("recv5");
break;
}
write(file_fd, buffer, len); // 将接收的数据写入文件
received += len; // 更新已接收的字节数
}
close(file_fd); // 关闭文件描述符
printf("已将文件【%s】上传\n", file_name); // 打印成功接收的文件名
}
// 判断为下载图片操作
else if (opcode == 0x02)
{
int 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");
}
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);
}
close(file_fd); // 关闭文件描述符
printf("已将文件【%s】发送下载成功\n", file_name);
}
}
// 判断为查看图片是否存在操作
else if (opcode == 0x03)
{
// 使用 access 函数判断文件是否存在,存在返回1,不存在返回0
int exists = access(file_name, F_OK) != -1;
send(client_socket, &exists, sizeof(exists), 0);
printf("access : %d\n", exists);
printf("已查询文件【%s】是否存在\n", file_name);
}
}
// 关闭套接字,结束与客户端的连接
close(client_socket);
}
/**
* @brief 服务器主函数,初始化并启动服务器,持续监听客户端的连接请求
*
* @return int 程序退出状态码
*/
int server_socket;
int main(void)
{
int client_socket; // 服务器和客户端套接字描述符
struct sockaddr_in server_addr, client_addr; // 存储服务器和客户端地址信息的结构体
socklen_t client_addr_size; // 客户端地址结构体的大小
// 注册信号处理函数,用于处理 SIGINT (Ctrl+C)
signal(SIGINT, handle_sigint);
// 创建服务器套接字
server_socket = socket(PF_INET, SOCK_STREAM, 0);
if (server_socket == -1)
{
perror("socket");
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("服务器正在监听【%d】号端口号\n", PORT);
while (1) // 无限循环,持续接收客户端请求
{
// 初始化客户端结构体大小
client_addr_size = sizeof(client_addr);
client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_size);
if (client_socket == -1) // 接收客户端连接失败
{
perror("accept");
continue;
}
else // 成功接收客户端连接
{
printf("成功接收了客户端的连接\n");
handle_client(client_socket); // 处理客户端请求
}
}
}
//接收信号主动关闭服务器
void handle_sigint(int sig)
{
printf("\n接收到信号 %d,正在关闭服务器...\n", sig);
close(server_socket);
exit(0);
}
客户端代码
/**
******************************************************************************
* @file : tcp_client.c
* @author : niuniu
* @brief : 实现客户端程序,用于连接服务器并执行上传、下载、查询操作
* @attention : None
* @date : 2024/8/11
******************************************************************************
*/
#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>
#define SERVER_IP "192.168.216.149" // 服务器的IP地址
#define PORT 8080 // 服务器的端口号
#define BUFFER_SIZE 1024 // 缓冲区大小
// 上传图片函数
void upload_image(int client_socket, const char *file_path)
{
char buffer[BUFFER_SIZE]; // 存储读取的文件内容和发送数据的缓冲区
char file_name[256]; // 存储文件名的缓冲区
unsigned int file_size; // 文件大小
// 打开指定路径的文件
int file_fd = open(file_path, O_RDONLY);
if(file_fd < 0)
{
perror("open"); // 如果文件打开失败,输出错误信息
return;
}
// 复制文件路径到 file_name 缓冲区
strncpy(file_name, file_path, strlen(file_path));
// 查找文件路径最后一个 '/' 的位置,获取文件名
char *name_ptr = strrchr(file_name, '/');
if(name_ptr)
{
// 如果找到了 '/',该符号后的就是文件名
name_ptr++;
}
else
{
// 如果没有找到 '/',文件名就是整个路径
name_ptr = file_name;
}
// 发送操作码 0x01,表示上传图片
buffer[0] = 0x01;
send(client_socket, buffer, 1, 0);
// 发送文件名长度和文件名
buffer[0] = strlen(name_ptr);
send(client_socket, buffer, 1, 0); // 发送文件名长度
send(client_socket, name_ptr, strlen(name_ptr), 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("成功上传【%s】文件\n", file_name);
}
// 下载图片函数
void download_image(int client_socket, const char *file_name)
{
char buffer[BUFFER_SIZE]; // 存储接收服务器和发送数据的缓冲区
unsigned int file_size; // 用于存储文件的大小
// 发送操作码 0x02,表示下载图片
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); // 从服务器接收数据
write(file_fd, buffer, len); // 将数据写入本地文件
received += len; // 更新已接收的数据量
}
close(file_fd); // 关闭文件
printf("下载文件【%s】成功\n", file_name);
}
else
{
printf("未找到【%s】文件\n", file_name); // 如果文件大小为0,表示文件不存在
}
}
// 查询图片是否存在函数
void query_image(int client_socket, const char *file_path)
{
char buffer[BUFFER_SIZE]; // 定义缓冲区
int exists; // 存在标志位
char file_name[256] = {0}; // 文件名缓冲区,初始化为0
// 复制文件路径到 file_name 缓冲区
strncpy(file_name, file_path, strlen(file_path));
// 查找文件路径最后一个 '/' 的位置,获取文件名
char *name_ptr = strrchr(file_name, '/');
if(name_ptr)
{
// 如果找到了 '/',该符号后的就是文件名
name_ptr++;
}
else
{
// 如果没有找到 '/',文件名就是整个路径
name_ptr = file_name;
}
// 发送操作码 0x03,表示查询图片是否存在
buffer[0] = 0x03;
send(client_socket, buffer, 1, 0);
// 发送文件名长度和文件名
buffer[0] = strlen(name_ptr);
send(client_socket, buffer, 1, 0); // 发送文件名长度
send(client_socket, name_ptr, strlen(name_ptr), 0); // 发送文件名字符串
// 接收服务器的响应,判断图片是否存在
recv(client_socket, &exists, sizeof(exists), 0);
if(exists)
{
printf("文件【%s】存在\n", name_ptr); // 如果 exists 为真,表示文件存在
}
else
{
printf("文件【%s】不存在\n", name_ptr); // 如果 exists 为假,表示文件不存在
}
}
// 主函数
int main(int argc, char const *argv[])
{
if(argc != 2) // 检查命令行参数个数,要求只提供文件名作为启动参数
{
fprintf(stderr, "用法:%s <文件名>\n", argv[0]);
return 1;
}
const char *file_name = argv[1]; // 文件名从命令行参数获取
int client_socket;
struct sockaddr_in server_addr;
// 创建套接字
client_socket = socket(PF_INET, SOCK_STREAM, 0);
if(client_socket == -1)
{
perror("socket"); // 如果套接字创建失败,输出错误信息
exit(-1);
}
// 初始化服务器地址结构体
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); // 设置服务器端口号
// 连接到服务器
if(connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
{
perror("connect"); // 如果连接失败,输出错误信息
exit(-1);
}
printf("已连接到服务器\n");
while (1)
{
int opcode;
printf("\n请输入操作码 (1: 上传, 2: 下载, 3: 查询, 0: 退出): ");
scanf("%d", &opcode); // 从标准输入读取操作码
if (opcode == 0)
{
printf("退出程序\n");
break; // 如果输入0,退出循环
}
switch (opcode)
{
case 1:
upload_image(client_socket, file_name); // 执行上传图片操作
break;
case 2:
download_image(client_socket, file_name); // 执行下载图片操作
break;
case 3:
query_image(client_socket, file_name); // 执行查询图片是否存在操作
break;
default:
printf("请输入正确的操作码\n"); // 输入无效操作码时提示
break;
}
}
close(client_socket); // 关闭套接字
printf("客户端已关闭\n");
return 0;
}
```