使用Sockets和OpenSSL下载BMP图片时文件损坏问题求助
BMP图片下载损坏问题修复方案
你的代码下载的图片损坏,核心原因是服务器采用HTTP/1.1的分块编码返回数据,而代码直接将分块标识和图片数据混存,导致提取的图片包含无效内容。另外,依赖"BM"标识分割响应头和体的方式也存在误判风险,以下是具体修复方案:
问题分析
- 分块编码未处理:HTTP/1.1默认持久连接,服务器常用分块传输(Transfer-Encoding: chunked)返回数据。此时响应体不是纯二进制图片,而是被拆分为多个块,每个块开头带十六进制长度标识+
\r\n,末尾还有结束块标识,这些额外内容会破坏图片结构。 - 响应分割方式不严谨:靠"BM"找图片起始位置,若响应头中恰好出现该字符串,会导致错误截断。
- SSL读取错误处理缺失:
SSL_read返回<=0时,未区分正常关闭和异常错误,可能导致读取不完整或错误处理。
修复代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netdb.h> #include <openssl/ssl.h> #include <openssl/bio.h> #include <openssl/err.h> #define SERVER "filesamples.com" #define PORT 443 #define PATH "/samples/image/bmp/sample_640%C3%97426.bmp" // 读取HTTP响应,分离头和体并处理分块编码 int read_http_response(SSL *ssl, FILE *body_fp) { char buffer[4096]; int bytes_read; char *header_end = NULL; int in_header = 1; int in_chunk = 0; unsigned long chunk_len = 0; int chunk_remaining = 0; while (1) { bytes_read = SSL_read(ssl, buffer, sizeof(buffer)); if (bytes_read <= 0) { int ssl_err = SSL_get_error(ssl, bytes_read); if (ssl_err == SSL_ERROR_ZERO_RETURN) { // 连接正常关闭,退出循环 break; } else { fprintf(stderr, "SSL读取错误: %d\n", ssl_err); return -1; } } int pos = 0; while (pos < bytes_read) { if (in_header) { // 查找响应头与体的分隔符\r\n\r\n header_end = memmem(buffer + pos, bytes_read - pos, "\r\n\r\n", 4); if (header_end != NULL) { // 跳过响应头,开始处理响应体 pos = header_end - buffer + 4; in_header = 0; } else { // 未找到分隔符,跳过当前缓冲区数据 pos = bytes_read; } } else { if (!in_chunk) { // 读取分块长度(十六进制格式) char *chunk_end = memchr(buffer + pos, '\r', bytes_read - pos); if (chunk_end != NULL) { *chunk_end = '\0'; chunk_len = strtoul(buffer + pos, NULL, 16); *chunk_end = '\r'; // 恢复原数据 // 跳过长度标识和后面的\r\n pos = chunk_end - buffer + 2; in_chunk = 1; chunk_remaining = chunk_len; if (chunk_len == 0) { // 分块结束,返回成功 return 0; } } else { // 分块长度跨缓冲区,暂时跳过 pos = bytes_read; } } else { // 读取当前分块的实际数据 int copy_len = bytes_read - pos; if (copy_len > chunk_remaining) { copy_len = chunk_remaining; } fwrite(buffer + pos, 1, copy_len, body_fp); pos += copy_len; chunk_remaining -= copy_len; if (chunk_remaining == 0) { // 当前分块读取完成,跳过末尾的\r\n in_chunk = 0; if (pos + 2 <= bytes_read && memcmp(buffer + pos, "\r\n", 2) == 0) { pos += 2; } } } } } } return 0; } int main() { int sockfd; struct sockaddr_in serv_addr; struct hostent *server; // 初始化OpenSSL环境 SSL_library_init(); OpenSSL_add_all_algorithms(); SSL_load_error_strings(); SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); if (ctx == NULL) { ERR_print_errors_fp(stderr); return 1; } SSL *ssl = SSL_new(ctx); sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("创建socket失败"); return 1; } server = gethostbyname(SERVER); if (server == NULL) { fprintf(stderr, "解析服务器域名失败\n"); return 1; } memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); memcpy(&serv_addr.sin_addr.s_addr, server->h_addr, server->h_length); if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) { perror("连接服务器失败"); return 1; } BIO *sbio = BIO_new_socket(sockfd, BIO_NOCLOSE); SSL_set_bio(ssl, sbio, sbio); if (SSL_set_tlsext_host_name(ssl, SERVER) != 1) { fprintf(stderr, "设置SNI失败\n"); return 1; } if (SSL_connect(ssl) <= 0) { ERR_print_errors_fp(stderr); return 1; } // 构造HTTP请求,添加Connection: close让服务器返回数据后关闭连接 char request[1024]; snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", PATH, SERVER); int bytes_written = SSL_write(ssl, request, strlen(request)); if (bytes_written <= 0) { fprintf(stderr, "发送请求失败\n"); return 1; } // 直接打开图片文件准备写入 FILE *fp = fopen("sample.bmp", "wb"); if (fp == NULL) { perror("打开图片文件失败"); return 1; } if (read_http_response(ssl, fp) != 0) { fprintf(stderr, "读取响应失败\n"); fclose(fp); return 1; } fclose(fp); printf("图片下载成功\n"); // 清理资源 SSL_shutdown(ssl); SSL_free(ssl); SSL_CTX_free(ctx); close(sockfd); return 0; }
关键修改说明
- 添加Connection: close请求头:让服务器返回数据后主动关闭连接,避免HTTP/1.1持久连接导致的
SSL_read阻塞。 - 解析分块编码:去掉分块的长度标识和换行符,只保留实际的图片二进制数据。
- 正确分割响应头与体:用标准的
\r\n\r\n分隔符拆分,避免依赖"BM"标识的误判。 - 完善错误处理:区分SSL读取的正常关闭和异常错误,确保数据读取完整。
- 直接写入图片文件:省去中间的response.txt步骤,减少数据处理环节。
编译运行
编译时需要链接OpenSSL库:
gcc -o bmp_download bmp_download.c -lssl -lcrypto
运行生成的可执行文件,会得到可正常打开的sample.bmp。
内容的提问来源于stack exchange,提问作者Ilya
相关产品推荐
相关产品推荐

