并发编程不能离开的锁

一、锁的作用

前言 — 并发编程的问题

       计算机的任务处理经历了从单任务处理、单道批量处理到现在多道程序处理的发展过程。单任务处理,比如最早在电子管时代的穿孔打卡编程,任务是单个依次执行,其最大弊端就是效率太差。看下当时的卡片,还挺有意思的:

        现在的计算机都是多核处理器,然而多任务处理在提高效率的同时,会引入其他的问题,其中最重要的就是数据的并发安全性。当多个任务同时处理一个变量时,由于是并发处理,此外还会有编译器和处理器的指令重排序,所以会出现不可预知的结果。这就涉及到并发编程中的可见性、原子性和有序性三大问题。

        我们最近在开发一个联机交易引擎,其中要实现一个编排规则版本回滚的功能,我交给了一个小伙伴写,当看到他的流程图时,我第一个指出的就是他在处理版本号时的并发性问题。对于没有太多工作经验的人来说,并发性是最容易忽略的问题。

        如果做过电商的话,经常会遇到商品库存变更的问题,我上一份工作主要是财务结算业务的,所以会经常涉及到账户金额的修改。这两种场景都涉及到并发问题,尤其是对热点商品或者热点账户,问题更加突出。如果不加锁,那么可能导致两个并发扣减请求导致余额或库存字段变成负数或者是修改成一个错误的数字。在电商中,你会经常提到超卖的问题,这个就是典型的由并发引起的问题。

        如何通过锁的机制保证我们数据的一致性,准确性,且同时不会影响整体的性能,这是最关键的。

      像操作系统如Linux本身就是一个典型的多租户,多任务的操作系统,锁是不可或缺的,其广泛用于进程的调度,资源的管理,内存分配等各种并发场景。

这里再举两个简单的例子阐述锁的必要性。

public Integer a = 0;

public void getA(){
    return a;
}

public voic setA(Integer value){
    a = value;
}

public void modify(){
    Integer a = getA();
    setA(++a);
}

        上面代码中,主要是获取变量,并将值进行更新。当串行执行时,没有任何问题。但如果同时有两个任务进行处理,两个任务同时先取a,结果取到的都是0,随后更新a的值,最后a的值并不是想要的2,而是1。实际上这个更改账户余额是一样的,也是先取出余额,然后再更新。

        另外一个例子,说明因为指令重排序导致的问题。

int a = 1;
int b = 2;

public void read(){
   if (b == 3){
     int y = a*a;
   }
public void write(){
    a=2;
    b = 3;
} 

        write方法中的两个语句没有依赖关系,因此在单线程内会被重排序。这就可能导致b=3先执行,随后read方法先读取到b==3,然后执行y=a*a,这里 的a的当前值是1,即y=1。

锁的介绍

        为了解决上述的问题,一个最常见的解决方案就是加锁。通过加锁,保证了操作的原子性,只有获得锁的任务才能执行相关流程,从而保证最后得到的结果是可预知的。比如在电商系统中,下单支付后更改库存,通过使用悲观锁或者乐观锁来保证不会超售。

        锁是用于控制多任务同时访问共享资源的一种同步机制。

        通常,我们会将锁分成两大类,悲观锁和乐观锁。悲观锁即认为一定存在竞争,所以在处理之前加上互斥锁,存在排他性,只有获得到该锁的任务才能处理,其他的会被挂起。

        相比于悲观锁,乐观锁认为不是总是存在冲突,它在处理之前不会像悲观锁一样加上锁,而是在执行数据更新时,利用版本号和CAS来实现数据的更新。比如Mysql中的MVVC,Java中的CAS更是随处可见,比如atomic下的多种原子类,locks下的锁等等。

        当然这里不是说,乐观锁一定比悲观锁性能更好,只是说两者适用的场景不近相同,如果并发量异常高,你总是进行CAS操作,那性能可能更差,因为你会多出很多无效的操作。

       本文会介绍Linux,JAVA,Mysql的锁以及用于进程间的资源同步的分布式锁。

死锁、活锁

        锁是在并发编程中最重要的同步机制,但是也不能贪杯,不能过度使用锁,也不能随意使用。过度使用锁可能会降低整体性能,如果锁的粒度比较大,那就会导致几乎一直都是单个任务处理,此外由于上下文切换导致其性能还不如串行化处理方式。另外随意使用锁可能会出现死锁现象,这是一个比较常见,且严重的问题。想要避免死锁,就是要避免产生死锁的几大条件的发生。死锁产生的条件是:

1、非抢占式系统;非抢占保证了进程或者线程不会主动抢占其他已被占用的资源

2、互斥;资源占用具有排他性

3、请求和保持; 已经占用的资源不会被释放,会一直保持

4、循环等待;不同任务循环等待,首尾相接,形成了一个闭环;

看到了死锁的产生条件,我们就要避免其发生,这也是我们平时在开发时要注意的一点。解决策略主要也是分成两种,一是避免死锁,二是解决死锁。避免就是预防,不让死锁发生。比如对于不同的线程在申请资源时最好是同样的顺序。如有资源A和资源B,线程1,线程2在申请时,最好都保持先申请A,再申请资源B,不要反着来。此外还有一种著名的避免死锁的算法叫做银行家算法。感兴趣的可以参考:详解操作系统之银行家算法(附流程图) - 知乎

下面是一个简单的死锁例子:

public class DeadLock {
    public static final Object objA = new Object();
    public static final Object objB = new Object();
    public static class ThreadA extends Thread {

        @Override
        public void run() {
            synchronized (objA){
                try{
                    TimeUnit.SECONDS.sleep(10);
                }catch (Exception e){
                }
                synchronized (objB){
                    System.out.println(objB);
                }
            }
        }
    }

    public static class ThreadB extends Thread {

        @Override
        public void run() {
            synchronized (objB){
                try{
                    TimeUnit.SECONDS.sleep(10);
                }catch (Exception e){
                }
                synchronized (objA){
                    System.out.println(objA);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();
        threadA.start();
        threadB.start();
        threadA.join();
        threadB.join();
    }
}

除了预防死锁的发生,也可以在死锁发生时进行处理,比如可以通过定时检测死锁,解除死锁等方式。

        和死锁相对应的一个现象是活锁。活锁最大的特点它不会保持,还是上面的例子,线程1获取到A再获取资源B时,如果失败了,就会放弃资源A,再去重新获取资源A和B;线程2获取到B再获取资源A时,如果失败了,同样放弃资源B,再去重新获取资源B和A。也就是说,两个线程在获取不到全部资源时,就会放弃占用资源,随后接着再按照相同的顺序去获取资源,如此反复,导致两个线程永远获取不到全部资源,也不会执行响应任务。对于活锁,维基百科有比较好的解释:

两个人都没有停下来等对方让路,而是都有很有礼貌地给对方让路,但是两个人都在不断朝路的同一个方向移动,这样只是在做无用功,还是不能让对方通过。

        另外一个比较典型的例子就是RedisCluster集群,其基于Raft算法进行选举,当某个master节点挂掉,要从slave种选举出新的master,在同一个term内,很有可能出现几个slave获得了相同的投票,当获得相同投票时,Redis只能进行下一次选举,直到在某一个周期term内选举成功。上述这种现象就是一种活锁现象。

        如果了解Redis的人会知道,为了避免上述现象发生,Redis的做法是如果从节点发现主节点下线,就会等待一段随机时间长度后,向其他master节点发送一条广播消息,给自己拉票,希望自己成为主节点。每个slave等待的时间是随机的,而不是固定的时间,即有先后顺序。这也是解决活锁的标准方案。

二、Linux中的锁

        操作系统的锁是所有的锁的灵魂,是因为其他语言或者应用都是借助操作系统的锁机制来实现的,因为我平时都是使用Linux,所以这里介绍Linux中的锁。

        Linux做为一种典型的多用户、多任务的操作系统,通过特殊的同步机制来保证多任务并发或并行处理的安全性。主要包括信号量、互斥锁、自旋锁、读写锁、RCU等。

1、信号量

        信号量英文叫做Semaphore,看到这,JAVA程序员应该很熟悉吧,多线程编程中,JAVA并发包提供了一个类叫Semaphore。在Linux中,同样有个叫Semaphore的机制,如果你参加过面试,有时面试官会问你,进程间通信都有哪些机制,其中一个就是信号量,不过信号量不仅仅用于进程间通信,还可以干很多事情,如内核资源管理、中断处理、进程调度等等。

        信号量本质类似一个计数器,它会在初始时设置成一个固定值,随后进程成功获取资源后,会将信号量减去1(对应的是P操作),释放后,信号量加1(对应的是V操作)。linux对应的结构体如下:

   struct semaphore {
        spinlock_t                lock;
        unsigned int             count;
        struct list_head        wait_list;
    };

        其中count就是对应的信号量值,当然对count的加减操作是原子性的操作,且对其他进程(线程)可见。只有当信号量为非负数时,才能获取资源,否则将对应的进程会放入等待队列wait_list,并将其状态改为休眠。该等待队列本质上是一个链表结构,如下:

struct semaphore_waiter 
{         
        struct list_head list;         
        struct task_struct *task;         
        int up; 
};

        当进程执行完之后,如果等待队列是空的,直接将count值+1,否则就会唤醒等待队列中的进程(或线程)。

        通常情况,信号量主要是用于在同一时间只能被有限个任务访问处理的场景。比如同时我只允许最多100个人在打球,多了不可以。这有点像限流里的令牌桶,最多我发这么多,超过这些就不发了。为了直观,我用JAVA来演示信号量的使用。但还是要注意,Java的信号量的实现和Linux系统的是不一致的,只是适用场景比较类似。

  public class SemaphorePra {
  
    private static int count = 40;

    private static ExecutorService executorService = Executors.newFixedThreadPool(count);
   
   //这里定义了信号量的初始值,是10.
    private static Semaphore semaphore = new Semaphore(10);

    public static void main(String[] args){
        //这里可以观察线程处理的情况。
        for (int i=0;i< count;i++){
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try{
                        semaphore.acquire();
                        System.out.println(String.format("thread:%s",Thread.currentThread().getName()));
                    }catch (Exception e){
                        e.printStackTrace();
                    }finally {
                        semaphore.release();
                    }
                }
            });
        }
    }
}

2、互斥锁

Linux通过mutex来实现互斥锁,实际上无论我们使用Java还是其他语言实现的互斥锁,最终都是通过操作系统的互斥锁来实现的。

mutex结构体如下:

struct mutex {
        /* 1: unlocked, 0: locked, negative: locked, possible waiters */
        atomic_t                  count;//原子操作类型变量
        spinlock_t                wait_lock;//自旋锁类型变量
        struct list_head          wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
        struct thread_info        *owner;
        const char                *name;
        void                      *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
        struct lockdep_map         dep_map;
#endif
};

        可以看到mutex和信号量比较类似,区别是其count只能是0或1两个值,其本质上也是信号量,所以也被称为二进制信号量。

        那么问题是,count本身也是一个变量,又怎么保证获取并设置该标志位的过程是原子操作呢?

        比较著名的一种做法是 test-and-set-lock(TSL,实际上就是咱们经常说的CAS),是一种不可中断的原子操作,实现方式有硬件或者软件的方式,它将count变量读取寄存器 RX 中,然后在该内存地址上存一个非零值,读取操作和写入操作从硬件层面上保证是不可打断的,即保证原子性。

        上述原子操作,在单核系统是完全没有问题的,但在多核系统中,虽然是原子操作,但单靠它是完全没法做到CPU间的同步的。基于此,操作系统可以通过控制总线(比如保持同一时间只能有一个处理器操作),基于缓存锁定(保证同一时间只有一个处理器在操作缓存)或者其他实现方式。

        通过这种结构,可以保证资源使用的排他性,同时只能有一个任务占用该资源。传统的mutex中如果进程(线程)获取锁失败,会直接进入等待队列,等待锁释放重新竞争锁。后续Linux对此进行了改进,当获取锁失败时,可以先不进入等待队列,而是采用自旋的方式,不必挂起。这种方式的目的也是为了减少进程挂起唤醒等操作。

3、自旋锁

        自旋锁spinlock,刚才也提到了,它的主要思想就是当没有成功获取到锁时,并不会进入休眠状态,而是自旋等待,直到获取资源。相比于互斥锁,它并不需要挂起再被唤醒。其应用场景一般都是需要等待的时间比较短的情况,而互斥锁和信号量可以是执行时间比较长的场景。对于自旋锁,如果等待时间较长,自旋过程会一直占用CPU,这在一定程度上也浪费了资源。为了解决这个问题,一是尽量在等待锁的时间较短的情况下使用,此外也不能一味地等待,在等待一定时间时还获取不到就应该失败。这在后面介绍Java自旋锁的时候也会介绍。

4、读写锁

        读写锁rwlock,是针对互斥锁的一个改进,即当读操作时,锁是共享状态,当写时,是互斥锁。这样是为了避免所有情况都加互斥锁,从而提升了并发性能。在我们日常的开发中,也基本上是读多写少的场景,没必要一定要独占。这个就将共享和独占很好得结合到了一起,如果大家平常都使用Mysql得话,就会知道Mysql也提供了shared和独占的锁。

        读写锁存在的模式即读模式锁,写模式锁,无锁。其中写锁的优先级要高于读模式锁。当有写锁时,其他具有读锁的都会被阻塞。

5、RCU

        RCU,全称时Read-copy-update,是在linux2.6中引入的。有点像写时复制,即CopyOnWrite(这个在零拷贝中是比较有用的一个概念)。当只是读时,不需要加锁;当进行写操作时,会拷贝一份副本,然后在合适的时候会将执行旧数据的指针更新为执行新数据。

        相比于读写锁,RCU最大的变化是进行写操作时,读锁不需要被阻塞。此外,就是其修改对于其他任务来说可能不是立即可见的,有滞后性。因此对于数据具有敏感性,需要实时读到最新的场景,RCU是不合适的。另外,RCU由于需要进行副本拷贝,还要进行删除旧数据等一系列操作,所以其写锁的开销成本较大。因此,RCU在写操作越少的情况,其性能就越好。

        

        以上介绍了Linux的各种锁,并介绍各自的使用场景。在我们实际开发中可能并不会直接调用库函数来完成锁的实现,而是通过编程语言去调用,编程语言去负责Linux的系统调用。

三、JAVA中的锁

        JAVA中的锁是用来控制多线程同时访问临界区时最重要的方式。在1.5版本之前只有sychronized这一种内置锁,且是比较重量级的,在后续的版本中,引入了Lock接口,并提供多种锁,此外sychronized实现了四种不同状态的锁,从而提升系统的性能;同时加入了atomic类,引入了乐观锁,即CAS,有的地方不把他当做锁,不过我认为这个就是悲观锁和乐观锁的区别。

总结目前JAVA的锁存在几大类:

         - sychronized内置锁

        -  Lock接口实现的锁

        -  atomic包下的CAS乐观锁

        其中sychronized和Lock接口实现类本质上都是悲观锁,atomic包下的是采用CAS思想实现的乐观锁。悲观锁和乐观锁的区别不再赘述,首先说一下两种悲观锁的区别。

        从上面对比可以看到,Lock接口实现类提供了更加灵活、丰富的锁特性,而不是sychronized那种固化的加锁模式,但也不是说sychronized就不能用,如果我们不需要Lock所提供的特性,完全没必要使用Lock实现类,因为sychronized经过1.6的升级,性能也很好。接下来详细介绍一下这几种锁。

1、synchronized

        synchronized是Java的一个内置锁(JVM级别),是一种独占互斥锁。那么它到底是如何实现的呢?synchronized最传统(JDK1.6之前,现在只有膨胀到重量级锁才会出现)的锁依赖的是操作系统的Mutex Lock,关于mutex在Linux锁中已经有过介绍。这种传统的锁的方式比较重,因为如果线程抢不到锁的话,就会进入休眠状态,待有锁资源再被换起。休眠和换起的这两个状态,都是耗费CPU的,在1.6版本中又引入了轻量级锁和偏向锁(锁升级过程:无锁->偏向锁->轻量级锁->重量级锁)。其主要思想也是尽量无锁,或者采用自旋锁的方式避免线程休眠和唤醒。自旋锁在上也有讲过。在JDK1.6之后,在自旋锁的原有基础上又提出了自适应自旋锁。自适应自旋锁重试次数或时间不再固定,而是由前一次在同一个锁对象的自旋时间及锁地拥有着的状态来决定。如果上一个自旋时间很短。那它这次也认为自己可以获得锁,这时它就允许自旋时间长一些。

        虽然自旋锁有很多优点,但也只有在满足一定条件下才可能得到性能的提升,比如多核,比如任务不能太过耗费时间等等。

       这里需要注意的是Sychronized锁定的都是对象。我们可能在实际应用中会有不同的使用方式:

        1、实例前加synchronized,实际上锁住的是该实例对象;

        2、在类前面加synchronized,锁住的是该类对象;

        3、在同步块前加synchronized,锁住的是括号里的对象;

public void add(){
  synchronized (this){
  }
}

        假如说一个类对象里有两个不同的静态方法都加了synchronized,那么这个锁是这个类对象,相当于全局锁,因此即使访问把不同的方法,同样是互斥的。

public static synchronized add(){        
}

       对于非静态类方法,即实例方法,如果不同的线程操作的不同的对象,那么加锁是毫无影响的。如果是操作同一个实例对象,那会产生互斥。

public synchronized add(){    
}

         使用synchronized的代码块在编译的时侯会插入monitorenter(加锁)和monitorexit(解锁字节码指令。enter是插入到同步代码块开始的地方,exit是在方法结束和异常的情况。

        对于被synchorized修饰的方法,会被一个叫ACC_SYNCHRONIZED的修饰,有该修饰的对象就会尝试获取锁资源。

        注意这两种方式本质上没什么区别,都是创建monitor对象,只不过是前种需要显示地在字节码中指定加锁和线索。

        对象应用的锁是保存在对象头中的,对象头中的MARK WORD主要存储hashcode,分代年龄以及锁的标志位头中保存的锁信息是什么,还要根据具体的锁机制有关,有重量级锁,轻量级锁还有偏向锁。通过synchronized最终指向了操作系统的Monitor实现了加锁,如图(图片来源于网络):

2、Lock接口锁

 Lock接口的锁包括如下几种:

  •  ReentranLock,纯互斥锁,悲观锁。
  • ReentrantReadWriteLock,其中使用了ReadLock 、WriteLock,读锁共享,写锁互斥。说到这儿,我不得不提到CopyOnWriteArrayList,读写锁和CopyOnWriteArrayList都实现了读共享区别在于读写锁在出现写时会阻塞读,CopyOnWriteArrayList是一种写时复制的机制,在有写时,会复制一份到新的数组上,JAVA里有,Redis里同样有写时复制的思想。
  • StampedLock 在读写锁的基础上增加了乐观读。

Lock的实现类都借助了AQS实现的。其实不仅仅是锁,还有其他的包括同步容器,比如Semaphore,CountDownLatch,FutureTask也都是基于此实现的。

AQS,全称AbstractQueuedSynchronizer,即同步器,如果看过该类的代码,你会觉得这就是艺术品,Doug Lea大神充分考虑了各种复杂的应用场景以及潜在的风险。本文不对该源码做分析,感兴趣的可以见我自己网站的一篇文章: 千与千寻-Java多线程共享变量同步机制及各种锁 

简单说下AQS的基本思想。AQS本身维护了一个volatile变量叫state,这个大家应该都很熟悉了,可以保证可见性和禁止重排序。

此外,其维护了一个FIFO的双向队列,队列本质上就是一个双向链表,特别的是其head是个虚拟的节点,除此之外,每个节点都是一个Node,Node包含当前的waitStatus,前驱节点和后继节点。这个队列本质上就是一个等待队列。

AQS支持互斥锁和共享锁两种模式,互斥锁就是ReentranLock这种,共享锁就是Semaphore,CountDownLatch这种。

这里就拿互斥锁来说。其获取锁的过程是:

1、线程尝试获取锁,这里调用的都是具体锁的实现类覆盖的方法,如ReentrantLock的tryAcquire;

2、如果获取成功,则直接执行同步代码块即可;如果获取锁失败,则继续向下进行; 3、将获取锁失败的前程通过CAS添加到队列尾部(这里处理比较特殊,可以看下源代码);

4、线程开始自旋,如果前节点是头节点,就尝试获取锁,如果获取成功了,就更改头节点;如果前节点不是头节点或者获取锁失败了,且前节点的waitStatus是SIGNAL(这个表示处于休眠状态),也直接进入休眠;如果是取消状态,则prev指针往前移;如果是其他的,则将前节点的waitStatus设置为SIGNAL。随后将线程挂起,进入休眠状态。这里要说明一下,waitStatus不影响当前节点,只影响后节点使用;

5、如果持有锁的线程执行完释放锁之后,就会唤醒头节点的后继节点,这个地方也比较艺术,就是如果next节点是SIGNAL状态,就直接唤醒,否则从队尾从后往前遍历,直到找到队中第一个waitStatus小于0的,再唤醒。

上面说的互斥锁的释放比较简单,共享锁要麻烦一些,因为共享锁可以被多个线程持有,这就会出现释放锁的并发问题,因此AQS又引入了一个PROPGATE的waitStatus常量,该常量可以完美解决因为共享锁,多线程并发引起的bug,当然这也是后来才修复的,最开始还真没有,感兴趣的可以看源代码哈,这里不做过多阐述。

使用AQS的场景比较多,包括ReentrantLock,Semphore,CountDownLatch,ReentrantReadWriteLock,线程池ThreadPoolExecutor。都会有使用。

同步工具同步工具与AQS的关联
ReentrantLock使用AQS保存锁重复持有的次数。当一个线程获取锁时,ReentrantLock记录当前获得锁的线程标识,用于检测是否重复获取,以及错误线程试图解锁操作时异常情况的处理。
Semaphore使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数,acquireShared会减少计数。
CountDownLatch使用AQS同步状态来表示计数。计数为0时,所有的Acquire操作(CountDownLatch的await方法)才可以通过。
ReentrantReadWriteLock使用AQS同步状态中的16位保存写锁持有的次数,剩下的16位用于保存读锁的持有次数。
ThreadPoolExecutorWorker利用AQS同步状态实现对独占线程变量的设置(tryAcquire和tryRelease)。

这里说一下线程池ThreadPoolExecutor,其使用AQS的目的是,只能中断空闲线程,在尝试中断时,会调用tryAcquire,如果失败,则证明,线程正在执行任务,不可中断。在执行任务之前,会调用tryLock,其目的就是要加互斥锁,

protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

可以看到,其是不可重入的。这是因为setCorePoolSize时,如果超过了设置数量,会中断线程,如果是可重入的,则会中断我们的线程。

这里只展示一下ReentranLock的部分源码:

关系图

该锁具备的特点一是可重入性,二是支持非公平和公平两种锁,三是可中断。

可重入性可以参考一下代码。以非公平锁为例,加锁的过程:

 final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //同步状态state,volatile变量
   						int c = getState();
            //处理尝试CAS加锁,并设置值为1(acquires传入的值基本上都是1)
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果是当前线程,更新计数
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

释放锁:

     protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果重入锁全部释放了,就返回告知已经全部释放
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //更新释放后的state
            setState(c);
            return free;
        }

ReentrantLock第二大特点就是支持非公平和公平锁,非公平锁上面的代码已经展示了,即不会遵循FIFO,就算是后来了一个线程,也有可能先获得锁。其次是公平锁,公平锁会借助AQS的同步队列实现,调用如下函数:

    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

只有满足以上条件才会获取锁,从而保证了公平性。

3、两种锁的对比

    你可能经常会在网上看到,说Lock接口锁的性能会比sychronized要好,所以要考虑性能就要用Lock锁,实际上并不尽然。就像jdk源码中说的那样,Lock锁只是提供了相比于sychronized更多的功能特性,同时引入了支持不同条件的队列Condition。

 那具体说下Lock接口的新增的一些特性:

1、公平性。sychronized只支持非公平锁,而Lock接口两种均支持,其实现主要是借助于大名鼎鼎的AQS(AbstractQueuedSynchronizer),AQS维护着一个等待队列,如果是公平锁,当有其他线程等待时,后加入的线程不可直接获取锁,需要加入队列尾部。

2、可中断。这里说的可中断指的是在等待锁的过程中可中断,不是执行的过程中。sychronized不支持可中断的,而Lock接口时支持的,你会在源码中看到lockInterruptibly等带有Interruptibly字样的方法。

3、超时机制。超时机制同样指的是等待锁的超时时间,等待锁不是一直等待下去,而是到了一定时间,我就自动退出。sychronized是不支持超时的,而Lock支持的。

  到了这里,应该就清楚了,sychronized会一直等待其他线程释放锁,而Lock就比较灵活了,支持可中断、支持超时等。

       如果是从性能角度出发,而不考虑Lock接口的特性,那么就没必要使用Lock接口锁,而是使用内置的sychronized更好一些。在《Java并发编程实战》也有提到过说,sychronized的优势有三点:

  1、sychronized的释放锁并不需要开发人员自己显示做,而是由虚拟机帮着做,而我们使用lock接口,必须在finally中显示地unlock一下锁。如果忘了做这步骤或者不是finally里做,就是灾难的。

  2、sychronized这种内置锁使得我们可以输出线程堆栈的锁情况,也方便我们检测出出现死锁的情况。当然目前Lock接口也可以通过预留API的方式实现,但sychronized更加原生。

  3、sychronized有时性能是好于Lock接口的。因为sychronized不仅仅提供了锁膨胀升级的优化,虚拟机还为其进行了锁消除等优化手段,从而提升其性能。

4、原子类

JAVA并发包提供了一系列原子类,通过使用原子类可以实现无锁编程,但实际上其是借助计算机的CAS对应的指令。通过使用原子类,在并发编程中,不需要再显示去加锁,即开发者完全可以忽略线程安全性的问题。其基本原理就是使用volatile和CAS完成的,这里拿AtomicInteger为例。

 //value为volatile变量
private volatile int value;

//CAS操作
public final boolean compareAndSet(int expect, int update) {
       //valueOffset是内存偏移量
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

        Unsafe是JAVA提供的可以直接操作操作系统的类,可以进行内存管理,CAS操作,线程调度等等,但日常开发中,基本上是用不到的。

        当然,原子类虽然好,但CAS操作还存在一个比较普遍的问题,即ABA问题。就是有个线程在修改的时候值是A,但可能值是被别的线程从A改到B,又从B改到A。那解决ABA问题,常见的方案就是加上版本号,每次进行update时,不仅比较当前值是否相等,还要比较版本号是否一致。原子类中就有一个类似的实现类可以使用:AtomicStampedReference。该类通过一个二元组,即保存值,又保存版本号。

 private static class Pair<T> {
      //引用
        final T reference;
       //版本号 
       final int stamp;
    }

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
         //地址和版本号要同时相等
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

  那我们在比较一下原子类和上面提到的两种锁的区别。通常来讲,在一般竞争情况下,原子类的性能是优于锁的,因为他不需要把线程挂起;但是在高度竞争的情况下,锁的性能是优于原子类的,因为原子类会占用CPU资源,在锁竞争失败时会重试,如果竞争非常激烈这种情况反倒会降低整体的性能。

四、Mysql锁

本文主要谈论InnoDB引擎的锁。下面是我画的一个思维导图,主要从不同维度去划分锁。

基于锁的属性分类:共享锁、排他锁

共享锁(S):加上共享锁之后,对应数据集合(行锁或者表锁)只能读,不能修改。当然共享锁可重复加,他也会阻止其他事务获取对应的排他锁。

现在我开启一个事务并加上共享锁。

mysql> begin;select * from c_group where user_id=33 lock in share mode;
Query OK, 0 rows affected (0.01 sec)

+-------+---------+-----------+--------+
| id    | user_id | groupname | avatar |
+-------+---------+-----------+--------+
| 10016 |      33 | dwdaaa    | dwd    |
+-------+---------+-----------+--------+
1 row in set (0.00 sec)

我再开启一个事务,执行修改。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update c_group set groupname="dwd" where user_id=33;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

排他锁(X):排他锁就是我加上了,别人不可加任何的锁了,无论是共享锁还是排他锁,有时也称为排他写锁。

我再开启一个事务:

mysql> begin;select * from c_group where user_id=33 for update;
Query OK, 0 rows affected (0.00 sec)

+-------+---------+-----------+--------+
| id    | user_id | groupname | avatar |
+-------+---------+-----------+--------+
| 10016 |      33 | dwd       | dwd    |
+-------+---------+-----------+--------+

这个开启了一个排他锁。

然后再打开一个事务:

ysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> update c_group set groupname="dwd" where user_id=33;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from c_group where user_id=33 ;
+-------+---------+-----------+--------+
| id    | user_id | groupname | avatar |
+-------+---------+-----------+--------+
| 10016 |      33 | dwd       | dwd    |
+-------+---------+-----------+--------+
1 row in set (0.00 sec)

mysql> select * from c_group where user_id=33 lock in share mode;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

第二个事务的排他锁和共享锁都因为第一个事务已经加上了排他锁而报错了。

基于锁的粒度分类:表锁、行锁、记录锁、间隙锁、临键锁、插入意向锁。

表锁:即对整张表加锁,表锁的粒度比较粗,MyISAM存储引擎只支持表锁。当我们进行DDL(DML)操作或者查询条件没有索引的时候,都会使用表锁。当然也可以显示加表锁如Lock Table .....read (write)等。实际上表锁还可以细分,上面提到的DDL等操作实际上是属于元数据的锁,还有后面还提到的意向锁,以及自增锁

        自增锁是保证整个表全局的唯一性,最早的版本是只有事务执行完,自增锁才会释放,所以性能很差,后续的版本中为了提高性能,是只要获取了自增id就把自增锁释放掉,不必等到整个事务都执行完毕。

      注意,表锁也会和行锁出现冲突的,除了意向锁以外哈,后面会介绍意向锁的作用。

行锁:在MyIsam中不存在,InnoDB支持行锁。但生效必须是通过索引查询的记录,否则不会生效。通过索引加的锁可以是单行,也可以是多行数据。其实记录锁,间隙锁,临建锁,插入意向锁都属于行锁的范围。

记录锁:精准锁住一条记录,一般都过主键或者唯一索引查询即可假如记录锁;

间隙锁:索引区间锁。和记录锁只锁住一行不同,间隙锁会把一个区间锁住。

简单说就是我们查询一个范围:

   mysql> begin;select * from c_group where user_id in (1,2,3) for update;

事务提交前,不会允许插入或删除,解决了幻读的问题,当然只在RR隔离级别有效(这也是默认得隔离级别)。

比如我现在开启一个事务A,然后开启一个事务B

事务B执行:

 select * from c_group where user_id>=123  and user_id<130;
+-------+---------+-----------+--------+
| id    | user_id | groupname | avatar |
+-------+---------+-----------+--------+
| 10019 |     123 | wdw       | qee    |
| 10022 |     124 | wdw       | qee    |
| 10024 |     125 | wdw       | qee    |
+-------+---------+-----------+--------+

然后事务A执行insert,commit

insert into c_group (`user_id`,`groupname`,`avatar`) values(126,"wdw","qee");

因为MVCC快照读,事务B读到的还是上述结果集。

ysql> select * from c_group where user_id>=123  and user_id<130;
+-------+---------+-----------+--------+
| id    | user_id | groupname | avatar |
+-------+---------+-----------+--------+
| 10019 |     123 | wdw       | qee    |
| 10022 |     124 | wdw       | qee    |
| 10024 |     125 | wdw       | qee    |

好,事务B执行insert,插入事务A同样的数据。

mysql> insert into c_group (`user_id`,`groupname`,`avatar`) values(126,"wdw","qee");
ERROR 1062 (23000): Duplicate entry '126' for key 'user_id'

这就是幻读的一种。

一种解决方式就是使用间隙锁。下面方式就是一种间隙锁的方式。

SELECT * FROM child WHERE id > 100 FOR UPDATE;

在事务中查询直接用select for update,这样就加了间隙锁。加了间隙锁之后,整个范围区间都会被锁住,即使对应数据可能不存在。因此假如间隙锁之后,其他事务就没法插入数据了,会被锁住,从而也就不会出现幻读了。

插入意向锁:插入意向锁其实是一种特殊的间隙锁,当执行insert的时候会有插入意向锁。上面提到间隙锁阻止其他事务插入,解决幻读的问题,实际上就是间隙锁和插入意向锁的冲突。但是插入意向锁和插入意向锁之间是不会冲突的,即当我们在同一个间隙内执行两个不同id的插入,是不会冲突的。

临键锁:是记录锁和间隙锁的组合,既解决脏读,也解决幻读。它锁住的是当前记录以及记录之前的数据,和间隙锁的开区间不同,临建锁是闭区间。比如我查询的是 id in (1,5,7),那么它锁住的是(-∞, 1],(1, 5],(5,7]。

基于是否可以锁住同步资源:悲观锁和乐观锁

这个就比较好理解了,因为在前面很多地方都提到过。我们通过select for update或者update ,delete加的都是悲观锁。通过悲观锁可以确保数据的一致性和准确性。但有时我们遇到的都是读多写少的情况,并不需要总是加悲观锁,即认为多数情况不会冲突,因此可以使用乐观锁。Mysql并没有提供现成的乐观锁供开发者使用,开发者需要借助额外的字段来实现,比如最常见的就是增加版本号字段。当在执行更新操作时,只有版本号和查询版本号一致的时候才可更新成功。

select version from users where id=1;
update users set name='he' where id=1 and version=version;

基于状态:意向共享锁和意向排他锁

        这两种锁是表级锁。当事务A对某个表中的某一行执行update操作时,为该行加入了行锁的同时,还会加入一个意向排他锁,假如有另外一个事务B想要锁表,那发现有意向排他锁,就会被阻塞。通过意向排他锁,事务B不需要校验每一行是否有排他锁,只需要看是否有意向排他锁就可以,大大提高了效率。但要注意,意向排他锁只是会限制表锁,不会和行锁发生冲突。

死锁问题

        上面讲到了各种锁的分类,锁是保证同步的重要机制,但有锁的存在,势必会出现死锁的情况。那么出现死锁,Mysql是如何处理的呢?

        处理方式主要包括两种,一是超时,二是死锁检测。超时比较好理解,事务长时间不提交,通过变量innodb_lock_wait_timeout控制,超时就自动回滚;


mysql> show variables like "%innodb_lock_wait_timeout%";
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 120   |
+--------------------------+-------+

        二是死锁检测,其基本原理就是构建一个以事务为顶点、锁为边的有向图,判断有向图是否存在闭环,如果存在就证明有死锁。具体实现这里就不做分析了。

        上面是提到了死锁的处理方式,但解决死锁最好的方式还是预防,这就取决于我们平时写的sql了。总结几个尽量预防死锁的方式:

        1)尽量避免大事务,避免长时间占用资源。我们很忌讳写大事务,写Java不是直接在一个方法上打上Transactional就完事大吉了,数据的操作要单独放一层,且避免大批量操作等;

        2)要保证顺序性,这个在开始死锁的介绍也提到过。比如每个事务都是处理资源1,资源2,资源3,要保证每个事务都是按照同样的顺序去处理。;

        3)避免一个事务同时占用过多的资源。这可以通过优化索引设计,sql等环节来实现。

        4)事务中要使用索引或者主键来定位数据,避免占用过多的行,尤其是尽量避免表锁, 记录锁最好。

        好了,上面说了Mysql的各种锁的分类,那Mysql锁的底层是靠什么实现的呢?

        对于InnoDB引擎来说,其是一个多线程的处理引擎。其通过封装系统的mutex和信号量实现了互斥锁和读写锁。关于互斥锁和读写锁在Linux的锁介绍中已经介绍过,或者感兴趣的可以看下面的参考资料。

五、分布式锁

什么是分布式锁呢?

先看一段英文解释:

A distributed lock manager (DLM) runs in every machine in a cluster, with an identical copy of a cluster-wide lock database. In this way a DLM provides software applications which are distributed across a cluster on multiple machines with a means to synchronize their accesses to shared resources.

简单说,分布式锁的存在就是用来实现分布式系统中不同子系统的同步,控制不同节点对共享资源的同步访问,可以说成是分布式的互斥。

如何实现分布式锁?

分布式锁的种类

1、Mysql数据库锁

2、Redis分布式锁;

3、Zookeeper分布式锁;

4、基于ETCD实现的分布式锁;

1、Mysql数据库锁

        是的,Mysql也可以说是提供了分布式锁,这是其天然的优势,通过悲观锁可以实现排他性。可以创建一个记录锁的表,并使用主键或者唯一键,根据独占锁来实现同步。比如下面的一张数据表:

        但是用数据库实现分布式锁性能较差,首先写只能写单点,其次排他锁占用时间较长,会导致线程池耗尽,系统崩溃,再有就是很难实现复杂的场景,类如锁超时,中断,公平锁等。

2、Redis分布式锁

Redis简单使用方式:

set key value ex seconds||px milloseconds nx

        ex表示超时时间,nx表示只有不存在的时侯才可以创建成功。又由于Redis的命令执行是单线程的,这样就是分布式锁的完美实现。

        其实在这之前,Redis还有一个是先执行 setnx(不存在时侯创建),然后再执行expire,设置超时时间,但是因为这不是一个原子操作,这意味着如果中间出现各种异常,都会出现错误。比如setnx执行完之后,redis服务挂了,或者网络挂了,总之没执行成功expire,那么这个锁就一直不会释放,假如某个线程执行时间非常长,一直不释放锁,那么整个系统可能会因此出现阻塞。

redis-cli:

ip> set test:lock true ex 100 nx

上面就创建了一个锁,超时时间是100

用Java简单操作:

if(redisTemplate.opsForValue().setIfAbsent("test:lock",200,100,TimeUnit.SECONDS)){
            log.info("redis testlock设置锁成功,value:200,超时时间为100秒");
        }

注意,上面在spring-data-redis 2.0版本以后才有,之前的得用execute去执行一下。

redisTemplate.execute(new RedisCallback<Boolean>() {
        @Override
        public Boolean doInRedis(RedisConnection connection) throws DataAccessException {

            JedisCommands commands = (JedisCommands)connection.getNativeConnection();
            String result = commands.set(key, value, "NX", "PX", unit.toMillis(timeout));

            return "OK".equals(result);
        }
    });

上面展示了如何简单使用redis加锁。但现在考虑两个问题。

        1、假如某个线程成功设置了这个锁,但是由于其业务逻辑执行太久,导致该锁超时被释放了;此时,另外一个线程抢到了这个锁,在这个线程执行的过程中,之前的线程突然执行完了,它最后的操作可能是要释放锁,那它就会把后来的线程锁给释放了,从而引发各种问题。这种情况该怎么解决呢?

        其实很好解决,就是为锁的key设置不同的value,在线程中就是创建一个只属于该线程的随机数。这样在释放锁的时侯,如果该锁的value和自己一样,就释放,否则直接退出。简单逻辑是先用get方法取出该key的value,如果它和自己之前设置的value一致就执行del命令,删除。Redis可以使用lua脚本,使用lua可以轻松实现上面的原子操作。

下面的一个简单释放锁的例子:


@Component
public class RedisLock {

    @Autowired
    private ActivityRedisDao activityRedisDao;

    @Autowired
    private RedisTemplate redisTemplate;

    public void addSimpleLock(){
        Integer value = 200;
        String key = "test:lock";
         //加锁,并设置超时时间
        if(redisTemplate.opsForValue().setIfAbsent(key,value,100,TimeUnit.SECONDS)){
            log.info("redis testlock设置锁成功,value:200,超时时间为100秒");
        }
        try {
                  Thread.sleep(30000L);
        }finally {
            //释放锁
            String luaScript = "local ret = redis.call('get',KEYS[1])\n" +
                    "if ret == ARGV[1] then \n"
                    + "return redis.call('del',KEYS[1])\n"
                    +"else return 0\n"+
                    "end";
            List<String> keys = new ArrayList<>();
            keys.add("test:lock");
            Object ret = activityRedisDao.runLuaScript(luaScript,keys,value);
        }
    }

}

        上面是Redis分布式锁的简单使用场景,如果更复杂的,可以使用Redisson,这也是Redis官网推荐的客户端,除了咱们上述说的实现,其还支持可重入、读写锁、信号量,支持锁的自动续期,底层基于Netty。性能强劲。

        上面的例子就是Redis最简单的一种加锁方式,在单例中完全没问题,但实际应用中,Redis不可能只是以单例部署,部署方式都会采用哨兵或者Redis-Cluster等集群模式。在集群模式下,上面的方法是存在极大的不安全性。

        比如在哨兵集群中,某个时刻加了一把锁,但是正巧注节点挂掉了,而此时由于主从同步延迟,锁还没有来得及同步到从节点。从节点成为主节点,但它并没有这把锁,此时别的线程是完全可以再接着加锁的。

        基于此,Redis发布了一种加锁算法:RedLock。其实也是基于上面的基本思想。然后在整个集群中,通过同时加锁,达到目的。具体思想:

        假设现在有N个master节点,这几个节点相互独立,不存在主从复制,其实就是目前比较稳定的Redis-Cluster。

获取锁:

        1、首先获取当前时间戳,毫秒级;

        2、客户端轮流地从所有实例中获取锁,直到成功地从超过一半(N/2+1)个实例中获取了锁为止;

        3、此时这个锁的有效时间是最初始设置的有效时间减去获取锁的消耗时间。

        4、假如客户端轮询完整个集群的实例之后,依然没有获得半数以上的锁,那么就要把所有之前已经获得的锁释放掉,整个流程也以失败告终。

        5、如果获取了半数以上的锁,且最后的过期时间有效,那么就认为获取锁成功。

        注意第二点中,客户端与每个节点都会有一个最大设置锁时间,如果超时就不再该节点建立锁,避免消耗太常时间。

        此外,如果客户端没有获取到锁之后,也不要立即重试,否则会出现多个客户端申请同一把锁,然后导致谁也没成功。应该经过一段随机时间之后再重试。

        释放锁,释放锁是遍历集群所有master节点,不管它实际上有没有锁。

        好了,现在考虑这样一个场景。假如现在Redis集群有5个节点,一个客户端有了1,2,3三个节点的锁。

        此时,突然服务器宕机了(不是执行Restart命令),那么此时还没有进行AOF,可能这台机器重启时就没有该锁了,而恰好另外一个客户端又申请了锁,那么此时就会造成两个客户端同时申请了锁。

        这种情况目前最好的解决办法是等锁过期再重启,当然这种势必带来性能的下降。

RedLock基本上在常用的redis客户端中都有实现,Redisson更不必说,RedissonRedLock主要是继承了MultiLock,感兴趣的可以看看源码, 这里看一下python中的实现:

class Redlock(object):

    def lock(self, resource, ttl):
        retry = 0
        val = self.get_unique_id()

        # Add 2 milliseconds to the drift to account for Redis expires
        # precision, which is 1 millisecond, plus 1 millisecond min
        # drift for small TTLs.
        drift = int(ttl * self.clock_drift_factor) + 2

        redis_errors = list()
        while retry < self.retry_count:
            n = 0
            start_time = int(time.time() * 1000)
            del redis_errors[:]
            #遍历所有节点
            for server in self.servers:
                try:
                    if self.lock_instance(server, resource, val, ttl):
                        n += 1
                except RedisError as e:
                    redis_errors.append(e)
            elapsed_time = int(time.time() * 1000) - start_time
            validity = int(ttl - elapsed_time - drift)
            if validity > 0 and n >= self.quorum:
                if redis_errors:
                    raise MultipleRedlockException(redis_errors)
                return Lock(validity, resource, val)
            else:
                for server in self.servers:
                    try:
                        self.unlock_instance(server, resource, val)
                    except:
                        pass
                retry += 1
                time.sleep(self.retry_delay)
        return False

    def unlock(self, lock):
        redis_errors = []
        for server in self.servers:
            try:
                self.unlock_instance(server, lock.resource, lock.key)
            except RedisError as e:
                redis_errors.append(e)
        if redis_errors:
            raise MultipleRedlockException(redis_errors)

        不过,RedLock还是存在很大弊端的,不能完全保证准确性。此外它还严格依赖时钟,比如一个服务器时钟异常,锁提前被释放了、比如执行过程中发生较长时间GC,导致锁过期释放,GC结束之后,又来执行业务逻辑等等。

        总结Redis分布式锁,实现起来比较简单,但可能会在集群模式下出现数据不一致的问题。而且RedLock算法虽然一定程度上保证了安全性,但却损失了性能。注意,只是一定程度上,RedLock没有全部解决安全性。

        此外,如果应用执行过程中宕机了,锁没有释放,其他应用也拿不到锁,一直执行不了,只能等待锁自己过期。这也是一直被青睐基于ZK实现的分布式锁的人所诟病的一点。

3、Zookeeper分布式锁

        zookeeper分布式锁的优越性完全得益于其自身机制。

        相比于Redis,zk分布式锁在获得锁之前不需要不断地重试,只需要监听即可(利用Watch);在持有锁的服务器宕机之后,也不用等到过期后释放锁,zookeeper会自动将其摘除。

其主要使用了ZK的几个功能:

     1、ZK可以创建临时有序节点;

     2、节点down之后,可以将临时节点删除;

     3、watch监听机制

获取锁的基本流程:

1、客户端创建子节点,并编号,******-01,最早得编号为1,以此累加;

2、客户端判断自己是不是最早的那个子节点(有序节点),如果是就获取锁成功;如果不是,就监听上一个节点,比如编号2监听1,编号3监听2等等。

3、执行完业务代码直接释放锁。

   (下面的图片非原创,来源于网络)

        ZK的节点分为持久节点、有序持久节点、临时节点、临时有序节点。实现分布式锁用的是临时有序节点。ZK也经常用于注册中心,将暴露的服务url注册到ZK上。

        Watch监听是ZK比较重要的一个机制,它可以避免惊群效应。惊群效应这个概念接触过多进程的应该都熟悉,一次可能会唤醒所有进程,但实际上只能有其中一个执行;像Nginx为了避免惊群效应是通过全局互斥锁实现的。

        Watch的思想就是客户端注册watch事件到ZK,随后当事件发生后,会回调客户端注册的事件,当前一个节点持有锁且释放了(临时节点删除),客户端感知到就会执行获取锁的过程。

        ZK作为一个完美的分布式协调方案,其可靠性要高于Redis,因为zookeeper不用担心突然宕机的问题,因为当某节点出现问题后,会自动摘除该节点,也就意味着自动释放了锁。

        不过ZK在性能上也经常被吐槽,都说它不适合于高并发场景。这个观点的依据是ZK的加锁过程是每个客户端都会创建一个临时节点,释放锁的过程也是每个客户端都要删除一个临时节点,这种频繁的增加节点,删除节点的操作都是由ZK集群中的Leader统一完成的,那么当并发量较大时,Leader节点就一定会成为性能瓶颈的。

        Curator(org.apache.curator)客户端实现了分布式锁,它实现了多种类型的锁,包括互斥锁,读写锁,信号量等。可以直接用,关键类:InterProcessMutex,代码比较多,这里就不贴了,感兴趣的可以看一下源代码。

4、基于ETCD实现的分布式锁

        ETCD是K8s的衍生品,在K8s中发挥关键的作用,主要用于分享配置和服务发现,是用Go写的,现在也经常用来作为分布式锁。不过显然没有上述几种分布式锁常见,说实话我也是今年才接触的,主要是公司让我写一篇专利,我就选题就订了这个。现在市面上好用的ETCD分布式锁客户端也是用golang写的,Java也有,但我认为并不是特别成熟。而且,现在基本上只支持ETCD的互斥锁,没有读写锁等类似于Redisson客户端实现的复杂方案。因此我写的专利就是提出如何基于ETCD实现分布式读写锁,且支持写锁降级、读锁升级等。专利还在受理中,内容不能公布。只能先贴一个我画的一张图,主要展示了我在创建锁的过程中创建的key:

        ETCD集成了Redis的高性能(采用K-V存储方式),又借鉴了ZK的高可靠性,但不同于ZK通过自研的ZAB实现强一致性,ETCD通过raft协议来实现一致性,且采用基于HTTP2的grpc实现通信,因此性能要比ZK更好。

        和ZK一样,ETCD也有Watch机制,不同的是,其还存在Prefix,Lease以及Revision机制。

  1. Watch机制:一种监听机制,通过该机制可以对系统内的某个key或者节点进行监听,当监听对象发生变化时,系统会主动给listener发送节点变更事件通知,该机制减少了轮询导致的资源消耗,避免惊群效应。常用于Redis的事务监听、ZK的节点监听、ETCD的key监听。
  2. Prefix机制:ETCD提供的一种前缀匹配机制,可通过前缀寻找到当前已存在的所有key列表。
  3. Revision机制:对于ETCD的每一个key,ETCD都会维护一个全局的Revision列表,每新增一个事务,revision自动加1。
  4. Lease 机制:支持有效期,且可以续期。

总结上面提到的几种分布式锁,现在做一下对比。

从性能上来看,Redis>ETCD>ZK>Mysql,Redis做为一个NoSQL中间件,性能不可比拟,Mysql最差。

从可靠性来看:ETCD>ZK>Redis>Mysql

从实现难易来看:ZK>ETCD>Redis>Mysql。

说句实话,目前我用的比较多的还是Redis。

  • 21
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
主要是介绍各种格式流行的软件设计模式,对于程序员的进一步提升起推进作用,有时间可以随便翻翻~~ 23种设计模式汇集 如果你还不了解设计模式是什么的话? 那就先看设计模式引言 ! 学习 GoF 设计模式的重要性 建筑和软件中模式之异同 A. 创建模式 设计模式之 Singleton(单态/单件) 阎宏博士讲解:单例(Singleton)模式 保证一个类只有一个实例,并提供一个访问它的全局访问点 设计模式之 Factory(工厂方法和抽象工厂) 使用工厂模式就象使用 new 一样频繁. 设计模式之 Builder 汽车由车轮 方向盘 发动机很多部件组成,同时,将这些部件组装成汽车也是一件复杂的工作,Builder 模式就是将这两 种情况分开进行。 设计模式之 Prototype(原型) 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 B. 结构模式 设计模式之 Adapter(适配器) 使用类再生的两个方式:组合(new)和继承(extends),这个已经在 thinking in java中提到过. 设计模式之 Proxy(代理) 以 Jive 为例,剖析代理模式在用户级别授权机制上的应用 设计模式之 Facade(门面?) 可扩展的使用 JDBC针对不同的数据库编程,Facade提供了一种灵活的实现. 设计模式之 Composite(组合) 就是将类用树形结构组合成一个单位.你向别人介绍你是某单位,你是单位中的一个元素,别人和你做买卖,相当于 和单位做买卖。文章中还对 Jive再进行了剖析。 设计模式之 Decorator(装饰器) Decorator 是个油漆工,给你的东东的外表刷上美丽的颜色. 设计模式之 Bridge(桥连) 将牛郎织女分开(本应在一起,分开他们,形成两个接口),在他们之间搭建一个桥(动态的结合) 设计模式之 Flyweight(共享元) 提供 Java运行性能,降低小而大量重复的类的开销. C. 行为模式 设计模式之 Command(命令) 什么是将行为封装,Command 是最好的说明. 设计模式之 Observer(观察者) 介绍如何使用 Java API 提供的现成 Observer 设计模式之 Iterator(迭代器) 这个模式已经被整合入Java的Collection.在大多数场合下无需自己制造一个Iterator,只要将对象装入Collection中, 直接使用 Iterator 进行对象遍历。 设计模式之 Template(模板方法) 实际上向你介绍了为什么要使用 Java 抽象类,该模式原理简单,使用很普遍. 设计模式之 Strategy(策略) 不同算法各自封装,用户端可随意挑选需要的算法. 设计模式之 Chain of Responsibility(责任链) 各司其职的类串成一串,好象击鼓传花,当然如果自己能完成,就不要推委给下一个. 设计模式之 Mediator(中介) Mediator 很象十字路口的红绿灯,每个车辆只需和红绿灯交互就可以. 设计模式之 State(状态) 状态是编程中经常碰到的实例,将状态对象化,设立状态变换器,便可在状态中轻松切换. 设计模式之 Memento(注释状态?) 很简单一个模式,就是在内存中保留原来数据的拷贝. 设计模式之 Interpreter(解释器) 主要用来对语言的分析,应用机会不多. 设计模式之 Visitor(访问者) 访问者在进行访问时,完成一系列实质性操作,而且还可以扩展. 设计模式引言 设计面向对象软件比较困难,而设计可复用的面向对象软件就更加困难。你必须找到相关的对象,以适当的粒度将它们归 类,再定义类的接口和继承层次,建立对象之间的基本关系。你的设计应该对手头的问题有针对性,同时对将来的问题和需求 也要有足够的通用性。 你也希望避免重复设计或尽可能少做重复设计。有经验的面向对象设计者会告诉你,要一下子就得到复用性和灵活性好的设计, 即使不是不可能的至少也是非常困难的。一个设计在最终完成之前常要被复用好几次,而且每一次都有所修改。 有经验的面向对象设计者的确能做出良好的设计,而新手则面对众多选择无从下手,总是求助于以前使用过的非面向对象 技术。新手需要花费较长时间领会良好的面向对象设计是怎么回事。有经验的设计者显然知道一些新手所不知道的东西,这又 是什么呢? 内行的设计者知道:不是解决任何问题都要从头做起。他们更愿意复用以前使用过的解决方案。当找到一个好的解决方案,他 们会一遍又一遍地使用。这些经验是他们成为内行的部分原因。因此,你会在许多面向对象系统中看到类和相互通信的对象( c o m m u n i c a t i n go b j e c t)的重复模式。这些模式解决特定的设计问题,使面向对象设计更灵活、优雅,最终复用性更 好。它们帮助设计者将新的设计建立在以往工作的基础上,复用以往成功的设计方案。 一个熟悉这些模式的设计者不需要再去发现它们,而能够立即将它们应用于设计问题中。以下类比可以帮助说明这一点。 小说家和剧本作家很少从头开始设计剧情。他们总是沿袭一些业已存在的模式,像“悲剧性英雄”模式(《麦克白》、《哈姆雷特》 等)或“浪漫小说”模式(存在着无数浪漫小说)。同样地,面向对象设计员也沿袭一些模式,像“用对象表示状态”和“修饰对象以便 于你能容易地添加/删除属性”等。一旦懂得了模式,许多设计决策自然而然就产生了。 我们都知道设计经验的重要价值。你曾经多少次有过这种感觉—你已经解决过了一个问题但就是不能确切知道是在什么地 方或怎么解决的?如果你能记起以前问题的细节和怎么解决它的,你就可以复用以前的经验而不需要重新发现它。然而,我们 并没有很好记录下可供他人使用的软件设计经验。 学习 GoF设计模式的重要性 著名的 EJB 领域顶尖的专家 Richard Monson-Haefel 在其个人网站:www.EJBNow.com 中极力推荐的 GoF 的《设计模式》,原文 如下: Design Patterns Most developers claim to experience an epiphany reading this book. If you've never read the Design Patterns book then you have suffered a very serious gap in your programming education that should be remedied immediately. 翻译: 很多程序员在读完这本书,宣布自己相当于经历了一次"主显节"(纪念那稣降生和受洗的双重节日),如果你从来没有读 过这本书,你会在你的程序教育生涯里存在一个严重裂沟,所以你应该立即挽救弥补! 可以这么说:GoF 设计模式是程序员真正掌握面向对象核心思想的必修课。虽然你可能已经通过了 SUN 的很多令人炫目的 技术认证,但是如果你没有学习掌握 GoF 设计模式,只能说明你还是一个技工。 在浏览《Thingking in Java》(第一版)时,你是不是觉得好象这还是一本 Java 基础语言书籍?但又不纯粹是,因为这本书的作 者将面向对象的思想巧妙的融合在 Java 的具体技术上,潜移默化的让你感觉到了一种新的语言和新的思想方式的诞生。 但是读完这本书,你对书中这些蕴含的思想也许需要一种更明晰更系统更透彻的了解和掌握,那么你就需要研读 GoF 的《设 计模式》了。 《Thingking in Java》(第一版中文)是这样描述设计模式的:他在由 Gamma, Helm 和 Johnson Vlissides 简称 Gang of Four(四人 帮),缩写 GoF 编著的《Design Patterns》一书中被定义成一个“里程碑”。事实上,那本书现在已成为几乎所有 OOP(面向对象程 序设计)程序员都必备的参考书。(在国外是如此)。 GoF 的《设计模式》是所有面向对象语言(C++ Java C#)的基础,只不过不同的语言将之实现得更方便地使用。 GOF 的设计模式是一座"桥" 就 Java 语言体系来说,GOF 的设计模式是 Java 基础知识和 J2EE 框架知识之间一座隐性的"桥"。 会 Java 的人越来越多,但是一直徘徊在语言层次的程序员不在少数,真正掌握 Java 中接口或抽象类的应用不是很多,大家 经常以那些技术只适合大型项目为由,避开或忽略它们,实际中,Java 的接口或抽象类是真正体现 Java 思想的核心所在,这些 你都将在 GoF 的设计模式里领略到它们变幻无穷的魔力。 GoF 的设计模式表面上好象也是一种具体的"技术",而且新的设计模式不断在出现,设计模式自有其自己的发展轨道,而这 些好象和 J2EE .Net 等技术也无关! 实际上,GoF 的设计模式并不是一种具体"技术",它讲述的是思想,它不仅仅展示了接口或抽象类在实际案例中的灵活应用 和智慧,让你能够真正掌握接口或抽象类的应用,从而在原来的 Java 语言基础上跃进一步,更重要的是,GoF 的设计模式反复 向你强调一个宗旨:要让你的程序尽可能的可重用。 这其实在向一个极限挑战:软件需求变幻无穷,计划没有变化快,但是我们还是要寻找出不变的东西,并将它和变化的东 西分离开来,这需要非常的智慧和经验。 而 GoF 的设计模式是在这方面开始探索的一块里程碑。 J2EE 等属于一种框架软件,什么是框架软件?它不同于我们以前接触的 Java API 等,那些属于 Toolkist(工具箱),它不再被动 的被使用,被调用,而是深刻的介入到一个领域中去,J2EE 等框架软件设计的目的是将一个领域中不变的东西先定义好,比如 整体结构和一些主要职责(如数据库操作 事务跟踪 安全等),剩余的就是变化的东西,针对这个领域中具体应用产生的具体不同 的变化需求,而这些变化东西就是 J2EE 程序员所要做的。 由此可见,设计模式和 J2EE 在思想和动机上是一脉相承,只不过 1.设计模式更抽象,J2EE 是具体的产品代码,我们可以接触到,而设计模式在对每个应用时才会产生具体代码。 2.设计模式是比 J2EE 等框架软件更小的体系结构,J2EE 中许多具体程序都是应用设计模式来完成的,当你深入到 J2EE 的内 部代码研究时,这点尤其明显,因此,如果你不具备设计模式的基础知识(GoF 的设计模式),你很难快速的理解 J2EE。不能理解 J2EE,如何能灵活应用? 3.J2EE 只是适合企业计算应用的框架软件,但是 GoF 的设计模式几乎可以用于任何应用!因此 GoF 的设计模式应该是 J2EE 的重要理论基础之一。 所以说,GoF 的设计模式是 Java 基础知识和 J2EE 框架知识之间一座隐性的"桥"。为什么说隐性的? GOF 的设计模式是一座隐性的"桥" 因为很多人没有注意到这点,学完 Java 基础语言就直接去学 J2EE,有的甚至鸭子赶架,直接使用起 Weblogic 等具体 J2EE 软 件,一段时间下来,发现不过如此,挺简单好用,但是你真正理解 J2EE 了吗?你在具体案例中的应用是否也是在延伸 J2EE 的思 想? 如果你不能很好的延伸 J2EE 的思想,那你岂非是大炮轰蚊子,认识到 J2EE 不是适合所有场合的人至少是明智的,但我们更 需要将 J2EE 用对地方,那么只有理解 J2EE 此类框架软件的精髓,那么你才能真正灵活应用 Java 解决你的问题,甚至构架出你自 己企业的框架来。(我们不能总是使用别人设定好的框架,为什么不能有我们自己的框架?) 因此,首先你必须掌握 GoF 的设计模式。虽然它是隐性,但不是可以越过的。 关于本站“设计模式” Java 提供了丰富的 API,同时又有强大的数据库系统作底层支持,那么我们的编程似乎变成了类似积木的简单"拼凑"和调用, 甚至有人提倡"蓝领程序员",这些都是对现代编程技术的不了解所至. 在真正可复用的面向对象编程中,GoF 的《设计模式》为我们提供了一套可复用的面向对象技术,再配合 Refactoring(重构方法), 所以很少存在简单重复的工作,加上Java 代码的精炼性和面向对象纯洁性(设计模式是 java 的灵魂),编程工作将变成一个让你时刻 体验创造快感的激动人心的过程. 为能和大家能共同探讨"设计模式",我将自己在学习中的心得写下来,只是想帮助更多人更容易理解 GoF 的《设计模式》。由 于原著都是以C++为例, 以Java为例的设计模式基本又都以图形应用为例,而我们更关心Java在中间件等服务器方面的应用,因此, 本站所有实例都是非图形应用,并且顺带剖析 Jive论坛系统.同时为降低理解难度,尽量避免使用 UML 图. 如果你有一定的面向对象编程经验,你会发现其中某些设计模式你已经无意识的使用过了;如果你是一个新手,那么从开始就 培养自己良好的编程习惯(让你的的程序使用通用的模式,便于他人理解;让你自己减少重复性的编程工作),这无疑是成为一个优秀 程序员的必备条件. 整个设计模式贯穿一个原理:面对接口编程,而不是面对实现.目标原则是:降低耦合,增强灵活性. 建筑和软件中模式之异同 CSDN 的透明特别推崇《建筑的永恒之道》,认为从中探寻到软件的永恒之道,并就"设计模式"写了专门文章《探寻软件的永恒 之道 》,其中很多观点我看了很受启发,以前我也将"设计模式" 看成一个简单的解决方案,没有从一种高度来看待"设计模式"在软 件中地位,下面是我自己的一些想法: 建筑和软件某些地方是可以来比喻的 特别是中国传统建筑,那是很讲模式的,这些都是传统文化使然,比如京剧 一招一式都有套路;中国画,也有套路,树应该怎么画 法?有几种画法?艺术大家通常是创造出自己的套路,比如明末清初,水墨画法开始成熟,这时画树就不用勾勒这个模式了,而是一笔 下去,浓淡几个叶子,待毛笔的水墨要干枯时,画一下树干,这样,一个活生写意的树就画出来. 我上面这些描述其实都是一种模式,创建模式的人是大师,但是拘泥于模式的人永远是工匠. 再回到传统建筑中,中国的传统建筑是过分注重模式了,所以建筑风格发展不大,基本分南北两派,大家有个感觉,旅游时,到南 方,你发现古代名居建筑都差不多;北方由于受满人等少数民族的影响,在建筑色彩上有些与南方迥异,但是很多细节地方都差不多. 这些都是模式的体现. 由于建筑受材料和功用以及费用的影响,所用模式种类不多,这点是和软件很大的不同. 正因为这点不同,导致建筑的管理模式和软件的管理模式就有很多不同, 有些人认识不到这点,就产生了可以大量使用"软件 蓝领"的想法,因为他羡慕建筑中"民工"的低成本. 要知道软件还有一个与建筑截然相反的责任和用途,那就是:现代社会中,计划感不上变化,竞争激烈,所有一切变幻莫测,要应 付所有这些变化,首推信息技术中的软件,只有软件能够帮助人类去应付各种变化.而这点正好与建筑想反,建筑是不能帮助人类去 应付变化的,(它自己反而要求稳固,老老实实帮助人遮风避雨,总不能叫人类在露天或树叶下打开电脑编软件吧). 软件要帮助人类去应付变化,这是软件的首要责任,所以,软件中模式产生的目的就和建筑不一样了,建筑中的模式产生可以因 为很多原因:建筑大师的创意;材料的革新等;建筑中这些模式一旦产生,容易发生另外一个缺点,就是有时会阻碍建筑本身的发展, 因为很多人会不思创造,反复使用老的模式进行设计,阻碍建筑的发展. 但是在软件中,这点正好相反,软件模式的产生是因为变化的东西太多,为减轻人类的负担,将一些不变的东西先用模式固化,这 样让人类可以更加集中精力对付变化的东西,所以在软件中大量反复使用模式(我个人认为这样的软件就叫框架软件了,比如J2EE), 不但没阻碍软件的发展,反而是推动了软件的发展.因为其他使用这套软件的人就可以将更多精力集中在对付那些无法用模式的 应用上来. 可以关于建筑和软件中的模式作用可以总结如下: 在软件中,模式是帮助人类向"变化"战斗,但是在软件中还需要和'变化'直接面对面战斗的武器:人的思维,特别是创造 分析思 维等等,这些是软件真正的灵魂,这种思维可以说只要有实践需求(如有新项目)就要求发生,发生频度高,人类的创造或分析思 维决定了软件的质量和特点。 而在建筑中,模式可以构成建筑全部知识,当有新的需求(如有新项目),一般使用旧的模式都可以完成,因此对人类的创造以 及分析思维不是每个项目都必须的,也不是非常重要的,对创造性的思维的需求只是属于锦上添花(除非人类以后离开地球居 住了〕。 设计模式之 Singleton(单态) 模式实战书籍《Java实用系统开发指南》 单态定义: Singleton 模式主要作用是保证在 Java 应用程序中,一个类 Class 只有一个实例存在。 在很多操作中,比如建立目录 数据库连接都需要这样的单线程操作。 还有, singleton 能够被状态化; 这样,多个单态类在一起就可以作为一个状态仓库一样向外提供服务,比如,你要论坛中的 帖子计数器,每次浏览一次需要计数,单态类能否保持住这个计数,并且能 synchronize 的安全自动加 1,如果你要把这个数字 永久保存到数据库,你可以在不修改单态接口的情况下方便的做到。 另外方面,Singleton 也能够被无状态化。提供工具性质的功能, Singleton 模式就为我们提供了这样实现的可能。使用 Singleton 的好处还在于可以节省内存,因为它限制了实例的个数,有 利于 Java 垃圾回收(garbage collection)。 我们常常看到工厂模式中类装入器(class loader)中也用 Singleton 模式实现的,因为被装入的类实际也属于资源。 如何使用? 一般 Singleton 模式通常有几种形式: public class Singleton { private Singleton(){} //在自己内部定义自己一个实例,是不是很奇怪? //注意这是 private 只供内部调用 private static Singleton instance = new Singleton(); //这里提供了一个供外部访问本 class 的静态方法,可以直接访问 public static Singleton getInstance() { return instance; } } 第二种形式: public class Singleton { private static Singleton instance = null; public static synchronized Singleton getInstance() { //这个方法比上面有所改进,不用每次都进行生成对象,只是第一次 //使用时生成实例,提高了效率! if (instance==null) instance=new Singleton(); return instance; } } 使用 Singleton.getInstance()可以访问单态类。 上面第二中形式是 lazy initialization,也就是说第一次调用时初始 Singleton,以后就不用再生成了。 注意到 lazy initialization 形式中的 synchronized,这个 synchronized 很重要,如果没有 synchronized,那么使用 getInstance() 是有可能得到多个 Singleton 实例。关于 lazy initialization 的 Singleton 有很多涉及 double-checked locking (DCL)的讨论,有兴趣者 进一步研究。 一般认为第一种形式要更加安全些。 使用 Singleton 注意事项: 有时在某些情况下,使用 Singleton 并不能达到 Singleton 的目的,如有多个 Singleton 对象同时被不同的类装入器装载;在 EJB 这样的分布式系统中使用也要注意这种情况,因为 EJB 是跨服务器,跨 JVM 的。 我们以 SUN 公司的宠物店源码(Pet Store 1.3.1)的 ServiceLocator 为例稍微分析一下: 在 Pet Store中 ServiceLocator 有两种,一个是 EJB 目录下;一个是 WEB 目录下,我们检查这两个 ServiceLocator 会发现内容 差不多,都是提供 EJB 的查询定位服务,可是为什么要分开呢?仔细研究对这两种 ServiceLocator 才发现区别:在 WEB 中的 ServiceLocator 的采取 Singleton 模式,ServiceLocator 属于资源定位,理所当然应该使用 Singleton 模式。但是在 EJB 中,Singleton 模式已经失去作用,所以 ServiceLocator 才分成两种,一种面向 WEB 服务的,一种是面向 EJB 服务的。 Singleton 模式看起来简单,使用方法也很方便,但是真正用好,是非常不容易,需要对 Java 的类 线程 内存等概念有相当 的了解。 总之:如果你的应用基于容器,那么 Singleton 模式少用或者不用,可以使用相关替代技术。 进一步深入可参考: Double-checked locking and the Singleton pattern When is a singleton not a singleton? 设计模式如何在具体项目中应用见《Java 实用系统开发指南》。 设计模式之 Factory 工厂模式定义:提供创建对象的接口. 为何使用? 工厂模式是我们最常用的模式了,著名的Jive论坛 ,就大量使用了工厂模式,工厂模式在Java程序系统可以说是随处可见。 为什么工厂模式是如此常用?因为工厂模式就相当于创建实例对象的 new,我们经常要根据类 Class 生成实例对象,如 A a=new A() 工厂模式也是用来创建实例对象的,所以以后 new时就要多个心眼,是否可以考虑实用工厂模式,虽然这样做, 可能多做一些工作,但会给你系统带来更大的可扩展性和尽量少的修改量。 我们以类 Sample为例, 如果我们要创建 Sample的实例对象: Sample sample=new Sample(); 可是,实际情况是,通常我们都要在创建 sample实例时做点初始化的工作,比如赋值 查询数据库等。 首先,我们想到的是,可以使用 Sample的构造函数,这样生成实例就写成: Sample sample=new Sample(参数); 但是,如果创建 sample 实例时所做的初始化工作不是象赋值这样简单的事,可能是很长一段代码,如果也写入构造函数 中,那你的代码很难看了(就需要 Refactor 重整)。 为什么说代码很难看,初学者可能没有这种感觉,我们分析如下,初始化工作如果是很长一段代码,说明要做的工作很多, 将很多工作装入一个方法中,相当于将很多鸡蛋放在一个篮子里,是很危险的,这也是有背于 Java 面向对象的原则,面向对象 的封装(Encapsulation)和分派(Delegation)告诉我们,尽量将长的代码分派“切割”成每段,将每段再“封装”起来(减少段和段 之间偶合联系性),这样,就会将风险分散,以后如果需要修改,只要更改每段,不会再发生牵一动百的事情。 在本例中,首先,我们需要将创建实例的工作与使用实例的工作分开, 也就是说,让创建实例所需要的大量初始化工作从 Sample的构造函数中分离出去。 这时我们就需要 Factory 工厂模式来生成对象了,不能再用上面简单 new Sample(参数)。还有,如果 Sample 有个继承 如 MySample, 按照面向接口编程,我们需要将 Sample 抽象成一个接口.现在 Sample 是接口,有两个子类 MySample 和 HisSample .我们要实例化他们时,如下: Sample mysample=new MySample(); Sample hissample=new HisSample(); 随着项目的深入,Sample可能还会"生出很多儿子出来", 那么我们要对这些儿子一个个实例化,更糟糕的是,可能还要对以 前的代码进行修改:加入后来生出儿子的实例.这在传统程序中是无法避免的. 但如果你一开始就有意识使用了工厂模式,这些麻烦就没有了. 工厂方法 你会建立一个专门生产 Sample实例的工厂: public class Factory{ public static Sample creator(int which){ //getClass 产生 Sample 一般可使用动态类装载装入类。 if (which==1) return new SampleA(); else if (which==2) return new SampleB(); } } 那么在你的程序中,如果要实例化 Sample 时.就使用 Sample sampleA=Factory.creator(1); 这样,在整个就不涉及到 Sample 的具体子类,达到封装效果,也就减少错误修改的机会,这个原理可以用很通俗的话来比喻: 就是具体事情做得越多,越容易范错误.这每个做过具体工作的人都深有体会,相反,官做得越高,说出的话越抽象越笼统,范错误可 能性就越少.好象我们从编程序中也能悟出人生道理?呵呵. 使用工厂方法 要注意几个角色,首先你要定义产品接口,如上面的 Sample,产品接口下有 Sample接口的实现类,如 SampleA,其次要有一个 factory 类,用来生成产品 Sample,如下图,最右边是生产的对象 Sample: 进一步稍微复杂一点,就是在工厂类上进行拓展,工厂类也有继承它的实现类 concreteFactory 了。 抽象工厂 工厂模式中有: 工厂方法(Factory Method) 抽象工厂(Abstract Factory). 这两个模式区别在于需要创建对象的复杂程度上。如果我们创建对象的方法变得复杂了,如上面工厂方法中是创建一个对象 Sample,如果我们还有新的产品接口 Sample2. 这里假设:Sample有两个 concrete 类 SampleA 和 SamleB,而 Sample2 也有两个 concrete类 Sample2A 和 SampleB2 那么,我们就将上例中 Factory 变成抽象类,将共同部分封装在抽象类中,不同部分使用子类实现,下面就是将上例中的 Factory 拓展成抽象工厂: public abstract class Factory{ public abstract Sample creator(); public abstract Sample2 creator(String name); } public class SimpleFactory extends Factory{ public Sample creator(){ ......... return new SampleA } public Sample2 creator(String name){ ......... return new Sample2A } } public class BombFactory extends Factory{ public Sample creator(){ ...... return new SampleB } public Sample2 creator(String name){ ...... return new Sample2B } } 从上面看到两个工厂各自生产出一套 Sample和 Sample2,也许你会疑问,为什么我不可以使用两个工厂方法来分别生产 Sample和 Sample2? 抽象工厂还有另外一个关键要点,是因为 SimpleFactory 内,生产 Sample和生产 Sample2 的方法之间有一定联系, 所以才要将这两个方法捆绑在一个类中,这个工厂类有其本身特征,也许制造过程是统一的,比如:制造工艺比较简单,所以 名称叫 SimpleFactory。 在实际应用中,工厂方法用得比较多一些,而且是和动态类装入器组合在一起应用, 举例 我们以 Jive的 ForumFactory 为例,这个例子在前面的 Singleton 模式中我们讨论过,现在再讨论其工厂模式: public abstract class ForumFactory { private static Object initLock = new Object(); private static String className = "com.jivesoftware.forum.database.DbForumFactory"; private static ForumFactory factory = null; public static ForumFactory getInstance(Authorization authorization) { //If no valid authorization passed in, return null. if (authorization == null) { return null; } //以下使用了 Singleton 单态模式 if (factory == null) { synchronized(initLock) { if (factory == null) { ...... try { //动态转载类 Class c = Class.forName(className); factory = (ForumFactory)c.newInstance(); } catch (Exception e) { return null; } } } } //Now, 返回 proxy.用来限制授权对 forum 的访问 return new ForumFactoryProxy(authorization, factory, factory.getPermissions(authorization)); } //真正创建 forum 的方法由继承 forumfactory 的子类去完成. public abstract Forum createForum(String name, String description) throws UnauthorizedException, ForumAlreadyExistsException; .... } 因为现在的 Jive是通过数据库系统存放论坛帖子等内容数据,如果希望更改为通过文件系统实现,这个工厂方法 ForumFactory 就提供了提供动态接口: private static String className = "com.jivesoftware.forum.database.DbForumFactory"; 你可以使用自己开发的创建 forum 的方法代替 com.jivesoftware.forum.database.DbForumFactory 就可以. 在上面的一段代码中一共用了三种模式,除了工厂模式外,还有 Singleton 单态模式,以及 proxy模式,proxy 模式主要用来 授权用户对 forum 的访问,因为访问 forum 有两种人:一个是注册用户 一个是游客 guest,那么那么相应的权限就不一样,而且 这个权限是贯穿整个系统的,因此建立一个 proxy,类似网关的概念,可以很好的达到这个效果. 看看 Java 宠物店中的 CatalogDAOFactory: public class CatalogDAOFactory { /** * 本方法制定一个特别的子类来实现 DAO 模式。 * 具体子类定义是在 J2EE 的部署描述器中。 */ public static CatalogDAO getDAO() throws CatalogDAOSysException { CatalogDAO catDao = null; try { InitialContext ic = new InitialContext(); //动态装入 CATALOG_DAO_CLASS //可以定义自己的 CATALOG_DAO_CLASS,从而在无需变更太多代码 //的前提下,完成系统的巨大变更。 String className =(String) ic.lookup(JNDINames.CATALOG_DAO_CLASS); catDao = (CatalogDAO) Class.forName(className).newInstance(); } catch (NamingException ne) { throw new CatalogDAOSysException(" CatalogDAOFactory.getDAO: NamingException while getting DAO type : \n" + ne.getMessage()); } catch (Exception se) { throw new CatalogDAOSysException(" CatalogDAOFactory.getDAO: Exception while getting DAO type : \n" + se.getMessage()); } return catDao; } } CatalogDAOFactory 是典型的工厂方法, catDao 是通过动态类装入器 className 获得 CatalogDAOFactory 具体实现 子类,这个实现子类在 Java 宠物店是用来操作 catalog 数据库,用户可以根据数据库的类型不同,定制自己的具体实现子类, 将自己的子类名给与 CATALOG_DAO_CLASS 变量就可以。 由此可见,工厂方法确实为系统结构提供了非常灵活强大的动态扩展机制,只要我们更换一下具体的工厂方法,系统其他 地方无需一点变换,就有可能将系统功能进行改头换面的变化。 设计模式之 Builder Builder 模式定义: 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示. Builder 模式是一步一步创建一个复杂的对象,它允许用户可以只通过指定复杂对象的类型和内容就可以构建它们.用户不知 道内部的具体构建细节.Builder 模式是非常类似抽象工厂模式,细微的区别大概只有在反复使用中才能体会到. 为何使用? 是为了将构建复杂对象的过程和它的部件解耦.注意: 是解耦过程和部件. 因为一个复杂的对象,不但有很多大量组成部分,如汽车,有很多部件:车轮 方向盘 发动机还有各种小零件等等,部件很多,但 远不止这些,如何将这些部件装配成一辆汽车,这个装配过程也很复杂(需要很好的组装技术),Builder 模式就是为了将部件和组装 过程分开. 如何使用? 首先假设一个复杂对象是由多个部件组成的,Builder 模式是把复杂对象的创建和部件的创建分别开来,分别用 Builder 类和 Director 类来表示. 首先,需要一个接口,它定义如何创建复杂对象的各个部件: public interface Builder { //创建部件 A 比如创建汽车车轮 void buildPartA(); //创建部件 B 比如创建汽车方向盘 void buildPartB(); //创建部件 C 比如创建汽车发动机 void buildPartC(); //返回最后组装成品结果 (返回最后装配好的汽车) //成品的组装过程不在这里进行,而是转移到下面的 Director 类中进行. //从而实现了解耦过程和部件 Product getResult(); } 用 Director 构建最后的复杂对象,而在上面 Builder 接口中封装的是如何创建一个个部件(复杂对象是由这些部件组成的),也就 是说 Director 的内容是如何将部件最后组装成成品: public class Director { private Builder builder; public Director( Builder builder ) { this.builder = builder; } // 将部件 partA partB partC 最后组成复杂对象 //这里是将车轮 方向盘和发动机组装成汽车的过程 public void construct() { builder.buildPartA(); builder.buildPartB(); builder.buildPartC(); } } Builder 的具体实现 ConcreteBuilder: 通过具体完成接口 Builder 来构建或装配产品的部件; 定义并明确它所要创建的是什么具体东西; 提供一个可以重新获取产品的接口: public class ConcreteBuilder implements Builder { Part partA, partB, partC; public void buildPartA() { //这里是具体如何构建 partA 的代码 }; public void buildPartB() { //这里是具体如何构建 partB 的代码 }; public void buildPartC() { //这里是具体如何构建 partB 的代码 }; public Product getResult() { //返回最后组装成品结果 }; } 复杂对象:产品 Product: public interface Product { } 复杂对象的部件: public interface Part { } 我们看看如何调用 Builder 模式: ConcreteBuilder builder = new ConcreteBuilder(); Director director = new Director( builder ); director.construct(); Product product = builder.getResult(); Builder 模式的应用 在 Java 实际使用中,我们经常用到"池"(Pool)的概念,当资源提供者无法提供足够的资源,并且这些资源需要被很多用户反复共 享时,就需要使用池. "池"实际是一段内存,当池中有一些复杂的资源的"断肢"(比如数据库的连接池,也许有时一个连接会中断),如果循环再利用这 些"断肢",将提高内存使用效率,提高池的性能.修改 Builder 模式中 Director 类使之能诊断"断肢"断在哪个部件上,再修复这个部件. 设计模式之 Prototype(原型) 原型模式定义: 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象. Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,工作原理是:通过将一个 原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。 如何使用? 因为 Java 中的提供 clone()方法来实现对象的克隆,所以 Prototype模式实现一下子变得很简单. 以勺子为例: public abstract class AbstractSpoon implements Cloneable { String spoonName; public void setSpoonName(String spoonName) {this.spoonName = spoonName;} public String getSpoonName() {return this.spoonName;} public Object clone() { Object object = null; try { object = super.clone(); } catch (CloneNotSupportedException exception) { System.err.println("AbstractSpoon is not Cloneable"); } return object; } } 有个具体实现(ConcretePrototype): public class SoupSpoon extends AbstractSpoon { public SoupSpoon() { setSpoonName("Soup Spoon"); } } 调用 Prototype 模式很简单: AbstractSpoon spoon = new SoupSpoon(); AbstractSpoon spoon2 = spoon.clone(); 当然也可以结合工厂模式来创建 AbstractSpoon 实例。 在 Java 中 Prototype 模式变成 clone()方法的使用,由于 Java 的纯洁的面向对象特性,使得在 Java 中使用设计模式变 得很自然,两者已经几乎是浑然一体了。这反映在很多模式上,如 Interator 遍历模式。 设计模式之 Adapter(适配器) 适配器模式定义: 将两个不兼容的类纠合在一起使用,属于结构型模式,需要有 Adaptee(被适配者)和 Adaptor(适配器)两个身份. 为何使用? 我们经常碰到要将两个没有关系的类组合在一起使用,第一解决方案是:修改各自类的接口,但是如果我们没有源代码,或 者,我们不愿意为了一个应用而修改各自的接口。 怎么办? 使用 Adapter,在这两种接口之间创建一个混合接口(混血儿). 如何使用? 实现 Adapter 方式,其实"think in Java"的"类再生"一节中已经提到,有两种方式:组合(composition)和继承 (inheritance). 假设我们要打桩,有两种类:方形桩 圆形桩. public class SquarePeg{ public void insert(String str){ System.out.println("SquarePeg insert():"+str); } } public class RoundPeg{ public void insertIntohole(String msg){ System.out.println("RoundPeg insertIntoHole():"+msg); } } 现在有一个应用,需要既打方形桩,又打圆形桩.那么我们需要将这两个没有关系的类综合应用.假设 RoundPeg 我们没有源 代码,或源代码我们不想修改,那么我们使用 Adapter 来实现这个应用: public class PegAdapter extends SquarePeg{ private RoundPeg roundPeg; public PegAdapter(RoundPeg peg)(this.roundPeg=peg;) public void insert(String str){ roundPeg.insertIntoHole(str);} } 在上面代码中,RoundPeg 属于 Adaptee,是被适配者.PegAdapter 是 Adapter,将 Adaptee(被适配者 RoundPeg)和 Target(目标 SquarePeg)进行适配.实际上这是将组合方法(composition)和继承(inheritance)方法综合运用. PegAdapter 首先继承 SquarePeg,然后使用 new 的组合生成对象方式,生成 RoundPeg 的对象 roundPeg,再重载父 类 insert()方法。从这里,你也了解使用 new生成对象和使用 extends 继承生成对象的不同,前者无需对原来的类修改,甚至无 需要知道其内部结构和源代码. 如果你有些 Java 使用的经验,已经发现,这种模式经常使用。 进一步使用 上面的 PegAdapter 是继承了 SquarePeg,如果我们需要两边继承,即继承 SquarePeg 又继承 RoundPeg,因为 Java 中 不允许多继承,但是我们可以实现(implements)两个接口(interface) public interface IRoundPeg{ public void insertIntoHole(String msg); } public interface ISquarePeg{ public void insert(String str); } 下面是新的 RoundPeg 和 SquarePeg, 除了实现接口这一区别,和上面的没什么区别。 public class SquarePeg implements ISquarePeg{ public void insert(String str){ System.out.println("SquarePeg insert():"+str); } } public class RoundPeg implements IRoundPeg{ public void insertIntohole(String msg){ System.out.println("RoundPeg insertIntoHole():"+msg); } } 下面是新的 PegAdapter,叫做 two-way adapter: public class PegAdapter implements IRoundPeg,ISquarePeg{ private RoundPeg roundPeg; private SquarePeg squarePeg; // 构造方法 public PegAdapter(RoundPeg peg){this.roundPeg=peg;} // 构造方法 public PegAdapter(SquarePeg peg)(this.squarePeg=peg;) public void insert(String str){ roundPeg.insertIntoHole(str);} } 还有一种叫 Pluggable Adapters,可以动态的获取几个 adapters 中一个。使用 Reflection 技术,可以动态的发现类中的 Public 方法。 设计模式之 Proxy(代理) 理解并使用设计模式,能够培养我们良好的面向对象编程习惯,同时在实际应用中,可以如鱼得水,享受游刃有余的乐趣. 代理模式是比较有用途的一种模式,而且变种较多,应用场合覆盖从小结构到整个系统的大结构,Proxy 是代理的意思,我们 也许有代理服务器等概念,代理概念可以解释为:在出发点到目的地之间有一道中间层,意为代理. 设计模式中定义: 为其他对象提供一种代理以控制对这个对象的访问. 为什么要使用 Proxy? 1.授权机制 不同级别的用户对同一对象拥有不同的访问权利,如 Jive 论坛系统中,就使用 Proxy 进行授权机制控制,访问 论坛有两种人:注册用户和游客(未注册用户),Jive 中就通过类似 ForumProxy 这样的代理来控制这两种用户对论坛的访问权 限. 2.某个客户端不能直接操作到某个对象,但又必须和那个对象有所互动. 举例两个具体情况: (1)如果那个对象是一个是很大的图片,需要花费很长时间才能显示出来,那么当这个图片包含在文档中时,使用编辑器或浏 览器打开这个文档,打开文档必须很迅速,不能等待大图片处理完成,这时需要做个图片 Proxy 来代替真正的图片. (2)如果那个对象在 Internet 的某个远端服务器上,直接操作这个对象因为网络速度原因可能比较慢,那我们可以先用 Proxy来代替那个对象. 总之原则是,对于开销很大的对象,只有在使用它时才创建,这个原则可以为我们节省很多宝贵的 Java 内存. 所以,有些人认 为 Java 耗费资源内存,我以为这和程序编制思路也有一定的关系. 如何使用 Proxy? 以 Jive 论坛系统为例,访问论坛系统的用户有多种类型:注册普通用户 论坛管理者 系统管理者 游客,注册普通用户才能发 言;论坛管理者可以管理他被授权的论坛;系统管理者可以管理所有事务等,这些权限划分和管理是使用 Proxy完成的. Forum 是 Jive的核心接口,在 Forum 中陈列了有关论坛操作的主要行为,如论坛名称 论坛描述的获取和修改,帖子发表删 除编辑等. 在 ForumPermissions 中定义了各种级别权限的用户: public class ForumPermissions implements Cacheable { /** * Permission to read object. */ public static final int READ = 0; /** * Permission to administer the entire sytem. */ public static final int SYSTEM_ADMIN = 1; /** * Permission to administer a particular forum. */ public static final int FORUM_ADMIN = 2; /** * Permission to administer a particular user. */ public static final int USER_ADMIN = 3; /** * Permission to administer a particular group. */ public static final int GROUP_ADMIN = 4; /** * Permission to moderate threads. */ public static final int MODERATE_THREADS = 5; /** * Permission to create a new thread. */ public static final int CREATE_THREAD = 6; /** * Permission to create a new message. */ public static final int CREATE_MESSAGE = 7; /** * Permission to moderate messages. */ public static final int MODERATE_MESSAGES = 8; ..... public boolean isSystemOrForumAdmin() { return (values[FORUM_ADMIN] || values[SYSTEM_ADMIN]); } ..... } 因此,Forum 中各种操作权限是和 ForumPermissions 定义的用户级别有关系的,作为接口 Forum 的实现:ForumProxy 正是将这种对应关系联系起来.比如,修改 Forum 的名称,只有论坛管理者或系统管理者可以修改,代码如下: public class ForumProxy implements Forum { private ForumPermissions permissions; private Forum forum; this.authorization = authorization; public ForumProxy(Forum forum, Authorization authorization, ForumPermissions permissions) { this.forum = forum; this.authorization = authorization; this.permissions = permissions; } ..... public void setName(String name) throws UnauthorizedException, ForumAlreadyExistsException { //只有是系统或论坛管理者才可以修改名称 if (permissions.isSystemOrForumAdmin()) { forum.setName(name); } else { throw new UnauthorizedException(); } } ... } 而 DbForum 才是接口 Forum 的真正实现,以修改论坛名称为例: public class DbForum implements Forum, Cacheable { ... public void setName(String name) throws ForumAlreadyExistsException { .... this.name = name; //这里真正将新名称保存到数据库中 saveToDb(); .... } ... } 凡是涉及到对论坛名称修改这一事件,其他程序都首先得和ForumProxy打交道,由ForumProxy决定是否有权限做某一样 事情,ForumProxy 是个名副其实的"网关","安全代理系统". 在平时应用中,无可避免总要涉及到系统的授权或安全体系,不管你有无意识的使用 Proxy,实际你已经在使用 Proxy了. 我们继续结合 Jive谈入深一点,下面要涉及到工厂模式了,如果你不了解工厂模式,请看我的另外一篇文章:设计模式之 Factory 我们已经知道,使用 Forum 需要通过 ForumProxy,Jive 中创建一个 Forum 是使用 Factory 模式,有一个总的抽象类 ForumFactory,在这个抽象类中,调用 ForumFactory 是通过 getInstance()方法实现,这里使用了 Singleton(也是设计模式 之一,由于介绍文章很多,我就不写了),getInstance()返回的是 ForumFactoryProxy. 为什么不返回 ForumFactory,而返回 ForumFactory 的实现 ForumFactoryProxy? 原因是明显的,需要通过代理确定是否有权限创建 forum. 在 ForumFactoryProxy 中我们看到代码如下: public class ForumFactoryProxy extends ForumFactory { protected ForumFactory factory; protected Authorization authorization; protected ForumPermissions permissions; public ForumFactoryProxy(Authorization authorization, ForumFactory factory, ForumPermissions permissions) { this.factory = factory; this.authorization = authorization; this.permissions = permissions; } public Forum createForum(String name, String description) throws UnauthorizedException, ForumAlreadyExistsException { //只有系统管理者才可以创建 forum if (permissions.get(ForumPermissions.SYSTEM_ADMIN)) { Forum newForum = factory.createForum(name, description); return new ForumProxy(newForum, authorization, permissions); } else { throw new UnauthorizedException(); } } 方法 createForum 返回的也是 ForumProxy, Proxy 就象一道墙,其他程序只能和 Proxy交互操作. 注意到这里有两个 Proxy:ForumProxy 和 ForumFactoryProxy. 代表两个不同的职责:使用 Forum 和创建 Forum; 至于为什么将使用对象和创建对象分开,这也是为什么使用 Factory 模式的原因所在:是为了"封装" "分派";换句话说,尽可 能功能单一化,方便维护修改. Jive论坛系统中其他如帖子的创建和使用,都是按照 Forum 这个思路而来的. 以上我们讨论了如何使用Proxy进行授权机制的访问,Proxy还可以对用户隐藏另外一种称为copy-on-write的优化方式. 拷贝一个庞大而复杂的对象是一个开销很大的操作,如果拷贝过程中,没有对原来的对象有所修改,那么这样的拷贝开销就没有必 要.用代理延迟这一拷贝过程. 比如:我们有一个很大的 Collection,具体如 hashtable,有很多客户端会并发同时访问它.其中一个特别的客户端要进行连 续的数据获取,此时要求其他客户端不能再向 hashtable 中增加或删除 东东. 最直接的解决方案是:使用 collection 的 lock,让这特别的客户端获得这个 lock,进行连续的数据获取,然后再释放 lock. public void foFetches(Hashtable ht){ synchronized(ht){ //具体的连续数据获取动作.. } } 但是这一办法可能住 Collection 会很长时间,这段时间,其他客户端就不能访问该 Collection 了. 第二个解决方案是clone这个Collection,然后让连续的数据获取针对clone出来的那个Collection操作.这个方案前提是, 这个 Collection 是可 clone的,而且必须有提供深度 clone的方法.Hashtable 就提供了对自己的 clone方法,但不是 Key和 value对象的 clone,关于 Clone含义可以参考专门文章. public void foFetches(Hashtable ht){ Hashttable newht=(Hashtable)ht.clone(); } 问题又来了,由于是针对clone出来的对象操作,如果原来的母体被其他客户端操作修改了, 那么对clone出来的对象操作就 没有意义了. 最后解决方案:我们可以等其他客户端修改完成后再进行 clone,也就是说,这个特别的客户端先通过调用一个叫 clone的方 法来进行一系列数据获取操作.但实际上没有真正的进行对象拷贝,直至有其他客户端修改了这个对象 Collection. 使用 Proxy实现这个方案.这就是 copy-on-write操作. Proxy应用范围很广,现在流行的分布计算方式 RMI 和 Corba 等都是 Proxy模式的应用. 更多 Proxy应用,见 http://www.research.umbc.edu/~tarr/cs491/lectures/Proxy.pdf Sun 公司的 Explore the Dynamic Proxy API Dynamic Proxy Classes 设计模式之 Facade(外观 总管 Manager) Facade 模式的定义: 为子系统中的一组接口提供一个一致的界面. Facade一个典型应用就是数据库 JDBC 的应用,如下例对数据库的操作: public class DBCompare { Connection conn = null; PreparedStatement prep = null; ResultSet rset = null; try { Class.forName( "<driver>" ).newInstance(); conn = DriverManager.getConnection( "<database>" ); String sql = "SELECT * FROM <table> WHERE <column name> = ?"; prep = conn.prepareStatement( sql ); prep.setString( 1, "<column value>" ); rset = prep.executeQuery(); if( rset.next() ) { System.out.println( rset.getString( "<column name" ) ); } } catch( SException e ) { e.printStackTrace(); } finally { rset.close(); prep.close(); conn.close(); } } 上例是 Jsp 中最通常的对数据库操作办法. 在应用中,经常需要对数据库操作,每次都写上述一段代码肯定比较麻烦,需要将其中不变的部分提炼出来,做成一个接口,这 就引入了 facade 外观对象.如果以后我们更换 Class.forName 中的<driver>也非常方便,比如从 Mysql 数据库换到 Oracle 数据库,只要更换 facade接口中的 driver 就可以. 我们做成了一个 Facade 接口,使用该接口,上例中的程序就可以更改如下: public class DBCompare { String sql = "SELECT * FROM <table> WHERE <column name> = ?"; try { Mysql msql=new mysql(sql); prep.setString( 1, "<column value>" ); rset = prep.executeQuery(); if( rset.next() ) { System.out.println( rset.getString( "<column name" ) ); } } catch( SException e ) { e.printStackTrace(); } finally { mysql.close(); mysql=null; } } 可见非常简单,所有程序对数据库访问都是使用改接口,降低系统的复杂性,增加了灵活性. 如果我们要使用连接池,也只要针对 facade接口修改就可以. 由上图可以看出, facade实际上是个理顺系统间关系,降低系统间耦合度的一个常用的办法,也许你已经不知不觉在使用,尽 管不知道它就是 facade. 设计模式之 Composite(组合) Composite 模式定义: 将对象以树形结构组织起来,以达成“部分-整体” 的层次结构,使得客户端对单个对象和组合对象的使用具有一致性. Composite比较容易理解,想到 Composite 就应该想到树形结构图。组合体内这些对象都有共同接口,当组合体一个对象 的方法被调用执行时,Composite 将遍历(Iterator)整个树形结构,寻找同样包含这个方法的对象并实现调用执行。可以用牵一 动百来形容。 所以 Composite 模式使用到 Iterator 模式,和 Chain of Responsibility 模式类似。 Composite 好处: 1.使客户端调用简单,客户端可以一致的使用组合结构或其中单个对象,用户就不必关系自己处理的是单个对象还是整个 组合结构,这就简化了客户端代码。 2.更容易在组合体内加入对象部件. 客户端不必因为加入了新的对象部件而更改代码。 如何使用 Composite? 首先定义一个接口或抽象类,这是设计模式通用方式了,其他设计模式对接口内部定义限制不多, Composite 却有个规定, 那就是要在接口内部定义一个用于访问和管理 Composite组合体的对象们(或称部件 Component). 下面的代码是以抽象类定义,一般尽量用接口 interface, public abstract class Equipment { private String name; //实价 public abstract double netPrice(); //折扣价格 public abstract double discountPrice(); //增加部件方法 public boolean add(Equipment equipment) { return false; } //删除部件方法 public boolean remove(Equipment equipment) { return false; } //注意这里,这里就提供一种用于访问组合体类的部件方法。 public Iterator iter() { return null; } public Equipment(final String name) { this.name=name; } } 抽象类 Equipment 就是 Component 定义,代表着组合体类的对象们,Equipment 中定义几个共同的方法。 public class Disk extends Equipment { public Disk(String name) { super(name); } //定义 Disk 实价为 1 public double netPrice() { return 1.; } //定义了 disk 折扣价格是 0.5 对折。 public double discountPrice() { return .5; } } Disk是组合体内的一个对象,或称一个部件,这个部件是个单独元素( Primitive)。 还有一种可能是,一个部件也是一个组合体,就是说这个部件下面还有'儿子',这是树形结构中通常的情况,应该比较容易理解。 现在我们先要定义这个组合体: abstract class CompositeEquipment extends Equipment { private int i=0; //定义一个 Vector 用来存放'儿子' private Lsit equipment=new ArrayList(); public CompositeEquipment(String name) { super(name); } public boolean add(Equipment equipment) { this.equipment.add(equipment); return true; } public double netPrice() { double netPrice=0.; Iterator iter=equipment.iterator(); for(iter.hasNext()) netPrice+=((Equipment)iter.next()).netPrice(); return netPrice; } public double discountPrice() { double discountPrice=0.; Iterator iter=equipment.iterator(); for(iter.hasNext()) discountPrice+=((Equipment)iter.next()).discountPrice(); return discountPrice; } //注意这里,这里就提供用于访问自己组合体内的部件方法。 //上面 dIsk 之所以没有,是因为 Disk 是个单独(Primitive)的元素. public Iterator iter() { return equipment.iterator() ; { //重载 Iterator 方法 public boolean hasNext() { return i<equipment.size(); } //重载 Iterator 方法 public Object next() { if(hasNext()) return equipment.elementAt(i++); else throw new NoSuchElementException(); } } 上面 CompositeEquipment 继承了 Equipment,同时为自己里面的对象们提供了外部访问的方法,重载了 Iterator,Iterator 是 Java 的 Collection 的一个接口,是 Iterator 模式的实现. 我们再看看 CompositeEquipment 的两个具体类:盘盒 Chassis 和箱子 Cabinet,箱子里面可以放很多东西,如底板, 电源盒,硬盘盒等;盘盒里面可以放一些小设备,如硬盘 软驱等。无疑这两个都是属于组合体性质的。 public class Chassis extends CompositeEquipment { public Chassis(String name) { super(name); } public double netPrice() { return 1.+super.netPrice(); } public double discountPrice() { return .5+super.discountPrice(); } } public class Cabinet extends CompositeEquipment { public Cabinet(String name) { super(name); } public double netPrice() { return 1.+super.netPrice(); } public double discountPrice() { return .5+super.discountPrice(); } } 至此我们完成了整个 Composite模式的架构。 我们可以看看客户端调用 Composote 代码: Cabinet cabinet=new Cabinet("Tower"); Chassis chassis=new Chassis("PC Chassis"); //将 PC Chassis 装到 Tower 中 (将盘盒装到箱子里) cabinet.add(chassis); //将一个 10GB 的硬盘装到 PC Chassis (将硬盘装到盘盒里) chassis.add(new Disk("10 GB")); //调用 netPrice()方法; System.out.println("netPrice="+cabinet.netPrice()); System.out.println("discountPrice="+cabinet.discountPrice()); 上面调用的方法 netPrice()或 discountPrice(),实际上 Composite 使用 Iterator 遍历了整个树形结构,寻找同样包含这 个方法的对象并实现调用执行. Composite是个很巧妙体现智慧的模式,在实际应用中,如果碰到树形结构,我们就可以尝试是否可以使用这个模式。 以论坛为例,一个版(forum)中有很多帖子(message),这些帖子有原始贴,有对原始贴的回应贴,是个典型的树形结构, 那么当然可以使用 Composite模式,那么我们进入 Jive中看看,是如何实现的. Jive 解剖 在 Jive中 ForumThread 是 ForumMessages 的容器 container(组合体).也就是说,ForumThread 类似我们上例中的 CompositeEquipment.它和 messages 的关系如图: [thread] |- [message] |- [message] |- [message] |- [message] |- [message] 我们在 ForumThread 看到如下代码: public interface ForumThread { .... public void addMessage(ForumMessage parentMessage, ForumMessage newMessage) throws UnauthorizedException; public void deleteMessage(ForumMessage message) throws UnauthorizedException; public Iterator messages(); .... } 类似 CompositeEquipment, 提供用于访问自己组合体内的部件方法: 增加 删除 遍历. 结合我的其他模式中对 Jive的分析,我们已经基本大体理解了 Jive 论坛体系的框架,如果你之前不理解设计模式,而直接去看 Jive源代码,你肯定无法看懂。 参考文章: Composite 模式和树形结构的讨论 设计模式之 Decorator(油漆工) 装饰模式:Decorator 常被翻译成"装饰",我觉得翻译成"油漆工"更形象点,油漆工(decorator)是用来刷油漆的,那么被刷油漆的 对象我们称 decoratee.这两种实体在 Decorator 模式中是必须的. Decorator定义: 动态给一个对象添加一些额外的职责,就象在墙上刷油漆.使用 Decorator 模式相比用生成子类方式达到功能的扩充显得更为灵 活. 为什么使用 Decorator? 我们通常可以使用继承来实现功能的拓展,如果这些需要拓展的功能的种类很繁多,那么势必生成很多子类,增加系统的复杂性, 同时,使用继承实现功能拓展,我们必须可预见这些拓展功能,这些功能是编译时就确定了,是静态的. 使用Decorator的理由是:这些功能需要由用户动态决定加入的方式和时机.Decorator提供了"即插即用"的方法,在运行期间决 定何时增加何种功能. 如何使用? 举Adapter 中的打桩示例,在 Adapter 中有两种类:方形桩 圆形桩,Adapter模式展示如何综合使用这两个类,在Decorator模 式中,我们是要在打桩时增加一些额外功能,比如,挖坑 在桩上钉木板等,不关心如何使用两个不相关的类. 我们先建立一个接口: public interface Work { public void insert(); } 接口 Work有一个具体实现:插入方形桩或圆形桩,这两个区别对 Decorator 是无所谓.我们以插入方形桩为例: public class SquarePeg implements Work{ public void insert(){ System.out.println("方形桩插入"); } } 现在有一个应用:需要在桩打入前,挖坑,在打入后,在桩上钉木板,这些额外的功能是动态,可能随意增加调整修改,比如,可能又需 要在打桩之后钉架子(只是比喻). 那么我们使用 Decorator 模式,这里方形桩 SquarePeg 是 decoratee(被刷油漆者),我们需要在 decoratee 上刷些"油漆",这 些油漆就是那些额外的功能. public class Decorator implements Work{ private Work work; //额外增加的功能被打包在这个 List 中 private ArrayList others = new ArrayList(); //在构造器中使用组合 new方式,引入 Work 对象; public Decorator(Work work) { this.work=work; others.add("挖坑"); others.add("钉木板"); } public void insert(){ newMethod(); } //在新方法中,我们在 insert 之前增加其他方法,这里次序先后是用户灵活指定的 public void newMethod() { otherMethod(); work.insert(); } public void otherMethod() { ListIterator listIterator = others.listIterator(); while (listIterator.hasNext()) { System.out.println(((String)(listIterator.next())) + " 正在进行"); } } } 在上例中,我们把挖坑和钉木板都排在了打桩 insert 前面,这里只是举例说明额外功能次序可以任意安排. 好了,Decorator 模式出来了,我们看如何调用: Work squarePeg = new SquarePeg(); Work decorator = new Decorator(squarePeg); decorator.insert(); Decorator 模式至此完成. 如果你细心,会发现,上面调用类似我们读取文件时的调用: FileReader fr = new FileReader(filename); BufferedReader br = new BufferedReader(fr); 实际上 Java 的 I/O API 就是使用 Decorator 实现的,I/O变种很多,如果都采取继承方法,将会产生很多子类,显然相当繁琐. Jive 中的 Decorator 实现 在论坛系统中,有些特别的字是不能出现在论坛中如"打倒 XXX",我们需要过滤这些"反动"的字体.不让他们出现或者高亮度显 示. 在 IBM Java 专栏中专门谈 Jive的文章中,有谈及 Jive中 ForumMessageFilter.java 使用了 Decorator 模式,其实,该程序并 没有真正使用 Decorator,而是提示说:针对特别论坛可以设计额外增加的过滤功能,那么就可以重组 ForumMessageFilter 作 为 Decorator 模式了. 所以,我们在分辨是否真正是Decorator模式,以及会真正使用Decorator模式,一定要把握好Decorator模式的定义,以及其中 参与的角色(Decoratee 和 Decorator). 设计模式之 Bridge Bridge 模式定义 :将抽象和行为划分开来,各自独立,但能动态的结合。 任何事物对象都有抽象和行为之分,例如人,人是一种抽象,人分男人和女人等;人有行为,行为也有各种具体表现,所 以,“人”与“人的行为”两个概念也反映了抽象和行为之分。 在面向对象设计的基本概念中,对象这个概念实际是由属性和行为两个部分组成的,属性我们可以认为是一种静止的,是 一种抽象,一般情况下,行为是包含在一个对象中,但是,在有的情况下,我们需要将这些行为也进行归类,形成一个总的行 为接口,这就是桥模式的用处。 为什么使用? 不希望抽象部分和行为有一种固定的绑定关系,而是应该可以动态联系的。 如果一个抽象类或接口有多个具体实现(子类、concrete subclass),这些子类之间关系可能有以下两种情况: 1. 这多个子类之间概念是并列的,如前面举例,打桩,有两个 concrete class:方形桩和圆形桩;这两个形状上的桩是并列的, 没有概念上的重复。 2.这多个子类之中有内容概念上重叠.那么需要我们把抽象共同部分和行为共同部分各自独立开来,原来是准备放在一个接 口里,现在需要设计两个接口:抽象接口和行为接口,分别放置抽象和行为. 例如,一杯咖啡为例,子类实现类为四个:中杯加奶、大杯加奶、 中杯不加奶、大杯不加奶。 但是,我们注意到:上面四个子类中有概念重叠,可从另外一个角度进行考虑,这四个类实际是两个角色的组合:抽象 和 行为,其中抽象为:中杯和大杯;行为为:加奶 不加奶(如加橙汁 加苹果汁). 实现四个子类在抽象和行为之间发生了固定的绑定关系,如果以后动态增加加葡萄汁的行为,就必须再增加两个类:中杯 加葡萄汁和大杯加葡萄汁。显然混乱,扩展性极差。 那我们从分离抽象和行为的角度,使用 Bridge模式来实现。 如何实现?
TCPIP协议详解卷2:实现 pdf版,有目录,完美阅读体验。 中文书名:TCP/IP详解 卷2:实现 英文书名:TCP/IP Illustrated, Volume 2: The Implementation 作者:(美) Gary R. Wright ,W. Richard Stevens 译者:陆雪莹、蒋慧 等译;谢希仁 校 ISBN:7-111-7567-6 16开,924页,78元 内容简介: 本书完整而详细地介绍了TCP/IP协议是如何实现的。书中给出了约500个图例,15 000行实际操作的C代码,采用举例教学的方法帮助你掌握TCP/IP实现。本书不仅说明了插口API和协议族的关系以及主机实现与路由器实现的差别。还介绍了4.4BSD-Lite版的新的特点,如多播、长肥管道支持、窗口缩放、时间戳选项以及其他主题等等。读者阅读本书时,应当具备卷1中阐述的关于TCP/IP的基本知识。 本书针对任何希望理解TCP/IP协议是如何实现的读者设计;无论是编写网络应用的程序员,负责利用TCP/TP维护计算机系统和网络的系统管理员,还是任何有兴趣理解大块非凡代码的普通读者;本书都是一本优秀的教科书。 作者简介: W.Richard Stevens(1951-1999),国际知名的UNIX和网络专家,受人尊敬的作家。他的著作有《UNIX网络编程》(两卷本),《UNIX环境高级编程》,《TCP/IP详解》(三卷本)等,同时他还是广受欢迎的教师和顾问。Stevens先生1951年生于赞比亚,早年,他就读于美国弗吉尼亚州的费什本军事学校,后获得密歇根大学学士、亚利桑那大学系统工程硕士和博士学位。他曾就职于基特峰国家天文台,从事计算机编程。Stevens先生不幸病逝于1999年9月1日,他的离去是计算机界的巨大损失。 译、校者简介: 谢希仁,中国人民解放军理工大学(南京)计算机系教授,全军网络技术研究中心主任,博士研究生导师,1952年毕业于清华大学电机系电信专业。所编写的《计算机网络》于1992年获全国优秀教材奖。1999年再版的《计算机网络》第2版为普通高等教育“九五”国家级重点教材。近来还主持翻译了Comer写的《TCP/IP网际互联》计算机网络经典教材一套三卷本(电子工业出版社1998年出版),Harnedy写的《简单网络管理协议教程》(电子工业出版社1999年出版)。 陆雪莹,女,1973年1月出生。1994年7月毕业于南京通信工程学院无线通信专业,获工学学士学位。1997年2月于南京通信工程学院计算机软件专业毕业,并获硕士学位。1997年9月至今,任南京通信工程学院计算机教研室教员,同时于解放军理工大学攻读军事通信学博士学位,讲师职称,主要研究方向:智能化网络管理,计算机网络分布式处理。曾参加国家“863”项目,并参加编写专业著作2本,翻译专业著作3本,在各级学术刊物上发表论文5篇。 蒋慧,女,1973年2月出生。1995年毕业于南京通信工程学院计算机系,获计算机应用专业工学学士学位。1998年于南京通信工程学院计算机软件专业毕业,并获硕士学位。1998年9月至今,于解放军理工大学攻读博士学位。自1995年以来,在国内外重要学术刊物和会议上发表8篇论文,其中2篇论文被IEEE国际会议录用。已出版3本有关网络的译作。目前从事软件需求工程、网络协议验证形式化方法以及函数式语言等方面的研究。 译者序: 我们愿意向广大的读者推荐W. Richard Stevens关于TCP/IP的经典著作(共3卷)的中译本。本书是其中的第2卷:《TCP/IP详解 卷2:实现》。 大家知道,TCP/IP已成为计算机网络的事实上的标准。在关于TCP/IP的论著中,最有影响的就是两部著作。一部是Douglas E. Comer写的《用TCP/IP进行网际互连》,一套共3卷(中译本已由电子工业出版社于1998年出版),而另一部就是Stevens写的这3卷书。这两套巨著都很有名,各有其特点。无论是从事计算机网络教学的教师还是进行科研的技术人员,这两套书都应当是必读的。 本书的特点是内容丰富,概念清楚且准确,讲解详细,例子很多。作者在书中举出的所有例子均在作者安装的计算机网络上通过实际验证。各章都留有一定数量的习题。在附录A作者对部分习题给出了解答。在本书的最后,作者给出了许多经典的参考文献,并一一写出了评论。 第2卷是第1卷的继续深入。读者在学习这一卷时,应当先具备第1卷所阐述的关于TCP/IP的基本知识。本卷的特点是使用大量的源代码来讲述TCP/IP协议族中的各协议是怎样实现的。这些内容对于编写TCP/IP网络应用程序的程序员和负责维护基于TCP/IP协议的计算机网络的系统管理员来说,应当是必读的。 参加本书翻译的有:谢钧(序言和第1章~第7章),蒋慧(第8章~第14章,第22章~第23章),吴礼发(第15~第17章),端义峰(第18章~第19章),胥光辉(第20章~第21章)和陆雪莹(第24章~第32章以及全部附录)。全书由谢希仁教授审校。 限于水平,翻译中不妥或错误之处在所难免,敬请广大读者批评指正。 目录: 前言 第1章 概述 1 1.1 引言 1 1.2 源代码表示 1 1.2.1 将拥塞窗口设置为1 1 1.2.2 印刷约定 2 1.3 历史 2 1.4 应用编程接口 3 1.5 程序示例 4 1.6 系统调用和库函数 6 1.7 网络实现概述 6 1.8 描述符 7 1.9 mbuf与输出处理 11 1.9.1 包含插口地址结构的mbuf 11 1.9.2 包含数据的mbuf 12 1.9.3 添加IP和UDP首部 13 1.9.4 IP输出 14 1.9.5 以太网输出 14 1.9.6 UDP输出小结 14 1.10 输入处理 15 1.10.1 以太网输入 15 1.10.2 IP输入 15 1.10.3 UDP输入 16 1.10.4 进程输入 17 1.11 网络实现概述(续) 17 1.12 中断级别与并发 18 1.13 源代码组织 20 1.14 测试网络 21 1.15 小结 22 第2章 mbuf:存储器缓存 24 2.1 引言 24 2.2 代码介绍 27 2.2.1 全局变量 27 2.2.2 统计 28 2.2.3 内核统计 28 2.3 mbuf的定义 29 2.4 mbuf结构 29 2.5 简单的mbuf宏和函数 31 2.5.1 m_get函数 32 2.5.2 MGET宏 32 2.5.3 m_retry函数 33 2.5.4 mbuf 34 2.6 m_devget和m_pullup函数 34 2.6.1 m_devget函数 34 2.6.2 mtod和dtom宏 36 2.6.3 m_pullup函数和连续的协议首部 36 2.6.4 m_pullup和IP的分片与重组 37 2.6.5 TCP重组避免调用m_pullup 39 2.6.6 m_pullup使用总结 40 2.7 mbuf宏和函数的小结 40 2.8 Net/3联网数据结构小结 42 2.9 m_copy和簇引用计数 43 2.10 其他选择 47 2.11 小结 47 第3章 接口层 49 3.1 引言 49 3.2 代码介绍 49 3.2.1 全局变量 49 3.2.2 SNMP变量 50 3.3 ifnet结构 51 3.4 ifaddr结构 57 3.5 sockaddr结构 58 3.6 ifnet与ifaddr的专用化 59 3.7 网络初始化概述 60 3.8 以太网初始化 61 3.9 SLIP初始化 64 3.10 环回初始化 65 3.11 if_attach函数 66 3.12 ifinit函数 72 3.13 小结 73 第4章 接口:以太网 74 4.1 引言 74 4.2 代码介绍 75 4.2.1 全局变量 75 4.2.2 统计量 75 4.2.3 SNMP变量 76 4.3 以太网接口 77 4.3.1 leintr函数 79 4.3.2 leread函数 79 4.3.3 ether_input函数 81 4.3.4 ether_output函数 84 4.3.5 lestart函数 87 4.4 ioctl系统调用 89 4.4.1 ifioctl函数 90 4.4.2 ifconf函数 91 4.4.3 举例 94 4.4.4 通用接口ioctl命令 95 4.4.5 if_down和if_up函数 96 4.4.6 以太网、SLIP和环回 97 4.5 小结 98 第5章 接口:SLIP和环回 100 5.1 引言 100 5.2 代码介绍 100 5.2.1 全局变量 100 5.2.2 统计量 101 5.3 SLIP接口 101 5.3.1 SLIP线路规程:SLIPDISC 101 5.3.2 SLIP初始化:slopen和slinit 103 5.3.3 SLIP输入处理:slinput 105 5.3.4 SLIP输出处理:sloutput 109 5.3.5 slstart函数 111 5.3.6 SLIP分组丢失 116 5.3.7 SLIP性能考虑 117 5.3.8 slclose函数 117 5.3.9 sltioctl函数 118 5.4 环回接口 119 5.5 小结 121 第6章 IP编址 123 6.1 引言 123 6.1.1 IP地址 123 6.1.2 IP地址的印刷规定 123 6.1.3 主机和路由器 124 6.2 代码介绍 125 6.3 接口和地址小结 125 6.4 sockaddr_in结构 126 6.5 in_ifaddr结构 127 6.6 地址指派 128 6.6.1 ifioctl函数 130 6.6.2 in_control函数 130 6.6.3 前提条件:SIOCSIFADDR、 SIOCSIFNETMASK和 SIOCSIFDSTADDR 132 6.6.4 地址指派:SIOCSIFADDR 133 6.6.5 in_ifinit函数 133 6.6.6 网络掩码指派:SIOCSIFNETMASK 136 6.6.7 目的地址指派:SIOCSIFDSTADDR 137 6.6.8 获取接口信息 137 6.6.9 每个接口多个IP地址 138 6.6.10 附加IP地址:SIOCAIFADDR 139 6.6.11 删除IP地址:SIOCDIFADDR 140 6.7 接口ioctl处理 141 6.7.1 leioctl函数 141 6.7.2 slioctl函数 142 6.7.3 loioctl函数 143 6.8 Internet实用函数 144 6.9 ifnet实用函数 144 6.10 小结 145 第7章 域和协议 146 7.1 引言 146 7.2 代码介绍 146 7.2.1 全局变量 147 7.2.2 统计量 147 7.3 domain结构 147 7.4 protosw结构 148 7.5 IP 的domain和protosw结构 150 7.6 pffindproto和pffindtype函数 155 7.7 pfctlinput函数 157 7.8 IP初始化 157 7.8.1 Internet传输分用 157 7.8.2 ip_init函数 158 7.9 sysctl系统调用 159 7.10 小结 161 第8章 IP:网际协议 162 8.1 引言 162 8.2 代码介绍 163 8.2.1 全局变量 163 8.2.2 统计量 163 8.2.3 SNMP变量 164 8.3 IP分组 165 8.4 输入处理:ipintr函数 167 8.4.1 ipintr概观 167 8.4.2 验证 168 8.4.3 转发或不转发 171 8.4.4 重装和分用 173 8.5 转发:ip_forward函数 174 8.6 输出处理:ip_output函数 180 8.6.1 首部初始化 181 8.6.2 路由选择 182 8.6.3 源地址选择和分片 184 8.7 Internet检验和:in_cksum函数 186 8.8 setsockopt和getsockopt系统调用 190 8.8.1 PRCO_SETOPT的处理 192 8.8.2 PRCO_GETOPT的处理 193 8.9 ip_sysctl函数 193 8.10 小结 194 第9章 IP选项处理 196 9.1 引言 196 9.2 代码介绍 196 9.2.1 全局变量 196 9.2.2 统计量 197 9.3 选项格式 197 9.4 ip_dooptions函数 198 9.5 记录路由选项 200 9.6 源站和记录路由选项 202 9.6.1 save_rte函数 205 9.6.2 ip_srcroute函数 206 9.7 时间戳选项 207 9.8 ip_insertoptions函数 210 9.9 ip_pcbopts函数 214 9.10 一些限制 217 9.11 小结 217 第10章 IP的分片与重装 218 10.1 引言 218 10.2 代码介绍 219 10.2.1 全局变量 220 10.2.2 统计量 220 10.3 分片 220 10.4 ip_optcopy函数 223 10.5 重装 224 10.6 ip_reass函数 227 10.7 ip_slowtimo函数 237 10.8 小结 238 第11章 ICMP:Internet控制报文协议 239 11.1 引言 239 11.2 代码介绍 242 11.2.1 全局变量 242 11.2.2 统计量 242 11.2.3 SNMP变量 243 11.3 icmp结构 244 11.4 ICMP 的protosw结构 245 11.5 输入处理:icmp_input函数 246 11.6 差错处理 249 11.7 请求处理 251 11.7.1 回显询问:ICMP_ECHO和 ICMP_ECHOREPLY 252 11.7.2 时间戳询问:ICMP_TSTAMP和 ICMP_TSTAMPREPLY 253 11.7.3 地址掩码询问:ICMP_MASKREQ和 ICMP_MASKREPLY 253 11.7.4 信息询问:ICMP_IREQ和ICMP_ IREQREPLY 255 11.7.5 路由器发现:ICMP_ROUTERADVERT 和ICMP_ROUTERSOLICIT 255 11.8 重定向处理 255 11.9 回答处理 257 11.10 输出处理 257 11.11 icmp_error函数 258 11.12 icmp_reflect函数 261 11.13 icmp_send函数 265 11.14 icmp_sysctl函数 266 11.15 小结 266 第12章 IP多播 268 12.1 引言 268 12.2 代码介绍 269 12.2.1 全局变量 270 12.2.2 统计量 270 12.3 以太网多播地址 270 12.4 ether_multi结构 271 12.5 以太网多播接收 273 12.6 in_multi结构 273 12.7 ip_moptions结构 275 12.8 多播的插口选项 276 12.9 多播的TTL值 277 12.9.1 MBONE 278 12.9.2 扩展环搜索 278 12.10 ip_setmoptions函数 278 12.10.1 选择一个明确的多播接口:IP_ MULTICAST_IF 280 12.10.2 选择明确的多播TTL: IP_ MULTICAST_TTL 281 12.10.3 选择多播环回:IP_MULTICAST_ LOOP 281 12.11 加入一个IP多播组 282 12.11.1 in_addmulti函数 285 12.11.2 slioctl和loioctl函数:SIOCADDMULTI和SIOCDELMULTI 287 12.11.3 leioctl函数:SIOCADDMULTI和 SIOCDELMULTI 288 12.11.4 ether_addmulti函数 288 12.12 离开一个IP多播组 291 12.12.1 in_delmulti函数 292 12.12.2 ether_delmulti函数 293 12.13 ip_getmoptions函数 295 12.14 多播输入处理:ipintr函数 296 12.15 多播输出处理:ip_output函数 298 12.16 性能的考虑 301 12.17 小结 301 第13章 IGMP:Internet组管理协议 303 13.1 引言 303 13.2 代码介绍 304 13.2.1 全局变量 304 13.2.2 统计量 304 13.2.3 SNMP变量 305 13.3 igmp结构 305 13.4 IGMP的protosw的结构 306 13.5 加入一个组:igmp_joingroup函数 306 13.6 igmp_fasttimo函数 308 13.7 输入处理:igmp_input函数 311 13.7.1 成员关系查询:IGMP_HOST_ MEMBERSHIP_QUERY 312 13.7.2 成员关系报告:IGMP_HOST_ MEMBERSHIP_REPORT 313 13.8 离开一个组:igmp_leavegroup函数 314 13.9 小结 315 第14章 IP多播选路 316 14.1 引言 316 14.2 代码介绍 316 14.2.1 全局变量 316 14.2.2 统计量 317 14.2.3 SNMP变量 317 14.3 多播输出处理(续) 317 14.4 mrouted守护程序 318 14.5 虚拟接口 321 14.5.1 虚拟接口表 322 14.5.2 add_vif函数 324 14.5.3 del_vif函数 326 14.6 IGMP(续) 327 14.6.1 add_lgrp函数 328 14.6.2 del_lgrp函数 329 14.6.3 grplst_member函数 330 14.7 多播选路 331 14.7.1 多播选路表 334 14.7.2 del_mrt函数 335 14.7.3 add_mrt函数 336 14.7.4 mrtfind函数 337 14.8 多播转发:ip_mforward函数 338 14.8.1 phyint_send函数 343 14.8.2 tunnel_send函数 344 14.9 清理:ip_mrouter_done函数 345 14.10 小结 346 第15章 插口层 348 15.1 引言 348 15.2 代码介绍 349 15.3 socket结构 349 15.4 系统调用 354 15.4.1 举例 355 15.4.2 系统调用小结 355 15.5 进程、描述符和插口 357 15.6 socket系统调用 358 15.6.1 socreate函数 359 15.6.2 超级用户特权 361 15.7 getsock和sockargs函数 361 15.8 bind系统调用 363 15.9 listen系统调用 364 15.10 tsleep和wakeup函数 365 15.11 accept系统调用 366 15.12 sonewconn和soisconnected 函数 369 15.13 connect系统调用 372 15.13.1 soconnect函数 374 15.13.2 切断无连接插口和外部地址的 关联 375 15.14 shutdown系统调用 375 15.15 close系统调用 377 15.15.1 soo_close函数 377 15.15.2 soclose函数 378 15.16 小结 380 第16章 插口I/O 381 16.1 引言 381 16.2 代码介绍 381 16.3 插口缓存 381 16.4 write、writev、sendto和sendmsg 系统调用 384 16.5 sendmsg系统调用 387 16.6 sendit函数 388 16.6.1 uiomove函数 389 16.6.2 举例 390 16.6.3 sendit代码 391 16.7 sosend函数 392 16.7.1 可靠的协议缓存 393 16.7.2 不可靠的协议缓存 393 16.7.3 sosend函数小结 401 16.7.4 性能问题 401 16.8 read、readv、recvfrom和recvmsg 系统调用 401 16.9 recvmsg系统调用 402 16.10 recvit函数 403 16.11 soreceive函数 405 16.11.1 带外数据 406 16.11.2 举例 406 16.11.3 其他的接收操作选项 407 16.11.4 接收缓存的组织:报文边界 407 16.11.5 接收缓存的组织:没有报文边界 408 16.11.6 控制信息和带外数据 409 16.12 soreceive代码 410 16.13 select系统调用 421 16.13.1 selscan函数 425 16.13.2 soo_select函数 425 16.13.3 selrecord函数 427 16.13.4 selwakeup函数 428 16.14 小结 429 第17章 插口选项 431 17.1 引言 431 17.2 代码介绍 431 17.3 setsockopt系统调用 432 17.4 getsockopt系统调用 437 17.5 fcntl和ioctl系统调用 440 17.5.1 fcntl代码 441 17.5.2 ioctl代码 443 17.6 getsockname系统调用 444 17.7 getpeername系统调用 445 17.8 小结 447 第18章 Radix树路由表 448 18.1 引言 448 18.2 路由表结构 448 18.3 选路插口 456 18.4 代码介绍 456 18.4.1 全局变量 458 18.4.2 统计量 458 18.4.3 SNMP变量 459 18.5 Radix结点数据结构 460 18.6 选路结构 463 18.7 初始化:route_init和rtable_init 函数 465 18.8 初始化:rn_init和rn_inithead 函数 468 18.9 重复键和掩码列表 471 18.10 rn_match函数 473 18.11 rn_search函数 480 18.12 小结 481 第19章 选路请求和选路消息 482 19.1 引言 482 19.2 rtalloc和rtalloc1函数 482 19.3 宏RTFREE和rtfree函数 484 19.4 rtrequest函数 486 19.5 rt_setgate函数 491 19.6 rtinit函数 493 19.7 rtredirect函数 495 19.8 选路消息的结构 498 19.9 rt_missmsg函数 501 19.10 rt_ifmsg函数 503 19.11 rt_newaddrmsg函数 504 19.12 rt_msg1函数 505 19.13 rt_msg2函数 507 19.14 sysctl_rtable函数 510 19.15 sysctl_dumpentry函数 514 19.16 sysctl_iflist函数 515 19.17 小结 517 第20章 选路插口 518 20.1 引言 518 20.2 routedomain和protosw结构 518 20.3 选路控制块 519 20.4 raw_init函数 520 20.5 route_output函数 520 20.6 rt_xaddrs函数 530 20.7 rt_setmetrics函数 531 20.8 raw_input函数 532 20.9 route_usrreq函数 534 20.10 raw_usrreq函数 535 20.11 raw_attach、raw_detach和raw_disconnect函数 539 20.12 小结 540 第21章 ARP:地址解析协议 542 21.1 介绍 542 21.2 ARP和路由表 542 21.3 代码介绍 544 21.3.1 全局变量 544 21.3.2 统计量 544 21.3.3 SNMP变量 546 21.4 ARP结构 546 21.5 arpwhohas函数 548 21.6 arprequest函数 548 21.7 arpintr函数 551 21.8 in_arpinput函数 552 21.9 ARP定时器函数 557 21.9.1 arptimer函数 557 21.9.2 arptfree函数 557 21.10 arpresolve函数 558 21.11 arplookup函数 562 21.12 代理ARP 563 21.13 arp_rtrequest函数 564 21.14 ARP和多播 569 21.15 小结 570 第22章 协议控制块 572 22.1 引言 572 22.2 代码介绍 573 22.2.1 全局变量 574 22.2.2 统计量 574 22.3 inpcb的结构 574 22.4 in_pcballoc和in_pcbdetach函数 575 22.5 绑定、连接和分用 577 22.6 in_pcblookup函数 581 22.7 in_pcbbind函数 584 22.8 in_pcbconnect函数 589 22.9 in_pcbdisconnect函数 594 22.10 in_setsockaddr和in_setpeeraddr 函数 595 22.11 in_pcbnotify、in_rtchange和in_losing函数 595 22.11.1 in_rtchange函数 598 22.11.2 重定向和原始插口 599 22.11.3 ICMP差错和UDP插口 600 22.11.4 in_losing函数 601 22.12 实现求精 602 22.13 小结 602 第23章 UDP:用户数据报协议 605 23.1 引言 605 23.2 代码介绍 605 23.2.1 全局变量 606 23.2.2 统计量 606 23.2.3 SNMP变量 607 23.3 UDP 的protosw结构 607 23.4 UDP的首部 608 23.5 udp_init函数 609 23.6 udp_output函数 609 23.6.1 在前面加上IP/UDP首部和mbuf簇 612 23.6.2 UDP检验和计算和伪首部 612 23.7 udp_input函数 616 23.7.1 对收到的UDP数据报的一般确认 616 23.7.2 分用单播数据报 619 23.7.3 分用多播和广播数据报 622 23.7.4 连接上的UDP插口和多接口主机 625 23.8 udp_saveopt函数 625 23.9 udp_ctlinput函数 627 23.10 udp_usrreq函数 628 23.11 udp_sysctl函数 633 23.12 实现求精 633 23.12.1 UDP PCB高速缓存 633 23.12.2 UDP检验和 634 23.13 小结 635 第24章 TCP:传输控制协议 636 24.1 引言 636 24.2 代码介绍 636 24.2.1 全局变量 636 24.2.2 统计量 637 24.2.3 SNMP变量 640 24.3 TCP 的protosw结构 641 24.4 TCP的首部 641 24.5 TCP的控制块 643 24.6 TCP的状态变迁图 645 24.7 TCP的序号 646 24.8 tcp_init函数 650 24.9 小结 652 第25章 TCP的定时器 654 25.1 引言 654 25.2 代码介绍 655 25.3 tcp_canceltimers函数 657 25.4 tcp_fasttimo函数 657 25.5 tcp_slowtimo函数 658 25.6 tcp_timers函数 659 25.6.1 FIN_WAIT_2和2MSL定时器 660 25.6.2 持续定时器 662 25.6.3 连接建立定时器和保活定时器 662 25.7 重传定时器的计算 665 25.8 tcp_newtcpcb算法 666 25.9 tcp_setpersist函数 668 25.10 tcp_xmit_timer函数 669 25.11 重传超时:tcp_timers函数 673 25.11.1 慢起动和避免拥塞 675 25.11.2 精确性 677 25.12 一个RTT的例子 677 25.13 小结 679 第26章 TCP输出 680 26.1 引言 680 26.2 tcp_output概述 680 26.3 决定是否应发送一个报文段 682 26.4 TCP选项 691 26.5 窗口大小选项 692 26.6 时间戳选项 692 26.6.1 哪个时间戳需要回显,RFC1323 算法 694 26.6.2 哪个时间戳需要回显,正确的 算法 695 26.6.3 时间戳与延迟ACK 695 26.7 发送一个报文段 696 26.8 tcp_template函数 707 26.9 tcp_respond函数 708 26.10 小结 710 第27章 TCP的函数 712 27.1 引言 712 27.2 tcp_drain函数 712 27.3 tcp_drop函数 712 27.4 tcp_close函数 713 27.4.1 路由特性 713 27.4.2 资源释放 716 27.5 tcp_mss函数 717 27.6 tcp_ctlinput函数 722 27.7 tcp_notify函数 723 27.8 tcp_quench函数 724 27.9 TCP_REASS宏和tcp_reass函数 724 27.9.1 TCP_REASS宏 725 27.9.2 tcp_reass函数 727 27.10 tcp_trace函数 732 27.11 小结 736 第28章 TCP的输入 737 28.1 引言 737 28.2 预处理 739 28.3 tcp_dooptions函数 745 28.4 首部预测 747 28.5 TCP输入:缓慢的执行路径 752 28.6 完成被动打开或主动打开 752 28.6.1 完成被动打开 753 28.6.2 完成主动打开 756 28.7 PAWS:防止序号回绕 760 28.8 裁剪报文段使数据在窗口内 762 28.9 自连接和同时打开 768 28.10 记录时间戳 770 28.11 RST处理 770 28.12 小结 772 第29章 TCP的输入(续) 773 29.1 引言 773 29.2 ACK处理概述 773 29.3 完成被动打开和同时打开 774 29.4 快速重传和快速恢复的算法 775 29.5 ACK处理 778 29.6 更新窗口信息 784 29.7 紧急方式处理 786 29.8 tcp_pulloutofband函数 788 29.9 处理已接收的数据 789 29.10 FIN处理 791 29.11 最后的处理 793 29.12 实现求精 795 29.13 首部压缩 795 29.13.1 引言 796 29.13.2 首部字段的压缩 799 29.13.3 特殊情况 801 29.13.4 实例 802 29.13.5 配置 803 29.14 小结 803 第30章 TCP的用户需求 805 30.1 引言 805 30.2 tcp_usrreq函数 805 30.3 tcp_attach函数 814 30.4 tcp_disconnect函数 815 30.5 tcp_usrclosed函数 816 30.6 tcp_ctloutput函数 817 30.7 小结 820 第31章 BPF:BSD 分组过滤程序 821 31.1 引言 821 31.2 代码介绍 821 31.2.1 全局变量 821 31.2.2 统计量 822 31.3 bpf_if结构 822 31.4 bpf_d结构 825 31.4.1 bpfopen函数 826 31.4.2 bpfioctl函数 827 31.4.3 bpf_setif函数 830 31.4.4 bpf_attachd函数 831 31.5 BPF的输入 832 31.5.1 bpf_tap函数 832 31.5.2 catchpacket函数 833 31.5.3 bpfread函数 835 31.6 BPF的输出 837 31.7 小结 838 第32章 原始IP 839 32.1 引言 839 32.2 代码介绍 839 32.2.1 全局变量 839 32.2.2 统计量 840 32.3 原始 IP的protosw结构 840 32.4 rip_init函数 842 32.5 rip_input函数 842 32.6 rip_output函数 844 32.7 rip_usrreq函数 846 32.8 rip_ctloutput函数 850 32.9 小结 852 结束语 853 附录A 部分习题的解答 854 附录B 源代码的获取 872 附录C RFC 1122 的有关内容 874 参考文献 895
目 录 译者序 前言 第1章 概述 1 1.1 引言 1 1.2 源代码表示 1 1.2.1 将拥塞窗口设置为1 1 1.2.2 印刷约定 2 1.3 历史 2 1.4 应用编程接口 3 1.5 程序示例 4 1.6 系统调用和库函数 6 1.7 网络实现概述 6 1.8 描述符 7 1.9 mbuf与输出处理 11 1.9.1 包含插口地址结构的mbuf 11 1.9.2 包含数据的mbuf 12 1.9.3 添加IP和UDP首部 13 1.9.4 IP输出 14 1.9.5 以太网输出 14 1.9.6 UDP输出小结 14 1.10 输入处理 15 1.10.1 以太网输入 15 1.10.2 IP输入 15 1.10.3 UDP输入 16 1.10.4 进程输入 17 1.11 网络实现概述(续) 17 1.12 中断级别与并发 18 1.13 源代码组织 20 1.14 测试网络 21 1.15 小结 22 第2章 mbuf:存储器缓存 24 2.1 引言 24 2.2 代码介绍 27 2.2.1 全局变量 27 2.2.2 统计 28 2.2.3 内核统计 28 2.3 mbuf的定义 29 2.4 mbuf结构 29 2.5 简单的mbuf宏和函数 31 2.5.1 m_get函数 32 2.5.2 MGET宏 32 2.5.3 m_retry函数 33 2.5.4 mbuf 34 2.6 m_devget和m_pullup函数 34 2.6.1 m_devget函数 34 2.6.2 mtod和dtom宏 36 2.6.3 m_pullup函数和连续的协议首部 36 2.6.4 m_pullup和IP的分片与重组 37 2.6.5 TCP重组避免调用m_pullup 39 2.6.6 m_pullup使用总结 40 2.7 mbuf宏和函数的小结 40 2.8 Net/3联网数据结构小结 42 2.9 m_copy和簇引用计数 43 2.10 其他选择 47 2.11 小结 47 第3章 接口层 49 3.1 引言 49 3.2 代码介绍 49 3.2.1 全局变量 49 3.2.2 SNMP变量 50 3.3 ifnet结构 51 3.4 ifaddr结构 57 3.5 sockaddr结构 58 3.6 ifnet与ifaddr的专用化 59 3.7 网络初始化概述 60 3.8 以太网初始化 61 3.9 SLIP初始化 64 3.10 环回初始化 65 3.11 if_attach函数 66 3.12 ifinit函数 72 3.13 小结 73 第4章 接口:以太网 74 4.1 引言 74 4.2 代码介绍 75 4.2.1 全局变量 75 4.2.2 统计量 75 4.2.3 SNMP变量 76 4.3 以太网接口 77 4.3.1 leintr函数 79 4.3.2 leread函数 79 4.3.3 ether_input函数 81 4.3.4 ether_output函数 84 4.3.5 lestart函数 87 4.4 ioctl系统调用 89 4.4.1 ifioctl函数 90 4.4.2 ifconf函数 91 4.4.3 举例 94 4.4.4 通用接口ioctl命令 95 4.4.5 if_down和if_up函数 96 4.4.6 以太网、SLIP和环回 97 4.5 小结 98 第5章 接口:SLIP和环回 100 5.1 引言 100 5.2 代码介绍 100 5.2.1 全局变量 100 5.2.2 统计量 101 5.3 SLIP接口 101 5.3.1 SLIP线路规程:SLIPDISC 101 5.3.2 SLIP初始化:slopen和slinit 103 5.3.3 SLIP输入处理:slinput 105 5.3.4 SLIP输出处理:sloutput 109 5.3.5 slstart函数 111 5.3.6 SLIP分组丢失 116 5.3.7 SLIP性能考虑 117 5.3.8 slclose函数 117 5.3.9 sltioctl函数 118 5.4 环回接口 119 5.5 小结 121 第6章 IP编址 123 6.1 引言 123 6.1.1 IP地址 123 6.1.2 IP地址的印刷规定 123 6.1.3 主机和路由器 124 6.2 代码介绍 125 6.3 接口和地址小结 125 6.4 sockaddr_in结构 126 6.5 in_ifaddr结构 127 6.6 地址指派 128 6.6.1 ifioctl函数 130 6.6.2 in_control函数 130 6.6.3 前提条件:SIOCSIFADDR、 SIOCSIFNETMASK和 SIOCSIFDSTADDR 132 6.6.4 地址指派:SIOCSIFADDR 133 6.6.5 in_ifinit函数 133 6.6.6 网络掩码指派:SIOCSIFNETMASK 136 6.6.7 目的地址指派:SIOCSIFDSTADDR 137 6.6.8 获取接口信息 137 6.6.9 每个接口多个IP地址 138 6.6.10 附加IP地址:SIOCAIFADDR 139 6.6.11 删除IP地址:SIOCDIFADDR 140 6.7 接口ioctl处理 141 6.7.1 leioctl函数 141 6.7.2 slioctl函数 142 6.7.3 loioctl函数 143 6.8 Internet实用函数 144 6.9 ifnet实用函数 144 6.10 小结 145 第7章 域和协议 146 7.1 引言 146 7.2 代码介绍 146 7.2.1 全局变量 147 7.2.2 统计量 147 7.3 domain结构 147 7.4 protosw结构 148 7.5 IP 的domain和protosw结构 150 7.6 pffindproto和pffindtype函数 155 7.7 pfctlinput函数 157 7.8 IP初始化 157 7.8.1 Internet传输分用 157 7.8.2 ip_init函数 158 7.9 sysctl系统调用 159 7.10 小结 161 第8章 IP:网际协议 162 8.1 引言 162 8.2 代码介绍 163 8.2.1 全局变量 163 8.2.2 统计量 163 8.2.3 SNMP变量 164 8.3 IP分组 165 8.4 输入处理:ipintr函数 167 8.4.1 ipintr概观 167 8.4.2 验证 168 8.4.3 转发或不转发 171 8.4.4 重装和分用 173 8.5 转发:ip_forward函数 174 8.6 输出处理:ip_output函数 180 8.6.1 首部初始化 181 8.6.2 路由选择 182 8.6.3 源地址选择和分片 184 8.7 Internet检验和:in_cksum函数 186 8.8 setsockopt和getsockopt系统调用 190 8.8.1 PRCO_SETOPT的处理 192 8.8.2 PRCO_GETOPT的处理 193 8.9 ip_sysctl函数 193 8.10 小结 194 第9章 IP选项处理 196 9.1 引言 196 9.2 代码介绍 196 9.2.1 全局变量 196 9.2.2 统计量 197 9.3 选项格式 197 9.4 ip_dooptions函数 198 9.5 记录路由选项 200 9.6 源站和记录路由选项 202 9.6.1 save_rte函数 205 9.6.2 ip_srcroute函数 206 9.7 时间戳选项 207 9.8 ip_insertoptions函数 210 9.9 ip_pcbopts函数 214 9.10 一些限制 217 9.11 小结 217 第10章 IP的分片与重装 218 10.1 引言 218 10.2 代码介绍 219 10.2.1 全局变量 220 10.2.2 统计量 220 10.3 分片 220 10.4 ip_optcopy函数 223 10.5 重装 224 10.6 ip_reass函数 227 10.7 ip_slowtimo函数 237 10.8 小结 238 第11章 ICMP:Internet控制报文协议 239 11.1 引言 239 11.2 代码介绍 242 11.2.1 全局变量 242 11.2.2 统计量 242 11.2.3 SNMP变量 243 11.3 icmp结构 244 11.4 ICMP 的protosw结构 245 11.5 输入处理:icmp_input函数 246 11.6 差错处理 249 11.7 请求处理 251 11.7.1 回显询问:ICMP_ECHO和 ICMP_ECHOREPLY 252 11.7.2 时间戳询问:ICMP_TSTAMP和 ICMP_TSTAMPREPLY 253 11.7.3 地址掩码询问:ICMP_MASKREQ和 ICMP_MASKREPLY 253 11.7.4 信息询问:ICMP_IREQ和ICMP_ IREQREPLY 255 11.7.5 路由器发现:ICMP_ROUTERADVERT 和ICMP_ROUTERSOLICIT 255 11.8 重定向处理 255 11.9 回答处理 257 11.10 输出处理 257 11.11 icmp_error函数 258 11.12 icmp_reflect函数 261 11.13 icmp_send函数 265 11.14 icmp_sysctl函数 266 11.15 小结 266 第12章 IP多播 268 12.1 引言 268 12.2 代码介绍 269 12.2.1 全局变量 270 12.2.2 统计量 270 12.3 以太网多播地址 270 12.4 ether_multi结构 271 12.5 以太网多播接收 273 12.6 in_multi结构 273 12.7 ip_moptions结构 275 12.8 多播的插口选项 276 12.9 多播的TTL值 277 12.9.1 MBONE 278 12.9.2 扩展环搜索 278 12.10 ip_setmoptions函数 278 12.10.1 选择一个明确的多播接口:IP_ MULTICAST_IF 280 12.10.2 选择明确的多播TTL: IP_ MULTICAST_TTL 281 12.10.3 选择多播环回:IP_MULTICAST_ LOOP 281 12.11 加入一个IP多播组 282 12.11.1 in_addmulti函数 285 12.11.2 slioctl和loioctl函数:SIOCADDMULTI和SIOCDELMULTI 287 12.11.3 leioctl函数:SIOCADDMULTI和 SIOCDELMULTI 288 12.11.4 ether_addmulti函数 288 12.12 离开一个IP多播组 291 12.12.1 in_delmulti函数 292 12.12.2 ether_delmulti函数 293 12.13 ip_getmoptions函数 295 12.14 多播输入处理:ipintr函数 296 12.15 多播输出处理:ip_output函数 298 12.16 性能的考虑 301 12.17 小结 301 第13章 IGMP:Internet组管理协议 303 13.1 引言 303 13.2 代码介绍 304 13.2.1 全局变量 304 13.2.2 统计量 304 13.2.3 SNMP变量 305 13.3 igmp结构 305 13.4 IGMP的protosw的结构 306 13.5 加入一个组:igmp_joingroup函数 306 13.6 igmp_fasttimo函数 308 13.7 输入处理:igmp_input函数 311 13.7.1 成员关系查询:IGMP_HOST_ MEMBERSHIP_QUERY 312 13.7.2 成员关系报告:IGMP_HOST_ MEMBERSHIP_REPORT 313 13.8 离开一个组:igmp_leavegroup函数 314 13.9 小结 315 第14章 IP多播选路 316 14.1 引言 316 14.2 代码介绍 316 14.2.1 全局变量 316 14.2.2 统计量 317 14.2.3 SNMP变量 317 14.3 多播输出处理(续) 317 14.4 mrouted守护程序 318 14.5 虚拟接口 321 14.5.1 虚拟接口表 322 14.5.2 add_vif函数 324 14.5.3 del_vif函数 326 14.6 IGMP(续) 327 14.6.1 add_lgrp函数 328 14.6.2 del_lgrp函数 329 14.6.3 grplst_member函数 330 14.7 多播选路 331 14.7.1 多播选路表 334 14.7.2 del_mrt函数 335 14.7.3 add_mrt函数 336 14.7.4 mrtfind函数 337 14.8 多播转发:ip_mforward函数 338 14.8.1 phyint_send函数 343 14.8.2 tunnel_send函数 344 14.9 清理:ip_mrouter_done函数 345 14.10 小结 346 第15章 插口层 348 15.1 引言 348 15.2 代码介绍 349 15.3 socket结构 349 15.4 系统调用 354 15.4.1 举例 355 15.4.2 系统调用小结 355 15.5 进程、描述符和插口 357 15.6 socket系统调用 358 15.6.1 socreate函数 359 15.6.2 超级用户特权 361 15.7 getsock和sockargs函数 361 15.8 bind系统调用 363 15.9 listen系统调用 364 15.10 tsleep和wakeup函数 365 15.11 accept系统调用 366 15.12 sonewconn和soisconnected 函数 369 15.13 connect系统调用 372 15.13.1 soconnect函数 374 15.13.2 切断无连接插口和外部地址的 关联 375 15.14 shutdown系统调用 375 15.15 close系统调用 377 15.15.1 soo_close函数 377 15.15.2 soclose函数 378 15.16 小结 380 第16章 插口I/O 381 16.1 引言 381 16.2 代码介绍 381 16.3 插口缓存 381 16.4 write、writev、sendto和sendmsg 系统调用 384 16.5 sendmsg系统调用 387 16.6 sendit函数 388 16.6.1 uiomove函数 389 16.6.2 举例 390 16.6.3 sendit代码 391 16.7 sosend函数 392 16.7.1 可靠的协议缓存 393 16.7.2 不可靠的协议缓存 393 16.7.3 sosend函数小结 401 16.7.4 性能问题 401 16.8 read、readv、recvfrom和recvmsg 系统调用 401 16.9 recvmsg系统调用 402 16.10 recvit函数 403 16.11 soreceive函数 405 16.11.1 带外数据 406 16.11.2 举例 406 16.11.3 其他的接收操作选项 407 16.11.4 接收缓存的组织:报文边界 407 16.11.5 接收缓存的组织:没有报文边界 408 16.11.6 控制信息和带外数据 409 16.12 soreceive代码 410 16.13 select系统调用 421 16.13.1 selscan函数 425 16.13.2 soo_select函数 425 16.13.3 selrecord函数 427 16.13.4 selwakeup函数 428 16.14 小结 429 第17章 插口选项 431 17.1 引言 431 17.2 代码介绍 431 17.3 setsockopt系统调用 432 17.4 getsockopt系统调用 437 17.5 fcntl和ioctl系统调用 440 17.5.1 fcntl代码 441 17.5.2 ioctl代码 443 17.6 getsockname系统调用 444 17.7 getpeername系统调用 445 17.8 小结 447 第18章 Radix树路由表 448 18.1 引言 448 18.2 路由表结构 448 18.3 选路插口 456 18.4 代码介绍 456 18.4.1 全局变量 458 18.4.2 统计量 458 18.4.3 SNMP变量 459 18.5 Radix结点数据结构 460 18.6 选路结构 463 18.7 初始化:route_init和rtable_init 函数 465 18.8 初始化:rn_init和rn_inithead 函数 468 18.9 重复键和掩码列表 471 18.10 rn_match函数 473 18.11 rn_search函数 480 18.12 小结 481 第19章 选路请求和选路消息 482 19.1 引言 482 19.2 rtalloc和rtalloc1函数 482 19.3 宏RTFREE和rtfree函数 484 19.4 rtrequest函数 486 19.5 rt_setgate函数 491 19.6 rtinit函数 493 19.7 rtredirect函数 495 19.8 选路消息的结构 498 19.9 rt_missmsg函数 501 19.10 rt_ifmsg函数 503 19.11 rt_newaddrmsg函数 504 19.12 rt_msg1函数 505 19.13 rt_msg2函数 507 19.14 sysctl_rtable函数 510 19.15 sysctl_dumpentry函数 514 19.16 sysctl_iflist函数 515 19.17 小结 517 第20章 选路插口 518 20.1 引言 518 20.2 routedomain和protosw结构 518 20.3 选路控制块 519 20.4 raw_init函数 520 20.5 route_output函数 520 20.6 rt_xaddrs函数 530 20.7 rt_setmetrics函数 531 20.8 raw_input函数 532 20.9 route_usrreq函数 534 20.10 raw_usrreq函数 535 20.11 raw_attach、raw_detach和raw_disconnect函数 539 20.12 小结 540 第21章 ARP:地址解析协议 542 21.1 介绍 542 21.2 ARP和路由表 542 21.3 代码介绍 544 21.3.1 全局变量 544 21.3.2 统计量 544 21.3.3 SNMP变量 546 21.4 ARP结构 546 21.5 arpwhohas函数 548 21.6 arprequest函数 548 21.7 arpintr函数 551 21.8 in_arpinput函数 552 21.9 ARP定时器函数 557 21.9.1 arptimer函数 557 21.9.2 arptfree函数 557 21.10 arpresolve函数 558 21.11 arplookup函数 562 21.12 代理ARP 563 21.13 arp_rtrequest函数 564 21.14 ARP和多播 569 21.15 小结 570 第22章 协议控制块 572 22.1 引言 572 22.2 代码介绍 573 22.2.1 全局变量 574 22.2.2 统计量 574 22.3 inpcb的结构 574 22.4 in_pcballoc和in_pcbdetach函数 575 22.5 绑定、连接和分用 577 22.6 in_pcblookup函数 581 22.7 in_pcbbind函数 584 22.8 in_pcbconnect函数 589 22.9 in_pcbdisconnect函数 594 22.10 in_setsockaddr和in_setpeeraddr 函数 595 22.11 in_pcbnotify、in_rtchange和in_losing函数 595 22.11.1 in_rtchange函数 598 22.11.2 重定向和原始插口 599 22.11.3 ICMP差错和UDP插口 600 22.11.4 in_losing函数 601 22.12 实现求精 602 22.13 小结 602 第23章 UDP:用户数据报协议 605 23.1 引言 605 23.2 代码介绍 605 23.2.1 全局变量 606 23.2.2 统计量 606 23.2.3 SNMP变量 607 23.3 UDP 的protosw结构 607 23.4 UDP的首部 608 23.5 udp_init函数 609 23.6 udp_output函数 609 23.6.1 在前面加上IP/UDP首部和mbuf簇 612 23.6.2 UDP检验和计算和伪首部 612 23.7 udp_input函数 616 23.7.1 对收到的UDP数据报的一般确认 616 23.7.2 分用单播数据报 619 23.7.3 分用多播和广播数据报 622 23.7.4 连接上的UDP插口和多接口主机 625 23.8 udp_saveopt函数 625 23.9 udp_ctlinput函数 627 23.10 udp_usrreq函数 628 23.11 udp_sysctl函数 633 23.12 实现求精 633 23.12.1 UDP PCB高速缓存 633 23.12.2 UDP检验和 634 23.13 小结 635 第24章 TCP:传输控制协议 636 24.1 引言 636 24.2 代码介绍 636 24.2.1 全局变量 636 24.2.2 统计量 637 24.2.3 SNMP变量 640 24.3 TCP 的protosw结构 641 24.4 TCP的首部 641 24.5 TCP的控制块 643 24.6 TCP的状态变迁图 645 24.7 TCP的序号 646 24.8 tcp_init函数 650 24.9 小结 652 第25章 TCP的定时器 654 25.1 引言 654 25.2 代码介绍 655 25.3 tcp_canceltimers函数 657 25.4 tcp_fasttimo函数 657 25.5 tcp_slowtimo函数 658 25.6 tcp_timers函数 659 25.6.1 FIN_WAIT_2和2MSL定时器 660 25.6.2 持续定时器 662 25.6.3 连接建立定时器和保活定时器 662 25.7 重传定时器的计算 665 25.8 tcp_newtcpcb算法 666 25.9 tcp_setpersist函数 668 25.10 tcp_xmit_timer函数 669 25.11 重传超时:tcp_timers函数 673 25.11.1 慢起动和避免拥塞 675 25.11.2 精确性 677 25.12 一个RTT的例子 677 25.13 小结 679 第26章 TCP输出 680 26.1 引言 680 26.2 tcp_output概述 680 26.3 决定是否应发送一个报文段 682 26.4 TCP选项 691 26.5 窗口大小选项 692 26.6 时间戳选项 692 26.6.1 哪个时间戳需要回显,RFC1323 算法 694 26.6.2 哪个时间戳需要回显,正确的 算法 695 26.6.3 时间戳与延迟ACK 695 26.7 发送一个报文段 696 26.8 tcp_template函数 707 26.9 tcp_respond函数 708 26.10 小结 710 第27章 TCP的函数 712 27.1 引言 712 27.2 tcp_drain函数 712 27.3 tcp_drop函数 712 27.4 tcp_close函数 713 27.4.1 路由特性 713 27.4.2 资源释放 716 27.5 tcp_mss函数 717 27.6 tcp_ctlinput函数 722 27.7 tcp_notify函数 723 27.8 tcp_quench函数 724 27.9 TCP_REASS宏和tcp_reass函数 724 27.9.1 TCP_REASS宏 725 27.9.2 tcp_reass函数 727 27.10 tcp_trace函数 732 27.11 小结 736 第28章 TCP的输入 737 28.1 引言 737 28.2 预处理 739 28.3 tcp_dooptions函数 745 28.4 首部预测 747 28.5 TCP输入:缓慢的执行路径 752 28.6 完成被动打开或主动打开 752 28.6.1 完成被动打开 753 28.6.2 完成主动打开 756 28.7 PAWS:防止序号回绕 760 28.8 裁剪报文段使数据在窗口内 762 28.9 自连接和同时打开 768 28.10 记录时间戳 770 28.11 RST处理 770 28.12 小结 772 第29章 TCP的输入(续) 773 29.1 引言 773 29.2 ACK处理概述 773 29.3 完成被动打开和同时打开 774 29.4 快速重传和快速恢复的算法 775 29.5 ACK处理 778 29.6 更新窗口信息 784 29.7 紧急方式处理 786 29.8 tcp_pulloutofband函数 788 29.9 处理已接收的数据 789 29.10 FIN处理 791 29.11 最后的处理 793 29.12 实现求精 795 29.13 首部压缩 795 29.13.1 引言 796 29.13.2 首部字段的压缩 799 29.13.3 特殊情况 801 29.13.4 实例 802 29.13.5 配置 803 29.14 小结 803 第30章 TCP的用户需求 805 30.1 引言 805 30.2 tcp_usrreq函数 805 30.3 tcp_attach函数 814 30.4 tcp_disconnect函数 815 30.5 tcp_usrclosed函数 816 30.6 tcp_ctloutput函数 817 30.7 小结 820 第31章 BPF:BSD 分组过滤程序 821 31.1 引言 821 31.2 代码介绍 821 31.2.1 全局变量 821 31.2.2 统计量 822 31.3 bpf_if结构 822 31.4 bpf_d结构 825 31.4.1 bpfopen函数 826 31.4.2 bpfioctl函数 827 31.4.3 bpf_setif函数 830 31.4.4 bpf_attachd函数 831 31.5 BPF的输入 832 31.5.1 bpf_tap函数 832 31.5.2 catchpacket函数 833 31.5.3 bpfread函数 835 31.6 BPF的输出 837 31.7 小结 838 第32章 原始IP 839 32.1 引言 839 32.2 代码介绍 839 32.2.1 全局变量 839 32.2.2 统计量 840 32.3 原始 IP的protosw结构 840 32.4 rip_init函数 842 32.5 rip_input函数 842 32.6 rip_output函数 844 32.7 rip_usrreq函数 846 32.8 rip_ctloutput函数 850 32.9 小结 852 结束语 853 附录A 部分习题的解答 854 附录B 源代码的获取 872 附录C RFC 1122 的有关内容 874 参考文献 895
orchid是一个构建于强大的boost库基础上的C 库,类似于python下的gevent/eventlet,为用户提供基于协程的并发模型。 协程,顾名思义,协作式程序,其思想是,一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态。协程在控制离开时暂停执行,当控制再次进入时只能从离开的位置继续执行。 协程已经被证明是一种非常有用的程序组件,不仅被python、lua、ruby等脚本语言广泛采用,而且被新一代面向多核的编程语言如golang rust-lang等采用作为并发的基本单位。 协程可以被认为是一种用户空间线程,与传统的抢占式线程相比,有2个主要的优点: 与线程不同,协程是自己主动让出CPU,并交付他期望的下一个协程运行,而不是在任何时候都有可能被系统调度打断。因此协程的使用更加清晰易懂,并且多数情况下不需要机制。 与线程相比,协程的切换由程序控制,发生在用户空间而非内核空间,因此切换的代价非常的小。 green化 术语“green化”来自于python下著名的协程库greenlet,指改造IO对象以能和协程配合。某种意义上,协程与线程的关系类似与线程与进程的关系,多个协程会在同一个线程的上下文之中运行。因此,当出现IO操作的时候,为了能够与协程相互配合,只阻塞当前协程而非整个线程,需要将io对象“green化”。目前orchid提供的green化的io对象包括: tcp socket(还不支持udp) descriptor(目前仅支持非文件类型文件描述符,如管道和标准输入/输出,文件类型的支持会在以后版本添加) timer (定时器) signal (信号) chan:协程间通信 chan这个概念引用自golang的chan。每个协程是一个独立的执行单元,为了能够方便协程之间的通信/同步,orchid提供了chan这种机制。chan本质上是一个阻塞消息队列,后面我们将看到,chan不仅可以用于同一个调度器上的协程之间的通信,而且可以用于不同调度器上的协程之间的通信。 多核 建议使用的scheduler per cpu的的模型来支持多核的机器,即为每个CPU核心分配一个调度器,有多少核心就创建多少个调度器。不同调度器的协程之间也可以通过chan来通信。协程应该被创建在哪个调度器里由用户自己决定。 进一步信息请阅读doc目录下tutorial。如果您发现任何bug或者有任何改进意见,请联系[email protected] 标签:orchid
目录 译者序 前言 第1章 概述 1.1 引言 1.2 源代码表示 1.2.1 将拥塞窗口设置为1 1.2.2 印刷约定 1.3 历史 1.4 应用编程接口 1.5 程序示例 1.6 系统调用和库函数 1.7 网络实现概述 1.8 描述符 1.9 mbuf与输出处理 1.9.1 包含插口地址结构的mbuf 1.9.2 包含数据的mbuf 1.9.3 添加IP和UDP首部 1.9.4 IP输出 1.9.5 以太网输出 1.9.6 UDP输出小结 1.10 输入处理 1.10.1 以太网输入 1.10.2 IP输入 1.10.3 UDP输入 1.10.4 进程输入 1.11 网络实现概述(续) 1.12 中断级别与并发 1.13 源代码组织 1.14 测试网络 1.15 小结 第2章 mbuf:存储器缓存 2.1 引言 2.2 代码介绍 2.2.1 全局变量 2.2.2 统计 2.2.3 内核统计 2.3 mbufl的定义 2.4 mbuf结构 2.5 简单的mbuf宏和函数 2.5.1 m-get函数 2.5.2 MGET宏 2.5.3 m-etry函数 2.5.4 mbuf 2.6 m-devget和m-pullup函数 2.6.1 m-devget函数 2.6.2 mtod和dtom宏 2.6.3 pullup函数和连续的协议首部 2.6.4 m-pullup和IP的分片与重组 2.6.5 TCP重组避免调用m-pullup 2.6.6 m-pullup使用总结 2.7 mbuf宏和函数的小结 2.8 Neff3联网数据结构小结 2.9 m-Copy和簇引用计数 2.10 其他选择 2.11 小结 第3章 接口层 3.1 引言 3.2 代码介绍 3.2.1 全局变量 3.2.2 SNMP变量 3.3 ifnet结构 3.4 ifadck结构, 3.5 sockaddr结构 3.6 ifnet与土faddr的专用化 3.7 网络初始化概述 3.8 以太网初始化 3.9 suP初始化 3.10 环回初始化 3.11 if_attach函数 3.12 ifinit函数 3.13 小结 第4章 接口:以太网 4.1 引言 4.2 代码介绍 4.2.1 全局变量 4.2.2 统计量 4.2.3 SNMP变量 4.3 以太网接口 4.3.1 leintr函数 4.3.2 leread函数 4.3.3 ether_input函数 4.3.4 ether_output函数 4.3.5 lestart函数 4.4 ioctl系统调用 4.4.1 ifioctl函数 4.4.2 ifconf函数 4.4.3 举例 4.4.4 通用接口ioctl命令 4.4.5 if_down和if_up函数 4.4.6 以太网、SLIP和环回 4.5 小结 第5章 接口:SLIP和环回 5.1 引言 5.2 代码介绍 5.2.1 全局变量 5.2.2 统计量 5.3 SLIP接口 5.3.1 SLIP线路规程:SLIPDISC 5.3.2 SLIP初始化:slopen slinit 5.3.3 SLIP输入处理:slinput 5.3.4 SLIP输出处理:sloutput 5.3.5 slstart函数 5.3.6 SLIP分组丢失 5.3.7 SLIP性能考虑 5.3.8 slclose函数 5.3.9 sltioctl函数 5.4 环回接口 5.5 小结 第6章 IP编址 6.1 引言 6.1.IIP地址 6.I.2 IP地址的印刷规定 6.1.3 主机和路由器 6.2 代码介绍 6.3 接口和地址小结 6.4 sockaddr_in结构 6.5 in_ifaddr结构 6.6 地址指派 6.6.1 ifioctl函数 6.6.2 in_control函数 6.6.3 前提条件:SIOCSIFADDR、SIOCSIFNETMASK和SIOCSIFDSFADDR 6.6.4 地址指派:SIOCSIFADDR 6.6.5 in_ifinit函数 6.6.6 网络掩码指派:SIOCSIFNETMASK 6.6.7 目的地址指派:SIOCSIFDSTADDR 6.6.8 获取接口信息 6.6.9 每个接口多个IP地址 6.6.10 附加IP地址:SIOCAIFADDR 6.6.11 删除IP地址:SIOCDIFADDR 6.7 接口ioctl处理 6.7.1 leioctl函数 6.7.2 slioctl函数 6.7.3 loioctl函数 6.8 Internet实用函数 6.9 ifnet实用函数 6.10 小结 第7章 域和协议 7.1 引言 7.2 代码介绍 7.2.1 全局变量 7.2.2 统计量 7.3 domain结构 7.4 protosw结构 7.5 IP的domain和protosw结构 7.6 pffindproto~Hpffindtype函数 7.7 pfctlinput函数 7.8 IP初始化 7.8.1 Intemet传输分用 7.8.2 ip_init函数 7.9 sysctl系统调用 7.10 小结 第8章 IP:网际协议 8.1 引言 8.2 代码介绍 8.2.1 全局变量 8.2.2 统计量 8.2.3 SNMP变量 8.3 IP分组 8.4 输入处理:ipintr函数 8.4.1 ipintr概观 8.4.2 验证 8.4.3 转发或不转发 8.4.4 重装和分用 8.5 转发:ip_forward函数 8.6 输出处理:ip_output函数 8.6.1 首部初始化 8.6.2 路由选择 8.6.3 源地址选择和分片 8.7 Internet检验和:in_cksum函数 8.8 setsockopt和getsockopt系统调用 8.8.1 PRCO_SETOPT的处理 8.8.2 PRCO_GETOPT的处理 8.9 ip_sysctl函数 8.10 小结 第9章 IP选项处理 9.1 引言 9.2 代码介绍 9.2.1 全局变量 9.2.2 统计量 9.3 选项格式 9.4 ip_dooptions函数 9.5 记录路由选项 9.6 源站和记录路由选项 9.6.1 save_rte函数 9.6.2 ip_srcroute函数 9.7 时间戳选项: 9.8 ip_insertoptions函数 9.9 ip_pcbopts函数 9.10 一些限制 9.11 小结 笫10章 IP的分片与重装 10.1 引言 10.2 代码介绍 10.2.1 全局变量 10.2.2 统计量 10.3 分片 10.4 ip_optcopy函数 10.5 重装 10.6 ip_reass函数 10.7 ip_slowtimo函数 10.8 小结 第11章 ICMP:Internet控制报文协议 11.1 引言 11.2 代码介绍 11.2.1 全局变量 11.2.2 统计量 11.2.3 SNMP变量 11.3 icmp结构 11.4 ICMP的protosw结构 11.5 输入处理:icmp_input函数 11.6 差错处理 11.7 请求处理 11.7.1 回显询问:ICMP_ECHO和ICMLHCHOREPLY 11.7.2 时间戳询问:ICMP_TSTAMP和 ICMPTSTAMPREPLY 11.7.3 地址掩码询问:ICMP_MASKREQ和ICMP_MASKREPLY 11.7.4 信息询问:ICMP_IREQ和ICMP_IREQREPLY 11.7.5 路由器发现:ICMP_ROUTERADVERy和ICMP_ROUTERSOLICIF 11.8 重定向处理 11.9 回答处理 11.10 输出处理 11.11 icmp_error函数 11.12 icmpreflect函数 11.13 icmp_send函数 11.14 icmp_sysctl函数 11.15 小结 第12章 IP多播 12.1 引言 12.2 代码介绍 12.2.1 全局变量 12.2.2 统计量 12.3 以太网多播地址 12.4 ether_multi结构 12.5 以太网多播接收 12.6 inmulti结构 12.7 ip_moptions结构 12.8 多播的插口选项 12.9 多播的TTL值 12.9.1 MBONE 12.9.2 扩展环搜索 12.10 ip_setmoptions函数 12.10.1 选择一个明确的多播接口:IP_MULTICAS%_IF 12.10.2 选择明确的多播TTL:IP_MULTICASTTTL 12.10.3 选择多播环回:IPMULTICAST_LOOP 12.11 加入一个IP多播组 12.11.1 in_addmulti函数 12.11.2 slioctl和loioctl函数:SIOCADDMULTT和SIOCDRLTI 12.11.3 Leioctl函数:SIOCADDMULTI和SIOCDELMULTI 12.11.4 etheraddmulti函数 12.12 离开一个IP多播组 12.12.1 in_delmulti函数 12.12.2 ether_delmulti函数 12.13 ip_getmoptions函数 12.14 多播输入处理:ipintr函数 12.15 多播输出处理:ip_output函数 12.16 性能的考虑 12.17 小结 第13章 IGMP:Intemet组管理协议 13.1 引言 13.2 代码介绍 13.2.1 全局变量 13.2.2 统计量 13.2.3 SNMP变量 13.3 igrmp结构 13.4 IGMP的protosw的结构 13.5 加入一个组:igmp_joingroup函数 13.6 igmp_fasttimo函数 ]3.7 输入处理:igmp_input函数 13.7.1 成员关系查询:IGMP_HOST_M194BERSHIPQUERY 13.7.2 成员关系报告:IGMP_HOST_MEMBERSHIPREPORT 13.8 离开一个组:ignlo_leavegroup函数 13.9 小结 第14章 IP多播选路 14.1 引言 14.2 代码介绍 14.2.1 全局变量 14.2.2 统计量 14.2.3 SNMP变量 14.3 多播输出处理(续) 14.4 mrouted守护程序 14.5 虚拟接口 14.5.1 虚拟接口表 14.5.2 add_vif函数 14.5.3 del_vif函数 14.6 IGMP(续) 14.6.1 add_igrp函数 14.6.2 del_igrp函数 14.6.3 grplst_member函数 14.7 多播选路 …… 第15章 插口层 第16章 插口I/O 第17章 插口选项 第18章 Radix树路由表 第19章 选路请求和选路消息 第20章 选路插口

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值