内存管理

自第一台计算机诞生,其最小存储单元就被永久的定格了——一个由8个比特(bit)组成的称为字节(byte)的单位。计算机的所有内存以字节数组的方式进行编址。

当一个逻辑上长于一个字节的整形数据放置在内存中时(比如16位,32位,和64位的整数),计算机设计者需要考虑这些字节的存储顺序。一些体系结构的设计者选择了将字节的逻辑顺序与物理顺序一致,即将逻辑上较低的字节放置在物理上较低的字节上;另外一些设计者则选择了将字节的逻辑顺序与物理顺序相反,即将逻辑上较低的字节放置在物理上较高的字节上。前者被称为“little endian”,比如Intel x86系列;后者则被称为“big endian”,比如Motorola的PowerPC以及Sun Sparc。还有一些平台同时支持两种方案,由开发者决定使用哪一种。

两种选择为底层开发者带来了一定的困扰。比如,两个字节顺序不一致的平台之间进行通信,或者在两个字节顺序不一致的平台之间移植系统。这都是跨平台的例子,对于这些情况,字节顺序的问题是不能回避的。对于仅仅在一种平台上进行开发的程序员而言,如果它能够避免强制类型转换(比如将字节数组强制转换为一个长整数),一贯的以逻辑顺序来操作大于一个字节的整数,应该可以回避这个问题。但由于C语言是一种非常灵活的语言,有时候通过强制类型转换可以让代码非常精简,甚至达到非常巧妙的效果,所以,要求C程序员完全回避这个问题,几乎是不现实的。

由于Little Endian提供了逻辑顺序与物理顺序的一致性,让编程者摆脱了不一致性所带来的困扰,C语言开发者可以无所顾忌的按照自己的意愿进行强制类型转换,所以现代体系结构几乎都支持Little Endian。但Big Endian也有其优点,尤其对于汇编程序员:他们对于任意长度的整数,总是可以通过判断Byte 0的bit-7来查看一个整数的正负;对于Little Endian则不得不首先知道当前整数的长度,然后查看最高byte的bit-7来判断其正负。对于这种情况,big endian的开发者可以写出非常高效的代码。

两派的支持者争论不休,正像他们所支持名词(big endian和little endian)的典故所讲述的那样:Little Endian和Big Endian这两个名词来源于Jonathan Swift的《格利佛游记》其中交战的两个派别无法就应该从哪一端--小端还是大端--打开一个半熟的鸡蛋达成一致。:) 在那个时代,Swift是在讽刺英国和法国之间的持续冲突,Danny Cohen,一位网络协议的早期开创者,第一次使用这两个术语来指代字节顺序,后来这个术语被广泛接纳了(摘自《深入理解计算机系统》)。

需要特别指出的是,通常所提到的Little Endian和Big Endian仅仅指字节顺序。在硬件设计者的术语中,对于一个字节内部的bit顺序也分Little Endian和Big Endian,但对于程序员而言,这些bit顺序的不同是透明的,也就是说,程序员只需要按照逻辑顺序来看待和操作字节内部的bit即可。

Endian的不同不仅仅带来字节顺序的不同,还有更多的问题。如果C程序员在定义一个结构体时,使用了bitwise的域定义,比如:

struct foo {
 int  a:3;
 int b:7;
 int c:13;
 int d:9;
};

这个结构体的一个对象会占用4个字节。由于a,b,c,d的类型都是int,所以他们都在以int32为单位的整数上分配bit,另外,由于他们的bit数量正好等于int32的bit数,所以,它们都分配于一个int所占用的空间。关键问题在于这些字节在这4个字节内是分配顺序是怎么样的?

对于little endian,其分配顺序与逻辑顺序是一致的,即在byte[0]的bit[0~2]上分配a,在byte[0]的bit[3,7]以及byte[1]的bit[0,1]上分配b,依次类推。

对于big endian,其方案会带来很大的问题。其分配顺序为:

字节物理顺序:从低到高;
字节内bit顺序:从高到底;

也就是说,big endian在bitwise的分配方案上,从字节顺序到bit顺序都反过来了(因为其正向存储顺序为:字节从高到底,bit从低到高(从程序员的观点看))。换句话说:big endian的bit分配顺序为,按照bit的逻辑顺序,从高到底进行分配。

          

                     |--------|--------|--------|--------|
Logical Byte Order   | byte 3 | byte 2 | byte 1 | byte 0 |
                     |--------|--------|--------|--------|

Bitwise allocation   |-a-|---b---|------c------|----d----|

请注意,并不是硬件平台使用的这种方案,而是C语言编译器。这是一种荒谬的方案,我想可能是C语言编译器的早期开发者希望通过编译器屏蔽掉big endian和little endian在bitwise allocation上的差异,而都与物理存储顺序一致。但由于其采用了bit order的反向分配,反而加剧了这种差异,随后的编译器为了保持兼容,也只好将错误延续了下来。

基于这种原因,在C语言中直接使用bitwise的方式定义结构体是一种危险的方式,因为这些代码是平台依赖的。当进行跨平台移植的时候必须重新定义这些结构体。

有两种方式可以消除这种风险:

1、使用逻辑移位的方式来操作bit;以上面的例子为例,我们可以这么做:

struct foo {
 int value;
};

#define SET_A(f,a)  do { (f) |= ((a)&0x7); } while(0)
#define SET_B(f,b)  do { (f) |= (((b)&0x7F)<<3); } while(0)
#define SET_C(f,c)  do { (f) |= (((c)&0x1FFF)<<10); } while(0)
#define SET_D(f,d)  do { (f) |= (((d)&0x1FF)<<23); } while(0)

#define GET_A(f)  ((f)&0x7)
#define GET_B(f)  (((f)>>3)&0x7F)
#define GET_C(f)  (((f)>>10)&0x1FFF)
#define GET_D(f)  (((f)>>23)&0x1FF)

2、对于big endian,我们可以使用相反的顺序来声明bitwise fields。仍然以上例为例:

#if LITTLE_ENDIAN
#define BITWISE(type,a,b,c,d)  type a, b, c, d
#else
#define BITWISE(type,a,b,c,d)  type d, c, b, a
#endif

struct foo {
 BITWISE(int, a:3, b:7, c:13, d:9);
};

对于little endian,逻辑顺序与物理顺序一致,只需要按照原样定义;而对于big endian,由于其整体的bit顺序恰好与逻辑顺序是相反的,所以,我们将顺序反过来,使其bit的分配顺序与逻辑顺序一致即可。

 
触类旁通 举一反三
让我们来看看WIN32编程中用到的"窗口"概念,当消息(message)被发送给不同窗口的时候,该窗口的消息处理函数(是一个callback函数)最终被调用,而在该窗口的消息处理函数中,又根据消息的类型调用了该窗口中的对应处理函数。通过这种方式,WIN32有效的组织了不同的窗口,并处理不同窗口情况下的消息。

  我们从中学习到的就是:

  (1)将不同的画面类比为WIN32中不同的窗口,将窗口中的各种元素(菜单、按钮等)包含在窗口之中;

  (2)给各个画面提供一个功能键"消息"处理函数,该函数接收按键信息为参数;

  (3)在各画面的功能键"消息"处理函数中,判断按键类型和当前焦点元素,并调用对应元素的按键处理函数。

/* 将窗口元素、消息处理函数封装在窗口中 */
struct windows
{
 BYTE currentFocus;
 ELEMENT element[ELEMENT_NUM];
 void (*messageFun) (BYTE keyvalue);
 …
};
/* 消息处理函数 */
void messageFunction(BYTE keyvalue)
{
 BYTE i = 0;
 /* 获得焦点元素 */
 while ( (element [i].ID!= currentFocus)&& (i < ELEMENT_NUM) )
 {
  i++;
 }
 /* "消息映射" */
 if(i < ELEMENT_NUM)
 {
  switch(keyvalue)
  {
   case OK:
    element[i].OnOk();
    break;
   …
  }
 }


  在窗口的消息处理函数中调用相应元素按键函数的过程类似于"消息映射",这是我们从WIN32编程中学习到的。编程到了一个境界,很多东西都是相通的了。其它地方的思想可以拿过来为我所用,是为编程中的"拿来主义"。

  在这个例子中,如果我们还想玩得更大一点,我们可以借鉴MFC中处理MESSAGE_MAP的方法,我们也可以学习MFC定义几个精妙的宏来实现"消息映射"。
处理数字键

  用户输入数字时是一位一位输入的,每一位的输入都对应着屏幕上的一个显示位置(x坐标,y坐标)。此外,程序还需要记录该位置输入的值,所以有效组织用户数字输入的最佳方式是定义一个结构体,将坐标和数值捆绑在一起:

/* 用户数字输入结构体 */
typedef struct tagInputNum
{
 BYTE byNum; /* 接收用户输入赋值 */
 BYTE xPos; /* 数字输入在屏幕上的显示位置x坐标 */
 BYTE yPos; /* 数字输入在屏幕上的显示位置y坐标 */
}InputNum, *LPInputNum;
 

  那么接收用户输入就可以定义一个结构体数组,用数组中的各位组成一个完整的数字:

InputNum inputElement[NUM_LENGTH]; /* 接收用户数字输入的数组 */
/* 数字按键处理函数 */
extern void onNumKey(BYTE num)
{
if(num==0|| num==1) /* 只接收二进制输入 */
{
 /* 在屏幕上显示用户输入 */
 DrawText(inputElement[currentElementInputPlace].xPos, inputElement[currentElementInputPlace].yPos, "%1d", num);
 /* 将输入赋值给数组元素 */
 inputElement[currentElementInputPlace].byNum = num; 
 /* 焦点及光标右移 */
 moveToRight();
}
}  

  将数字每一位输入的坐标和输入值捆绑后,在数字键处理函数中就可以较有结构的组织程序,使程序显得很紧凑。

  整理用户输入

  继续第2节的例子,在第2节的onNumKey函数中,只是获取了数字的每一位,因而我们需要将其转化为有效数据,譬如要转化为有效的XXX数据,其方法是:

/* 从2进制数据位转化为有效数据:XXX */
void convertToXXX()
{
 BYTE i;
 XXX = 0; 
 for (i = 0; i < NUM_LENGTH; i++)
 { 
  XXX += inputElement[i].byNum*power(2, NUM_LENGTH - i - 1);
 } 


  反之,我们也可能需要在屏幕上显示那些有效的数据位,因为我们也需要能够反向转化:

/* 从有效数据转化为2进制数据位:XXX */
void convertFromXXX()
{
 BYTE i;
 XXX = 0; 
 for (i = 0; i < NUM_LENGTH; i++)
 { 
  inputElement[i].byNum = XXX / power(2, NUM_LENGTH - i - 1) % 2;
 } 


  当然在上面的例子中,因为数据是2进制的,用power函数不是很好的选择,直接用"<< >>"移位操作效率更高,我们仅是为了说明问题的方便。试想,如果用户输入是十进制的,power函数或许是唯一的选择了。
是思想而不是语言本身
菜单操作

  无数人为之绞尽脑汁的问题终于出现了,在这一节里,我们将看到,在C语言中哪怕用到一丁点的面向对象思想,软件结构将会有何等的改观!

  笔者曾经是个笨蛋,被菜单搞晕了,给出这样的一个系统:
要求以键盘上的"← →"键切换菜单焦点,当用户在焦点处于某菜单时,若敲击键盘上的OK、CANCEL键则调用该焦点菜单对应之处理函数。我曾经傻傻地这样做着:

/* 按下OK键 */
void onOkKey()
{
 /* 判断在什么焦点菜单上按下Ok键,调用相应处理函数 */
 Switch(currentFocus)
 {
  case MENU1:
   menu1OnOk();
   break;
  case MENU2:
   menu2OnOk();
   break;
  …
 }
}
/* 按下Cancel键 */
void onCancelKey()
{
 /* 判断在什么焦点菜单上按下Cancel键,调用相应处理函数 */
 Switch(currentFocus)
 {
  case MENU1:
   menu1OnCancel();
   break;
  case MENU2:
   menu2OnCancel();
   break;
  …
 }


  终于有一天,我这样做了:

/* 将菜单的属性和操作"封装"在一起 */
typedef struct tagSysMenu
{
 char *text; /* 菜单的文本 */
 BYTE xPos; /* 菜单在LCD上的x坐标 */
 BYTE yPos; /* 菜单在LCD上的y坐标 */
 void (*onOkFun)(); /* 在该菜单上按下ok键的处理函数指针 */
 void (*onCancelFun)(); /* 在该菜单上按下cancel键的处理函数指针 */
}SysMenu, *LPSysMenu; 

  当我定义菜单时,只需要这样:

static SysMenu menu[MENU_NUM] =
{
 {
  "menu1", 0, 48, menu1OnOk, menu1OnCancel
 }
 ,
 {
  " menu2", 7, 48, menu2OnOk, menu2OnCancel
 }
 ,
 {
  " menu3", 7, 48, menu3OnOk, menu3OnCancel
 }
 ,
 {
  " menu4", 7, 48, menu4OnOk, menu4OnCancel
 }
 …
}; 

  OK键和CANCEL键的处理变成:

/* 按下OK键 */
void onOkKey()

 menu[currentFocusMenu].onOkFun(); 
}
/* 按下Cancel键 */
void onCancelKey()
{
 menu[currentFocusMenu].onCancelFun(); 


  程序被大大简化了,也开始具有很好的可扩展性!我们仅仅利用了面向对象中的封装思想,就让程序结构清晰,其结果是几乎可以在无需修改程序的情况下在系统中添加更多的菜单,而系统的按键处理函数保持不变。

  面向对象,真神了! 
用C开发的小知识

     在使用绝对地址指针时,要注意指针自增自减操作的结果取决于指针指向的数据类别。上例中p++后的结果是p= 0xF000FF01,若p指向int,即:

int *p = (int *)0xF000FF00; 

  p++(或++p)的结果等同于:p = p+sizeof(int),而p-(或-p)的结果是p = p-sizeof(int)。

  同理,若执行:

long int *p = (long int *)0xF000FF00; 

  则p++(或++p)的结果等同于:p = p+sizeof(long int) ,而p-(或-p)的结果是p = p-sizeof(long int)。

  记住:CPU以字节为单位编址,而C语言指针以指向的数据类型长度作自增和自减。理解这一点对于以指针直接操作内存是相当重要的。

 

函数指针

  首先要理解以下三个问题:

  (1)C语言中函数名直接对应于函数生成的指令代码在内存中的地址,因此函数名可以直接赋给指向函数的指针;

  (2)调用函数实际上等同于"调转指令+参数传递处理+回归位置入栈",本质上最核心的操作是将函数生成的目标代码的首地址赋给CPU的PC寄存器;

  (3)因为函数调用的本质是跳转到某一个地址单元的code去执行,所以可以"调用"一个根本就不存在的函数实体,晕?请往下看: 

  请拿出你可以获得的任何一本大学《微型计算机原理》教材,书中讲到,186 CPU启动后跳转至绝对地址0xFFFF0(对应C语言指针是0xF000FFF0,0xF000为段地址,0xFFF0为段内偏移)执行,请看下面的代码:

typedef void (*lpFunction) ( ); /* 定义一个无参数、无返回类型的 */
/* 函数指针类型 */
lpFunction lpReset = (lpFunction)0xF000FFF0; /* 定义一个函数指针,指向*/
/* CPU启动后所执行第一条指令的位置 */
lpReset(); /* 调用函数 */ 

  在以上的程序中,我们根本没有看到任何一个函数实体,但是我们却执行了这样的函数调用:lpReset(),它实际上起到了"软重启"的作用,跳转到CPU启动后第一条要执行的指令的位置。

  记住:函数无它,唯指令集合耳;你可以调用一个没有函数体的函数,本质上只是换一个地址开始执行指令!

关键字const

  const意味着"只读"。区别如下代码的功能非常重要,也是老生长叹,如果你还不知道它们的区别,而且已经在程序界摸爬滚打多年,那只能说这是一个悲哀:

const int a;
int const a;
const int *a;
int * const a;
int const * a const; 

  (1) 关键字const的作用是为给读你代码的人传达非常有用的信息。例如,在函数的形参前添加const关键字意味着这个参数在函数体内不会被修改,属于"输入参数"。在有多个形参的时候,函数的调用者可以凭借参数前是否有const关键字,清晰的辨别哪些是输入参数,哪些是可能的输出参数。

  (2)合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改,这样可以减少bug的出现。

  const在C++语言中则包含了更丰富的含义,而在C语言中仅意味着:"只能读的普通变量",可以称其为"不能改变的变量"(这个说法似乎很拗口,但却最准确的表达了C语言中const的本质),在编译阶段需要的常数仍然只能以#define宏定义!故在C语言中如下程序是非法的:

const int SIZE = 10;
char a[SIZE]; /* 非法:编译阶段不能用到变量 */ 

=======================================

 

 关键字volatile

  C语言编译器会对用户书写的代码进行优化,譬如如下代码:

int a,b,c;
a = inWord(0x100); /*读取I/O空间0x100端口的内容存入a变量*/
b = a;
a = inWord (0x100); /*再次读取I/O空间0x100端口的内容存入a变量*/
c = a; 

  很可能被编译器优化为:

int a,b,c;
a = inWord(0x100); /*读取I/O空间0x100端口的内容存入a变量*/
b = a;
c = a; 

  但是这样的优化结果可能导致错误,如果I/O空间0x100端口的内容在执行第一次读操作后被其它程序写入新值,则其实第2次读操作读出的内容与第一次不同,b和c的值应该不同。在变量a的定义前加上volatile关键字可以防止编译器的类似优化,正确的做法是:

volatile int a; 

  volatile变量可能用于如下几种情况:

  (1) 并行设备的硬件寄存器(如:状态寄存器,例中的代码属于此类);

  (2) 一个中断服务子程序中会访问到的非自动变量(也就是全局变量);

  (3) 多线程应用中被几个任务共享的变量。

  CPU字长与存储器位宽不一致处理

  在背景篇中提到,本文特意选择了一个与CPU字长不一致的存储芯片,就是为了进行本节的讨论,解决CPU字长与存储器位宽不一致的情况。80186的字长为16,而NVRAM的位宽为8,在这种情况下,我们需要为NVRAM提供读写字节、字的接口,如下: 

typedef unsigned char BYTE;
typedef unsigned int WORD; 
/* 函数功能:读NVRAM中字节 
* 参数:wOffset,读取位置相对NVRAM基地址的偏移
* 返回:读取到的字节值
*/
extern BYTE ReadByteNVRAM(WORD wOffset)
{
 LPBYTE lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 为什么偏移要×2? */

 return *lpAddr;
}

/* 函数功能:读NVRAM中字
* 参数:wOffset,读取位置相对NVRAM基地址的偏移
* 返回:读取到的字
*/
extern WORD ReadWordNVRAM(WORD wOffset)
{
 WORD wTmp = 0;
 LPBYTE lpAddr;
 /* 读取高位字节 */
 lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 为什么偏移要×2? */ 
 wTmp += (*lpAddr)*256;
 /* 读取低位字节 */
 lpAddr = (BYTE*)(NVRAM + (wOffset +1) * 2); /* 为什么偏移要×2? */
 wTmp += *lpAddr;
 return wTmp;
}

/* 函数功能:向NVRAM中写一个字节 
*参数:wOffset,写入位置相对NVRAM基地址的偏移
* byData,欲写入的字节
*/
extern void WriteByteNVRAM(WORD wOffset, BYTE byData)
{
 …
}

/* 函数功能:向NVRAM中写一个字 */
*参数:wOffset,写入位置相对NVRAM基地址的偏移
* wData,欲写入的字
*/
extern void WriteWordNVRAM(WORD wOffset, WORD wData)
{
 …


  子贡问曰:Why偏移要乘以2?

  子曰:请看图1,16位80186与8位NVRAM之间互连只能以地址线A1对其A0,CPU本身的A0与NVRAM不连接。因此,NVRAM的地址只能是偶数地址,故每次以0x10为单位前进!
 [ 相关贴图 ]

子贡再问:So why 80186的地址线A0不与NVRAM的A0连接?

  子曰:请看《IT论语》之《微机原理篇》,那里面讲述了关于计算机组成的圣人之道。



 
内存的问题

今天想了我们模块合在现在机顶盒软件上的问题,主要还是内存分配的问题,我想模块之间要尽量独立,尽量不能让别的模块的问题牵扯进来

我首先想到的一个办法是

/* allocate File data buffer */

BufferData= (char*) memory_allocate( SystemPartition, (U32) FileSize);

原来是在SystemPartition 上分配 。ST_Partition_t *SystemPartition = &TheSystemPartition;

初始的堆栈为 partition_init_heap(&TheSystemPartition,

(U8 *) SystemPartitionBase,

SystemPartitionSize);

 

那我现在可以自己定义一个MyUserPartition,先在alliref_common.cfg 中做一下修改

 

## Addresses & sizes of cached & non-cached areas of External Memory.

## The area defined by NCACHE_BASE and NCACHE_SIZE will contain the

## 'Non-cached' memory partition. The size of this area may be reduced if

## required (NB subject to hardware limitations!)

## IMPORTANT: These definitions reflect the constants of the same

## name in mb361.h. If one of these files is modified then the

## other must be manually updated accordingly. */

 

NCACHE_SIZE = (1*M)

NCACHE_BASE = EXTMEM_BASE

 

CACHED_SIZE = (EXTMEM_SIZE - NCACHE_SIZE - TH_SIZE - TRACE_BUFFER_SIZE-UDATA_SIZE)

CACHED_BASE = (EXTMEM_BASE + NCACHE_SIZE+UDATA_SIZE)

 

USERDATA_SIZE = (4500*K)

USERDATA_BASE = (EXTMEM_BASE + NCACHE_SIZE)

 

## divide the stack and heap

stack EXTERNAL (64*K)

heap EXTERNAL (800*K)

 

define NcachePartitionBase "NCACHE_BASE"

define NcachePartitionSize "NCACHE_SIZE"

define InternalPartitionBase "(addressof INTERNAL) + (sizeused INTERNAL)"

define InternalPartitionSize "(sizeof INTERNAL) - (sizeused INTERNAL)"

define UserDataPartitionBase "USERDATA_BASE"

define UserDataPartitionSize "USERDATA_SIZE"

define SystemPartitionBase "(addressof EXTERNAL) + (sizeused EXTERNAL)"

define SystemPartitionSize "(sizeof EXTERNAL) - (sizeused EXTERNAL)"

define CacheBaseAddress "SMI_CACHED_BASE"

define CacheSize "SMI_CACHED_SIZE"

define AVMEM_BASE_ADDRESS "AVMEM_SMI_BASE"

##define AVMEM_SMI_BASE "AVMEM_SMI_BASE"

define AVMEM_SMI_SIZE "AVMEM_SMI_SIZE"

 

 

partition_init_heap(&TheUserPartition,

(U8 *) UserDataPartitionBase,

UserDataPartitionSize);

 

ST_Partition_t *MyUserPartition= &TheUserPartition;

 

typedef struct

{

ST_Partition_t* DriverPartition;

}User_Handle_t;

 

User_Handle_t *UserHandle;

UserHandle = (User_Handle_t*) memory_allocate(MyUserPartition,1024*1024*4);

//分出4M空间,如果分不出的话直接使用 MyUserPartition 当然MyUserPartition 都用不起来就没有办法了

if( UserHandle ==NULL )

{

if ( MyUserPartition !=NULL)

{

(UserHandle)->DriverPartition = MyUserPartition ;

}

else

{

return(MALLOC_FAIL);

 

}

}

然后定义一个#define MEMALLOC(_size_) memory_allocate(UserHandle->DriverPartition, _size_)

以后就可以这样分配内存了,这里面最大的危险就是我并不能确定 MyUserPartition 能不能用起来除非别的模块也用这个MEMALLOC

 

第二种办法就是和别的模块共用内存

EVENT_INFO_STRUCT pastSchEvtInfoTable[EIT_PROGRAM_NUM][EIT_DAY_NUM][EIT_EVENT_NUM_PER_DAY]; /*[ProgNo][Day][Event]*/

EVENT_INFO_STRUCT pastPfEvtInfoTable[EIT_PROGRAM_NUM][2];

EVENT_INTRO_STRUCT pastEvtIntroTable[EIT_SHORT_EVENT_NUM];

这三个用了4M内存

我用union 的结构共用内存

typedef struct

{

EVENT_INFO_STRUCT pastSchEvtInfoTable1[EIT_PROGRAM_NUM][EIT_DAY_NUM][EIT_EVENT_NUM_PER_DAY];

}STRUCT_1;

typedef struct

{

EVENT_INFO_STRUCT pastPfEvtInfoTable2[EIT_PROGRAM_NUM][2];

}STRUCT_2;

 

typedef struct

{

EVENT_INTRO_STRUCT pastEvtIntroTable3[EIT_SHORT_EVENT_NUM];

}STRUCT_3;

 

union

{

struct

{

STRUCT_1 pastSchEvtInfoTable1;

STRUCT_2 pastPfEvtInfoTable2;

STRUCT_3 pastEvtIntroTable3;

}vedio;

struct

{

ST_Partition_t DriverPartition;

}UserHandle;

}myBss;

STRUCT_1 * myStruct1 = &myBss.vedio.pastSchEvtInfoTable1;

STRUCT_2 * myStruct2 = &myBss.vedio.pastPfEvtInfoTable2;

STRUCT_3 * myStruct3 = &myBss.vedio.pastEvtIntroTable3;

 

#define pastSchEvtInfoTable myStruct1->pastSchEvtInfoTable1

#define pastPfEvtInfoTable myStruct2->pastPfEvtInfoTable2

#define pastEvtIntroTable myStruct3->pastEvtIntroTable3

 

#define MEMALLOC(_size_) memory_allocate(myBss.UserHandle.DriverPartition, _size_)

这个办法不知道可不可以? 我总觉得这个union的结构怪怪的,好象绕了一圈又绕回原来的地方了

用 C# 读取二进制文件
     当想到所有文件都转换为 XML时,确实是一件好事。但是,这并非事实。仍旧还有大量的文件格式不是XML,甚至也不是ASCII。二进制文件仍然在网络中传播,储存在磁盘上,在应用程序之间传递。相比之下,在处理这些问题方面,它们比文本文件显得更有效率些。
 
Click here to find out more! 

     在 C 和 C++ 中,读取二进制文件还是很容易的。除了一些开始符(carriage return)和结束符(line feed)的问题,每一个读到C/C++中的文件都是二进制文件。事实上,C/C++ 只知道二进制文件,以及如何让二进制文件像文本文件一样。当我们使用的语言越来越抽象时,我们最后使用的语言就不能直接、容易的读取创建的文件了。这些语言想要用它们自己独特的方式来自动处理输出数据。

问题的所在

在许多计算机科学领域,C 和 C++ 仍旧直接依照数据结构来储存和读取数据。在C和C++中,依照内存中的数据结构来读取和写文件,是十分简单的。在C中,你只需要使用fwrite()函数,并提供下列参数:一个指向你的数据的指针,告诉它有多少个数据,一个数据有多大。这样,就直接用二进制格式把数据写成文件了。

如上所述的那样把数据写成文件,同时如果你也知道其正确的数据结构的话,那么也就意味着读取文件也很容易。你只要使用 fread() 函数,并提供下列参数:一个文件句柄,一个指向数据的指针,读取多少个数据,每一个数据的长度。 fread() 函数帮你把其余的事都做了。突然,数据又回到了内存中。没有采用解析以及也没有对象模型的方式,它只是把文件直接的读到内存中。

在C和C++中,最大的两个问题就是数据对齐(structure alignment)和字节交换(byte swapping)。数据对齐指的是有时编译器会跳过数据中间的字节,因为如果处理器访问到那些字节,就不再处于最优化状态下了,要花费更多的时间(一般情况,处理器访问未对齐数据花费的时间是访问对齐数据的两倍),花费更多的指令。因此,编译器要为了执行速度而进行优化,跳过了那些字节并重新进行排序。另一方面,字节交换指的是:由于不同处理器对字节排序的方式不同,需要对数据的字节重新排序的过程。

数据对齐

因为处理器能够一次处理更多的信息(在一个时钟周期内),所以它们希望它们所处理的信息能以一种确定的方式排列。大多数的 Intel 处理器使整数类型(32位的)的储存首地址能被4除尽(即:从能被4除尽的地址上开始储存)。如果内存中的整数不是储存在4的倍数的地址上的话,它们是不会工作的。编译器知道这些。因此当编译器遇到一个可能引起这种问题的数据时,它们就有下面三种选择。

第一种,它们可以选择在数据中添加一些无用的白空格符,这样可以使整数的开始地址能被4除尽。这是一种最普遍的做法。第二种,它们可以对字段重新排序,以便使整数处于4位的边界上。因为这样会造成其它有趣的问题,因此,这种方式较少使用。第三种选择是,允许数据中的整数不处于4位的边界上,但是把代码复制到一个合适的地方从而使那些整数处于4位的边界上。这种方式需要一些额外的时间花费,但是,如果必须压缩的话,那么它就很有用了。

以上所说的这些大都是编译器的细节问题,你用不着过多的担心。如果你对写数据的程序和读数据的程序使用同样的编译器,同样的设定,那么,这些就不成其为问题了。编译器用同样的方法来处理同样的数据,一切都OK。但是当你涉及到跨平台文件转换问题时,用正确的方式来排列所有数据就显得很重要了,这样才能保证信息能被转换。另外,一些程序员还了解怎样让编译器不用理睬他们的数据。

 

字节交换(byte swapping):高位优先(big endians)和低位优先(little endians

 
Click here to find out more! 

   高位优先和低位优先,指的是两种不同的方式,把整数储存在计算机中的的方式。因为整数是多于一个字节的,那么,问题在于:最重要的字节是否应该首先被读写。最不重要的字节是变化的最频繁的。这就是,如果你不断给一个整数加一,最不重要的字节要改变256次,次不重要的字节才只变化一次。

不同的处理器用不同的方式储存整数。Intel 处理器一般用低位优先方式来储存整数,换句话说,低位首先被读写。大多数其它处理器用高位优先方式来储存整数。因此,当二进制文件在不同平台上读写时,你就有可能不得不对字节重新排序以便得到正确的顺序。

在 UNIX 平台上,还有一种特殊的问题,因为UNIX可以在Sun Sparc处理器、HP处理器、IBM Power PC、Inter的芯片等多种处理器上运行。当从一种处理器转移到另一种处理器上时,就意味着那些变量的字节排列顺序必须翻转,以便于它们能满足新处理器所要求的顺序。

用 C# 处理二进制文件

用 C# 处理二进制文件的话,就会有另外两项新的挑战。第一项挑战是:所有的 .NET 语言都是强类型的。因此,你不得不从文件中的字节流转换为你所想要的数据类型。第二项挑战就是:一些数据类型比它们表面上要复杂的多,需要某种转换。

类型破坏(type breaking

因为 .NET 语言,包括 C#,都是强类型的,你不能只是任意的从文件中读取一段字节,然后塞到数据结构中就一切OK了。因此当你要破坏类型转换规则时,你就不得不这样做了,首先读取你所需要的字节数到一个字节数组中,然后把它们从头到尾的复制到数据结构中。

在 Usenet (注:世界性的新闻组网络系统)的文档中搜寻,你会找到几个构架在 microsoft.public.dotnet层次上的一组程序,它们可以容许你把任何对象转换为一系列字节,并可以重新转换回对象。它们可以在下面地址找到 Listing A

复杂的数据类型

在 C++ 中,你明白什么是对象,什么是数组,什么既不是对象又不是数组。但是在 C# 中,事情并不像看起来的那样简单。一个字符串(string)就是一个对象,因此也是一个数组。因为在 C# 中,既没有真正的数组,许多对象也没有固定尺寸,因此一些复杂数据类型并不适合成为固定尺寸的二进制数据。

幸好, .NET 提供了一种方式来解决这种问题。你可以告诉 C# ,你想怎样处理你的字符串(string)和其它类型的数组。这将通过 MarshalAs 属性来完成。下面这个例子,就是在 C# 中使用字符串,这属性必须要在所控制的数据使用之前被使用:

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 50)]

你想要从二进制文件中读取,或者储存到二进制文件中的字符串(string)的长度就决定了参数 SizeConst 的大小。这样就确定了字符串长度的最大值。

 

解决以前的问题

 
Click here to find out more! 

   现在,你知道了 .NET 引入的问题是怎样被解决的了。那么,在后面,你就可以了解到,解决前面所遇到的二进制文件问题是那么的容易。

包装(pack

不用麻烦的去设定编译器来控制如何排列数据。你只需使用 StructLayout 属性就可以使数据依照你的意愿来排列或打包。当你需要不同的数据有着不同的包装方式的时候,这就显得十分有用了。这就像装扮你的汽车一样,任你的喜好。使用 StructLayout 属性就像你很小心的决定是否把每一个数据都紧凑包装或者还是只将它们随便打发,只要它们能够被重新读出来就行了。 StructLayout 属性的使用如下面所示:

[StructLayout(LayoutKind.Sequential, Pack = 1)]

这样做可以使数据忽略边界对齐,让数据尽可能的紧凑包装。这个属性应当和你从二进制文件中读取的任何数据的属性都保持一致(即:你写到文件中的属性应和从文件读出来属性保持不变)。

你也许会发现,即使给你的数据加上了这个属性后,也没有完全解决问题。在某些情况下,你可能不得不进行沉闷冗长的反复实验。由于不同计算机和编译器在二进制层次上的有着不同的运行处理方式,这就是引起上述问题的原因。特别是在跨平台时,我们都必须特别小心的处理二进制数据。 .NET 是个好工具,适合其它二进制文件,但是也并不是一个完美的工具。

字节排列顺序的翻转(endian flipping

读写二进制文件的经典问题之一就是:某些计算机首先是储存最不重要的字节(如:Inter),而另外一些计算机是首先储存最重要的字节。在 C 和 C++ 中,你不得不手动处理这个问题,而且只能是一个字段一个字段的翻转。而 .NET 框架的优点之一就是:代码可以在运行时访问类型的元数据(metadata),你也就能够读取信息,并使用它来自动解决数据中每一段的字节排列顺序问题。在 Listing B 上可以找到源代码,你可以了解是如何处理的。

一旦你得知对象的类型,你能够获得数据里的每个部分,并开始检查每一个部分,并确定其是否是一个16位或32位的无符号整数。在任何一种上述情况下,你都可以改变字节的排序顺序,而且不会破坏数据。

注意:你不是用字符串类(string)来完成所有的事。是采用高位优先还是低位优先,并不会影响到字符串类。那些字段是不受翻转代码的影响。你也只是要注意无符号整数而已。因为,负数在不同的系统上,并不是使用同一种表示方式的。负数可以只用一个记号(一位字节)表示,但是更常用的,却是使用两个记号(两位字节)表示。这使得负数在跨平台时有些更困难。幸运的是,负数在二进制文件中极少使用。

这只是多说几句了,同样的,浮点数有时并不是用标准方式表示的。尽管大多数系统是以IEEE格式为基础来设置浮点数的,但是还是有一小部分老的系统使用了其它的格式来设置浮点数的。

克服困难

尽管 C# 还是有一些问题,但是你依旧能够使用它来读取二进制文件。实际上,由于 C# 所使用的那种用来访问对象的元数据(metadata)的方式,使它成为一种能够更好读取二进制文件的语言。因此, C# 能够自动解决整个数据的字节交换(byte swapping)问题。

添加评论
单击隐藏此项的评论。
8月30日
对几组sizeof信息的分析

对几组sizeof信息的分析

对于很多C ++新手而言,对象或变量的 sizeof信息总是让人捉摸不透,以下程序列举了几个典型的 sizeof信息,希望能解答大家在使用 sizeof时的疑问。
在列举这几个例子前需要说明以下几点:
1
、在Win32平台上,指针长度都是 4字节, char *int *double *如此,vbptr ( virtual base table pointer )、vfptr ( virtual function table pointer )也是如此;
2
、对于结构体 (或类 ),编译器会自动进行成员变量的对齐,以提高运算效率。自然对齐 (natural alignment )也称默认对齐方式是按结构体的成员中size最大的成员对齐的,强制指定大于自然对齐大小的对齐方式是不起作用的。
3
、不推荐强制对齐,大量使用强制对齐会严重影响处理器的处理效率。

范例 1(一个简单的C语言的例子 )
void
 f ( int arr [])
{

    cout  <<  "sizeof(arr) = "  <<  sizeof (arr ) << endl ;  //当被作为参数进行传递时,数组失去了其大小信息
}

void
 main ()
{

    char
 szBuf [] =  "abc" ;
    cout  <<  "sizeof(szBuf) = "  <<  sizeof (szBuf ) << endl ;  //输出数组占用空间大小

    char
* pszBuf  = szBuf ;
    cout  <<  "sizeof(pszBuf) = "  <<  sizeof (pszBuf ) << endl ;  //输出的是指针的大小

    int
 iarr [ 3 ]; iarr ;
    cout  <<  "sizeof(iarr) = "  <<  sizeof (iarr ) << endl ;  //输出数组占用空间大小
    f (iarr );

    int
* piarr  = iarr ;
    cout  <<  "sizeof(piarr) = "  <<  sizeof (piarr ) << endl ;  //输出指针的大小
}

范例 2(一个涉及alignment的例子 )
struct
 DATA1
{

    char
    c1 ;  //偏移量0,累积size = 1
    char    c2 ;  //偏移量1,累积size = 1 + 1 = 2
    short    si ;  //偏移量2,累积size = 2 + 2
};

struct
 DATA2
{

    char
    c1 ;  //偏移量0,累积size = 1
    short    si ;  //偏移量1 + (1),累积size = 1 + (1) + 2 = 4
    char    c2 ;  //偏移量4,累积size = 4 + 1 = 5,但按最大长度sizeof(short) = 2对齐,故最后取6
};

struct
 DATA3
{

    char
    c1 ;  //偏移量0,累积size = 1
    double    d ;  //偏移量1 + (7),累积size = 1 + (7) + 8 = 16
    char    c2 ;  //偏移量16,累积size = 16 + 1 = 17,但按最大长度sizeof(double) = 8对齐,故最后取24
};

#pragma pack(push,1) //强制1字节对齐
struct DATA4
{

    char
    c1 ;  //偏移量0,累积size = 1
    double    d ;  //偏移量1,累积size = 1 + 8 = 9
    char    c2 ;  //偏移量9,累积size = 9 + 1 = 10
};
#pragma pack(pop) //恢复默认对齐方式

struct
 DATA5
{

    char
    c1 ;
    double
    d ;
    char
    c2 ;
};


void
 main ()
{

    cout  <<  "sizeof(DATA1) = "  <<  sizeof (DATA1 ) << endl ;
    cout  <<  "sizeof(DATA2) = "  <<  sizeof (DATA2 ) << endl ;
    cout  <<  "sizeof(DATA3) = "  <<  sizeof (DATA3 ) << endl ;
    cout  <<  "sizeof(DATA4) = "  <<  sizeof (DATA4 ) << endl ;
    cout  <<  "sizeof(DATA5) = "  <<  sizeof (DATA5 ) << endl ;
}


范例 3(C ++语言特征对 sizeof的影响 )
class
 CA
{
};


class
 CB  :  public CA
{

public
:
    void
 func () {}
};


class
 CC  :  virtual public CA
{
};


class
 CD
{

    int
 k ;  //私有成员
public :
    CD () {k  = - 1 ;}
    void
 printk () { cout  <<  "k = "  << k  << endl ; }
};


class
 CE  :  public CD
{
};


class
 CF
{

    virtual
 void func () {}
};


void
 main ()
{

    cout  <<  "sizeof(CA) = "  <<  sizeof (CA ) << endl ;  //为了区分不包含任何成员的类的不同的元素,编译器会自动为类添加一个匿名元素
    cout  <<  "sizeof(CB) = "  <<  sizeof (CB ) << endl ;  //与上面类似,编译器也为CB添加了一个匿名元素
    cout  <<  "sizeof(CC) = "  <<  sizeof (CC ) << endl ;  //虚拟继承中vbptr(virtual base table pointer)占用4个字节

    cout  <<  "sizeof(CD) = "  <<  sizeof (CD ) << endl ;
    cout  <<  "sizeof(CE) = "  <<  sizeof (CE ) << endl ;  //访问权限控制是在编译期间由编译器控制的,所以虽然不能访问CD类的成员k,这里仍然占用了sizeof(int)大小的空间
    //下面的代码进一步说明上述观点,由于在复杂的类层次结构中,当涉及到虚函数或者虚拟继承等时,有些信息是运行期动态生成的,故请勿效仿以下方法对对象进行修改
    CE e ;
    e .printk ();
    memset (&e ,  0 ,  sizeof (CE ));
    e .printk ();  //从这里可以看出,上面的memset操作修改了CD类的私有成员k

    cout  <<  "sizeof(CF) = "  <<  sizeof (CF ) << endl ;  //虚函数表指针占有4个字节
}
添加评论
单击隐藏此项的评论。
结构体对齐的具体含义(#pragma pack)

结构体对齐的具体含义(#pragma pack)

作者:panic 2005年4月2日

还是来自csdn的帖子:
主  题:   探讨:内存对齐
作  者:   typedef_chen ((名未定)(我要骗人))
等  级:   
信 誉 值:   100
所属论坛:   C/C++ C++ 语言
问题点数:   50
回复次数:   1
发表时间:   2005-04-02 22:53:27
  
  
朋友帖了如下一段代码:
  #pragma pack(4)
  class TestB
  {
  public:
    int aa;
    char a;
    short b;
    char c;
  };
  int nSize = sizeof(TestB);
  这里nSize结果为12,在预料之中。

  现在去掉第一个成员变量为如下代码:
  #pragma pack(4)
  class TestC
  {
  public:
    char a;
    short b;
    char c;
  };
  int nSize = sizeof(TestC);
  按照正常的填充方式nSize的结果应该是8,为什么结果显示nSize为6呢?

事实上,很多人对#pragma pack的理解是错误的。
#pragma pack规定的对齐长度,实际使用的规则是:
结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
也就是说,当#pragma pack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。
而结构整体的对齐,则按照结构体中最大的数据成员 和 #pragma pack指定值 之间,较小的那个进行。

具体解释
#pragma pack(4)
  class TestB
  {
  public:
    int aa; //第一个成员,放在[0,3]偏移的位置,
    char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。
    short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。
    char c; //第四个,自身长为1,放在[8]的位置。
  };
这个类实际占据的内存空间是9字节
类之间的对齐,是按照类内部最大的成员的长度,和#pragma pack规定的值之中较小的一个对齐的。
所以这个例子中,类之间对齐的长度是min(sizeof(int),4),也就是4。
9按照4字节圆整的结果是12,所以sizeof(TestB)是12。


如果
#pragma pack(2)
    class TestB
  {
  public:
    int aa; //第一个成员,放在[0,3]偏移的位置,
    char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。
    short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。
    char c; //第四个,自身长为1,放在[8]的位置。
  };
//可以看出,上面的位置完全没有变化,只是类之间改为按2字节对齐,9按2圆整的结果是10。
//所以 sizeof(TestB)是10。

最后看原贴:
现在去掉第一个成员变量为如下代码:
  #pragma pack(4)
  class TestC
  {
  public:
    char a;//第一个成员,放在[0]偏移的位置,
    short b;//第二个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[2,3]的位置。
    char c;//第三个,自身长为1,放在[4]的位置。
  };
//整个类的大小是5字节,按照min(sizeof(short),4)字节对齐,也就是2字节对齐,结果是6
//所以sizeof(TestC)是6。

感谢 Michael 提出疑问,在此补充:

当数据定义中出现__declspec( align() )时,指定类型的对齐长度还要用自身长度和这里指定的数值比较,然后取其中较大的。最终类/结构的对齐长度也需要和这个数值比较,然后取其中较大的。

可以这样理解, __declspec( align() ) 和 #pragma pack是一对兄弟,前者规定了对齐的最小值,后者规定了对齐的最大值,两者同时出现时,前者拥有更高的优先级。
__declspec( align() )的一个特点是,它仅仅规定了数据对齐的位置,而没有规定数据实际占用的内存长度,当指定的数据被放置在确定的位置之后,其后的数据填充仍然是按照#pragma pack规定的方式填充的,这时候类/结构的实际大小和内存格局的规则是这样的:
在__declspec( align() )之前,数据按照#pragma pack规定的方式填充,如前所述。当遇到__declspec( align() )的时候,首先寻找距离当前偏移向后最近的对齐点(满足对齐长度为max(数据自身长度,指定值) ),然后把被指定的数据类型从这个点开始填充,其后的数据类型从它的后面开始,仍然按照#pragma pack填充,直到遇到下一个__declspec( align() )。
当所有数据填充完毕,把结构的整体对齐数值和__declspec( align() )规定的值做比较,取其中较大的作为整个结构的对齐长度。
特别的,当__declspec( align() )指定的数值比对应类型长度小的时候,这个指定不起作用。

 

引自:http://blog.vckbase.com/panic/archive/2005/04/02/4340.aspx

添加评论
单击隐藏此项的评论。
sizeof和内存对齐
    本来,一般是不自己计算sizeof的,知道内存对齐会对sizeof有影响,所以从来不手算,而是代码里写上sizeof。今天又看到 http://blog.vckbase.com/smileonce/archive/2005/08/08/10658.html,翻来了 http://blog.vckbase.com/billdavid/archive/2004/06/23/509.html ,自己想想还是也记录一下,万一以后自己真的也要计算sizeof,忘了,还能有个提示,也给不是很明白的朋友一个参考。
    struct sample1
   {
        char a;  /// sizeof(char) = 1
        double b; /// sizeof(double) = 8
    };
///default(  缺省#pragam pack(8) ——VC6和VC71,其它编译器,个人未知 )
    ///1+8 = 9 —> 16(  8 < 9 < 16  )

#pragma pack( 4 )
    ///1+8 = 9 —> 12(  8 < 9 < 12  )

#pragma pack( 2 )
    ///1+8 = 9 —> 10(  8 < 9 < 10  )

#pragma pack( 1 )
    ///1+8 = 9 —> 9

#pragma pack( 16 )
    ///1+8 = 9 —> 16(  16—>8 ---- 8 < 9 < 16  )

    struct sample2
    {
        char a;  ///1
        int b;  ///4
    };
#pragma pack( 8 )
    /// 1 + 4  = 5 —> 8(  8 —> 4  )

#pragma pack( 16 )
     /// 1 + 4 = 5 —> 8( 16 —> 4  )

说明:#pragma pack告诉编译器进行内存边界对齐,一般都是采用编译器的设置对整个项目采用同一对齐方案,而且通常为缺省8字节对齐。
 
添加评论
单击隐藏此项的评论。
sizeof(联合)这个值是怎么计算的 内存管理

解惑:sizeof(联合)这个值是怎么计算的

[不要只做技术]在论坛上问如下代码结果为什么是24?

union DATE
{
    char a;
    int i[5];
    double b;
};

DATE max;

cout<< sizeof(max) << endl;

这个问题很好回答,并且我把这个问题归结于基本概念题(就是入门书必须介绍的)。我想一般来说,做过内存管理的,对这个语言特性肯定不会陌生。

摘几句The C Programming Language里面讲述这个问题的原话,以说明读书还是必要的:
①联合就是一个结构,②它的所有成员相对于基地址的偏移量都为0,③此结构空间要大到足够容纳最“宽”的成员,④并且,其对齐方式要适合于联合中所有类型的成员。

怕有的兄弟还不明白,特附图一个帮助理解:

 

char a;=>x                       
int i[5];=>>

 x

 x

 x

 x

 x

x

double b;=>

 x

 

 


该结构要放得下int i[5]必须要至少占4×5=20个字节。如果没有double的话20个字节够用了,此时按4字节对齐。但是加入了double就必须考虑double的对齐方式,double是按照8字节对齐的,所以必须添加4个字节使其满足8×3=24,也就是必须也是8的倍数,这样一来就出来了24这个数字。综上所述,最终联合体的最小的size也要是所包含的所有类型的基本长度的最小公倍数才行。(这里的字节数均指winnt下的值,平台、编译器不同值也有可能不同。)

联合在存储分配的时候用的机会最多,因为很少有像存储分配这样需要给多种不同类型的变量分配空间而又打算尽可能的节约内存的,这很适合联合的特性。上述对齐的方式有个很有趣的用法也就常在存储分配里面使用。(下面依旧用 The C Programming Language中的例子作答)

typedef long Align;

union header {
    struct {
        union header *ptr;
        unsigned size;
    } s;
    Align x;
}

这里的Align有什么用?作用只有一个,就是强迫分配的结构体按long的长度对齐。

添加评论
单击隐藏此项的评论。
C语言的异常机制 setjump longjump函数

C语言的异常机制

晚上回家翻vckbase的帖子,看到这么一段:
usr_root : c99支持异常吗?
周星星    : 当然不
usr_root : 为什么是当然不?异常机制和c无缘?
七猫     : 当然,到现在还有很多人不喜欢异常。连有些C++的标准库都支持不带异常的。
周星星    : :)俺就是这样的人
usr_root : 哦,我是你的影子
汗!  为了C语言偶不得不说几句申一下冤,真是比窦娥还冤: C语言中,标准库函数setjmp和longjmp形成了结构化异常工具的基础。简单的说即setjmp实例化异常处理程序,而longjmp产生异常。举个例子如下:

/* -------------------------------------------------------------------------
** File : cexcept.c                                                        *
** Coder: Spark Song.                                                      * 
** Note : Use the example code from 《C Interfaces and Implementations》   *  
** -------------------------------------------------------------------------
*/

#include <setjmp.h>
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>

int Allocation_handled = 0;
jmp_buf Allocate_Failed;

void *allocate(unsigned n)
{
    void * new = (void *)malloc(n);

    if (new)
        return new;

    if (Allocation_handled)          /* 如果实例化了异常处理程序的话... */
        longjmp(Allocate_Failed, 1); /* 产生异常,并抛出 */
    
    assert(0); /* 如果没有实例化异常处理程序,则此断言会报出运行期的错误 */
}


int main(int argc, char *argv[])
{
    char *buf = 0;
    int count = 0;


    Allocation_handled = 1;         /* 设置实例化异常的标志,设为1表示实例化了异常处理程序 */ 
    if (setjmp(Allocate_Failed))    /* 实例化异常 */
    {
        fprintf(stderr, "EXCEPT: Couldn't allocate the buffer/n"); 
        exit(EXIT_FAILURE);
    }      
        
    while(1)    /* 测试代码:一直分配内存,直到没有内存为止。没有内存时会触发异常 */
    {
        buf = (char *)allocate(4096000);         
        printf("Allocate successs, the count is: %d/n", count++);
    }

    Allocation_handled = 0;  
	return 0;
}

上面这个例子在MingW下通过,编译时使用了-std=c89 -pedantic的编译开关(强制使用c89的语法检查)和-std=iso9899:199409  -pedantic(强制使用c99的语法检查)。运行结果如下:

Allocate successs, the count is:   1
... ...
Allocate successs, the count is: 447
Allocate successs, the count is: 448
Allocate successs, the count is: 449
Allocate successs, the count is: 450
EXCEPT: Couldn't allocate the buffer

简要讲述一下代码的流程:setjmp用来实例化异常处理程序,在这里我们的异常处理程序就是往stderr输出一个字符串并退出应用程序。setjmp会返回2次值(颇有些fork()的味道)。setjmp第一次返回值是在应用代码(这里就是main函数里面)调用setjmp的地方,这时候它实例化了异常处理程序,并返回0,所以异常处理程序的代码并没有被执行。在allocate中调用longjmp的时候,会引起setjmp第二次值的返回,此时的返回值由longjmp的第二个参数所决定。文中我们调用longjmp的时候,传给它的第二个参数是1,所以setjmp返回时会执行if中的异常处理程序。

这个例子就是最最简单的C语言处理异常的原型,我们完全可以利用它来构造出一整套的异常处理体系,一点也不比C++之类的高级语言差。为什么不把异常加入语言本身?我想这是由C语言的设计理念和设计目的决定的。C语言是面向底层和系统开发的较低级的语言,所以语言本身并不复杂,强大的功能完全可以通过函数库来实现。

欲更深入的了解C语言的异常处理体系的设计,可以参考David R.Hanson的C Interfaces and Implementations(中文版《C语言接口与实现》)。

添加评论
单击隐藏此项的评论。
8月19日
Anyone know of a tasteful LGPL HTML parser in C?
 
Jeff Pitman wrote:

> On Thursday 25 November 2004 06:53, Jeff Johnson wrote:
>  
>
>> I'm certainly willing to listen to other ideas, the above is what
>> makes sense to me.
>>    
>>
>
> For whatever it's worth, I'd rather just see a quick expat parse to
> extract what you need.  You certainly don't want DOM and full SAX API
> is debatable.  I think libxml2 brings a lot of baggage to solve
> globbing in html urls.  I've used libxml2 in several projects, and it's
> really nice, mem is good, api is sane.  But, if you're just grepping
> out URLs from <a>, then expat.  Shoot, a regex hack would be quick and
> easy.
>

Yah, I'm rapidly discovering just how much HTML sucketh mightily.

I have several alternative fallbacks, including grabbing the quite
predictable and
easy to parse ls-lR file, or sticking with the already implemented
WebDAV Glob()
instead of grepping HTML directly. I do know that users will not be able to
tell when servers are WebDAV enabled, and I cannot force WebDAV on any
web mistresses who maintain package repositories.

There's also a chance that I might get permission to use the wget
html-parse.c,
which is rather tidy and appealing compared to other solutions I've
looked at.

So I'm trying to give it a shot, but if the solution is really pugly, I
will bail in a hurry.

>
> Not sure about your b) and d) reasons, though.  Don't forget about all
> the crappy HTML out there ... it ain't pretty.
>

Luckily, the presentation of package repositories is difficult to
prettify, so perhaps
the HTML subset necessary to parse from package repositories is not so bad.

Dunno yet, just kicking tires so far.

b) There is info regarding XAR at
http://www.opendarwin.org/mailman/listinfo/xar.

There's a lot right with XAR format including
    a) XML for metadata with DTD, and version, so additions to metadata
are perhaps easier
     than with existing rpm Headers. The rpmlib API really really sux
(well, not as bad as HTML
     parsers ;-) and it's the only way to get at metadata.
    b) XAR format is similar to *.rpm packages, i.e. all the metatdata
is contiguous at the front, with payload behind.
    c) XAR can represent extended attr's from several operating systems,
including linux, a non-trivial implementation.
    d) XAR is a leading contendor for OpenDarwin's package format.
So an XAR like format may well be next generation *.rpm format. I know
of no better atm.

d) DV (aka Daniel Veillard) has been immensely helpful with rpm for
years, see http://rpmfind.net for one
useful service, DistURL: within *.spec syntax for another useful idea
(an xml URI used for package discovery).

But your warnings are noted and appreciated.

73 de Jeff
添加评论
单击隐藏此项的评论。
LYNX-DEV pre-announcing a new Lynx SGML.c parser
Exciting news (well, you may disagree...),

I have finished modifying the first stage "SGML" parsing in lynx
to be somewhat closer to a real SGML parser.  Essentially, I have
extended the per-tag information (given in HTMLDTD.c) to include
more of the content model info of a real DTD, and done away with the
special treatment of some tags in SGML.c.  end_element and also
start_element in SGML.c now do partial stack wind-downs, depending
on whether an element is "allowed" to close another one (and, in some
cases, whether the other element's end tag can be legally omitted).

This delivers to the next stage (HTML.c) a series of
HTML_{start,end}_element which are always correctly ordered, for all
elements which are not declared as SGML_EMPTY, and I have removed the
SGML_EMPTY flags from a number of tags that were specially treated
before, including P and (recently) FORM.  Note that I haven't made
*any* changes to HTML.c to accomodate the changes in SGML.c and
HTMLDTD.c.  It works with the unchanged HTML.c, which is great and
shows that these modules have remained reasonably independent of
each other; it does however not always give identical results
(screen appearance) even for valid HTML, which shows that sometimes
HTML.c is relying on specific hacks for specific elements in SGML.c
and the old "DTD". (for example, declaring P as SGML_EMPTY *and*
converting </P> to <P>).

I would like to have HTML.c in a form that it could deal equally with
being called from the modified SGML.c parser, as well as from the
old-style parser (with a, possibly increasing, number of hacks).  This
would allow testing of recovery heuristics with the new parser and
comparison with the old way, without each time having to modify HTML.c.

Fote, I would appreciate your help here :).  It would help if you at
least did not make changes to HTML.c that depend on new hacks introduced
in SGML.c and the HTMLDTD.  (I am not saying that you *did* make such
changes recently; this is just a just-in-case request, I still haven't
checked whether the recent me->inUnderline changes fall in this
category.  Your clarification sounded a bit like it, but I am not sure
so will have to check the code.)

(Also, I know and accept that you don't want to be considered an
"active developer" at this point.  However, as long as you are often
the first to make required and/or useful changes, and make them
available, you'll have to accept that your mods continue to be at least
an important source of input for our development code :).  Given that,
my request above could help cut down on my [not your] time.)

I will make the code available as a more-experimental-than-usual update
to the devel code, as soon as I have considered some other misc. unrelated
changes.  Still without adapting HTML.c, 'cause I want this to get out
the door now, and would like people to test it... THe first goal then is
to reproduce Lynx's current behavior (as far as it is correct :) ) for
valid HTML, tweaking the recovery heuristics for invalid HTML will come
later.  I am not sure whether there is any screwed-up HTML out there where
my approach *already* gives better results, or whether it finally can be
made sophisticated enough to generally improve treatment of bad HTML (over
that already done by Fote's latest hacks).  Maybe a combination of
approaches will finally give best results.
添加评论
单击隐藏此项的评论。
解析Html页面:HTML Parser的试用

最近在研究lucene的全文检索,在很多地方需要解析或者说分析Html内容或者Html页面,Lucene本身的演示程序中也提供了一个Html Parser,但是不是纯Java的解决方案.于是到处搜索,在网上找到了一个"HTMLParser".

网址是: http://htmlparser.sourceforge.net ,当前版本为1.5.

下载下来,试用一番,感觉不错,完全能满足lucene解析Html的需求.

过几天贴出lucene进行全文检索的代码.(检索本站的文章等).

试用代码如下,供大家参考:

package com.jscud.test;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;

import org.htmlparser.Node;
import org.htmlparser.NodeFilter;
import org.htmlparser.Parser;
import org.htmlparser.filters.NodeClassFilter;
import org.htmlparser.filters.OrFilter;
import org.htmlparser.nodes.TextNode;
import org.htmlparser.tags.LinkTag;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;
import org.htmlparser.visitors.HtmlPage;
import org.htmlparser.visitors.TextExtractingVisitor;

import com.jscud.util.LogMan; //一个日志记录类

/**
 * 演示了Html Parse的应用.
 *
 * @author scud http://www.jscud.com
 */

public class ParseHtmlTest
{

    public static void main(String[] args) throws Exception
    {
        String aFile = "e:/jscud/temp/test.htm";

        String content = readTextFile(aFile, "GBK");

        test1(content);
        System.out.println("====================================");

        test2(content);
        System.out.println("====================================");

        test3(content);
        System.out.println("====================================");

        test4(content);
        System.out.println("====================================");

        test5(aFile);
        System.out.println("====================================");

        //访问外部资源,相对慢
        test5("http://www.jscud.com");
        System.out.println("====================================");

    }

    /**
     * 读取文件的方式来分析内容.
     * filePath也可以是一个Url.
     *
     * @param resource 文件/Url
     */
    public static void test5(String resource) throws Exception
    {
        Parser myParser = new Parser(resource);

        //设置编码
        myParser.setEncoding("GBK");

        HtmlPage visitor = new HtmlPage(myParser);

        myParser.visitAllNodesWith(visitor);

        String textInPage = visitor.getTitle();

        System.out.println(textInPage);
    }

    /**
     * 按页面方式处理.对一个标准的Html页面,推荐使用此种方式.
     */
    public static void test4(String content) throws Exception
    {
        Parser myParser;
        myParser = Parser.createParser(content, "GBK");

        HtmlPage visitor = new HtmlPage(myParser);

        myParser.visitAllNodesWith(visitor);

        String textInPage = visitor.getTitle();

        System.out.println(textInPage);
    }

    /**
     * 利用Visitor模式解析html页面.
     *
     * 小优点:翻译了<>等符号
     * 缺点:好多空格,无法提取link
     *  
     */
    public static void test3(String content) throws Exception
    {
        Parser myParser;
        myParser = Parser.createParser(content, "GBK");

        TextExtractingVisitor visitor = new TextExtractingVisitor();

        myParser.visitAllNodesWith(visitor);

        String textInPage = visitor.getExtractedText();

        System.out.println(textInPage);
    }

    /**
     * 得到普通文本和链接的内容.
     *
     * 使用了过滤条件.
     */
    public static void test2(String content) throws ParserException
    {
        Parser myParser;
        NodeList nodeList = null;

        myParser = Parser.createParser(content, "GBK");

        NodeFilter textFilter = new NodeClassFilter(TextNode.class);
        NodeFilter linkFilter = new NodeClassFilter(LinkTag.class);

        //暂时不处理 meta
        //NodeFilter metaFilter = new NodeClassFilter(MetaTag.class);

        OrFilter lastFilter = new OrFilter();
        lastFilter.setPredicates(new NodeFilter[] { textFilter, linkFilter });

        nodeList = myParser.parse(lastFilter);

        Node[] nodes = nodeList.toNodeArray();

        for (int i = 0; i < nodes.length; i++)
        {
            Node anode = (Node) nodes[i];

            String line = "";
            if (anode instanceof TextNode)
            {
                TextNode textnode = (TextNode) anode;
                //line = textnode.toPlainTextString().trim();
                line = textnode.getText();
            }
            else if (anode instanceof LinkTag)
            {
                LinkTag linknode = (LinkTag) anode;

                line = linknode.getLink();
                //@todo 过滤jsp标签:可以自己实现这个函数
                //line = StringFunc.replace(line, "<%.*%>", "");
            }

            if (isTrimEmpty(line))
                continue;

            System.out.println(line);
        }
    }

    /**
     * 解析普通文本节点.
     *
     * @param content
     * @throws ParserException
     */
    public static void test1(String content) throws ParserException
    {
        Parser myParser;
        Node[] nodes = null;

        myParser = Parser.createParser(content, null);

        nodes = myParser.extractAllNodesThatAre(TextNode.class); //exception could be thrown here

        for (int i = 0; i < nodes.length; i++)
        {
            TextNode textnode = (TextNode) nodes[i];
            String line = textnode.toPlainTextString().trim();
            if (line.equals(""))
                continue;
            System.out.println(line);
        }

    }

    /**
     * 读取一个文件到字符串里.
     *
     * @param sFileName  文件名
     * @param sEncode   String
     * @return 文件内容
     */
    public static String readTextFile(String sFileName, String sEncode)
    {
        StringBuffer sbStr = new StringBuffer();

        try
        {
            File ff = new File(sFileName);
            InputStreamReader read = new InputStreamReader(new FileInputStream(ff),
                    sEncode);
            BufferedReader ins = new BufferedReader(read);

            String dataLine = "";
            while (null != (dataLine = ins.readLine()))
            {
                sbStr.append(dataLine);
                sbStr.append("/r/n");
            }

            ins.close();
        }
        catch (Exception e)
        {
            LogMan.error("read Text File Error", e);
        }

        return sbStr.toString();
    }

    /**
     * 去掉左右空格后字符串是否为空
     * @param astr String
     * @return boolean
     */
    public static boolean isTrimEmpty(String astr)
    {
        if ((null == astr) || (astr.length() == 0))
        {
            return true;
        }
        if (isBlank(astr.trim()))
        {
            return true;
        }
        return false;
    }

    /**
     * 字符串是否为空:null或者长度为0.
     * @param astr 源字符串.
     * @return boolean
     */
    public static boolean isBlank(String astr)
    {
        if ((null == astr) || (astr.length() == 0))
        {
            return true;
        }
        else
        {
            return false;
        }
    }

}

添加评论
单击隐藏此项的评论。
A Lexical Analyzer for HTML and Basic SGML

Specs, Drafts, and Reports

See also: abstracts of MIMESGML drafts

In reverse order, by publication/revision date:

A Lexical Analyzer for HTML and Basic SGML
W3C Tech Report on SGML low-level parsing details. Includes flex spec, test file, change log, and source distribution (which is missing filter.c):
 -rw-rw-r--   1 connolly 69          50650 Feb  7 11:59 sgml-lex-19960207.tar.gz
 -rw-rw-r--   1 connolly 69          57182 Feb  7 12:00 sgml-lex-19960207.zip
 21f7b70ec7135531bc84fd4c5e3cdf3d  sgml-lex-19960207.tar.gz (pgp sig)
 083e21759d223b1005402120cdbf8169  sgml-lex-19960207.zip (pgp sig)
 
HTML 2.0 Specification Review Materials Toward a Formalism for Communication on the Web)
unpublished draft by Dan Connolly
 
引自:  http://www.w3.org/MarkUp/SGML/ 
可以做为文献查阅

W3C

HTML Parser and Generator Implementations

This is a sort of "Family Tree" of HTML parser implementations, annotated with notes on features and bugs.

I'm working on updating the HTML parser in our reference code. See: A Lexical Analyzer for HTML and Basic SGML.

See also: HTML Testing and Certification

SGML.c in LibWWW
The first HTML parser ever released was in the library/linemode distribution back in '92 or so. It supported broken markup such as:
<xmp>... </foo> ... </xmp>
<a href=http://foo.bar/>...</a>
  • NCSA Mosaic 2.4 -- didn't use CERN code, but was inspired by it.
    • Spyglass Mosaic -- re-write of NCSA code
    • Netscape -- re-implementation of NCSA code
      • MS IE -- inspired by Netscape
htmllib.py used in grail
Based on regexps. Guido wrote the first web spider, I believe. This parser treats P, LI, DT, DD as empty elements. Nifty formatter code.
SGML Lexical Analyzer

Tools that Write HTML

LaTeX2HTML
Creates documents with missing quotes around the attribute values.

引自:http://www.w3.org/MarkUp/implementations.html

添加评论
单击隐藏此项的评论。
8月11日
PHP的XML分析函数
本文讨论任何用PHP内建的Expat解析器来处理XML文档。通过范例,我将演示Expat的处理方法。同时,范例可以告诉你如何: 

建立你自己的处理函数 
将XML文档转换成你自己的PHP数据结构 

介绍Expat 

XML的解析器,同样称为XML处理器,可以使程序访问XML文档的结构和内容。Expat是PHP脚本语言的XML解析器。它同时也运用在其它项目中,例如Mozilla、Apache和Perl。 

什么是基于事件的解析器? 

XML解析器的两种基本类型: 

基于树型的解析器:将XML文档转换成树型结构。这类解析器分析整篇文章,同时提供一个API来访问所产生树的每个元素。其通用的标准为DOM(文档对象模式)。 
基于事件的解析器:将XML文档视为一系列的事件。当一个特殊事件发生时,解析器将调用开发者提供的函数来处理。 
基于事件的解析器有一个XML文档的数据集中视图,也就是说它集中在XML文档的数据部分,而不是其结构。这些解析器从头到尾处理文档,并将类似于-元素的开始、元素的结尾、特征数据的开始等等-事件通过回调(callback)函数报告给应用程序。以下是一个"Hello-World"的XML文档范例: 

<greeting> 
Hello World 
</greeting> 

基于事件的解析器将报告为三个事件: 

开始元素:greeting 
CDATA项的开始,值为:Hello World 
结束元素:greeting 
不像基于树型的解析器,基于事件的解析器不产生描述文档的结构。在CDATA项中,基于事件的解析器不会让你得到父元素greeting的信息。 
然而,它提供一个更底层的访问,这就使得可以更好地利用资源和更快地访问。通过这种方式,就没有必要将整个文档放入内存;而事实上,整个文档甚至可以大于实际内存值。 


Expat就是这样的一种基于事件的解析器。当然如果使用Expat,必要时它一样可以在PHP中生成完全的原生树结构。 


上面Hello-World的范例包括完整的XML格式。但它是无效的,因为既没有DTD(文档类型定义)与其联系,也没有内嵌DTD。 


对于Expat,这并没有区别:Expat是一个不检查有效性的解析器,因此忽略任何与文档联系的DTD。但应注意的是文档仍然需要完整的格式,否则Expat(和其他符合XML标准的解析器一样)将会随着出错信息而停止。 


作为不检查有效性的解析器,Exapt的快速性和轻巧性使其十分适合互联网程序。 


编译Expat 

Expat可以编译进PHP3.0.6版本(或以上)中。从Apache1.3.9开始,Expat已经作为Apache的一部分。在Unix系统中,通过-with-xml选项配置PHP,你可以将其编译入PHP。 


如果你将PHP编译为Apache的模块,而Expat将默认作为Apache的一部分。在Windows中,你则必须要加载XML动态连接库。 

XML范例:XMLstats 

了解Expat的函数的一个办法就是通过范例。我们所要讨论的范例是使用Expat来收集XML文档的统计数据。 


对于文档中每个元素,以下信息都将被输出: 

该元素在文档中使用的次数 
该元素中字符数据的数量 
元素的父元素 
元素的子元素 
注意:为了演示,我们利用PHP来产生一个结构来保存元素的父元素和子元素 

准备 

用于产生XML解析器实例的函数为xml_parser_create()。该实例将用于以后的所有函数。这个思路非常类似于PHP中MySQL函数的连接标记。在解析文档前,基于事件的解析器通常要求你注册回调函数-用于特定的事件发生时调用。Expat没有例外事件,它定义了如下七个可能事件: 


对象 XML解析函数 描述 

元素 xml_set_element_handler() 元素的开始和结束 

字符数据 xml_set_character_data_handler() 字符数据的开始 

外部实体 xml_set_external_entity_ref_handler() 外部实体出现 

未解析外部实体 xml_set_unparsed_entity_decl_handler() 未解析的外部实体出现 

处理指令 xml_set_processing_instruction_handler() 处理指令的出现 

记法声明 xml_set_notation_decl_handler() 记法声明的出现 

默认 xml_set_default_handler() 其它没有指定处理函数的事件 

所有的回调函数必须将解析器的实例作为其第一个参数(此外还有其它参数)。 


对于本文最后的范例脚本。你需要注意的是它既用到了元素处理函数又用到了字符数据处理函数。元素的回调处理函数通过xml_set_element_handler()来注册。 


这个函数需要三个参数: 

解析器的实例 
处理开始元素的回调函数的名称 
处理结束元素的回调函数的名称 
当开始解析XML文档时,回调函数必须存在。它们必须定义为与PHP手册中所描述的原型一致。 


例如,Expat将三个参数传递给开始元素的处理函数。在脚本范例中,其定义如下: 


function start_element($parser, $name, $attrs) 


第一个参数是解析器标示,第二个参数是开始元素的名称,第三参数为包含元素所有属性和值的数组。 


一旦你开始解析XML文档,Expat在遇到开始元素是都将调用你的start_element()函数并将参数传递过去。 


XML的Case Folding选项 

用xml_parser_set_option()函数将Case folding选项关闭。这个选项默认是打开的,使得传递给处理函数的元素名自动转换为大写。但XML对大小写是敏感的(所以大小写对统计XML文档是非常重要的)。对于我们的范例,case folding选项必须关闭。 


解析文档 

在完成所有的准备工作后,现在脚本终于可以解析XML文档: 

Xml_parse_from_file(),一个自定义的函数,打开参数中指定的文件,并以4kb的大小进行解析 
xml_parse()和xml_parse_from_file()一样,当发生错误时,即XML文档的格式不完全时,将会返回false。 
你可以使用xml_get_error_code()函数来得到最后一个错误的数字代码。将此数字代码传递给xml_error_string()函数即可得到错误的文本信息。 
输出XML当前的行数,使得调试更容易。 
在解析的过程中,调用回调函数。 
描述文档结构 

当解析文档时,对于Expat需要强调问题的是:如何保持文档结构的基本描述? 


如前所述,基于事件的解析器本身并不产生任何结构信息。 


不过标签(tag)结构是XML的重要特性。例如,元素序列<book><title>表示的意思不同于<figure><title>。也就是说,任何作者都会告诉你书名和图名是没有关系的,虽然它们都用到"title"这个术语。因此,为了更有效地使用基于事件的解析器处理XML,你必须使用自己的栈(stacks)或列表(lists)来维护文档的结构信息。 


为了产生文档结构的镜像,脚本至少需要知道目前元素的父元素。用Exapt的API是无法实现的,它只报告目前元素的事件,而没有任何前后关系的信息。因此,你需要建立自己的栈结构。 


脚本范例使用先进后出(FILO)的栈结构。通过一个数组,栈将保存全部的开始元素。对于开始元素处理函数,目前的元素将被array_push()函数推到栈的顶部。相应的,结束元素处理函数通过array_pop()将最顶的元素移走。 


对于序列<book><title></title></book>,栈的填充如下: 

开始元素book:将"book"赋给栈的第一个元素($stack[0])。 
开始元素title:将"title"赋给栈的顶部($stack[1])。 
结束元素title:从栈中将最顶部的元素移去($stack[1])。 
结束元素title:从栈中将最顶部的元素移去($stack[0])。 
PHP3.0通过一个$depth变量手动控制元素的嵌套来实现范例。这就使脚本看起来比较复杂。PHP4.0通过array_pop()和array_push()两个函数来使脚本看起来更简洁。 


收集数据 

为了收集每个元素的信息,脚本需要记住每个元素的事件。通过使用一个全局的数组变量$elements来保存文档中所有不同的元素。数组的项目是元素类的实例,有4个属性(类的变量) 

$count -该元素在文档中被发现的次数 
$chars -元素中字符事件的字节数 
$parents -父元素 
$childs - 子元素 
正如你所看到的,将类实例保存在数组中是轻而易举的。 


注意:PHP的一个特性是你可以通过while(list() = each())loop遍历整个类结构,如同你遍历整个相应的数组一样。所有的类变量(当你用PHP3.0时还有方法名)都以字符串的方式输出。 


当发现一个元素时,我们需要增加其相应的记数器来跟踪它在文档中出现多少次。在相应的$elements项中的记数元素也要加一。 


我们同样要让父元素知道目前的元素是它的子元素。因此,目前元素的名称将会加入到父元素的$childs数组的项目中。最后,目前元素应该记住谁是它的父元素。所以,父元素被加入到目前元素$parents数组的项目中。 


显示统计信息 

剩下的代码在$elements数组和其子数组中循环显示其统计结果。这就是最简单的嵌套循环,尽管输出正确的结果,但代码既不简洁又没有任何特别的技巧,它仅仅是一个你可能每天用他来完成工作的循环。 


脚本范例被设计为通过PHP的CGI方式的命令行来调用。因此,统计结果输出的格式为文本格式。如果你要将脚本运用到互联网上,那么你需要修改输出函数来产生HTML格式。 

总结 

Exapt是PHP的XML解析器。作为基于事件的解析器,它不产生文档的结构描述。但通过提供底层访问,这就使得可以更好地利用资源和更快地访问。 


作为一个不检查有效性的解析器,Expat忽略与XML文档连接的DTD,但如果文档的格式不完整,它将会随着出错信息而停止。 


提供事件处理函数来处理文档 
建立自己的事件结构例如栈和树来获得XML结构信息标记的优点。 
每天都有新的XML程序出现,而PHP对XML的支持也不断加强(例如,增加了支持基于DOM的XML解析器LibXML)。 


有了PHP和Expat,你就可以为即将出现的有效、开放和独立于平台的标准作准备。 

范例 

<? 
/***************************************************************************** 
* 名称:XML解析范例:XML文档信息统计 
* 描述 
* 本范例通过PHP的Expat解析器收集和统计XML文档的信息(例如:每个元素出现的次数、父元素和子元素 
* XML文件作为一个参数 ./xmlstats_PHP4.php3 test.xml 
* $Requires: Expat 要求:Expat PHP4.0编译为CGI模式 
*****************************************************************************/ 

// 第一个参数是XML文件 
$file = $argv[1]; 

// 变量的初始化 
$elements = $stack = array(); 
$total_elements = $total_chars = 0; 

// 元素的基本类 
class element 

var $count = 0; 
var $chars = 0; 
var $parents = array(); 
var $childs = array(); 


// 解析XML文件的函数 
function xml_parse_from_file($parser, $file) 

if(!file_exists($file)) 

die("Can't find file /"$file/"."); 


if(!($fp = @fopen($file, "r"))) 

die("Can't open file /"$file/"."); 


while($data = fread($fp, 4096)) 

if(!xml_parse($parser, $data, feof($fp))) 

return(false); 



fclose($fp); 

return(true); 


// 输出结果函数(方框形式) 
function print_box($title, $value) 

printf("/n+%'-60s+/n", ""); 
printf("|%20s", "$title:"); 
printf("%14s", $value); 
printf("%26s|/n", ""); 
printf("+%'-60s+/n", ""); 


// 输出结果函数(行形式) 
function print_line($title, $value) 

printf("%20s", "$title:"); 
printf("%15s/n", $value); 


// 排序函数 
function my_sort($a, $b) 

return(is_object($a) && is_object($b) ? $b->count - $a->count: 0); 


function start_element($parser, $name, $attrs) 

global $elements, $stack; 

// 元素是否已在全局$elements数组中? 
if(!isset($elements[$name])) 

// 否-增加一个元素的类实例 
$element = new element; 
$elements[$name] = $element; 


// 该元素的记数器加一 
$elements[$name]->count++; 

// 是否有父元素? 
if(isset($stack[count($stack)-1])) 

// 是-将父元素赋给$last_element 
$last_element = $stack[count($stack)-1]; 

// 如果目前元素的父元素数组为空,初始化为0 
if(!isset($elements[$name]->parents[$last_element])) 

$elements[$name]->parents[$last_element] = 0; 


// 该元素的父元素记数器加一 
$elements[$name]->parents[$last_element]++; 

// 如果目前元素的父元素的子元素数组为空,初始化为0 

if(!isset($elements[$last_element]->childs[$name])) 

$elements[$last_element]->childs[$name] = 0; 


// 该元素的父元素的子元素记数器加一 
$elements[$last_element]->childs[$name]++; 


// 将目前的元素加入到栈中 
array_push($stack, $name); 


function stop_element($parser, $name) 

global $stack; 

// 从栈中将最顶部的元素移去 
array_pop($stack); 


function char_data($parser, $data) 

global $elements, $stack, $depth; 

// 增加目前元素的字符数目 
$elements[$stack][count($stack)-1]]->chars += strlen(trim($data)); 


// 产生解析器的实例 
$parser = xml_parser_create(); 

// 设置处理函数 
xml_set_element_handler($parser, "start_element", "stop_element"); 
xml_set_character_data_handler($parser, "char_data"); 
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); 

// 解析文件 
$ret = xml_parse_from_file($parser, $file); 
if(!$ret) 

die(sprintf("XML error: %s at line %d", 
xml_error_string(xml_get_error_code($parser)), 
xml_get_current_line_number($parser))); 


// 释放解析器 
xml_parser_free($parser); 

// 释放协助元素 
unset($elements["current_element"]); 
unset($elements["last_element"]); 

// 根据元素的次数排序 
uasort($elements, "my_sort"); 

// 在$elements中循环收集元素信息 
while(list($name, $element) = each($elements)) 

print_box("Element name", $name); 

print_line("Element count", $element->count); 
print_line("Character count", $element->chars); 

printf("/n%20s/n", "* Parent elements"); 

// 在该元素的父中循环,输出结果 
while(list($key, $value) = each($element->parents)) 

print_line($key, $value); 

if(count($element->parents) == 0) 

printf("%35s/n", "[root element]"); 


// 在该元素的子中循环,输出结果 
printf("/n%20s/n", "* Child elements"); 
while(list($key, $value) = each($element->childs)) 

print_line($key, $value); 

if(count($element->childs) == 0) 

printf("%35s/n", "[no childs]"); 


$total_elements += $element->count; 
$total_chars += $element->chars; 


// 最终结果 
print_box("Total elements", $total_elements); 
print_box("Total characters", $total_chars); 
?>

 
添加评论
单击隐藏此项的评论。
8月9日
HTML解析器
一个html parser的开源项目,jericho-html而且已经到了1.5版本了.
相关连接:
http://sourceforge.net/projects/jerichohtml/ projectpage
http://jerichohtml.sourceforge.net/ homepage
http://freshmeat.net/projects/jerichohtml/ projectpage
http://jerichohtml.sourceforge.net/api/index.html apidoc

作者也是在苦于其他的htmlparser无法满足自己的需求情况下开发的一个,感觉非常不错.
看样子也应该比较成熟了,于是下载看看能否适应我的需求.发现一共只有50个类左右(大部分都是数据bean,比如emlment,tag之类的),不依赖于其他三方的lib,所以可以直接就使用了.看了一下使用的sample:
截取部分代码如下:
import au.id.jericho.lib.html.*;
import java.util.*;
import java.io.*;
import java.net.*;
 
public class DisplayAllElements {
	public static void main(String[] args) throws Exception {
		String sourceUrlString="data/test.html";
		if (args.length==0)
		  System.err.println("Using default argument of /""+sourceUrlString+'"');
		else
			sourceUrlString=args[0];
		if (sourceUrlString.indexOf(':')==-1) sourceUrlString="file:"+sourceUrlString;
		URL sourceUrl=new URL(sourceUrlString);
 		String htmlText=Util.getString(new InputStreamReader(sourceUrl.openStream()));
		Source source=new Source(htmlText);
		source.setLogWriter(new OutputStreamWriter(System.err)); 
                  // send log messages to stderr
		for (Iterator i=source.findAllElements().iterator(); i.hasNext(); ) 
 
{
			Element element=(Element)i.next();
 
			System.out.println("---------------------------------"
                                 +"----------------------------------------------");
			System.out.println(element.getDebugInfo());
			System.out.println(element);
		}
  }
}
 

可以看得出来处理过程先通过一个uri获取到html的字符串,其中uri可能是本地的协议(file://)也可能是其他的协议:http,ftp等方式的.html串以后就把该串传入一个Source类构造出实例来,这个Source的实例有几个方法可以进行内容的访问和标签的遍历(类的方法和api用法见http://jerichohtml.sourceforge.net/api/au/id/jericho/lib/html/Source.html).上述例子里面是使用了:findAllElements(),找出所有的elment.父类Segment里面还有一些可以找到注释等的函数.

其实这个html paser的处理方式类似于dom的解析方式,把整个字串都全部读入内存中,可以随机访问内容的每一个元素和内容.但是针对于海量的html恐怕速度上面会有些问题.

其他的几个开源html parser:

JavaCC HTML Parser by Quiotix Corporation (http://www.quiotix.com/downloads/html-parser/)
GNU GPL licence, expensive licence fee to use in commercial application. Does not support document structure (parses into a flat node stream).

Demonstrational HTML 3.2 parser bundled with JavaCC. Virtually useless.

JTidy (http://jtidy.sourceforge.net/)
Supports document structure, but by its very nature it "tidies" up anything it doesn't like in the source document. On first glance it looks like the positions of nodes in the source are accessible, at least in protected start and end fields in the Node class, but these are pointers into a different buffer and are of no use.

javax.swing.text.html.parser.Parser
Comes standard in the JDK. Supports document structure. Does not track the positions of nodes in the source text, but can be easily modified to do so (although not sure of legal implications of modifications). Requires a DTD to function, but only comes with HTML3.2 DTD which is unsuitable. Even if an HTML 4.01 DTD were found, the parser itself might need tweaking to cater for the new element types. The DTD needs to be in the format of a "bdtd" file, which is a binary format used only by Sun in this parser implementation. I have found many requests for a 4.01 bdtd file in newsgroups etc on the web, but they all reamain unanswered. Building it from scratch is not so easy.

Kizna HTML Parser v1.1 (http://htmlparser.sourceforge.net/)
GNU LGPL licence. Version 1.1 was very simple without support for document structure. I have since revisited this project at sourceforge (early 2004), where version 1.4 is now available. There are now two separate libraries, one with and one without document structure support. It claims to now also be capable of reproducing source text verbatim.

CyberNeko HTML Parser (http://www.apache.org/~andyc/neko/doc/html/index.html)
Apache-style licence. Supports document structure. Based on the very popular Xerces XML parser. At the time of evaluation this parser didn't regenerate the source accurately enough.

 
添加评论
单击隐藏此项的评论。
8月4日
昨天听朋友讨论网络穿透的问题
问题描述:一个internet 用户如何发送数据至公司内网

 

问题分析:公司内部通常采用192。168。0。12 这类私有地址,然后通过一台路由器与internet相连(先不考虑防火墙)

我192.168.0.12向internet请求。www.google.com这个页面,那么路由器会开一个端口来和我192.168.0.12关联,并且它也是通过这个端口 来与www.google.com的http服务器交换数据。最终,网内机器->代理网关->HTTP服务器,在一个会话期间,各自的端口保持了映射关系,特别是代理网关(就是那台路由器)和网内机器的端口映射,使得代理网关不会把接收到的数据向网内转发时,发错了机器。好,现在问题归结为局域网内的机器在网关处,是靠什么技术来实现映射端口,并以此来实现Internet连接。

 

答案是NAT(Network Address Translators),网络地址转换 (Rfc1631 详见http://www.watersprings.org/pub/rfc/rfc1631.txt)

Stun协议(Rfc3489、详见http://www.watersprings.org/pub/rfc/rfc3489.txt)将NAT粗略分为4种类型,即 Full Cone、Restricted Cone、Port Restricted Cone和Symmetric。举个实际例子(例1)来说明这四种NAT的区别:

A机器在私网(192.168.0.4)

NAT服务器(210.21.12.140)

B机器在公网(210.15.27.166)

C机器在公网(210.15.27.140)

现在,A机器连接过B机器,假设是 A(192.168.0.4:5000)-> NAT(转换后210.21.12.140:8000)-> B(210.15.27.166:2000)。

同时A从来没有和C通信过。

则对于不同类型的NAT,有下列不同的结果:

Full Cone NAT:C发数据到210.21.12.140:8000,NAT会将数据包送到A(192.168.0.4:5000)。因为NAT上已经有了192.168.0.4:5000到210.21.12.140:8000的映射。

Restricted Cone:C无法和A通信,因为A从来没有和C通信过,NAT将拒绝C试图与A连接的动作。但B可以通过210.21.12.140:8000与A的 192.168.0.4:5000通信,且这里B可以使用任何端口与A通信。如:210.15.27.166:2001 -> 210.21.12.140:8000,NAT会送到A的5000端口上。

Port Restricted Cone:C无法与A通信,因为A从来没有和C通信过。而B也只能用它的210.15.27.166:2000与A的192.168.0.4:5000通信,因为A也从来没有和B的其他端口通信过。该类型NAT是端口受限的。

Symmetric NAT:上面3种类型,统称为Cone NAT,有一个共同点:只要是从同一个内部地址和端口出来的包,NAT都将它转换成同一个外部地址和端口。但是Symmetric有点不同,具体表现在:只要是从同一个内部地址和端口出来,且到同一个外部目标地址和端口,则NAT也都将它转换成同一个外部地址和端口。但如果从同一个内部地址和端口出来,是到另一个外部目标地址和端口,则NAT将使用不同的映射,转换成不同的端口(外部地址只有一个,故不变)。而且和Port Restricted Cone一样,只有曾经收到过内部地址发来包的外部地址,才能通过NAT映射后的地址向该内部地址发包。

现针对Symmetric NAT举例说明(例2):

A机器连接过B机器,假使是 A(192.168.0.4:5000)-> NAT(转换后210.21.12.140:8000)-> B(210.15.27.166:2000)

如果此时A机器(192.168.0.4:5000)还想连接C机器(210.15.27.140:2000),则NAT上产生一个新的映射,对应的转换可能为A(192.168.0.4:5000)-> NAT(转换后210.21.12.140:8001)-> C(210.15.27.140:2000)。此时,B只能用它的210.15.27.166:2000通过NAT的210.21.12.140: 8000与A的192.168.0.4:5000通信, C也只能用它的210.15.27.140:2000通过NAT的210.21.12.140:8001与A的192.168.0.4:5000通信,而 B或者C的其他端口则均不能和A的192.168.0.4:5000通信。

 

以上引自 http://linux.chinaunix.net/doc/netconf/2005-01-18/817.shtml

所以我们靠NAT来映射端口并实现Internet连接,因此,NAT也直接被称为“端口映射”。端口映射之后,在一个会话期间保持,对于TCP连接是直到连接断开才销毁,而对于UDP,却存在一个不定的生存期,例如2秒(不知道对不对)。

 

好,下面总结一下

因为企业内部通常采用私有地址,所以会启用地址转换NAT/PAT功能。NAT是将私有地址和公有地址一一对应,并在一定时间内保持这种对应关系。对于UDP通信,有两种类型的NAT。PAT则是将私有地址+TCP/UDP端口转换并为一个公有地址+一个TCP/UDP端口,通常会将多个私有地址对应一个公有地址,用不同的TCP/UDP端口进行区分。一些通信协议在穿越NAT/PAT时都会出现问题,因为通常NAT/PAT只对IP包头中的源IP地址/源端口号进行转换,不能相应地修改这些协议包内的源地址,这样协议在回传响应时都按其协议包内的源地址发送,结果就造成无法建立通信。

 

好,现在问题归结为如何穿越NAT实现P2P的通信 这个网上的资料比较多

觉得最后价值的是以下几篇

http://serious-code.net/moin.cgi/NatAndP2pApplications

http://www.brynosaurus.com/pub/net/p2pnat/

http://www.watersprings.org/pub/id/draft-takeda-symmetric-nat-traversal-00.txt

 

P2P之UDP穿透NAT的原理与实现(附源代码) http://www.ppcn.net/n1306c2.aspx

P2P之UDP穿透NAT的原理与实现--增强篇(附源代码) http://www.ppcn.net/n2422c38.aspx

 

 

 

添加评论
单击隐藏此项的评论。
不同局域网内经Internet的P2P通信技术总结
不同局域网内经Internet的P2P通信技术总结 作者:佚名 文章来源:SNS 社区 点击数:114 更新时间:2005-8-1 以下将要用到一个叫做NAT的重要名词,先做点解释。 NAT是Net Address Translation(网络地址转换)的简称,就是说,局域网通常靠一个具有公网IP的代理网关服务器连到Internet共享上网。局域网内的机器并不具备公网IP地址,它只有内网地址,假设它要和Internet上的HTTP服务器通信,代理网关便会新建一个端口来和这个网内机器关联,并通过这个端口来和HTTP服务器交换数据。最终,网内机器->代理网关->HTTP服务器,在一个会话期间,各自的端口保持了映射关系,特别是代理网关和网内机器的端口映射,使得代理网关不会把接收到的数据向网内转发时,发错了机器。 局域网内的机器在网关处,就是靠NAT来映射端口并实现Internet连接,因此,NAT也直接被称为“端口映射”。端口映射之后,在一个会话期间保持,对于TCP连接是直到连接断开才销毁,而对于UDP,却存在一个不定的生存期,例如2秒。 如果两台机器A和B,分别处于两个局域网内,它们要通过Internet通信,这就是P2P(点到点)连接通信。 目前的Internet使用IPv4协议,采用32位IP地址,主要被用来进行C/S形式的通信,需要共享的资源集中放于Internet服务器上。IPv4对于P2P分布式资源共享的支持,极不友好。首先,32位IP地址已经不敷使用,公网IP地址日趋紧张,只能使用局域网共享公网IP的方式,局域网正是为了临时应对IP耗尽而出现的,长远的解决办法是研究IPv6。其次,分别处于两个局域网内的机器要通信,由于对方没有公网IP,直接呼叫对方是不可能的,必须借助第三方“中介”(机器或者软件)间接地连通,解决办法下列几种: 第一:实现局域网内的数据链路层协议,就是写一个类似于TCP/IP的协议,由它来代替Windows系统里的TCP/IP协议,由它直接基于网卡硬件获取数据。这是十分复杂的。 第二:用Internet上的公网服务器中转数据,但对于大数据量的中转,显然受到服务器和网络的负载极限的限制。 第三:依靠Internet上的公网服务器做“媒人”,将这两台分别处于不同局域网的机器相互介绍给对方,在它们建立连接之后,服务器即脱离关系。这种方式下,服务器把A的NAT端口映射关系告诉B,又把B的NAT端口映射关系告诉A,这样AB相互知道对方的端口映射关系之后,就能建立连接。因为A和B各自的端口映射关系是靠各自的代理网关动态建立的,动态建立的映射端口不得不告知对方。 第四:上面的第三种办法,也可以采用静态端口映射方式,这样就不需要中介服务器对A和B做介绍。在各方的代理网关上,可以在代理工具里将某个端口(如1350)和局域网内的某台机器(如内网IP为200.200.200.100,端口1360)做好静态映射,这样,代理网关会自动地将出入于1350端口的数据发往200.200.200.100的1360端口。当然,通信之前,必须对对方的端口映射关系做配置。有多少台网内机器要通信,就得映射多少个不同的端口,同时在另一个局域网内的机器就要做多少个配置。在局域网内搭建HTTP、FTP等服务器就是通过静态映射端口来实现的,这个端口一般不是HTTP、FTP的默认80和23,所以对这类站点的访问往往会在URL里加上端口号。 由此可见,上述前两种办法在简单应用中是不可取的,只有后两种可行。它们又各有缺点,第三种动态映射端口,需要增加中间服务器,第四种静态映射端口,在需要通信的各方机器很多的情况下,做手工端口映射和配置都是很繁琐的,并且一方添加一台机器,就需要在其余对方增加配置。 采用动态和静态相结合的办法是可以推想的,然而其可行性还必须经过测试。可以这样设计,为了让所有通信机器彼此知晓并定位。我们可以在局域网里,只对一台机器在代理网关处做静态端口映射,本局域网内的机器都向它登记。而两个局域网各自只做一项对对方的映射配置。两个局域网之间,没有静态映射端口的机器要通信,就靠有映射的机器来担当“介绍”。 就局域网和NAT的问题实际上还很多,比如各自的局域网的结构不同,局域网里可能又有子局域网,局域网可能是NAT代理结构,但也可能是HTTP代理,Sock4、Sock5代理等结构,NAT又分严格的和非严格NAT,严格NAT限制很多,更不便于P2P。不过,软件不能实现的地方,可以考虑改变硬件结构,例如将严格NAT变为非严格NAT。如果硬件改变不得,那么Internet整体上就有10%的系统不能实现P2P,除非等到正处于研发的IPv6协议出来。 P2P要解决的唯一技术难题是如何发现、定位和寻址对方,就是如何穿透NAT、HTTP、Sock等代理和如何穿透防火墙找到对方并建立起通信的问题。由于绝大多数局域网是NAT代理结构,所以前面对NAT论述比较详细,也是网上讨论最多的话题,相比之下,穿透Http、Sock代理就简单一些。此外,穿透NAT发现对等点的办法还有一些,例如多播,但由于现有Internet对多播并不友好,同时多播是无连接和不可靠的,其实现有难度。 许多软件都是按照上述一些技术实现了P2P通信,著名的有MSN、QQ和BitTorrent下载软件等。 实际上,围绕P2P通信,尤其是两个不同局域网间的P2P,已经有许多的P2P协议和开发包涌现。例如,Sun公司以Java写的开发包Jxta,微软在Windows XP平台上有P2P的β版开发包,Intel公布.Net平台上的P2P应用开发工具包,放到微软有关.Net平台的新闻站点www.gotdotnet.com上供用户免费下载。 但是利用它们来开发程序,非常繁琐,我们需要用简单的实现完成功能就可以了。 如果想研究得更深入仔细,请从Sun公司的网站和微软网站下载开发包,或者在Google里 搜索协议和开发包。 下面其实有两个实例,讲述连通的过程,包括简单伪代码。 我们不希望在IP层实现我们的P2P,而是希望在应用层,利用Windows提供的Socket建立P2P,至多下到用原始Raw Socket来写P2P。 首先看,我们对于公网有服务器做“中介(非中转)”的P2P怎么实现。 原理讲述: 例如AB两台机器分别处于两个不同的局域网后,由Server做中介,先看连接过程。 A首先连接服务器,采用UDP发包给Server,这个包包括了A的用户信息,类似于QQ的 QQ号、呢称等。Server方,可以用CSocket::GetPeerName()得到A的IP及端口,但得到的IP和端口应该是A的代理网关的公网PublicIP及其映射端口NatPort,该映射端口就是A的代理网关为A的本次UDP通信临时分配的Nat端口。可以断言,得到的端口一定不是A的内网IP和内网UDP端口。 服务器然后将A的公网IP、映射端口、用户信息等保存到(内存列表或者数据库),这样标志着A已经上线。服务器马上将其它在线的用户信息发回给A,包括其它用户的代理网关的公网IP及Nat端口。A同样将在线用户的这些信息保存并显示为列表,期待A用户做出选择。 对于B,同样有上述的上线过程。 当A用户做出选择,要和在线的B用户通信时,A首先发UDP包给B的公网IP及Nat端口,并立即发一个UDP包给服务器,让服务器去通知B,叫B给A也发一个UDP包。 换句话说,1、A发包给PublicB, 2、A发包给Server,3、Server发包给PublicB,4、B发包给PublicA。 上面的叙述用到了"Public"字样,它代表代理网关的公网IP及其映射端口。 由于A和B各自的网关都保存了各自的端口映射关系,发到网关的数据,网关会按照这个映射关系转发给A和B。 当A和B都分别收到对方发来的UDP包以后,连接宣告成功,服务器即可以脱离,AB即可以用UDP通信。 何以如此麻烦? A在发UDP包给Server上线时,A的网关(A.Gate)就分配一个Nat端口(A.NatPort)给A,用于A和Server间的本次UDP会话,但A的网关明确标记,这个Nat端口,仅能用于A和Server之间的UDP通信,不能挪着它用。并且,这个临时分配的端口,只能保持一个很短的时效,也许是一两秒吧。这个时间内,如果A与Server没有任何通信,那么这个映射端口就宣告无效。下次,A和Server又要通信时,A的网关又会重新分配一个新的端口。这段表明三点: 1、A与Server的通信,需要A网关分配Nat端口来中转。 2、Nat端口只能用于A和Server间的通信。 3、Nat端口存在生存期,长时间A和Server无通信,该端口即宣告无效。 就是这些麻烦,使得我们的连接过程必须绕很多弯。 A和B的通信,就是借助事先AB分别与Server连接时,在各自的网关处建立的端口映射来通信。为避免上面的2、3点麻烦,A和B在初次连接时,必须几乎同时向对方发包。 如果A、B不同时发包给对方,它们各自的网关就会虑掉对方的包,因为该包不是Server发来的包,叫做不请自来的包。 并且,即便AB各自的网关不虑掉非Server发来的包,它们各自的Nat端口也有一个时效。那么A与Server,B与Server就不得不发心跳包,以维持各自的映射端口,保证其不失效。 上面的过程中,如果A和B建立连接失败,可以循环这个过程,直到一个有限的次数之后,仍不能连接则宣告失败。本文一部分的原文如下: Clients Behind Different NATs Suppose clients A and B both have private IP addresses and lie behind different network address translators. The peer-to-peer application running on clients A and B and on server S each use UDP port 1234. A and B have each initiated UDP communication sessions with server S, causing NAT A to assign its own public UDP port 62000 for A's session with S, and causing NAT B to assign its port 31000 to B's session with S, respectively. Server S 18.181.0.31:1234 | | +----------------------+----------------------+ | | NAT A NAT B 155.99.25.11:62000 138.76.29.7:31000 | | | | Client A Client B 10.0.0.1:1234 10.1.1.3:1234 Now suppose that client A wants to establish a UDP communication session directly with client B. If A simply starts sending UDP requests to B's public address, 138.76.29.7:31000, then NAT B will typically discard these incoming messages because the source address and port number does not match those of S, with which the original outgoing session was established. Similarly, if B simply starts sending UDP requests to A's public address, then NAT A will discard these messages. Suppose A starts sending UDP requests to B's public address, however, and simultaneously relays a request through server S to B, asking B to start sending UDP requests to A's public address. A's outgoing messages directed to B's public address (138.76.29.7:31000) will cause NAT A to open up a new communication session between A's private address and B's public address. At the same time, B's messages to A's public address (155.99.25.11:62000) will cause NAT B to open up a new communication session between B's private address and A's public address. Once the new UDP sessions have been opened up in each direction, client A and B can communicate with each other directly without further reference to or burden on the "introduction" server S 大致就如此。以下是上述过程的一些伪代码,不太容易懂,不如看前面的论述。 下面将讨论,A和B同时发包的另外一个办法,以及关于静态端口映射、谁能做"中介"、 是否可以建立TCP通信等细节及其引申。 const TCHAR* ServerIP = _T("61.10.10.10"); //中介服务器,事先配置的 const UINT ServerPort = 4500; //中介服务器UDP端口,事先配置的 TCHAR PubIP[256]; //对方机器的代理网关的公网IP UINT PubPort; //对方机器的代理网关的公网映射端口 UINT ClientPort = 4501; //A和B的UDP端口 连接服务器用UDP,下面是客户方(AB的Socket) class CClientSocket : public CSocket { public: virtual void OnReceive( int nErrCode ); } void CClientSocket::OnReceive( int nErrCode ) { int nFlag, *pFlag; //UDP包标志,位于包头4个字节 TCHAR PeerIP[128]; //UDP对方的IP UINT PeerPort; //UDP对方的端口 char RecBuf[64]; //假定包尺寸为64 //此处分析出对方IP和端口,即PeerIP和PeerPort SOCKADDR sa; int SockAddrLen = sizeof(sa); GetPeerName( &sa, &SockAddrLen ); //从sa中取出PeerIP和PeerPort Receive( RecBuf, 64 ); pFlag = (int*)RecBuf; nFlag = *pFlag; if( lstrcmp( PeerIP, ServerIP ) == 0 ) //如果是服务器返回的信息 { switch( nFlag ) { case 0: //标识和服务器连接成功,RecBuf是服务器返回的其它在线用户 //的信息(包括对方的公网IP及端口),这里假定是B的信息 //从RecBuf取出B的代理网关的IP和端口放入PeerIP和PeerPort lstrcpy( PubIP, PeerIP ); //PeerIP应该是对方的代理网关的公网IP PubPort = PeerPort; //同时PeerPort应该是对方的代理网关的公网映射端口 //接下来,给B的代理公网IP发一个UDP包,填充RecBuf,并使标志为0 SendTo( RecBuf, 64, PubPort, PubIP ); //马上叫服务器通知B,要B给A发一个UDP包,填充RecBuf,并使标志为1 SendTo( RecBuf, 64, ServerPort, ServerIP ); break; case 1: //标识是来自服务器的通知,叫我(B)发一个UDP给A //RecBuf里有A的代理公网IP和端口,取出来,放入PeerIP和PeerPort lstrcpy( PubIP, PeerIP ); //PeerIP应该是对方的代理网关的公网IP PubPort = PeerPort; //同时PeerPort应该是对方的代理网关的公网映射端口 //给A发一个UDP包,填充RecBuf,并使标志为1 SendTo( RecBuf, 64, PubPort, PubIP ); break; . . . default: break; } } else //其它对等客户返回的信息 { switch( nFlag ) { case 0: //标识直接收到A发的UDP包 break; case 1: //标识是B发回的UDP包,但,是靠服务器通知B发的 //至此,可以判断AB互相是否连接成功,就是判断A发给B的UDP包B收到,而B发 //给A的UDP包A也收到,那么连接就是成功的。否则重复上面的过程。 break; . . . default: break; } } } 客户方先连接服务器,服务器收到后,返回所有在线的对等点用户信息 CClientSocket cs; char sBuf[64]; //sBuf的包标识为0,你可以填充其它任何信息,比如用户信息 cs.Create( ClientPort, SOCK_DGRAM ); cs.SendTo( sBuf, 64, ServerPort, ServerIP ); class CServerSocket : public CSocket { public: virtual void OnReceive( int nErrCode ); } void CServerSocket::OnReceive( int nErrCode ) { int nFlag, *pFlag; //UDP包标志,位于包头4个字节 TCHAR PeerIP[128]; //UDP对方的IP UINT PeerPort; //UDP对方的端口 char RecBuf[64]; //假定包尺寸为64 //此处分析出对方IP和端口,即PeerIP和PeerPort SOCKADDR sa; int SockAddrLen = sizeof(sa); GetPeerName( &sa, &SockAddrLen ); //从sa中取出PeerIP和PeerPort Receive( RecBuf, 64 ); pFlag = (int*)RecBuf; nFlag = *pFlag; switch( nFlag ) { case 0: //标识有客户连接 //给该客户返回所有在线用户 break; case 1: //来自A的通知,要我通知B,叫B发一个UDP包给A的公网IP //从RecBuf中取出B的IP和端口放入PeerIP和PeerPort,填充RecBuf,标志为1 SendTo( RecBuf, 64, PeerPort, PeerIP ); break; } }
 
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值