转载于http://www.phppan.com/2009/12/zend-hashtable/

在PHP的Zend引擎中,有一个数据结构非常重要,它无处不在,是PHP数据存储的核心,各种常量、变量、函数、类、对象等都用它来组织,这个数据结构就是HashTable。

HashTable在通常的数据结构教材中也称作散列表或者哈希表。

哈希表的定义是:散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

哈希表基本原理比较简单,但PHP的实现有其独特的地方。理解了HashTable的数据存储结构,对我们分析PHP的源代码,特别是Zend Engine中的虚拟机的实现时,有很重要的帮助。它可以帮助我们在大脑中模拟一个完整的虚拟机的形象。它也是PHP中其它一些数据结构如数组实现的基 础。

Zend HashTable的实现结合了双向链表和向量(数组)两种数据结构的优点,为PHP提供了非常高效的数据存储和查询机制。

Let’s begin!

一、 HashTable的数据结构

在Zend Engine中的HashTable的实现代码主要包括zend_hash.h, zend_hash.c这两个文件中。Zend HashTable包括两个主要的数据结构,其一是Bucket(桶)结构,另一个是HashTable结构。Bucket结构是用于保存数据的容器,而 HashTable结构则提供了对所有这些Bucket(或桶列)进行管理的机制。

  1. typedef struct bucket {
  2. ulong h; /* Used for numeric indexing */
  3. uint nKeyLength; /* key 长度 */
  4. void *pData; /* 指向Bucket中保存的数据的指针 */
  5. void *pDataPtr; /* 指针数据 */
  6. struct bucket *pListNext; /* 指向HashTable桶列中下一个元素 */
  7. struct bucket *pListLast; /* 指向HashTable桶列中前一个元素 */
  8. struct bucket *pNext; /* 指向具有同一个hash值的桶列的后一个元素 */
  9. struct bucket *pLast; /* 指向具有同一个hash值的桶列的前一个元素 */
  10. char arKey[1]; /* 必须是最后一个成员,key名称*/
  11. } Bucket;

在Zend HashTable中,每个数据元素(Bucket)有一个键名(key),它在整个HashTable中是唯一的,不能重复。根据键名可以唯一确定 HashTable中的数据元素。键名有两种表示方式。第一种方式使用字符串arKey作为键名,该字符串的长度为nKeyLength。注意到在上面的 数据结构中arKey虽然只是一个长度为1的字符数组,但它并不意味着key只能是一个字符。实际上Bucket是一个可变长的结构体,由于arKey是 Bucket的最后一个成员变量,通过arKey与nKeyLength结合可确定一个长度为nKeyLength的key。这是C语言编程中的一个比较 常用的技巧。另一种键名的表示方式是索引方式,这时nKeyLength总是0,长整型字段h就表示该数据元素的键名。简单的来说,即如果 nKeyLength=0,则键名为h;否则键名为arKey, 键名的长度为nKeyLength。

当nKeyLength > 0时,并不表示这时的h值就没有意义。事实上,此时它保存的是arKey对应的hash值。不管hash函数怎么设计,冲突都是不可避免的,也就是说不同 的arKey可能有相同的hash值。具有相同hash值的Bucket保存在HashTable的arBuckets数组(参考下面的解释)的同一个索 引对应的桶列中。这个桶列是一个双向链表,其前向元素,后向元素分别用pLast, pNext来表示。新插入的Bucket放在该桶列的最前面。

在Bucket中,实际的数据是保存在pData指针指向的内存块中,通常这个内存块是系统另外分配的。但有一种情况例外,就是当Bucket保存 的数据是一个指针时,HashTable将不会另外请求系统分配空间来保存这个指针,而是直接将该指针保存到pDataPtr中,然后再将pData指向 本结构成员的地址。这样可以提高效率,减少内存碎片。由此我们可以看到PHP HashTable设计的精妙之处。如果Bucket中的数据不是一个指针,pDataPtr为NULL。

HashTable中所有的Bucket通过pListNext, pListLast构成了一个双向链表。最新插入的Bucket放在这个双向链表的最后。

注意在一般情况下,Bucket并不能提供它所存储的数据大小的信息。所以在PHP的实现中,Bucket中保存的数据必须具有管理自身大小的能力。

  1. typedef struct _hashtable {
  2. uint nTableSize;
  3. uint nTableMask;
  4. uint nNumOfElements;
  5. ulong nNextFreeElement;
  6. Bucket *pInternalPointer;
  7. Bucket *pListHead;
  8. Bucket *pListTail;
  9. Bucket **arBuckets;
  10. dtor_func_t pDestructor;
  11. zend_bool persistent;
  12. unsigned char nApplyCount;
  13. zend_bool bApplyProtection;
  14.  
  15. #if ZEND_DEBUG
  16. int inconsistent;
  17. #endif
  18. } HashTable;

在HashTable结构中,nTableSize指定了HashTable的大小,同时它限定了HashTable中能保存Bucket的最大数量,此 数越大,系统为HashTable分配的内存就越多。为了提高计算效率,系统自动会将nTableSize调整到最小一个不小于nTableSize的2 的整数次方。也就是说,如果在初始化HashTable时指定一个nTableSize不是2的整数次方,系统将会自动调整nTableSize的值。即

nTableSize = 2ceil(log(nTableSize, 2)) 或 nTableSize = pow(ceil(log(nTableSize,2)))

例如,如果在初始化HashTable的时候指定nTableSize = 11,HashTable初始化程序会自动将nTableSize增大到16。

arBuckets是HashTable的关键,HashTable初始化程序会自动申请一块内存,并将其地址赋值给arBuckets,该内存大 小正好能容纳nTableSize个指针。我们可以将arBuckets看作一个大小为nTableSize的数组,每个数组元素都是一个指针,用于指向 实际存放数据的Bucket。当然刚开始时每个指针均为NULL。

nTableMask的值永远是nTableSize – 1,引入这个字段的主要目的是为了提高计算效率,是为了快速计算Bucket键名在arBuckets数组中的索引。

nNumberOfElements记录了HashTable当前保存的数据元素的个数。当nNumberOfElement大于nTableSize时,HashTable将自动扩展为原来的两倍大小。

nNextFreeElement记录HashTable中下一个可用于插入数据元素的arBuckets的索引。

pListHead, pListTail则分别表示Bucket双向链表的第一个和最后一个元素,这些数据元素通常是根据插入的顺序排列的。也可以通过各种排序函数对其进行重 新排列。pInternalPointer则用于在遍历HashTable时记录当前遍历的位置,它是一个指针,指向当前遍历到的Bucket,初始值是 pListHead。

pDestructor是一个函数指针,在HashTable的增加、修改、删除Bucket时自动调用,用于处理相关数据的清理工作。

persistent标志位指出了Bucket内存分配的方式。如果persisient为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数。具体请参考PHP的内存管理。

nApplyCount与bApplyProtection结合提供了一个防止在遍历HashTable时进入递归循环时的一种机制。

inconsistent成员用于调试目的,只在PHP编译成调试版本时有效。表示HashTable的状态,状态有四种:

状态值 含义
HT_IS_DESTROYING 正在删除所有的内容,包括arBuckets本身
HT_IS_DESTROYED 已删除,包括arBuckets本身
HT_CLEANING 正在清除所有的arBuckets指向的内容,但不包括arBuckets本身
HT_OK 正常状态,各种数据完全一致

  1. typedef struct _zend_hash_key {
  2. char *arKey; /* hash元素key名称 */
  3. uint nKeyLength; /* hash 元素key长度 */
  4. ulong h; /* key计算出的hash值或直接指定的数值下标 */
  5. } zend_hash_key;

现在来看zend_hash_key结构就比较容易理解了。它通过arKey, nKeyLength, h三个字段唯一确定了HashTable中的一个元素。

根据上面对HashTable相关数据结构的解释,我们可以画出HashTable的内存结构图:

hashtable结构

二、 Zend HashTable的实现

本节具体介绍一下PHP中HashTable的实现。以下函数均取自于zend_hash.c。只要充分理解了上述数据结构,HashTable实现的代码并不难理解。

1 HashTable初始化

HashTable提供了一个zend_hash_init宏来完成HashTable的初始化操作。实际上它是通过下面的内部函数来实现的:

  1. ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, hash_func_t pHashFunction, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC)
  2. {
  3. uint i = 3;
  4. Bucket **tmp;
  5.  
  6. SET_INCONSISTENT(HT_OK);
  7.  
  8. if (nSize >= 0×80000000) {
  9. /* prevent overflow */
  10. ht->nTableSize = 0×80000000;
  11. } else {
  12. while ((1U << i) < nSize) { /* 自动调整nTableSize至2的n次方 */ i++; } ht->nTableSize = 1 << i; /* i的最小值为3,因此HashTable大小最小为8 */ } ht->nTableMask = ht->nTableSize - 1;
  13. ht->pDestructor = pDestructor;
  14. ht->arBuckets = NULL;
  15. ht->pListHead = NULL;
  16. ht->pListTail = NULL;
  17. ht->nNumOfElements = 0;
  18. ht->nNextFreeElement = 0;
  19. ht->pInternalPointer = NULL;
  20. ht->persistent = persistent;
  21. ht->nApplyCount = 0;
  22. ht->bApplyProtection = 1;
  23.  
  24. /* 根据persistent使用不同方式分配arBuckets内存,并将其所有指针初始化为NULL*/
  25. /* Uses ecalloc() so that Bucket* == NULL */
  26. if (persistent) {
  27. tmp = (Bucket **) calloc(ht->nTableSize, sizeof(Bucket *));
  28. if (!tmp) {
  29. return FAILURE;
  30. }
  31. ht->arBuckets = tmp;
  32. } else {
  33. tmp = (Bucket **) ecalloc_rel(ht->nTableSize, sizeof(Bucket *));
  34. if (tmp) {
  35. ht->arBuckets = tmp;
  36. }
  37. }
  38.  
  39. return SUCCESS;
  40. }

在以前的版本中,可以使用pHashFunction来指定hash函数。但现PHP已强制使用DJBX33A算法,因此实际上pHashFunction这个参数并不会用到,保留在这里只是为了与以前的代码兼容。

2 增加、插入和修改元素

向HashTable中添加一个新的元素最关键的就是要确定将这个元素插入到arBuckets数组中的哪个位置。根据上面对Bucket结构键名 的解释,我们可以知道有两种方式向HashTable添加一个新的元素。第一种方法是使用字符串作为键名来插入Bucket;第二种方法是使用索引作为键 名来插入Bucket。第二种方法具体又可以分为两种情况:指定索引或不指定索引,指定索引指的是强制将Bucket插入到指定的索引位置中;不指定索引 则将Bucket插入到nNextFreeElement对应的索引位置中。这几种插入数据的方法实现比较类似,不同的只是定位Bucket的方法。

修改HashTable中的数据的方法与增加数据的方法也很类似。

我们先看第一种使用字符串作为键名增加或修改Bucket的方法:

  1. ZEND_API int _zend_hash_add_or_update(HashTable *ht, char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC)
  2. {
  3. ulong h;
  4. uint nIndex;
  5. Bucket *p;
  6.  
  7. IS_CONSISTENT(ht); // 调试信息输出
  8.  
  9. if (nKeyLength <= 0) { #if ZEND_DEBUG ZEND_PUTS(”zend_hash_update: Can’t put in empty key\n”); #endif return FAILURE; } /* 使用hash函数计算arKey的hash值 */ h = zend_inline_hash_func(arKey, nKeyLength); /* 将hash值和nTableMask按位与后生成该元素在arBuckets中的索引。让它和 * nTableMask按位与是保证不会产生一个使得arBuckets越界的数组下标。 */ nIndex = h & ht->nTableMask;
  10.  
  11. p = ht->arBuckets[nIndex]; /* 取得相应索引对应的Bucket的指针 */
  12.  
  13. /* 检查对应的桶列中是否包含有数据元素(key, hash) */
  14. while (p != NULL) {
  15. if ((p->h == h) && (p->nKeyLength == nKeyLength)) {
  16. if (!memcmp(p->arKey, arKey, nKeyLength)) {
  17. if (flag & HASH_ADD) {
  18. return FAILURE; // 对应的数据元素已存在,不能进行插入操作
  19. }
  20. HANDLE_BLOCK_INTERRUPTIONS();
  21. #if ZEND_DEBUG
  22. if (p->pData == pData) {
  23. ZEND_PUTS(”Fatal error in zend_hash_update: p->pData == pData\n”);
  24. HANDLE_UNBLOCK_INTERRUPTIONS();
  25. return FAILURE;
  26. }
  27. #endif
  28. if (ht->pDestructor) {
  29. /* 如果数据元素存在,对原来的数据进行析构操作 */
  30. ht->pDestructor(p->pData);
  31. }
  32. /* 用新的数据来更新原来的数据 */
  33. UPDATE_DATA(ht, p, pData, nDataSize);
  34. if (pDest) {
  35. *pDest = p->pData;
  36. }
  37. HANDLE_UNBLOCK_INTERRUPTIONS();
  38. return SUCCESS;
  39. }
  40. }
  41. p = p->pNext;
  42. }
  43.  
  44. /* HashTable中没有key对应的数据,新增一个Bucket */
  45. p = (Bucket *) pemalloc(sizeof(Bucket) - 1 + nKeyLength, ht->persistent);
  46. if (!p) {
  47. return FAILURE;
  48. }
  49. memcpy(p->arKey, arKey, nKeyLength);
  50. p->nKeyLength = nKeyLength;
  51. INIT_DATA(ht, p, pData, nDataSize);
  52. p->h = h;
  53. // 将Bucket加入到相应的桶列中
  54. CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]);
  55. if (pDest) {
  56. *pDest = p->pData;
  57. }
  58.  
  59. HANDLE_BLOCK_INTERRUPTIONS();
  60. // 将Bucket 加入到HashTable的双向链表中
  61. CONNECT_TO_GLOBAL_DLLIST(p, ht);
  62. ht->arBuckets[nIndex] = p;
  63. HANDLE_UNBLOCK_INTERRUPTIONS();
  64.  
  65. ht->nNumOfElements++;
  66. // 如果HashTable已满,重新调整HashTable的大小。
  67. ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* If the Hash table is full, resize it */
  68. return SUCCESS;
  69. }

因为这个函数是使用字符串作为键名来插入数据的,因此它首先检查nKeyLength的值是否大于0,如果不是的话就直接退出。然后计算arKey对应的 hash值h,将其与nTableMask按位与后得到一个无符号整数nIndex。这个nIndex就是将要插入的Bucket在arBuckets数 组中的索引位置。
现在已经有了arBuckets数组的一个索引,我们知道它包括的数据是一个指向Bucket的双向链表的指针。如果这个双向链表不为空的话我们首先检查 这个双向链表中是否已经包含了用字符串arKey指定的键名的Bucket,这样的Bucket如果存在,并且我们要做的操作是插入新Bucket(通过 flag标识),这时就应该报错 – 因为在HashTable中键名不可以重复。如果存在,并且是修改操作,则使用在HashTable中指定了析构函数pDestructor对原来的 pData指向的数据进行析构操作;然后将用新的数据替换原来的数据即可成功返回修改操作。
如果在HashTable中没有找到键名指定的数据,就将该数据封装到Bucket中,然后插入HashTable。这里要注意的是如下的两个宏:
CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex])
CONNECT_TO_GLOBAL_DLLIST(p, ht)
前者是将该Bucket插入到指定索引的Bucket双向链表中,后者是插入到整个HashTable的Bucket双向链表中。两者的插入方式也不同,前者是将该Bucket插入到双向链表的最前面,后者是插入到双向链表的最末端。

下面是第二种插入或修改Bucket的方法,即使用索引的方法:

  1. ZEND_API int _zend_hash_index_update_or_next_insert(HashTable *ht, ulong h, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC)
  2. {
  3. uint nIndex;
  4. Bucket *p;
  5.  
  6. IS_CONSISTENT(ht);
  7.  
  8. if (flag & HASH_NEXT_INSERT) {
  9. h = ht->nNextFreeElement;
  10. }
  11. nIndex = h & ht->nTableMask;
  12.  
  13. p = ht->arBuckets[nIndex];
  14.  
  15. // 检查是否含有相应的数据
  16. while (p != NULL) {
  17. if ((p->nKeyLength == 0) && (p->h == h)) {
  18. if (flag & HASH_NEXT_INSERT || flag & HASH_ADD) {
  19. return FAILURE;
  20. }
  21. //
  22. // …… 修改Bucket数据,略
  23. //
  24. if ((long)h >= (long)ht->nNextFreeElement) {
  25. ht->nNextFreeElement = h + 1;
  26. }
  27. if (pDest) {
  28. *pDest = p->pData;
  29. }
  30. return SUCCESS;
  31. }
  32. p = p->pNext;
  33. }
  34. p = (Bucket *) pemalloc_rel(sizeof(Bucket) - 1, ht->persistent);
  35. if (!p) {
  36. return FAILURE;
  37. }
  38. p->nKeyLength = 0; /* Numeric indices are marked by making the nKeyLength == 0 */
  39. p->h = h;
  40. INIT_DATA(ht, p, pData, nDataSize);
  41. if (pDest) {
  42. *pDest = p->pData;
  43. }
  44.  
  45. CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]);
  46.  
  47. HANDLE_BLOCK_INTERRUPTIONS();
  48. ht->arBuckets[nIndex] = p;
  49. CONNECT_TO_GLOBAL_DLLIST(p, ht);
  50. HANDLE_UNBLOCK_INTERRUPTIONS();
  51.  
  52. if ((long)h >= (long)ht->nNextFreeElement) {
  53. ht->nNextFreeElement = h + 1;
  54. }
  55. ht->nNumOfElements++;
  56. ZEND_HASH_IF_FULL_DO_RESIZE(ht);
  57. return SUCCESS;
  58. }

flag标志指明当前操作是HASH_NEXT_INSERT(不指定索引插入或修改), HASH_ADD(指定索引插入)还是HASH_UPDATE(指定索引修改)。由于这些操作的实现代码基本相同,因此统一合并成了一个函数,再用flag加以区分。
本函数基本与前一个相同,不同的是如果确定插入到arBuckets数组中的索引的方法。如果操作是HASH_NEXT_INSERT,则直接使用nNextFreeElement作为插入的索引。注意nNextFreeElement的值是如何使用和更新的。
3 访问元素
同样,HashTable用两种方式来访问元素,一种是使用字符串arKey的zend_hash_find();另一种是使用索引的访问方式zend_hash_index_find()。由于其实现的代码很简单,分析工作就留给读者自已完成。
4 删除元素
HashTable删除数据均使用zend_hash_del_key_or_index()函数来完成,其代码也较为简单,这里也不再详细分析。需要的是注意如何根据arKey或h来计算出相应的下标,以及两个双向链表的指针的处理。
5 遍历元素

  1. /* This is used to recurse elements and selectively delete certain entries
  2. * from a hashtable. apply_func() receives the data and decides if the entry
  3. * should be deleted or recursion should be stopped. The following three
  4. * return codes are possible:
  5. * ZEND_HASH_APPLY_KEEP - continue
  6. * ZEND_HASH_APPLY_STOP - stop iteration
  7. * ZEND_HASH_APPLY_REMOVE - delete the element, combineable with the former
  8. */
  9.  
  10. ZEND_API void zend_hash_apply(HashTable *ht, apply_func_t apply_func TSRMLS_DC)
  11. {
  12. Bucket *p;
  13.  
  14. IS_CONSISTENT(ht);
  15.  
  16. HASH_PROTECT_RECURSION(ht);
  17. p = ht->pListHead;
  18. while (p != NULL) {
  19. int result = apply_func(p->pData TSRMLS_CC);
  20.  
  21. if (result & ZEND_HASH_APPLY_REMOVE) {
  22. p = zend_hash_apply_deleter(ht, p);
  23. } else {
  24. p = p->pListNext;
  25. }
  26. if (result & ZEND_HASH_APPLY_STOP) {
  27. break;
  28. }
  29. }
  30. HASH_UNPROTECT_RECURSION(ht);
  31. }

因为HashTable中所有Bucket都可以通过pListHead指向的双向链表来访问,因此遍历HashTable的实现也比较简单。这里值得一 提的是对当前遍历到的Bucket的处理使用了一个apply_func_t类型的回调函数。根据实际需要,该回调函数返回下面值之一:

ZEND_HASH_APPLY_KEEP
ZEND_HASH_APPLY_STOP
ZEND_HASH_APPLY_REMOVE

它们分别表示继续遍历,停止遍历或删除相应元素后继续遍历。

还有一个要注意的问题就是遍历时的防止递归的问题,也就是防止对同一个HashTable同时进行多次遍历。这是用下面两个宏来实现的:
HASH_PROTECT_RECURSION(ht)
HASH_UNPROTECT_RECURSION(ht)
其主要原理是如果遍历保护标志bApplyProtection为真,则每次进入遍历函数时将nApplyCount值加1,退出遍历函数时将nApplyCount值减1。开始遍历之前如果发现nApplyCount > 3就直接报告错误信息并退出遍历。

上面的apply_func_t不带参数。HashTable还提供带一个参数或可变参数的回调方式,对应的遍历函数分别为:

  1. typedef int (*apply_func_arg_t)(void *pDest,void *argument TSRMLS_DC);
  2. void zend_hash_apply_with_argument(HashTable *ht,
  3. apply_func_arg_t apply_func, void *data TSRMLS_DC);
  4.  
  5. typedef int (*apply_func_args_t)(void *pDest,
  6. int num_args, va_list args, zend_hash_key *hash_key);
  7. void zend_hash_apply_with_arguments(HashTable *ht,
  8. apply_func_args_t apply_func, int numargs, …);

除了上面提供的几种提供外,还有许多其它操作HashTable的API。如排序、HashTable的拷贝与合并等等。只要充分理解了上述HashTable的数据结构,理解这些代码并不困难。

PHP源代码分析(第一章):Zend HashTable详解【转】的更多相关文章

  1. PHP数据核心:Zend HashTable详解

    最近看了篇关于php内的hashtable的文章,PHP数据存储的核心,各种常量.变量.函数.类.对象等都用它来组织的.转载地址 http://www.phppan.com/2009/12/zend- ...

  2. “全栈2019”Java第一百零三章:匿名内部类详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  3. 第3章 TCP协议详解

    第3章 TCP协议详解 3.1 TCP服务的特点 传输协议主要有两个:TCP协议和UDP协议,TCP协议相对于UDP协议的特点是 面向连接使用TCP协议通信的双方必须先建立连接,完成数据交换后,通信双 ...

  4. 第14章 启动文件详解—零死角玩转STM32-F429系列

    第14章     启动文件详解 全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn 野火视频教程优酷观看网址:http://i.youku.com/firege ...

  5. 协议分析 - DHCP协议解码详解

    协议分析 - DHCP协议解码详解 [DHCP协议简介]         DHCP,全称是 Dynamic Host Configuration Protocol﹐中文名为动态主机配置协议,它的前身是 ...

  6. pyhanlp 共性分析与短语提取内容详解

    pyhanlp 共性分析与短语提取内容详解   简介 HanLP中的词语提取是基于互信息与信息熵.想要计算互信息与信息熵有限要做的是 文本分词进行共性分析.在作者的原文中,有几个问题,为了便于说明,这 ...

  7. 入木三分学网络第一篇--VRRP协议详解第一篇(转)

    因为keepalived使用了VRRP协议,所有有必要熟悉一下. 虚拟路由冗余协议(Virtual Router Redundancy Protocol,简称VRRP)是解决局域网中配置静态网关时,静 ...

  8. 第二章 IP协议详解

    第二章 IP协议详解 2.1 IP服务的特点 它为上层协议提供了无状态,无连接,不可靠的服务 名称 简介 优点 缺点 对付缺点的方法 无状态 IP通信双方不同步传输数据的状态信息 无须为保持通信的状态 ...

  9. Cobalt Strike系列教程第二章:Beacon详解

    上周更新了Cobalt Strike系列教程第一章:简介与安装,文章发布后,深受大家的喜爱,遂将该系列教程的其他章节与大家分享,提升更多实用技能! 第二章:Beacon详解 一.Beacon命令 大家 ...

随机推荐

  1. ThinkPHP3.1新特性: 多层MVC支持

    ThinkPHP基于MVC(Model-View-Controller,模型-视图-控制器)模式,不过均支持多层(multi-Layer)设计. 模型(Model)层:默认的模型层由Model类构成, ...

  2. logback使用

    须要的jar包: slf4j-api-1.7.7.jar logback-classic-1.1.2.jar logback-core-1.1.2.jar logback.xml配置文件,放在proj ...

  3. Android开源git40个App源码

    http://www.itbbu.com/1039.html (JamsMusicPlayer)很棒的音乐播放器(new)   (F8)日程安排的软件   (Conversations)基于XMPP的 ...

  4. webstrom热键[持续更新]

    1.Ctrl+ Shift + A  --  为了加快寻找菜单命令或工具栏操作,你并不需要看菜单.只有按Ctrl+ Shift + A(说明|查找操作主菜单上),并开始输入动作的名称. . 2.Ctr ...

  5. 1.关于UltraEdit中的FTP和Tenent配置,UE远程连接Linux进行文件操作

     1  安装UltraEdit 2  配置FTP相关的配置 文件àFTP/Tenet(T)à watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdG90b3 ...

  6. AFNetworking 3.0迁移指南

    AFNetworking是一款在OS X和iOS下都令人喜爱的网络库.为了迎合iOS新版本的升级, AFNetworking在3.0版本中删除了基于 NSURLConnection API的所有支持. ...

  7. 利用java开发一个双击执行的小程序

    之前我们利用java写了很多东西,但是好像都没有什么实际意义. 因为有意义桌面小程序怎么都得有个界面,可是界面又不太好搞.或者 了解到这一层的人就少之又少了. 呀,是不是还得开辟一些版面来介绍awt和 ...

  8. 重温css系列01

    2016-01-07——解决背景层透明度的问题 需要ie9+ 问题:如果我对div设置opacity: 0.8;这个透明属性后 希望内容不发生改变怎么弄? A:做两层,或者rgba 解决后的效果图: ...

  9. Android 设计随便说说之简单实践(消息流动)

    在上面两篇分别说明了设计中较为简单也是很关键的实践点. 第一模块划分,它是根据每个模块所承载的业务,进行划分,是应用程序一个静态的描述. 第二合理组合,它是是将每个模块调动起来,共同实现业务,是一个准 ...

  10. 20160421javaweb之上传下载小案例---网盘

    一.建立数据库: CREATE TABLE IF NOT EXISTS `netdisk` ( `id` ) NOT NULL AUTO_INCREMENT, `uuidname` ) NOT NUL ...