背景

最近项目中遇到了下载大文件(大于2G)失败的问题。查看了代码之后发现从一开始实现的时候就没有考虑对大文件的支持。老板追的紧,没办法只能硬着头皮上。整个的解决过程就好像打怪升级一般,一步一个脚印。

Round 1

下载文件size每次都是2G

为什么每次下载的文件都是2G,明明在server端的文件是大于2G的。通过调试发现原来在下载的代码实现中,一些基本变量的定义都是int类型的,也就是说最大只能存储 $2^{31}-1$ 的正整数。所以需要将这些有影响的变量类型改为范围更大的类型。考虑到项目是多平台程序,所以参考了一下ANSI C/C++的基本数据类型:

Type Size 数值范围
无值型void 0 byte 无值域
布尔型bool 1 byte true false
有符号短整型short [int] /signed short [int] 2 byte -32768~32767
无符号短整型unsigned short [int] 2 byte 0~65535
有符号整型int /signed [int] 4 byte -2147483648~2147483647
无符号整型unsigned [int] 4 byte 0~4294967295
有符号长整型long [int]/signed long [int] 4 byte -2147483648~2147483647
无符号长整型unsigned long [int] 4 byte 0~4294967295
有符号 long long 8 byte -9223372036854775808~9223372036854775807
无符号long long 8 byte 0~18446744073709552000
有符号字符型char/signed char 1 byte -128~127
无符号字符型unsigned char 1 byte 0~255
宽字符型wchar_t (unsigned short.) 2 byte 0~65535
单精度浮点型float 4 byte -3.4E-38~3.4E+38
双精度浮点型double 8 byte 1.7E-308~1.7E+308
long double 8 byte

这里我们选用signed long long类型, 它可表示的最大的正整数是 $2^{63}-1$ ,足以应付4G的文件。增加如下定义:

1
2
3
4
#ifndef sint64_t_defined
#define sint64_t_defined
typedef signed long long Tsint64;//It's illegal in C90. It's legel in C99.
#endif

接着把所有有影响的变量类型改为Tsint64。OK,第一关算是勉强通过了。

Round 2

检查文件size失败

通过前面的努力,终于可以将大于2G的文件下下来了。可是好景不长,程序走到检查文件size的时候fail了。手工检查了下文件的size是没有问题的,但是程序中获取的size是一个很奇怪的数。其中获取size的代码如下:

1
2
3
4
5
6
7
8
9
10
11
unsigned int filelen(const char *fname) {
if (fname == NULL || fname[0] == '\0')
return 0;
FILE *fp = fopen (fname, "rb");
if (!fp)
return 0;
fseek (fp, 0, SEEK_END);
long length = ftell (fp);
fclose (fp);
return length == -1 ? 0 : (unsigned int)length;
}

通过调试发现问题出现在fseek和ftell函数。以下是两者的定义:

int fseek(FILE *stream, long offset, int fromwhere);
函数设置文件指针stream的位置。如果执行成功,stream将指向以fromwhere(偏移起始位置:文件头0(SEEK_SET),当前位置1(SEEK_CUR),文件尾2(SEEK_END))为基准,偏移offset(指针偏移量)个字节的位置。如果执行失败(比如offset超过文件自身大小),则不改变stream指向的位置。
long ftell(FILE *stream);
函数 ftell 用于得到文件位置指针当前位置相对于文件首的偏移字节数。

其中fseek的offset还有ftell的返回值都是long型,根据long型的取值范围 $-2^{31}$~$2^{31}-1$ (-2147483648~2147483647),故对大于2.1G的文件进行操作时出错。
下面是一段引述自维基百科的解释:

Many old interfaces, especially C-based ones, explicitly specified argument types in a way that did not allow straightforward or transparent transition to 64-bit types. For example, the C functions fseek and ftell operate on file positions of type long int, which is typically 32 bits wide on 32-bit platforms, and cannot be made larger without sacrificing backward compatibility. (This was resolved by introducing new functions fseeko and ftello in POSIX. On Windows machines, under Visual C++, functions _fseeki64 and _ftelli64 are used.)

大概意思是说对于大文件的支持,处理文件操作时,windows平台上需要用fopen_ftelli64_fseeki64,linux平台则用fopen64ftello64fseeko64
于是在代码中增加如下定义:

1
2
3
4
5
6
7
8
9
#if defined(WIN32) || defined(WIN64)
#define FOPEN_FUNC(filename, mode) fopen(filename, mode)
#define FTELL_FUNC(stream) _ftelli64(stream)
#define FSEEK_FUNC(stream, offset, origin) _fseeki64(stream, offset, origin)
#else
#define FOPEN_FUNC(filename, mode) fopen64(filename, mode)
#define FTELL_FUNC(stream) ftello64(stream)
#define FSEEK_FUNC(stream, offset, origin) fseeko64(stream, offset, origin)
#endif

同时将原来的fopenfseekftell函数调用换为FOPEN_FUNCFSEEK_FUNCFTELL_FUNC。顺利通过第二关。

Round 3

解压文件失败

项目中解压用到了第三方的解压缩库zlib。网上有人说zlib就是不支持大文件的解压,-_-!,不会这么坑吧。如果真是这样,那还得换一个支持大文件解压缩的库(PS此刻想死的心都有)。
后来通过调试发现,问题不是出在zlib库的内部,而是出在了调用zlib的函数里面。主要原因如第二关中所说,于是将其中用到的fopenfseekftell函数调用换为FOPEN_FUNCFSEEK_FUNCFTELL_FUNC。重新编译运行,问题解决了。所以说,网上的有些说法是不靠谱的。zlib库是支持大文件解压缩的。
至此,windows平台下大文件下载的问题算是解决了。

Round 4

linux平台不work

既然windows平台已经可以支持大文件的下载了,下面只要测试一下linux平台就OK了。然而事情并没有想象的那么简单,用改过的代码编译之后(庆幸没有遇到问题),跑了一个下载流程。意外发生了:程序跑到一半的时候出错,File size limit exceeded。做了一番google之后,发现原来对于linux平台下大文件的支持需要注意以下事项:

  1. 在所需要的头文件的#include之前添加如下几行代码:
    #ifndef __USE_FILE_OFFSET64
    #define __USE_FILE_OFFSET64
    #endif
    #ifndef __USE_LARGEFILE64
    #define __USE_LARGEFILE64
    #endif
    #ifndef _LARGEFILE64_SOURCE
    #define _LARGEFILE64_SOURCE
    #endif

  2. 在编译程序的时候,加入如下选项:
    -D_FILE_OFFSET_BITS=64 -D_LARGE_FILE

  3. 在程序的函数中,要注意如下几个方面:

  • 首先32位机器用fopen/fclose打开大文件没有问题,顺序读写操作while(!feof(fp)){ fread / fgets / fscanf }或while(1){ fwrite / fputs / fprintf} 也没有问题。
  • 由于32位机器下long是32位,故
    (FILE *stream, off_t offset, int whence)
    1
    off_t ftello(FILE *stream)

不能访问4G以上文件。此时要用

(FILE *stream, off_t offset, int whence)
1
off_t ftello(FILE *stream)

依葫芦画瓢,按照以上步骤改过之后再编译测试就通过了。

OK, 经过以上的努力总算是通关了。整个过程一步一步摸索得来,为了让和我遇到类似问题的广大猿友们少走些弯路,故作此文。