TCP服务器上传、下载、查询操作V1.0

目录

一、主要思路

 客户端思路

 服务器思路

二、优缺点

服务器优点

服务器缺点

客户端优点

客户端缺点

完整代码

服务器代码

客户端代码


更多文章和源码在我的个人博客:首页 (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;
}
```

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值