当前位置: 首页 > news >正文

磁盘文件的正常读写与异步读写

磁盘文件的正常读写与异步读写

转自:http://222.30.226.10/hhcmc/study/teach_vc/teach_sp_52.htm

在Win32系统下文件可以支持平常的同步读写和异步读写(但在Win9X下,Win32系统不支持磁盘文件的异步读写)。本节在后面部分将会介绍文件的异步读写,最后一段内容将向大家讲解一下文件的区域加锁。

在Win32系统中支持64位长度的文件,所以在很多文件操作函数中需要两个DWORD参数来表示文件长度,一个DWORD用来表示低32位,另一个用来表示高32位。

文件的读写进行在文件被正确打开后,但请确认在打开文件时设置了正确的读写标记。在Win32的文件操作中没有了以前类似与以前ANSI C中的fputs fgets fprintf fscanf等函数,只有类似于fread和fwrite的ReadFile和WriteFile函数。

ReadFile用于文件读,函数原型为:

BOOL ReadFile(
  HANDLE hFile,                // handle to file
  LPVOID lpBuffer,             // data buffer
  DWORD nNumberOfBytesToRead,  // number of bytes to read
  LPDWORD lpNumberOfBytesRead, // number of bytes read
  LPOVERLAPPED lpOverlapped    // overlapped buffer
);

其中各项参数的含义为:

  • hFile:文件句柄,为CreateFile时返回的句柄
  • lpBuffer:保存读入的数据的指针
  • nNumberOfBytesToRead:指定需要读入的字节数
  • lpNumberOfBytesRead:返回实际读入的字节数
  • lpOverlapped:在文件异步读写时使用的数据,在同步读写中全部都设置为NULL,在Win9X中只支持对串口的异步操作。

如果返回值为FALSE并且读入的字节数也返回为0,则表示文件到达了末尾。

WriteFile用于文件写,函数原型为:
BOOL WriteFile(
  HANDLE hFile,                    // handle to file
  LPCVOID lpBuffer,                // data buffer
  DWORD nNumberOfBytesToWrite,     // number of bytes to write
  LPDWORD lpNumberOfBytesWritten,  // number of bytes written
  LPOVERLAPPED lpOverlapped        // overlapped buffer
);

参数的含义和ReadFile类似。

如果需要移动文件指针到相关位置(和文件读写不同,这个函数没有异步版本),使用

DWORD SetFilePointer(
  HANDLE hFile,                // handle to file
  LONG lDistanceToMove,        // bytes to move pointer
  PLONG lpDistanceToMoveHigh,  // bytes to move pointer
  DWORD dwMoveMethod           // starting point
);

其中各项参数的含义为:

  • hFile:文件句柄,为CreateFile时返回的句柄
  • lpBuffer:保存读入的数据的指针
  • lDistanceToMove:移动的字节数低DWORD
  • lpDistanceToMoveHigh:移动的字节数高DWORD,为了支持64位(2的64次方字节)长度的大文件,而用来指定64字节的高32位,如果文件大小只需要32位就可以表示,则设置为NULL
  • ldwMoveMethod:移动方法,可以选择下面的值。
    FILE_BEGIN 从文件开始处开始移动
    FILE_CURRENT 从文件开始除开始移动
    FILE_END 从文件末尾开始移动

函数返回值和参数lpDistanceToMoveHigh(当该参数不为NULL时)表明文件指针当前的位置(从文件头到当前的字节数),但当参数lpDistanceToMoveHigh为NULL时如果返回INVALID_SET_FILE_POINTER表明执行失败,当参数 lpDistanceToMoveHigh不为NULL时如果返回INVALID_SET_FILE_POINTER还需要判断GetLastError 的返回值是否不为NO_ERROR。下面是两种情况下判断错误。

//第一种情况
DWORD dwPtr = SetFilePointer (hFile, lDistance, NULL, FILE_BEGIN) ; 
 
if (dwPtr == INVALID_SET_FILE_POINTER) // Test for failure
{ 
    // Obtain the error code. 
    dwError = GetLastError() ; 
 
    // 处理错误
    // . . . 
 
} // End of error handler 

//第二种情况
dwPtrLow = SetFilePointer (hFile, lDistLow, & lDistHigh, FILE_BEGIN) ; 
 
// Test for failure
if (dwPtrLow == INVALID_SET_FILE_POINTER && (dwError = GetLastError()) != NO_ERROR )
{ 
    // 处理错误 
    // . . . 

} // End of error handler 

在Win32中没有提供得到文件当前指针位置的函数,但通过SetFilePointer也可以确定文件指针当前的位置。在MSDN中提供了两个宏来得到当前文件的指针位置:

//对32位长度的文件
    #define GetFilePointer(hFile) SetFilePointer(hFile, 0, NULL, FILE_CURRENT)
//对超过32位长度的文件
    #define GetVLFilePointer(hFile, lpPositionHigh) \
        (*lpPositionHigh = 0, \
        SetFilePointer(hFile, 0, lpPositionHigh, FILE_CURRENT))

对了可以通过SetFilePointer来得到文件长度,方法就是从文件位结束处移动0字节,返回值就是文件的长度。

HANDLE hFile = CreateFile(...);//打开文件进行读
DWORD dwLen;
dwLen = SetFilePointer (hFile, 0, NULL, FILE_END) ; 
CloseHandle( hFile ) ;

当然Win32中也提供了专门的函数来得到文件的大小

BOOL GetFileSizeEx(
  HANDLE hFile,              // handle to file
  PLARGE_INTEGER lpFileSize  // file size
);
typedef union _LARGE_INTEGER { 
  struct {
      DWORD LowPart; 
      LONG  HighPart; 
  };
  LONGLONG QuadPart;
} LARGE_INTEGER, *PLARGE_INTEGER; 

其中lpFileSize是一个联合数据,用来表明文件的大小。

文件的异步读写主要是用在大文件的读写上,当使用异步读写时,ReadFile和WriteFile会马上返回,并在读写完成时通知应用程序。

要使用异步功能首先需要在打开文件时指定FILE_FLAG_OVERLAPPED作为标记(dwFlagsAndAttributes),在读写文件时可以使用ReadFile/WriteFile或者ReadFileEx /WriteFileEx来进行读写,当调用不同的函数时读写完成后通知应用程序的方法有所不同的,。下面分别介绍这两种方法:

第一种方法,利用ReadFile/WriteFile,这对函数使用事件信号来进行读写完成的通知,由于磁盘读写是一个比较耗费时间的操作,而且现在的磁盘子系统可以在磁盘读写时脱离CPU而单独进行,例如使用DMA方式,所以在磁盘读写时可以进行其他一些操作以充分利用CPU。关于事件信号相关内容请参考4.4节 进程/线程间同步中事件部分内容。并且在文件读写时提供OVERLAPPED数据。

结构定义如下:
typedef struct _OVERLAPPED { 
    ULONG_PTR  Internal; //系统使用
    ULONG_PTR  InternalHigh; //系统使用
    DWORD  Offset; // 文件读或写的开始位置低32位,对于命名管道和其他通信设备必须设置为0
    DWORD  OffsetHigh; // 文件读或写的开始位置高32位,对于命名管道和其他通信设备必须设置为0
    HANDLE hEvent; // 事件量,当操作完成时,这个时间会变为有信号状态
} OVERLAPPED; 

//下面的代码演示了文件异步读取
//并且比较了同步和异步之间的性能差异
void DoDataDeal(BYTE *pbData,int iLen)
{//对字节进行操作
	Sleep(3*1000);//假设每次计算需要2秒钟
}
//下面是使用异步读的示范代码,假设c:\temp\large_file.dat文件有130MB大小()
//每次读入10MB字节,并且对文件中每个字节进行操作,由于可以使用异步操作所以可以在下一次读入数据的同时进行计算
void ReadM1(void)
{
	HANDLE hFile = CreateFile("c:\\temp\\large_file.dat",GENERIC_READ,0,
				NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED|FILE_ATTRIBUTE_NORMAL,NULL);
	if( INVALID_HANDLE_VALUE != hFile )
	{
		HANDLE hEvent = CreateEvent(NULL,FALSE,FALSE,"read_event");
		BYTE *pbRead = new BYTE[1024*1024*10];//10MB字节
		BYTE *pbBuf = new BYTE[1024*1024*10];
		DWORD dwRead,dwCount=0;
		OVERLAPPED overlap;
		overlap.Offset = 0;
		overlap.OffsetHigh =0;
		overlap.hEvent = hEvent;
		
		DWORD dwBegin= GetTickCount();//记录开始时间
		ReadFile(hFile,pbRead,1024*1024*10,&dwRead,&overlap);
		{//开始计算
			for(int i=1;i<13;i++)
			{
				printf("M1 i=%d\n",i);
				WaitForSingleObject(hEvent,INFINITE);//等待上一次完成
				memcpy(pbBuf,pbRead,1024*1024*10);
				overlap.Offset = i * (1024*1024*10);
				overlap.OffsetHigh =0;
				overlap.hEvent = hEvent;
				ReadFile(hFile,pbRead,1024*1024*10,&dwRead,&overlap);
				//在文件进行读的同时进行计算
				DoDataDeal(pbBuf,1024*1024*10);
			}
			WaitForSingleObject(hEvent,INFINITE);//等待最后一次完成
			memcpy(pbBuf,pbRead,1024*1024*10);
			//数据处理
			DoDataDeal(pbBuf,1024*1024*10);
		}//结束计算
		printf("耗时 %d\n",GetTickCount()-dwBegin);
		//操作完成
		CloseHandle(hEvent);
		CloseHandle(hFile);
		delete pbRead;
		delete pbBuf;
	}
}
//下面是上面功能的文件同步读版本
void ReadM2(void)
{
	HANDLE hFile = CreateFile("c:\\temp\\large_file.dat",GENERIC_READ,0,
				NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
	if( INVALID_HANDLE_VALUE != hFile )
	{
		DWORD dwRead,dwCount=0;
		BYTE *pbRead = new BYTE[1024*1024*10];//10MB字节
		BYTE *pbBuf = new BYTE[1024*1024*10];//10MB字节
		
		DWORD dwBegin= GetTickCount();//记录开始时间
		for(int i=0;i<13;i++)
		{
			printf("M2 i=%d\n",i);
			if(!ReadFile(hFile,pbRead,1024*1024*10,&dwRead,NULL))
			{
				printf("error read");
				break;
			}
			memcpy(pbBuf,pbRead,1024*1024*10);
			//计算
			DoDataDeal(pbBuf,1024*1024*10);
		}
		printf("耗时 %d\n",GetTickCount()-dwBegin);
		//操作完成
		CloseHandle(hFile);
		delete pbRead;
		delete pbBuf;
	}
}

在文件的异步读写中,如果ReadFile/WriteFile返回FALSE,则需要通过LastError来进一步确认是否发生错误,例如下面的错误检测代码:

bResult = ReadFile(hFile, &inBuffer, nBytesToRead, &nBytesRead, 
    &gOverlapped) ; 
if (!bResult) 
    switch (dwError = GetLastError()) 
    { 
        case ERROR_HANDLE_EOF: 
        { 
          //到达文件尾
        } 
 
        case ERROR_IO_PENDING: 
        { 
            //正在进行异步操作
        } // end case 
 
    } // end switch 
} // end if 
此外可以通过GetOverlappedResult函数来得到异步函数的执行情况
BOOL GetOverlappedResult(
  HANDLE hFile,                       // handle to file, pipe, or device
  LPOVERLAPPED lpOverlapped,          // overlapped structure
  LPDWORD lpNumberOfBytesTransferred, // bytes transferred
  BOOL bWait                          // wait option
);

如果函数调用返回FALSE则可以用GetLastError来得到错误,如果返回成功则可以通过lpNumberOfBytesTransferred 参数来确定当前有多少数据已经被读或写。lpOverlapped参数必须与调用ReadFile或WriteFile时使用同一个数据区。最后一个参数 bWait表明是否等待异步操作结束时才返回,如果设置为TRUE就可以等待文件读写完成时返回,否则就会马上返回,利用这个特点可以利用它来等待异步文件操作的结束(就如同等待事件变为有信号状态一样起到相同的作用)。

在上面的例子中没有过多的进行错误检查,如果大家有兴趣可以自己运行一下这个例子,看看异步文件读写对性能的影响。在我自己的计算机PII 450 IBM 30GB硬盘上同步读比异步读慢了大约10%左右,这主要时因为数据处理时间我设置为两秒钟,如果设置得足够长,会显示出异步和同步处理时的差异极限。此外由于磁盘缓存得作用也会影响结果,所以如果读入的数据量更大将会产生更明显的差异,这是因为虽然异步读写会在调用等待函数上也会耗费一些时间,所以如果数据量小就无法分辨出差异。

请记住OVERLAPPED参数在文件操作执行完以前不要手工修改结构内的值,因为系统会使用结构中的数据。

对于WriteFile操作也可以用相同方法,在WriteFile中磁盘操作将耗费更多的时间,所以使用异步写更能体现优势,你可以将这个例子改为磁盘写后自己测试一下。

下载利用ReadFile进行异步文件读的示范代码

第二种方法,利用ReadFileEx/WriteFileEx,这对函数使用回调函数来进行读写完成的通知。

BOOL ReadFileEx(
  HANDLE hFile,                                       // handle to file
  LPVOID lpBuffer,                                    // data buffer
  DWORD nNumberOfBytesToRead,                         // number of bytes to read
  LPOVERLAPPED lpOverlapped,                          // offset
  LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // completion routine
);
  • hFile为文件句柄。
  • lpBuffer指明了写入数据的内存区指针。
  • nNumberOfBytesToRead为要求读入的数据字节数。
  • lpOverlapped为一个OVERLAPPED的结构,这个结构hEvent字段将被系统忽略,但是通过Offset和OffsetHigh字段来表明开始读文件的位置。
  • lpCompletionRoutine为一个通知用的回调函数。

函数的最后一个参数指明了一个回调函数,这个回调函数称为一个告警函数。函数必须具有这样的原型:

VOID CALLBACK FileIOCompletionRoutine(
  DWORD dwErrorCode,                // completion code
  DWORD dwNumberOfBytesTransfered,  // number of bytes transferred
  LPOVERLAPPED lpOverlapped         // I/O information buffer
);
  • dwErrorCode为错误代码,如果为0表示正确,为ERROR_HANDLE_EOF表示到达文件的末尾。
  • dwNumberOfBytesTransfered为成功传送的字节数,如果发生错误,这个值为0。
  • lpOverlapped为一个OVERLAPPED的结构,这个结构必须与调用ReadFileEx时指向相同的数据区,并且在调用ReadFileEx后不能手工更改这个结构中的字段。

那么如何检测回调函数已经被调用了(文件操作已经完成),你可以设置一个全局的同步量来进行通知,但是系统提供了其他简便的方法供开发者使用,这就是SleepEx WaitForMultipleObjectsEx 和WaitForSingleObjectEx。

当线程调用ReadFileEx或WriteFileEx时,提供了一个告警函数,这个告警函数会被放入一个队列,当操作完成时操作系统会从队列中取出这些函数进行调用。所以对于属于本进程内部所产生的需要等待被调用的告警函数来说可以直接使用SleepEx对当前线程进行休眠并等待当前提交所有的告警函数被执行。如果希望等待指定文件操作上的告警函数被调用你可以使用WaitForMultipleObjectsEx或 WaitForSingleObjectEx。这三个函数的原型为:

DWORD SleepEx(
  DWORD dwMilliseconds,  // time-out interval
  BOOL bAlertable        // early completion option
);
DWORD WaitForSingleObjectEx(
  HANDLE hHandle,        // handle to object
  DWORD dwMilliseconds,  // time-out interval
  BOOL bAlertable        // alertable option
);
DWORD WaitForMultipleObjectsEx(
  DWORD nCount,             // number of handles in array
  CONST HANDLE *lpHandles,  // object-handle array
  BOOL fWaitAll,            // wait option
  DWORD dwMilliseconds,     // time-out interval
  BOOL bAlertable           // alertable option
);

这三个函数和Sleep WaitForSingleObject WaitForMultipleObjects的差别就在于多了最后一个参数bAlertable,这个参数需要设置为TRUE表明等待文件异步操作的完成。通过检测函数返回值可以得知文件操作是否完成,例如下面的代码:

	ReadFileEx(hFile,pbRead,1024*1024*50,&overlap,MyIORoutine);
	while(WAIT_IO_COMPLETION != SleepEx(1,TRUE) )//检测文件操作是否完成
	//while (WaitForSingleObjectEx(hFile,1,TRUE) != WAIT_OBJECT_0 ) 
	//在这里WaitForSingleObjectEx和SleepEx具有相同作用
	{
		DoSomething();
	}

对于SleepEx来说如果返回WAIT_IO_COMPLETION则表示异步操作完成,而对于文件对象来说如果异步操作完成文件对象就会变为有信号状态。下面的例子是一个利用告警回调函数实现的文件异步读写。

VOID CALLBACK MyIORoutine(
  DWORD dwErrorCode,                // completion code
  DWORD dwNumberOfBytesTransfered,  // number of bytes transferred
  LPOVERLAPPED lpOverlapped         // I/O information buffer
)
{//定义一个简单的回调函数
	printf("文件读完成\n");
}

void DoSomething(void)
{
	printf("current time %d\n",GetTickCount());
	Sleep(2000);//假设耗时的操作需要两秒钟
}

//下面是使用异步读的示范代码,假设c:\temp\large_file.dat文件有130MB大小()
//一次性读入50MB字节,在读入的过程中进行一些其他操作
void ReadM(void)
{
	HANDLE hFile = CreateFile("c:\\temp\\large_file.dat",GENERIC_READ,0,
				NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED|FILE_ATTRIBUTE_NORMAL,NULL);
	if( INVALID_HANDLE_VALUE != hFile )
	{
		BYTE *pbRead = new BYTE[1024*1024*50];//50MB字节
		OVERLAPPED overlap;
		overlap.Offset = 0;
		overlap.OffsetHigh =0;
		overlap.hEvent = NULL; //使用告警函数时无需要使用事件
		
		DWORD dwBegin= GetTickCount();//记录开始时间
		printf("begin time %d\n",dwBegin);
		ReadFileEx(hFile,pbRead,1024*1024*50,&overlap,MyIORoutine);
		while(WAIT_IO_COMPLETION != SleepEx(1,TRUE) )//检测文件操作是否完成
		//while (WaitForSingleObjectEx(hFile,1,TRUE) != WAIT_OBJECT_0 ) 
		//在这里WaitForSingleObjectEx和SleepEx具有相同作用
		{
			DoSomething();//在文件读的执行过程中进行其他操作
		}
		printf("耗时 %d\n",GetTickCount()-dwBegin);
		//操作完成
		CloseHandle(hFile);
		delete pbRead;
	}
}

WriteFileEx的用法与ReadFileEx的用法是类似的。

下载利用ReadFileEx进行异步文件读的示范代码

在磁盘操作中磁盘写比读需要花更多的时间,并且大文件的异步写可以更加有效的提高CPU利用率。但是异步操作会给开发和调试带来一些麻烦,所以我建议除非在非常必要(性能要求非常高,文件非常大)的情况下才使用异步的磁盘读写。再提一点,对于磁盘文件的异步操作的方式同样可以用于上章所讲的命名管道,或者是串口的异步操作。

文件加锁时在打开文件后对文件的某个区域加锁,加锁后可以防止其他进程对该区域数据进行读取。相关的函数为:

BOOL LockFile(
  HANDLE hFile,                   // 文件句柄
  DWORD dwFileOffsetLow,          // 文件加锁开始位置低32位
  DWORD dwFileOffsetHigh,         // 文件加锁开始位置高32位
  DWORD nNumberOfBytesToLockLow,  // 区域长度低32位
  DWORD nNumberOfBytesToLockHigh  // 区域长度高32位
);
BOOL UnlockFile(
  HANDLE hFile,                    // 文件句柄
  DWORD dwFileOffsetLow,          // 文件解锁开始位置低32位
  DWORD dwFileOffsetHigh,         // 文件解锁开始位置高32位
  DWORD nNumberOfBytesToLockLow,  // 区域长度低32位
  DWORD nNumberOfBytesToLockHigh  // 区域长度高32位
);

在文件加锁和解锁上需要有对应关系,这种对应关系就是对A区域加锁后必须对A区域解锁后才可以对其他区域解锁,而且必须是一对一的关系,也就是说调用一次对A区域的加锁函数就必须调用一次对A区域的解锁函数,而不能对一个区域加锁后分次对该区域的不同部分解锁。

在MFC中对文件操作进行了封装,CFile中封装了各种文件操作。在CFile中常用的成员函数有以下这些:

CFile( LPCTSTR lpszFileName, UINT nOpenFlags ); //打开文件

virtual BOOL Open( LPCTSTR lpszFileName, UINT nOpenFlags, CFileException* pError = NULL ); //打开文件
uOpenFlags为打开文件时的参数,可以取的以下值的组合:
CFile::modeRead / CFile::modeReadWrite / CFile::modeWrite 读写模式
CFile::modeCreate 创建文件
CFile::shareDenyNone / CFile::shareDenyRead / CFile::shareDenyWrite 共享设置
CFile::typeText / CFile::typeBinary 以文本形式还时二进制形式打开文件

virtual void Close( ); //关闭文件

virtual UINT Read( void* lpBuf, UINT nCount ); //读文件

virtual void Write( const void* lpBuf, UINT nCount ); // 写文件

virtual LONG Seek( LONG lOff, UINT nFrom ); //设置文件指针

void SeekToBegin( );//移动文件指针到文件头

DWORD SeekToEnd( );//移动文件指针到文件尾

virtual void LockRange( DWORD dwPos, DWORD dwCount ); //锁定文件

virtual void UnlockRange( DWORD dwPos, DWORD dwCount ); //解锁文件

CStdioFile是CFile的派生类,主要是完成对文本文件的操作,它只有两个成员函数:

BOOL ReadString(CString& rString); //读入文件中一行

void WriteString( LPCTSTR lpsz );//将字符串作为一行写入文件

相关文章:

  • 洛谷P1725 琪露诺(单调队列+dp)
  • Linux wait_on_buffer函数研究
  • POJ - 2796 Feel Good(经典单调栈)
  • 基于Linux0.11源代码的操作系统内核典型处理过程分析1
  • POJ - 3494 Largest Submatrix of All 1’s(单调栈+降维)
  • 在批处理文件中实现按日期命名的目录迁移
  • HDU - 6806 Equal Sentences(dp)
  • UltraWinGrid自适应列宽/行高
  • HDU - 6812 Kindergarten Physics(分块/规律)
  • UltraGrid 卡片模式列自适应宽度
  • 2020牛客暑期多校第七场 B - Mask Allocation(思维)
  • 编程修改BIN等二进制文件
  • 2020牛客暑期多校第七场 H - Dividing(整除分块)
  • 2020牛客暑期多校第八场 K - Kabaleo Lite(贪心)
  • 什么是程序员正确的职场心态?
  • 【Redis学习笔记】2018-06-28 redis命令源码学习1
  • create-react-app项目添加less配置
  • echarts花样作死的坑
  • es6--symbol
  • express + mock 让前后台并行开发
  • java2019面试题北京
  • JS+CSS实现数字滚动
  • magento2项目上线注意事项
  • Mysql优化
  • SegmentFault 社区上线小程序开发频道,助力小程序开发者生态
  • Webpack4 学习笔记 - 01:webpack的安装和简单配置
  • 阿里云购买磁盘后挂载
  • 安卓应用性能调试和优化经验分享
  • 从地狱到天堂,Node 回调向 async/await 转变
  • 分布式熔断降级平台aegis
  • 每天10道Java面试题,跟我走,offer有!
  • 巧用 TypeScript (一)
  • 使用 QuickBI 搭建酷炫可视化分析
  • 网络应用优化——时延与带宽
  • 用quicker-worker.js轻松跑一个大数据遍历
  • 如何用纯 CSS 创作一个菱形 loader 动画
  • ​LeetCode解法汇总2696. 删除子串后的字符串最小长度
  • ​渐进式Web应用PWA的未来
  • #define
  • #HarmonyOS:Web组件的使用
  • #ifdef 的技巧用法
  • $(function(){})与(function($){....})(jQuery)的区别
  • (04)Hive的相关概念——order by 、sort by、distribute by 、cluster by
  • (ctrl.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MDd_DynamicDebug”不匹配值“
  • (poj1.3.2)1791(构造法模拟)
  • (附源码)spring boot建达集团公司平台 毕业设计 141538
  • (附源码)springboot美食分享系统 毕业设计 612231
  • (附源码)springboot人体健康检测微信小程序 毕业设计 012142
  • (附源码)ssm高校志愿者服务系统 毕业设计 011648
  • (原)本想说脏话,奈何已放下
  • *** 2003
  • ./mysql.server: 没有那个文件或目录_Linux下安装MySQL出现“ls: /var/lib/mysql/*.pid: 没有那个文件或目录”...
  • .net Stream篇(六)
  • .net使用excel的cells对象没有value方法——学习.net的Excel工作表问题
  • .NET中winform传递参数至Url并获得返回值或文件