从DLL中导出类(4)

前面介绍了怎么从DLL中导出函数和变量,实际上导出类的方法也是大同小异,废话就不多说了,下面给个简单例子示范一下,也就不多做解释了。

DLL头文件:

#ifndef _DLL_SAMPLE_H
#define _DLL_SAMPLE_H

// 通过宏来控制是导入还是导出
#ifdef _DLL_SAMPLE
#define DLL_SAMPLE_API __declspec(dllexport)
#else
#define DLL_SAMPLE_API __declspec(dllimport)
#endif

// 导出/导入变量声明
DLL_SAMPLE_API class DLLClass
{
  public:
    void Show();
};

#undef DLL_SAMPLE_API

#endif

DLL实现文件:

#include “stdafx.h”
#define _DLL_SAMPLE

#ifndef _DLL_SAMPLE_H
#include “DLLSample.h”
#endif

#include “stdio.h”

//APIENTRY声明DLL函数入口点
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
 switch (ul_reason_for_call)
 {
  case DLL_PROCESS_ATTACH:
  case DLL_THREAD_ATTACH:
  case DLL_THREAD_DETACH:
  case DLL_PROCESS_DETACH:
   break;
 }
 return TRUE;
}

void DLLClass::Show()
{
  printf(“DLLClass show!”);
}

应用程序调用DLL

#include “DLLSample.h”

#pragma comment(lib,”DLLSample.lib”)


int main(int argc, char *argv[])
{
 DLLClass dc;
  dc.Show();
 return 0;
}

大家可能发现了,上面我没有使用模块定义文件(.def)声明导出类也没有用显式链接导入DLL。
用Depends查看前面编译出来的DLL文件,会发现里面导出了很奇怪的symbol,这是因为C++编译器在编译时会对symbol进行修饰。
这是我从别人那儿转来的截图。

从DLL中导出类(4)文章图片

网上找了下,发现了C++编译时函数名的修饰约定规则

__stdcall调用约定:

1、以”?”标识函数名的开始,后跟函数名;
2、函数名后面以”@@YG”标识参数表的开始,后跟参数表;
3、参数表以代号表示:

X——void,
D——char,
E——unsigned char,
F——short,
H——int,
I——unsigned int,
J——long,
K——unsigned long,
M——float,
N——double,
_N——bool,
….

PA——表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以”0″代替,一个”0″代表一次重复;
4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
5、参数表后以”@Z”标识整个名字的结束,如果该函数无参数,则以”Z”标识结束。
其格式为”?functionname@@YG*****@Z”或?functionname@@YG*XZ

__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的”@@YG”变为”@@YA”。

__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的”@@YG”变为”@@YI”。

VC++对函数的省缺声明是”__cedcl”,将只能被C/C++调用。

虽然因为C++编译器对symbol进行修饰的原因不能直接用def文件声明导出类和显式链接,但是可以用另外一种取巧的方式。

在头文件中类的声明中添加一个友元函数:
friend DLLClass* CreatDLLClass();
然后声明CreatDLLClass()为导出函数,通过调用该函数返回一个DLLClass类的对象,同样达到了导出类的目的。
这样,就可以用显式链接来调用CreatDLLClass(),从而得到类对象了。

从DLL中导出变量(3)

前面介绍了怎么从DLL中导出函数,下面我们来看一下如何从DLL中导出变量来。

声明为导出变量时,同样有两种方法:
第一种是用__declspec进行导出声明

#ifndef _DLL_SAMPLE_H
#define _DLL_SAMPLE_H

// 如果定义了C++编译器,那么声明为C链接方式
#ifdef __cplusplus
extern “C” {
#endif

// 通过宏来控制是导入还是导出
#ifdef _DLL_SAMPLE
#define DLL_SAMPLE_API __declspec(dllexport)
#else
#define DLL_SAMPLE_API __declspec(dllimport)
#endif

// 导出/导入变量声明
DLL_SAMPLE_API extern int DLLData;

#undef DLL_SAMPLE_API

#ifdef __cplusplus
}
#endif

#endif

 

第二种是用模块定义文件(.def)进行导出声明

LIBRARY DLLSample
DESCRIPTION “my simple DLL”
EXPORTS
        DLLData DATA  ;DATA表示这是数据(变量)

 

下面是DLL的实现文件

#include “stdafx.h”
#define _DLL_SAMPLE

#ifndef _DLL_SAMPLE_H
#include “DLLSample.h”
#endif

#include “stdio.h”

int DLLData;

//APIENTRY声明DLL函数入口点
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
 switch (ul_reason_for_call)
 {
  case DLL_PROCESS_ATTACH:
      DLLData = 123;  // 在入口函数中对变量进行初始化
      break
  case DLL_THREAD_ATTACH:
  case DLL_THREAD_DETACH:
  case DLL_PROCESS_DETACH:
   break;
 }
 return TRUE;
}

同样,应用程序调用DLL中的变量也有两种方法。
第一种是隐式链接:

#include <stdio.h>
#include “DLLSample.h”

#pragma comment(lib,”DLLSample.lib”)


int main(int argc, char *argv[])
{
 printf(“%d “, DLLSample);
 return 0;
}

第二种是显式链接:

#include <iostream>
#include <windows.h>

int main()
{
        int my_int;
        HINSTANCE hInstLibrary = LoadLibrary(“DLLSample.dll”);

        if (hInstLibrary == NULL)
        {
         FreeLibrary(hInstLibrary);
        }
        my_int = *(int*)GetProcAddress(hInstLibrary, “DLLData”);
        if (dllFunc == NULL)
        {
         FreeLibrary(hInstLibrary);
        }
        std::cout<<my_int;
        std::cin.get();
        FreeLibrary(hInstLibrary);
        return(1);
}

 

通过GetProcAddress取出的函数或者变量都是地址,因此,需要解引用并且转类型。

如何使用DLL(2)

上文我简单的介绍了如何建立一个简单DLL,下面再我简单的介绍一下如何使用一个DLL。当一个DLL被生成后,它创建了一个.dll文件和一个.lib文件;这两个都是你需要的。要使用DLL,就需要载入这个DLL。

隐式链接

这里有两个方法来载入一个DLL;一个方法是捷径另一个则相比要复杂些。捷径是只链接到你.lib 文件并将.dll文件置入你的新项目的路径中去。因此,创建一个新的空的Win32控制台项目并添加一个源文件。将你做的DLL放入你的新项目相同的目录下。

#include “stdafx.h”
#include “DLLSample.h”

#pragma comment(lib, “DLLSample.lib”) //你也可以在项目属性中设置库的链接

int main()
{
TestDLL(123);
return(1);
}

这就是载入一个DLL的简单方法。

显式链接

难点的加载DLL的方法稍微有点复杂。你将需要函数指针和一些Windows函数。但是,通过这种载入DLLs的方法,你不需要DLL的.lib或头文件,而只需要DLL。

#include <iostream>
#include <windows.h>
typedef void (*DLLFunc)(int);
int main()
{
DLLFunc dllFunc;
HINSTANCE hInstLibrary = LoadLibrary(“DLLSample.dll”);

if (hInstLibrary == NULL)
{
FreeLibrary(hInstLibrary);
}
dllFunc = (DLLFunc)GetProcAddress(hInstLibrary, “TestDLL”);
if (dllFunc == NULL)
{
FreeLibrary(hInstLibrary);
}
dllFunc(123);
std::cin.get();
FreeLibrary(hInstLibrary);
return(1);
}
首先你会注意到:这里包括进了文件“windows.h”同时移走了“DLLSample.h”。原因很简单:因为windows.h包含了一些Windows函数,当然你现在将只需要其中几个而已。它也包含了一些将会用到的Windows特定变量。你可以去掉DLL的头文件(DLLSample.h)因为-如我前面所说-当你使用这个方法载入DLL时你并不需要它。

下面你会看到:下面的一句代码:

typedef void (*DLLFunc)(int);

这是一个函数指针类型的定义。指向一个函数是一个int型的参数,返回值为void类型。

一个HINSTANCE是一个Windows数据类型:是一个实例的句柄;在此情况下,这个实例将是这个DLL。你可以通过使用函数LoadLibrary()获得DLL的实例,它获得一个名称作为参数。在调用LoadLibrary函数后,你必需查看一下函数返回是否成功。你可以通过检查HINSTANCE是否等于NULL(在Windows.h中定义为0或Windows.h包含的一个头文件)来查看其是否成功。如果其等于NULL,该句柄将是无效的,并且你必需释放这个库。换句话说,你必需释放DLL获得的内存。如果函数返回成功,你的HINSTANCE就包含了指向DLL的句柄。

一旦你获得了指向DLL的句柄,你现在可以从DLL中重新获得函数。为了这样做,你必须使用函数GetProcAddress(),它将DLL的句柄(你可以使用HINSTANCE)和函数的名称作为参数。你可以让函数指针获得由GetProcAddress()返回的值,同时你必需将GetProcAddress()转换为那个函数定义的函数指针。举个例子,对于Add()函数,你必需将GetProcAddress()转换为AddFunc;这就是它知道参数及返回值的原因。现在,最好先确定函数指针是否等于NULL以及它们拥有DLL的函数。这只是一个简单的if语句;如果其中一个等于NULL,你必需如前所述释放库。

一旦函数指针拥有DLL的函数,你现在就可以使用它们了,但是这里有一个需要注意的地方:你不能使用函数的实际名称;你必需使用函数指针来调用它们。在那以后,所有你需要做的是释放库如此而已。

模块句柄

进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识。进程自己还有一个HINSTANCE句柄。所有这些模块句柄都只有在特定的进程内部有效,它们代表了DLL或EXE模块在进程虚拟空间中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这个两种类型可以替换使用。进程模块句柄几乎总是等于0x400000,而DLL模块的加载地址的缺省句柄是0x10000000。如果程序同时使用了几个DLL模块,每一个都会有不同的HINSTANCE值。这是因为在创建DLL文件时指定了不同的基地址,或者是因为加载程序对DLL代码进行了重定位。
模块句柄对于加载资源特别重要。Win32 的FindResource函数中带有一个HINSTANCE参数。EXE和DLL都有其自己的资源。如果应用程序需要来自于DLL的资源,就将此参数指定为DLL的模块句柄。如果需要EXE文件中包含的资源,就指定EXE的模块句柄。
但是在使用这些句柄之前存在一个问题,你怎样得到它们呢?如果需要得到EXE模块句柄,调用带有Null参数的Win32函数GetModuleHandle;如果需要DLL模块句柄,就调用以DLL文件名为参数的Win32函数GetModuleHandle。

应用程序怎样找到DLL文件

如果应用程序使用LoadLibrary显式链接,那么在这个函数的参数中可以指定DLL文件的完整路径。如果不指定路径,或是进行隐式链接,Windows将遵循下面的搜索顺序来定位DLL:
1. 包含EXE文件的目录,
2. 进程的当前工作目录,
3. Windows系统目录,
4. Windows目录,
5. 列在Path环境变量中的一系列目录。
这里有一个很容易发生错误的陷阱。如果你使用VC++进行项目开发,并且为DLL模块专门创建了一个项目,然后将生成的DLL文件拷贝到系统目录下,从应用程序中调用DLL模块。到目前为止,一切正常。接下来对DLL模块做了一些修改后重新生成了新的DLL文件,但你忘记将新的DLL文件拷贝到系统目录下。下一次当你运行应用程序时,它仍加载了老版本的DLL文件,这可要当心!

调试DLL程序

Microsoft 的VC++是开发和测试DLL的有效工具,只需从DLL项目中运行调试程序即可。当你第一次这样操作时,调试程序会向你询问EXE文件的路径。此后每次在调试程序中运行DLL时,调试程序会自动加载该EXE文件。然后该EXE文件用上面的搜索序列发现DLL文件,这意味着你必须设置Path环境变量让其包含DLL文件的磁盘路径,或者也可以将DLL文件拷贝到搜索序列中的目录路径下。
或者当你调试EXE程序时,在Project Setting中,将Debug选项卡中的Category设置为Additional DLLs。就可以同时调试EXE和它调用的DLL(当然,你需要有DLL的源代码)了。

如何建立DLL(1)

初学DLL,结合教程,总结一下自己的所得,希望对DLL初学者们有所帮助。

动态链接库(DLL)是从C语言函数库和Pascal库单元的概念发展而来的。所有的C语言标准库函数都存放在某一函数库中。在链接应用程序的过程中,链接器从库文件中拷贝程序调用的函数代码,并把这些函数代码添加到可执行文件中。这种方法同只把函数储存在已编译的OBJ文件中相比更有利于代码的重用。但随着Windows这样的多任务环境的出现,函数库的方法显得过于累赘。如果为了完成屏幕输出、消息处理、内存管理、对话框等操作,每个程序都不得不拥有自己的函数,那么Windows程序将变得非常庞大。Windows的发展要求允许同时运行的几个程序共享一组函数的单一拷贝。动态链接库就是在这种情况下出现的。动态链接库不用重复编译或链接,一旦装入内存,DLL函数可以被系统中的任何正在运行的应用程序软件所使用,而不必再将DLL函数的另一拷贝装入内存。

下面我们一步一步来建立一个DLL。

一、建立一个DLL工程
新建一个工程,选择Win32 控制台项目(Win32 Console Application),并且在应用程序设置标签(the advanced tab)上,选择DLL和空项目选项。

二、声明导出函数
这里有两种方法声明导出函数:一种是通过使用__declspec(dllexport),添加到需要导出的函数前,进行声明;另外一种就是通过模块定义文件(Module-Definition File即.DEF)来进行声明。
第一种方法,建立头文件DLLSample.h,在头文件中,对需要导出的函数进行声明。

#ifndef _DLL_SAMPLE_H
#define _DLL_SAMPLE_H

// 如果定义了C++编译器,那么声明为C链接方式
#ifdef __cplusplus
extern “C” {
#endif

// 通过宏来控制是导入还是导出
#ifdef _DLL_SAMPLE
#define DLL_SAMPLE_API __declspec(dllexport)
#else
#define DLL_SAMPLE_API __declspec(dllimport)
#endif

// 导出/导入函数声明
DLL_SAMPLE_API void TestDLL(int);

#undef DLL_SAMPLE_API

#ifdef __cplusplus
}
#endif

#endif

这个头文件会分别被DLL和调用DLL的应用程序引入,当被DLL引入时,在DLL中定义_DLL_SAMPLE宏,这样就会在DLL模块中声明函数为导出函数;当被调用DLL的应用程序引入时,就没有定义_DLL_SAMPLE,这样就会声明头文件中的函数为从DLL中的导入函数。

第二种方法:模块定义文件是一个有着.def文件扩展名的文本文件。它被用于导出一个DLL的函数,和__declspec(dllexport)很相似,但是.def文件并不是Microsoft定义的。一个.def文件中只有两个必需的部分:LIBRARY 和 EXPORTS。

LIBRARY DLLSample
DESCRIPTION “my simple DLL”
EXPORTS
        TestDLL @1  ;@1表示这是第一个导出函数

第一行,”LIBRARY”是一个必需的部分。它告诉链接器(linker)如何命名你的DLL。下面被标识为”DESCRIPTION”的部分并不是必需的。该语句将字符串写入 .rdata 节,它告诉人们谁可能使用这个DLL,这个DLL做什么或它为了什么(存在)。再下面的部分标识为”EXPORTS”是另一个必需的部分;这个部分使得该函数可以被其它应用程序访问到并且它创建一个导入库。当你生成这个项目时,不仅是一个.dll文件被创建,而且一个文件扩展名为.lib的导出库也被创建了。除了前面的部分以外,这里还有其它四个部分标识为:NAME, STACKSIZE, SECTIONS, 和 VERSION。另外,一个分号(;)开始一个注解,如同”//”在C++中一样。定义了这个文件之后,头文件中的__declspec(dllexport)就不需要声明了。

三、编写DllMain函数和导出函数
DllMain函数是DLL模块的默认入口点。当Windows加载DLL模块时调用这一函数。系统首先调用全局对象的构造函数,然后调用全局函数DLLMain。DLLMain函数不仅在将DLL链接加载到进程时被调用,在DLL模块与进程分离时(以及其它时候)也被调用。

#include “stdafx.h”
#define _DLL_SAMPLE

#ifndef _DLL_SAMPLE_H
#include “DLLSample.h”
#endif

#include “stdio.h”

//APIENTRY声明DLL函数入口点
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
 switch (ul_reason_for_call)
 {
  case DLL_PROCESS_ATTACH:
  case DLL_THREAD_ATTACH:
  case DLL_THREAD_DETACH:
  case DLL_PROCESS_DETACH:
   break;
 }
 return TRUE;
}

void TestDLL(int arg)
{
  printf(“DLL output arg %d\n”, arg);
}

如果程序员没有为DLL模块编写一个DLLMain函数,系统会从其它运行库中引入一个不做任何操作的缺省DLLMain函数版本。在单个线程启动和终止时,DLLMain函数也被调用。
然后,F7编译,就得到一个DLL了。

合并dll到exe中

前言
  你可能不希望在发布程序时附带上一个外部的 DLL,因为可能会有些用户在无意中把 DLL 删除了而造成 EXE 不能正确运行,也有可能该 DLL 会被别人拿去使用,也有可能,此 DLL 会成为破解者破解你的程序的突破口。无论出于何种原因,如果你想把一个 DLL 合并到一个 EXE 中的话,本文向你介绍这种方法。
Win32 程序调用 DLL 的机制

   Win32 EXE 在调用一个外部 DLL 中的函数时,首先要调用 LoadLibary 函数来载入此 DLL 到程序的进程地址空间。如果 LoadLibary 载入此 DLL 成功,将返回一个该 DLL 的句柄。 这个句柄实际上就是该 DLL 在内存中的起始地址。 在载入 DLL 成功后,还必须调用 GetProcAddress 函数来获取要调用的函数的地址。然后再根据该地址来调用这个函数。
根据上述原理,我们可以把一个 DLL 作为资源文件放到 EXE 文件中,在程序运行时,分配一块内存,然后将此资源复制到该分配的内存中,并根据该内存地址计算得到相关的导出函数地址,然后,当我们需要调用某一函数时,可以用该函数在内存中的地址来调用它。
程序实现。
  首先,把要合并的 DLL 作为资源加入到项目的资源文件中,然后在程序运行时,从资源中载入该资源,以得到该 DLL 在内存中的位置:

然后,从资源中载入 DLL 到内存函数 LoadPbDllFromMemory 将载入 DLL 到内存中,该函数有两个参数,第一个参数是指向 DLL 资源在内存中的地址的指针,也就是前面代码中的 LockResource 函数的返回值。第二个参数是一个空的指针,如果函数 LoadPbDllFromMemory 运行成功,该指针将指向重新组合后的内存中的 DLL 的起始地址。该函数还有一个功能就是如果运行成功,它将手动地用 DLL_PROCESS_ATTACH 参数调用 DLL 的入口函数 DllMain 来初始化该 DLL。除此之外,它还会手动地载入合并的 DLL 的入口表中导入的 DLL 并调整它们在内存中的相对地址。以下是该函数代码:

一但 DLL 被正确地载入到内存中,我们就可以通过自定义函数 GetProcAddressDirectly 来获取某函数在内存中的地址,并根据该地址来调用该函数,该函数也有两个参数,第一个参数是指向载入到内存中的 DLL 的起始地址的指针,第二个是要调用的函数的函数名。以下是 GetProcAddressDirectly 函数代码:

在调用完函数后,不要忘记用 UnloadPbDllFromMemory 来从内存中移去 DLL 以释放分配的内存,该函数还会用 DLL_PROCESS_DETACH 参数调用 DLL 的入口函数 DllMain 来从调用进程的地址空间卸载该 DLL。 以下是 UnloadPbDllFromMemory 函数代码:

关于示例代码的说明
  在本文附带的示例代码中,合并了一个名为 hardware.dll 的动态连接库,该动态连接库是一个获取系统硬件信息的库文件,其中包括了以下函数:

在示例代码中,只调用了其中一个函数 getbios 来获取主板 BIOS ID, 这里说明一下,该函数实际上好象只能检测 AWARD 主板的 BIOS, 也就是说它是读取的是系统内存 0x000fex71 处的值。因为其它牌子的主板 BIOS 的位置稍有不同,但在程序中没有进行这方面的处理,所以在读其它牌子的主板 BIOS 时可能会有些问题(读出的内容可能不正确)。关于此 DLL 的内容和实现,也许我会在另一篇文章中论及。

在VC中使用自定义资源,FindResource,LoadResource,LockResource

一、前言

  在VC环境中除了我们所常用的Dialog、Menu和Bitmap等标准资源类型之外,它还支持自定义资源类型(Custom Resource),我们自定义的资源类型能做些什么呢?呵呵,用处多多。
1. 默认的皮肤压缩包或语言包。一些支持换肤的软件特别是一些媒体播放器常常有自定义的皮肤文件(你可以尝试将Media Player或千千静听等软件的Skins目录下的文件的扩展名改为.zip,然后使用WinZip打开看一下),但为了防止Skin文件丢失导致软件无 法显示,他们常常会在EXE文件中内置一套Skin作为默认的皮肤文件。同样,一些支持多语言的EXE文件中存在默认语言包也是这个道理(当然也可以使用 “String Table”资源类型);
2. 做为一些病毒/木马程序的寄生方式。如果不小心执行了带有病毒/木马的程序,它们会在你运行时释放出病毒/木马文件。当然许多病毒是将自身写入PE文件头来实现;
3. 合并EXE与它所需要的DLL文件。出于某些原因程序作者有时可能需要将DLL文件嵌入到可执行的EXE文件中,这可以通过使用自定义资源来实现;
4. 其它需要在程序中播放一个AVI动画等等,都可以通过将二进制的数据文件作为自定义资源加入到可执行文件中来实现;
二、添加

  添加资源时选择自定义,IDE会为你生成一个新的二进制资源,然后你就可以将你已经存在的二进制文件作为自定义的资源类型导入到项目中来了。

三、使用

  要使用自定义资源,我们可能要用到的几个API函数有FindResource、LoadResource和LockResource等,这里每一个函数的返回值分别作为下一个函数的参数,我来简要介绍一下。

  另外我们还需要用SizeofResource来确定资源的尺寸,我们在操作资源时要用到它。在资源使用完毕后我们不需要使用 UnlockResource和FreeResource来手动地释放资源,因为它们都是16位Windows遗留下来的,在Win32中,在使用完毕后系统会自动回收。它们的使用很简单,大致上是这个样子的:

提醒:

最后将数据写入文件的时候,最好不要用fwrite函数,因为虽然此函数是二进制写入的,但遇到资源是exe文件的时候,从资源中写入文件的exe是运行不了的,不知道为什么,但用WriteFile就可以。。。

VS2008 Release模式下的调试设置

VS2008 C++项目 Release模式下的调试设置

Solution Explorer
选中项目,右键–>properties:

Configuration Properties
—>C++
——>General
———>Debug Information Format->Program DataBase for Eidt & Continue(/ZI)

——>Optimization
———>Optimization—>Disable(/Od)
———>Whole Program Optimization—>No

3.Linker->Debugging->Generate Debug Info->Yes (/DEBUG)
4.Linker->Debugging->Generate Program Database File->$(TargetDir)$(TargetName).pdb

SOCKET通信中TCP、UDP数据包大小的确定

UDP和TCP协议利用端口号实现多项应用同时发送和接收数据。数据通过源端口发送出去,通过目标端口接收。有的网络应用只能使用预留或注册的静态端口;而另外一些网络应用则可以使用未被注册的动态端口。因为UDP和TCP报头使用两个字节存放端口号,所以端口号的有效范围是从0到65535。动态端口的范围是从1024到65535。

    MTU最大传输单元,这个最大传输单元实际上和链路层协议有着密切的关系,EthernetII帧的结构DMAC+SMAC+Type+Data+CRC由于以太网传输电气方面的限制,每个以太网帧都有最小的大小64bytes最大不能超过1518bytes,对于小于或者大于这个限制的以太网帧我们都可以视之为错误的数据帧,一般的以太网转发设备会丢弃这些数据帧。

    由于以太网EthernetII最大的数据帧是1518Bytes这样,刨去以太网帧的帧头(DMAC目的MAC地址48bit=6Bytes+SMAC源MAC地址48bit=6Bytes+Type域2bytes)14Bytes和帧尾CRC校验部分4Bytes那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes这个值我们就把它称之为MTU。

UDP 包的大小就应该是 1500 – IP头(20) – UDP头(8) = 1472(BYTES)
TCP 包的大小就应该是 1500 – IP头(20) – TCP头(20) = 1460 (BYTES)

注*PPPoE所谓PPPoE就是在以太网上面跑“PPP”。随着宽带接入(这种宽带接入一般为Cable Modem或者xDSL或者以太网的接入),因为以太网缺乏认证计费机制而传统运营商是通过PPP协议来对拨号等接入服务进行认证计费的,所以引入PPPoE。PPPoE导致MTU变小了以太网的MTU是1500,再减去PPP的包头包尾的开销(8Bytes),就变成1492。不过目前大多数的路由设备的MTU都为1500。

   如果我们定义的TCP和UDP包没有超过范围,那么我们的包在IP层就不用分包了,这样传输过程中就避免了在IP层组包发生的错误;如果超过范围,既IP数据报大于1500字节,发送方IP层就需要将数据包分成若干片,而接收方IP层就需要进行数据报的重组。更严重的是,如果使用UDP协议,当IP层组包发生错误,那么包就会被丢弃。接收方无法重组数据报,将导致丢弃整个IP数据报。UDP不保证可靠传输;但是TCP发生组包错误时,该包会被重传,保证可靠传输。

    UDP数据报的长度是指包括报头和数据部分在内的总字节数,其中报头长度固定,数据部分可变。数据报的最大长度根据操作环境的不同而各异。从理论上说,包含报头在内的数据报的最大长度为65535字节(64K)。

    我们在用Socket编程时, UDP协议要求包小于64K,TCP没有限定。

    不过鉴于Internet上的标准MTU值为576字节,所以建议在进行Internet的UDP编程时,最好将UDP的数据长度控制在548字节 (576-8-20)以内。

 

就具体函数而言:

    用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。

    用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。

UDP/TCP穿越NAT的P2P通信方法研究

内容概述:在p2p通信领域中,由NAT(Network Address Translation,网络地址转换)引起的问题已经众所周知了,它会导致在NAT内部的p2p客户端在无论以何种有效的公网ip都无法访问的问题。虽然目前已经发展出多种穿越NAT的技术,但相关的技术文档却很少,用来证明这些技术的稳定性和优点的实际数据更少。本文的目的在于描述和分析在实际中运用得最广泛、最可靠同时也是最简单的一种NAT穿越技术,该技术通常被称为“打洞”技术。目前,“打洞”技术已经在UDP通信领域中得到了广泛的理解和应用,在此,也将讨论如何利用它实现可靠的p2p的TCP流通信。在收集了大量的“打洞”技术可以穿越的NAT设备和网络的数据以后,我们发现82%的已测 NAT设备支持UDP形式的“打洞”穿越,64%的已测NAT设备支持TCP流形式的“打洞”穿越。由于重量级p2p应用程序(如,VOIP、BT、在线游戏等)的用户需求量持续上升,并且该事实也已经引起了NAT设备生产厂商的广泛关注,因此,我们认为未来会有越来越多的NAT设备提供对“打洞”穿越技术的支持。
继续阅读 »

MFC中用户自定义类响应自定义消息

这篇技术文章不是讨论经典的MFC中的消息工作机理的,讨论消息工作原理、方式和路径的文章在网上和书本中随处可见。网上众多的讨论都是关于如何响应并进行用户自定义消息映射的;网上还有一些文章介绍如何在自定义类中响应Windows消息,在本文中都简略叙述。但是,网上大部分的文章没用透彻阐述如何在用户自定义类中响应自定义消息这一通用方法。

问题定义如下:用户自定义一个类,这个类不一定要有界面(完全可以是不可视的),要求自定义的类可以响应某个自定义消息。

首先能够响应消息的类必须都从CCmdTarget类中派生,因为只有以这个类中提供了消息的框架和处理机制,而CWnd类也派生与此类。CWinApp类、CDocument类、CDocTemplate类等都是CCmdTarget的派生类,即子类;而CFrameWnd类、CView类、CDialog类等都是从CWnd中派生的,其实也是CCmdTarget的子孙,所以都能够响应消息,但是响应消息的种类不太相同。

那么,如果自己定义的类要求响应命令消息(就是WM_COMMAND,也就是一些菜单、工具栏中的消息,包括快捷键,这类消息处理的机制与其他以WM_开头的消息处理机制不同,它具有一条层次明确的消息流动路径),那么自定义的类可以从CCmdTarget中派生。由于CWnd窗体类派生于CCmdTarget父类,那么从CWnd中派生的类也可以理所应当的响应命令消息。这种命令消息无论是往已有的一些诸如CWinApp类中还是自定义的类中添加都是一件非常容易的事情,只需用向导即可,在此不再叙述。

如果用户自定义的类要求响应普通的Windows消息(也就是以WM_开头,除了WM_COMMAND以外的消息,这类消息在WM_USER以下的是系统消息,WM_USER以上的可以由用户自己定义),那就要求自定义的类必须从CWnd中派生。这是由于此类消息的处理机制决定的,这类消息没有命令消息那条繁琐的流动路径,而是消息发出者直接发给对应CWnd的窗体句柄,由CWnd负责消息的响应。所以这类消息必须同一个CWnd类对应,更精确的说必须与一个HWND类型的窗体句柄相对应。这样得出一个重要的结论,就是从CCmdTarget中派生而没有从CWnd派生的类没有处理此类消息的能力。

综上所述,就是为什么命令消息可以放到大部分类中处理,包括CWinThread、CWinApp、CDocument、CView、CFrameWnd或是自定义的类中,而普通Windows消息和用户自定义的消息只能放到CFrameWnd和CView等派生与CWnd中的类中处理。

由此可见,我们自定义的类要想响应自定义消息就只能从CWnd中派生(当然不响应任何消息的类可以从CObject中派生)。先来看看如何自定义消息:

在.h中做的工作:

第一步要声明消息:

#define WM_MYMSG WM_USER+8

第二步要在类声明中声明消息映射:

DECLARE_MESSAGE_MAP()

第三步要在类声明中定义消息处理函数:

afx_msg LRESULT MyMsgHandler(WPARAM,LPARAM);

在.cpp中做的工作:

第四步要实现消息映射:

BEGIN_MESSAGE_MAP(CMainFrame, CMDIFrameWnd)
ON_MESSAGE(WM_MYMSG,OnMyMsgHandler)
END_MESSAGE_MAP()

第五步要实现消息处理函数(当然可以不实现):

LRESULT CMainFrame::OnMyMsgHandler(WPARAM w,LPARAM l)
{
AfxMessageBox(“Hello,World!”);
return 0;
}

在引发或发出消息的地方只用写上:

::SendMessge(::AfxGetMainWnd()->m_hWnd,WM_MYMSG,0,0);

到此,自定义消息完毕,这是好多网上文章都写的东西。大家会发现上面代码是在CMainFrame类中实现的,但是如果要用自定义类,就没有那么简单了。显然把第四步与第五步的CMainFrame换成自定义的类名(这里我用CMyTestObject来代表自定义类)是不能正常工作的。原因在于在发送消息的SendMessage函数中的第一个参数是要响应消息对应的HWND类型的窗体句柄,而CMyTestObject类中的m_hWnd中在没有调用CWnd::Create之前是没有任何意义的,也就是没有调用CWnd::Create或CWnd::CreateEx函数时,CWnd不对应任何窗体,消息处理不能正常运作。

所以,又一个重要的结论,在自定义类能够处理任何消息之前一定要确保m_hWnd关联到一个窗体,即便这个窗体是不可见的。那么有人说,在自定义类的构造函数中调用Create函数就行了,不错,当然也可以在别处调用,只要确保在消息发送之前。但是,Create的调用很有说法,要注意两个地方,第一个参数是类的名称,我建议最好设为NULL;第五个参数是父窗体对象的指针,这个函数指定的对象一定要存在,我建议最好为整个程序的主窗体。还有很多人问第六个参数的意义,这个参数关系不大,是子窗体ID,用于传给父窗体记录以便识别。如下是我的自定义类的构造函数:

CMyTestObject::CMyTestObject()
{
CWnd::Create(NULL,”MyTestObject”,WS_CHILD,CRect(0,0,0,0),::AfxGetMainWnd(),1234);
}    //一定要在生成主窗体后使用,在主窗体完成OnCreate消息的处理后

CMyTestObject::CMyTestObject(CWnd *pParent)
{
CWnd::Create(NULL,”MyTestObject”,WS_CHILD,CRect(0,0,0,0),pParent,1234);
}

不能如下调用Create,因为此时CMyTestObject不关联任何窗体,所以this中的m_hWnd无效:

CWnd::Create(NULL,”MyTestObject”,WS_CHILD,CRect(0,0,0,0),this,1234);

这时上面四、五两步修改成:

BEGIN_MESSAGE_MAP(CMyTestObject, CWnd)
ON_MESSAGE(WM_MYMSG,OnMyMsgHandler)
END_MESSAGE_MAP()

LRESULT CMyTestObject::OnMyMsgHandler(WPARAM w,LPARAM l)
{
AfxMessageBox(“My Messge Handler in My Self-Custom Class!”);
return 0;
}

在类外部发出消息:

CMyTestObject *test=new CMyTestObject();
::SendMessage(test->m_hWnd,WM_MYMSG,0,0);

在类内部某个成员函数(方法)中发出消息:

::SendMessage(m_hWnd,WM_MYMSG,0,0);

最后一个问题便是容易产生警告错误的窗体回收,自定义的类要显式调用窗体销毁,析构函数如下:

CMyTestObject::~CMyTestObject()
{
CWnd::DestroyWindow();
}