本篇主要讲述动态库的开发、动态库的测试与调用、添加log日志记录的应用。
动态库原理浅析
1. 动态库是什么,它有什么作用? |
Windows系统平台上提供了一种完全不同的较有效的编程和运行环境,你可以将独立的程序模块创建为较小的DLL(Dynamic Linkable Library)文件,并可对它们单独编译和测试。在运行时,只有当EXE程序确实要调用这些DLL模块的情况下,系统才会将它们装载到内存空间中。这种方式不仅减少了EXE文件的大小和对内存空间的需求,而且使这些DLL模块可以同时被多个应用程序使用。详见文档《Win32环境下动态链接库(DLL)编程原理》。 |
2. 动态库的导出 |
动态库的导出有两种方法: (1) __declspec(dllexport)intMyFunction(int n); 这样就可以将动态库中的函数MyFunction()导出了。 (2) 需要一个Des文件,通过该文件来导出动态库。 (Master没细说这种方法,不过说了这两种方法都较常用) 说明:在后面的例程中将使用第一种导出方法,并且将__declspec(dllexport)int放到宏定义中去。 |
3. 与DLL模块建立链接(应用程序加载动态库) |
与DLL模块建立链接也有两种方法: (1) 隐式链接方式 程序员在建立一个DLL文件时,链接程序会自动生成一个与之对应的LIB导入文件。该文件包含了每一个DLL导出函数的符号名和可选的标识号,但是并不含有实际的代码。LIB文件作为DLL的替代文件被编译到应用程序项目中。具体操作方法会在后面提到。 (2) 显式链接方式(动态加载) 显式链接方式对于集成化的开发语言(例如VB)比较适合。 说明:在后面的例程中将使用第一种链接方法。 |
4. 应用程序(exe文件)怎样找到动态库? |
(Master)这对于不同的系统版本,其方式不一样,如在XP、win7、win8系统下都不一样。例如在XP系统下,应用程序是优先到C:\Windows\System32路径下去找,完了之后再到对应目录下去找。 |
动态库的调用
先不说如何开发动态库,先来感受一下动态库是如何被使用的。
Step1:复制所需文件到指定位置 |
这里需要三个文件,socketclient.dll、socketclient.lib、socketclientdll.h,将这三个文件复制到项目目录下,如下图所示。然后再将头文件socketclientdll.h添加到工程里。 |
|
dll和lib文件打不开,这里就只展示socketclientdll.h文件了。 |
/* 下面定义了一套socket客户端发送报文接受报文的api接口 请写出这套接口api的调用方法 */ #ifndef _INC_Demo01_H #define _INC_Demo01_H #ifdef __cplusplus extern "C" { #endif //------------------第一套api接口---Begin--------------------------------// //客户端初始化获取handle上下 int cltSocketInit(void **handle /*out*/); //客户端发报文 int cltSocketSend(void *handle /*in*/, unsigned char *buf /*in*/, int buflen /*in*/); //客户端收报文 int cltSocketRev(void *handle /*in*/, unsigned char *buf /*in*/, int *buflen /*in out*/); //客户端释放资源 int cltSocketDestory(void *handle/*in*/); //------------------第一套api接口---End-----------------------------------// //------------------第二套api接口---Begin--------------------------------// //客户端初始化 int cltSocketInit2(void **handle); //客户端发报文 int cltSocketSend2(void *handle, unsigned char *buf, int buflen); //客户端收报文 int cltSocketRev2(void *handle, unsigned char **buf, int *buflen); int cltSocketRev2_Free(unsigned char **buf); //客户端释放资源 int cltSocketDestory2(void **handle); //------------------第二套api接口---End--------------------------------// #ifdef __cplusplus } #endif #endif /* _INC_Demo01_H */ |
|
Step2:链接lib文件 |
右击工程名→properties,弹出如下对话框,
将socketclient.lib文件的文件名复制到图中所示位置,完了如下图。 |
|
|
Step3:新建源文件,编写调用动态库的代码。 |
main.c文件 |
#include "stdio.h" #include "string.h" #include "socketclientdll.h" int main() { int rv = 0; void *handle = NULL; unsigned char buf[100];//in int buflen = 100;//in unsigned char buf2[2048]; int buflen2 = 2048; strcpy(buf, "abcdef");//将字符串"abcdef"拷贝到buf中,包括。 buflen = strlen(buf); //C语言的数据,并不一定都是以结尾的字符串, //比如说对明文加密的密文。 //客户端初始化 rv = cltSocketInit(&handle);//显然是在被调函数cltSocketInit()中为handle分配内存。(千万不能将handle声明为二级指针,然后再传递给该函数。) if(rv != 0) { return rv; } //客户端发报文 rv = cltSocketSend(handle, buf, buflen);//debug时只能看到handle中存放的地址,看不到该地址所指内存空间的值,所以不确定handle所指字符是否以结尾。 if(rv != 0) { return rv; } //客户端收报文 rv = cltSocketRev(handle, buf2, &buflen2);//debug时看到buf2中只有abcdef六个字符,f后不是。所以handle所指空间的字符f后应该不是了。 if(rv != 0) { return rv; } printf("%s\n", buf2);//buf2并非以结尾,程序执行到这里时还没有崩溃就已经很给面子了。 //若真想正确打印接收到的报文,可以按照长度buflen2一个字符一个字符地打印。 cltSocketDestory(handle); printf("hello...\n"); getchar(); return 0; } |
上面的例程是已经定义了一套socket客户端发送报文、接受报文的api接口,并且发送、接收等函数都已经写好了,封装在库文件中(lib文件),我们只需调用即可。那么这些动态库库究竟是如何生成的呢?这些库文件(这里其实就是dll和lib文件)里面装的是什么呢?这在后面会详细讲到。 |
前面一节是讲述如何使用已经创建好的动态库,这一节则是讲述如何开发动态库。这里就以实现前一节的动态库功能为例来讲述实际工程开发中是如何实现动态库的创建与开发的。
动态库的建立
Step1:新建工程 |
第一步和之前的新建工程一样,如下图所示。 接下来的一步和之前略有不同,如下图所示。 |
Step2:添加头文件itcast_comm.h。头文件内容如下。 |
#ifndef _SOCEKETCLIET_H_ #define _SOCEKETCLIET_H_ #ifdef __cplusplus extern "C" { #endif #define _ITCAST_OS_WIN32_ #define _ITCAST_OS_LINUX_ #ifdef _ITCAST_OS_WIN32_ #define ITCAST_FUNC_EXPORT(returnType) __declspec(dllexport) returnType #else #ifdef _ITCAST_OS_LINUX #define ITCAST_FUNC_EXPORT(returnType) __attribute__ ((visibility("default"))) returnType #else #define ITCAST_FUNC_EXPORT(returnType) returnType #endif #endif #ifdef __cplusplus } #endif #endif |
Step3:新建源文件MySocketLib.c |
接下来就是在要在文件MySocketLib.c里编辑自己的库函数了。我们就以master给的第一套api接口为例来编写这些函数的具体源代码。 这里先打桩,然后再编写个动态库测试程序来看看有木有成功,若成功了则继续往下编写动态库函数。 |
#include "stdio.h" #include "stdlib.h" #include "string.h" #include "itcast_comm.h" //------------------第一套api接口---Begin--------------------------------// //客户端初始化 int cltSocketInit(void **handle /*out*/) { printf("func cltSocketInit() begin....\n");//这里先随便输出个东西,然后再测试动态库有没有创建成功,若成功了然后再继续往下编写动态库。 printf("func cltSocketInit() end....\n"); return 0; } //客户端发报文 int cltSocketSend(void *handle /*in*/, unsigned char *buf /*in*/, int buflen /*in*/) { return 0; } //客户端收报文 int cltSocketRev(void *handle /*in*/, unsigned char *buf /*in*/, int *buflen /*in out*/) { return 0; } //客户端释放资源 int cltSocketDestory(void *handle/*in*/) { return 0; } //------------------第一套api接口---End-----------------------------------// |
一般的函数是像上述这样定义的,可是动态库开发不一样,这里需要将各个函数的返回类型用itcast_comm.h头文件中的宏定义ITCAST_FUNC_EXPORT(returnType)来替换,完了如下所示。 |
#include "stdio.h" #include "stdlib.h" #include "string.h" #include "itcast_comm.h" //------------------第一套api接口---Begin--------------------------------// //客户端初始化 ITCAST_FUNC_EXPORT(int) cltSocketInit(void **handle /*out*/) { printf("func cltSocketInit() begin....\n");//这里先随便输出个东西,然后再测试动态库有没有创建成功,若成功了然后再继续往下编写动态库。 printf("func cltSocketInit() end....\n"); return 0; } //客户端发报文 ITCAST_FUNC_EXPORT(int) cltSocketSend(void *handle /*in*/, unsigned char *buf /*in*/, int buflen /*in*/) { return 0; } //客户端收报文 ITCAST_FUNC_EXPORT(int) cltSocketRev(void *handle /*in*/, unsigned char *buf /*in*/, int *buflen /*in out*/) { return 0; } //客户端释放资源 ITCAST_FUNC_EXPORT(int) cltSocketDestory(void *handle/*in*/) { return 0; } //------------------第一套api接口---End-----------------------------------// |
|
Step4:编译生成动态库文件,如下所示。 |
动态库测试程序的建立
类似动态库调用那一节,需要添加lib文件到测试工程的链接器。
Step1:新建测试工程 |
新建一个工程,这个工程是用来测试刚刚开发的动态库的。注意这时候就是新建一个普通的工程,与新建动态库开发环境有所不同,不同点如下图所示。 |
Step2:复制所需文件到指定位置 |
如“同动态库的调用”一节所述,这里也需要三个文件。 将在动态库开发中生成的两个文件MySocketLib.dll、MySocketLib.lib和socketclientdll.h共三个文件复制到指定位置,如下图所示。然后再将头文件socketclientdll.h添加到工程里。 |
Step3:链接lib文件 |
同样是右击工程名→properties,将MySocketLib.lib文件的文件名复制到下图所示位置。 |
Step4:新建源文件,编写调用动态库的代码。 |
#include "stdio.h" #include "stdlib.h" #include "string.h" #include "socketclientdll.h" int main() { int rv = 0; void *handle = NULL; rv = cltSocketInit(&handle); if(rv != 0) { printf("func cltSocketInit():%d.\n",rv); return rv; } printf("hello.\n"); system("pause"); return 0; } |
|
Step5:编译调试 |
在“rv = cltSocketInit(&handle);”处设置断点,如果能够成功进入该函数(也就是进入了动态库),并成功执行该函数里面的代码,则说明动态库调用成功了。 |
编写动态库函数
当测试程序成功测试了动态库的调用后,就说明动态库已经创建成功了。接下来就是要继续完善动态库函数了。
最后动态库的源代码如下。 |
#include "stdio.h" #include "stdlib.h" #include "string.h" #include "itcast_comm.h" //句柄只不过是一套api运行时的资源环境,说白了它就是内存首地址。 typedef struct _SocketHandle { char ip[128]; int port; unsigned char *buf; int buflen; }SocketHandle; //------------------第一套api接口---Begin--------------------------------// //客户端初始化 ITCAST_FUNC_EXPORT(int) cltSocketInit(void **handle /*out*/) { int ret = 0; SocketHandle *sh = NULL; //printf("func cltSocketInit() begin....\n"); sh = (SocketHandle *)malloc(sizeof(SocketHandle)); if(sh == NULL) { ret = -1; return ret; } memset(sh, 0, sizeof(SocketHandle)); strcpy(sh->ip, "192.168.1.254");//随便写得一个IP地址,演示而已。 sh->port = 0x8;//随便写得一个数字。 printf("func cltSocketInit() end....\n"); *handle = sh; return ret; } //客户端发报文 ITCAST_FUNC_EXPORT(int) cltSocketSend(void *handle /*in*/, unsigned char *buf /*in*/, int buflen /*in*/) { int ret = 0; SocketHandle *sh = NULL; if(handle==NULL || buf==NULL || buflen>2048*10) { ret = -1; return ret; } sh = (SocketHandle *)handle; sh->buf = (char *)malloc(buflen); if(sh->buf == NULL) { ret = -2; return ret; } memcpy(sh->buf, buf, buflen); sh->buflen = buflen; return ret; } //客户端收报文 ITCAST_FUNC_EXPORT(int) cltSocketRev(void *handle /*in*/, unsigned char *buf /*in*/, int *buflen /*in out*/) { int ret = 0; SocketHandle *sh = NULL; if(handle==NULL || buf==NULL || buflen==NULL) { ret = -1; return ret; } sh = (SocketHandle *)handle; memcpy(buf, sh->buf, sh->buflen); *buflen = sh->buflen; return ret; } //客户端释放资源 ITCAST_FUNC_EXPORT(int) cltSocketDestory(void *handle/*in*/) { int ret = 0; SocketHandle *sh = NULL; if(handle==NULL) { ret = -1; return ret; } sh = (SocketHandle *)handle; if(sh->buf != NULL) { free(sh->buf); sh->buf = NULL; } if(sh != NULL) { free(sh); sh = NULL; } return ret; } //------------------第一套api接口---End-----------------------------------// |
编译该动态库工程即可更新在对应路径下的动态库文件MySocketLib.dll和MySocketLib.lib。 |
动态库文件弄好之后,就要测试动态库中的函数是否可用,那么就要将新生成的两个库文件拷贝到之前的测试程序路径下,以替换之前的库文件。具体方法在前面已经说过了。 |
将动态库文件拷贝后,就要编写动态库测试程序了,以便于测试这些动态库函数是否可用,测试程序如下所示。 |
#include "stdio.h" #include "stdlib.h" #include "string.h" #include "socketclientdll.h" //句柄只不过是一套api运行时的资源环境,说白了它就是内存首地址。 int main() { int rv = 0; void *handle = NULL; unsigned char buf[2048]; int buflen = 0; unsigned char buf2[2048]; int buflen2 = 0; strcpy(buf, "abcdefg123456789"); buflen = 10;//buf中可以有个字符,但是需要发送多少个是由buflen来决定滴。 rv = cltSocketInit(&handle);//为handle分配内存,初始化handle。 if(rv != 0) { printf("func cltSocketInit():%d.\n",rv); return rv; } rv = cltSocketSend(handle, buf, buflen);//从buf中拷贝buflen个字符到handle中的buf。 if(rv != 0) { printf("func cltSocketSend():%d.\n",rv); return rv; } rv = cltSocketRev(handle, buf2, &buflen2);//从handle中的buf拷贝buflen2个字符到buf2中。 if(rv != 0) { printf("func cltSocketRev():%d.\n",rv); return rv; } rv = cltSocketDestory(handle); if(rv != 0) { printf("func cltSocketDestory():%d.\n",rv); return 0; } printf("hello.\n"); system("pause"); return 0; } |
编译该测试工程后,就可以调试测试动态库函数是否编写成功了,如果没有则还需要到动态库开发环境中去修改库函数,然后将新生成的库文件拷贝到测试程序路径下再次测试,这样直到测试成功为止。 |
添加日志记录
工程开发中,常常需要将程序的执行状态记录下来保存到一个日志文件中去,以便于调试。
这里需要两个文件:itcastlog.h和itcastlog.c,哪里需要用到日志记录,就将这个两个文件复制到哪里,比如上一节中的动态库想要用日志来记录程序执行状态,就将这两个文件复制到动态库程序目录下,然后将头文件itcastlog.h添加到工程中。 若在MySocketLib.c文件中想要用日志记录,那么还要在MySocketLib.c中包含头文件itcastlog.h。 |
|
itcastlog.h文件如下 |
//written by [email protected] //20140323 //itcastlog.h 日志头文件 #ifndef _ITCAST_LOG_H_ #define _ITCAST_LOG_H_ /* #define IC_NO_LOG_LEVEL 0 #define IC_DEBUG_LEVEL 1 #define IC_INFO_LEVEL 2 #define IC_WARNING_LEVEL 3 #define IC_ERROR_LEVEL 4; */ /************************************************************************/ /* const char *file:文件名称 int line:文件行号 int level:错误级别 0 -- 没有日志 1 -- debug级别 2 -- info级别 3 -- warning级别 4 -- err级别 int status:错误码 const char *fmt:可变参数 */ /************************************************************************/ //实际使用的Level extern int LogLevel[5]; void ITCAST_LOG(const char *file, int line, int level, int status, const char *fmt, ...); #endif |
|
itcastlog.c |
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdarg.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <time.h> #include "itcastLog.h" #define ITCAST_DEBUG_FILE_ "socketclient.log" #define ITCAST_MAX_STRING_LEN 10240 //Level类别 #define IC_NO_LOG_LEVEL 0 #define IC_DEBUG_LEVEL 1 #define IC_INFO_LEVEL 2 #define IC_WARNING_LEVEL 3 #define IC_ERROR_LEVEL 4 int LogLevel[5] = {IC_NO_LOG_LEVEL, IC_DEBUG_LEVEL, IC_INFO_LEVEL, IC_WARNING_LEVEL, IC_ERROR_LEVEL}; //Level的名称 char ICLevelName[5][10] = {"NOLOG", "DEBUG", "INFO", "WARNING", "ERROR"}; static int ITCAST_Error_GetCurTime(char* strTime) { struct tm* tmTime = NULL; size_t timeLen = 0; time_t tTime = 0; tTime = time(NULL); tmTime = localtime(&tTime); //timeLen = strftime(strTime, 33, "%Y(Y)%m(M)%d(D)%H(H)%M(M)%S(S)", tmTime); timeLen = strftime(strTime, 33, "%Y.%m.%d %H:%M:%S", tmTime); return timeLen; } static int ITCAST_Error_OpenFile(int* pf) { char fileName[1024]; memset(fileName, 0, sizeof(fileName)); #ifdef WIN32 sprintf(fileName, "c:\\itcast\\%s",ITCAST_DEBUG_FILE_); #else sprintf(fileName, "%s/log/%s", getenv("HOME"), ITCAST_DEBUG_FILE_); #endif *pf = open(fileName, O_WRONLY|O_CREAT|O_APPEND, 0666); if(*pf < 0) { return -1; } return 0; } static void ITCAST_Error_Core(const char *file, int line, int level, int status, const char *fmt, va_list args) { char str[ITCAST_MAX_STRING_LEN]; int strLen = 0; char tmpStr[64]; int tmpStrLen = 0; int pf = 0; //初始化 memset(str, 0, ITCAST_MAX_STRING_LEN); memset(tmpStr, 0, 64); //加入LOG时间 tmpStrLen = ITCAST_Error_GetCurTime(tmpStr); tmpStrLen = sprintf(str, "[%s] ", tmpStr); strLen = tmpStrLen; //加入LOG等级 tmpStrLen = sprintf(str+strLen, "[%s] ", ICLevelName[level]); strLen += tmpStrLen; //加入LOG状态 if (status != 0) { tmpStrLen = sprintf(str+strLen, "[ERRNO is %d] ", status); } else { tmpStrLen = sprintf(str+strLen, "[SUCCESS] "); } strLen += tmpStrLen; //加入LOG信息 tmpStrLen = vsprintf(str+strLen, fmt, args); strLen += tmpStrLen; //加入LOG发生文件 tmpStrLen = sprintf(str+strLen, " [%s]", file); strLen += tmpStrLen; //加入LOG发生行数 tmpStrLen = sprintf(str+strLen, " [%d]\n", line); strLen += tmpStrLen; //打开LOG文件 if(ITCAST_Error_OpenFile(&pf)) { return ; } //写入LOG文件 write(pf, str, strLen); //IC_Log_Error_WriteFile(str); //关闭文件 close(pf); return ; } void ITCAST_LOG(const char *file, int line, int level, int status, const char *fmt, ...) { va_list args; //判断是否需要写LOG // if(level!=IC_DEBUG_LEVEL && level!=IC_INFO_LEVEL && level!=IC_WARNING_LEVEL && level!=IC_ERROR_LEVEL) if(level == IC_NO_LOG_LEVEL) { return ; } //调用核心的写LOG函数 va_start(args, fmt); ITCAST_Error_Core(file, line, level, status, fmt, args); va_end(args); return ; } |
|
那么再实践中是如何使用的呢?其实很简单,作为示例,下面就展示一下cltSocketInit()函数是如何调用日志显示的,如下所示。 |
//客户端初始化 ITCAST_FUNC_EXPORT(int) cltSocketInit(void **handle /*out*/) { int ret = 0; SocketHandle *sh = NULL; //printf("func cltSocketInit() begin....\n");也可以将这一句也改为用log日志记录的方式,如下所示。 ITCAST_LOG(__FILE__, __LINE__, LogLevel[2], ret, "func cltSocketInit() begin....\n"); sh = (SocketHandle *)malloc(sizeof(SocketHandle)); if(sh == NULL) { ret = -1; ITCAST_LOG(__FILE__, __LINE__, LogLevel[4], ret, "(SocketHandle *)malloc(sizeof(SocketHandle))"); return ret; } memset(sh, 0, sizeof(SocketHandle)); strcpy(sh->ip, "192.168.1.254");//随便写得一个IP地址,演示而已。 sh->port = 0x8;//随便写得一个数字。 //printf("func cltSocketInit() end....\n"); ITCAST_LOG(__FILE__, __LINE__, LogLevel[2], ret, "func cltSocketInit() end....\n"); *handle = sh; return ret; } 执行完程序后,看C:\itcast目录下的log文件,如下图所示。 |
生成的日志是被放在了C盘根目录下的itcast文件夹里,这可以由itcastlog.c文件中的ITCAST_Error_OpenFile()函数看出来。注意是要先在C盘根目录下新建一个文件名为itcast的文件夹。 |
第二套API函数实现动态库开发
前面讲述动态库的开发是基于第一套API函数接口来实现的,接下来要用第二套API函数来实现。
其实基本原理和第一套API函数实现是一样的,只不过第二套API的部分函数的接口不一样,所以导致了函数内容有些变化。如下所示。 |
#include "stdio.h" #include "stdlib.h" #include "string.h" #include "itcast_comm.h" #include "itcastlog.h" typedef struct _SocketHandle { char ip[128]; int port; unsigned char *buf; int buflen; }SocketHandle; //------------------第二套api接口---Begin--------------------------------// //客户端初始化 ITCAST_FUNC_EXPORT(int) cltSocketInit2(void **handle) { //同第一套API函数 } //客户端发报文 ITCAST_FUNC_EXPORT(int) cltSocketSend2(void *handle, unsigned char *buf, int buflen) { //同第一套API函数 } //客户端收报文 ITCAST_FUNC_EXPORT(int) cltSocketRev2(void *handle, unsigned char **buf, int *buflen) { int ret = 0; SocketHandle *sh = NULL; char *tmp = NULL; if(handle==NULL || buf==NULL || buflen==NULL) { ret = -1; ITCAST_LOG(__FILE__, __LINE__, LogLevel[4], ret, "cltSocketRev2():(handle==NULL || buf==NULL || buflen==NULL)"); return ret; } sh = (SocketHandle *)handle; tmp = (char *)malloc(sh->buflen); if(tmp == NULL) { ret = -1; ITCAST_LOG(__FILE__, __LINE__, LogLevel[4], ret, "tmp = (char *)malloc(sh->buflen)"); return ret; } *buf = tmp; *buflen = sh->buflen; return ret; } ITCAST_FUNC_EXPORT(int) cltSocketRev2_Free(unsigned char **buf) { int ret = 0; unsigned char *tmp = NULL; if(buf == NULL) { ret = -1; ITCAST_LOG(__FILE__, __LINE__, LogLevel[4], ret, "cltSocketRev2_Free():(buf == NULL)"); return ret; } tmp = *buf; if(tmp != NULL) { free(tmp); tmp = NULL; } *buf = NULL; return ret; } //客户端释放资源 ITCAST_FUNC_EXPORT(int) cltSocketDestory2(void **handle) { int ret = 0; SocketHandle *sh = NULL; if(handle==NULL) { ret = -1; return ret; } sh = (SocketHandle *)*handle; //强制类型转换与取内容运算符优先级相同,但这里遵循从又向左的优先级。 if(sh->buf != NULL) { free(sh->buf); sh->buf = NULL; } if(sh != NULL) { free(sh); sh = NULL; } *handle = NULL; return ret; } //------------------第二套api接口---End--------------------------------// |