kafka学习笔记:知识点整理

一、为什么需要消息系统

复制代码
复制代码
1.解耦:
  允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
2.冗余:
  消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。
3.扩展性:
  因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。
4.灵活性 & 峰值处理能力:
  在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
5.可恢复性:
  系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
6.顺序保证:
  在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。(Kafka 保证一个 Partition 内的消息的有序性)
7.缓冲:
  有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。
8.异步通信:
  很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
复制代码
复制代码

 

二、kafka 架构

2.1 拓扑结构

如下图:

图.1

2.2 相关概念

如图.1中,kafka 相关名词解释如下:

复制代码
复制代码
1.producer:
  消息生产者,发布消息到 kafka 集群的终端或服务。
2.broker:
  kafka 集群中包含的服务器。
3.topic:
  每条发布到 kafka 集群的消息属于的类别,即 kafka 是面向 topic 的。
4.partition:
  partition 是物理上的概念,每个 topic 包含一个或多个 partition。kafka 分配的单位是 partition。
5.consumer:
  从 kafka 集群中消费消息的终端或服务。
6.Consumer group:
  high-level consumer API 中,每个 consumer 都属于一个 consumer group,每条消息只能被 consumer group 中的一个 Consumer 消费,但可以被多个 consumer group 消费。
7.replica:
  partition 的副本,保障 partition 的高可用。
8.leader:
  replica 中的一个角色, producer 和 consumer 只跟 leader 交互。
9.follower:
  replica 中的一个角色,从 leader 中复制数据。
10.controller:
  kafka 集群中的其中一个服务器,用来进行 leader election 以及 各种 failover。
12.zookeeper:
  kafka 通过 zookeeper 来存储集群的 meta 信息。
复制代码
复制代码

2.3 zookeeper 节点

kafka 在 zookeeper 中的存储结构如下图所示:

图.2

 

三、producer 发布消息

3.1 写入方式

producer 采用 push 模式将消息发布到 broker,每条消息都被 append 到 patition 中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障 kafka 吞吐率)。

3.2 消息路由

producer 发送消息到 broker 时,会根据分区算法选择将其存储到哪一个 partition。其路由机制为:

1. 指定了 patition,则直接使用;
2. 未指定 patition 但指定 key,通过对 key 的 value 进行hash 选出一个 patition
3. patition 和 key 都未指定,使用轮询选出一个 patition。

附上 java 客户端分区源码,一目了然:

复制代码
复制代码
//创建消息实例
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) {
     if (topic == null)
          throw new IllegalArgumentException("Topic cannot be null");
     if (timestamp != null && timestamp < 0)
          throw new IllegalArgumentException("Invalid timestamp " + timestamp);
     this.topic = topic;
     this.partition = partition;
     this.key = key;
     this.value = value;
     this.timestamp = timestamp;
}

//计算 patition,如果指定了 patition 则直接使用,否则使用 key 计算
private int partition(ProducerRecord<K, V> record, byte[] serializedKey , byte[] serializedValue, Cluster cluster) {
     Integer partition = record.partition();
     if (partition != null) {
          List<PartitionInfo> partitions = cluster.partitionsForTopic(record.topic());
          int lastPartition = partitions.size() - 1;
          if (partition < 0 || partition > lastPartition) {
               throw new IllegalArgumentException(String.format("Invalid partition given with record: %d is not in the range [0...%d].", partition, lastPartition));
          }
          return partition;
     }
     return this.partitioner.partition(record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
}

// 使用 key 选取 patition
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
     List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
     int numPartitions = partitions.size();
     if (keyBytes == null) {
          int nextValue = counter.getAndIncrement();
          List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
          if (availablePartitions.size() > 0) {
               int part = DefaultPartitioner.toPositive(nextValue) % availablePartitions.size();
               return availablePartitions.get(part).partition();
          } else {
               return DefaultPartitioner.toPositive(nextValue) % numPartitions;
          }
     } else {
          //对 keyBytes 进行 hash 选出一个 patition
          return DefaultPartitioner.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
     }
}
复制代码
复制代码

3.3 写入流程

producer 写入消息序列图如下所示:

图.3

流程说明:

复制代码
复制代码
1. producer 先从 zookeeper 的 "/brokers/.../state" 节点找到该 partition 的 leader
2. producer 将消息发送给该 leader
3. leader 将消息写入本地 log
4. followers 从 leader pull 消息,写入本地 log 后 leader 发送 ACK
5. leader 收到所有 ISR 中的 replica 的 ACK 后,增加 HW(high watermark,最后 commit 的 offset) 并向 producer 发送 ACK
复制代码
复制代码

3.4 producer delivery guarantee

一般情况下存在三种情况:

1. At most once 消息可能会丢,但绝不会重复传输
2. At least one 消息绝不会丢,但可能会重复传输
3. Exactly once 每条消息肯定会被传输一次且仅传输一次

当 producer 向 broker 发送消息时,一旦这条消息被 commit,由于 replication 的存在,它就不会丢。但是如果 producer 发送数据给 broker 后,遇到网络问题而造成通信中断,那 Producer 就无法判断该条消息是否已经 commit。虽然 Kafka 无法确定网络故障期间发生了什么,但是 producer 可以生成一种类似于主键的东西,发生故障时幂等性的重试多次,这样就做到了 Exactly once,但目前还并未实现。所以目前默认情况下一条消息从 producer 到 broker 是确保了 At least once,可通过设置 producer 异步发送实现At most once。

 

四、broker 保存消息

4.1 存储方式

物理上把 topic 分成一个或多个 patition(对应 server.properties 中的 num.partitions=3 配置),每个 patition 物理上对应一个文件夹(该文件夹存储该 patition 的所有消息和索引文件),如下:

图.4

4.2 存储策略

无论消息是否被消费,kafka 都会保留所有消息。有两种策略可以删除旧数据:

1. 基于时间:log.retention.hours=168
2. 基于大小:log.retention.bytes=1073741824

需要注意的是,因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高 Kafka 性能无关。

4.3 topic 创建与删除

4.3.1 创建 topic

创建 topic 的序列图如下所示:

图.5

流程说明:

复制代码
复制代码
1. controller 在 ZooKeeper 的 /brokers/topics 节点上注册 watcher,当 topic 被创建,则 controller 会通过 watch 得到该 topic 的 partition/replica 分配。
2. controller从 /brokers/ids 读取当前所有可用的 broker 列表,对于 set_p 中的每一个 partition:
	2.1 从分配给该 partition 的所有 replica(称为AR)中任选一个可用的 broker 作为新的 leader,并将AR设置为新的 ISR
	2.2 将新的 leader 和 ISR 写入 /brokers/topics/[topic]/partitions/[partition]/state
3. controller 通过 RPC 向相关的 broker 发送 LeaderAndISRRequest。
复制代码
复制代码

4.3.2 删除 topic

删除 topic 的序列图如下所示:

图.6

流程说明:

1. controller 在 zooKeeper 的 /brokers/topics 节点上注册 watcher,当 topic 被删除,则 controller 会通过 watch 得到该 topic 的 partition/replica 分配。
2. 若 delete.topic.enable=false,结束;否则 controller 注册在 /admin/delete_topics 上的 watch 被 fire,controller 通过回调向对应的 broker 发送 StopReplicaRequest。

 

五、kafka HA

5.1 replication

如图.1所示,同一个 partition 可能会有多个 replica(对应 server.properties 配置中的 default.replication.factor=N)。没有 replica 的情况下,一旦 broker 宕机,其上所有 patition 的数据都不可被消费,同时 producer 也不能再将数据存于其上的 patition。引入replication 之后,同一个 partition 可能会有多个 replica,而这时需要在这些 replica 之间选出一个 leader,producer 和 consumer 只与这个 leader 交互,其它 replica 作为 follower 从 leader 中复制数据。

Kafka 分配 Replica 的算法如下:

1. 将所有 broker(假设共 n 个 broker)和待分配的 partition 排序
2. 将第 i 个 partition 分配到第(i mod n)个 broker 上
3. 将第 i 个 partition 的第 j 个 replica 分配到第((i + j) mode n)个 broker上

5.2 leader failover

当 partition 对应的 leader 宕机时,需要从 follower 中选举出新 leader。在选举新leader时,一个基本的原则是,新的 leader 必须拥有旧 leader commit 过的所有消息。

kafka 在 zookeeper 中(/brokers/…/state)动态维护了一个 ISR(in-sync replicas),由3.3节的写入流程可知 ISR 里面的所有 replica 都跟上了 leader,只有 ISR 里面的成员才能选为 leader。对于 f+1 个 replica,一个 partition 可以在容忍 f 个 replica 失效的情况下保证消息不丢失。

当所有 replica 都不工作时,有两种可行的方案:

1. 等待 ISR 中的任一个 replica 活过来,并选它作为 leader。可保障数据不丢失,但时间可能相对较长。
2. 选择第一个活过来的 replica(不一定是 ISR 成员)作为 leader。无法保障数据不丢失,但相对不可用时间较短。

kafka 0.8.* 使用第二种方式。

kafka 通过 Controller 来选举 leader,流程请参考5.3节。

5.3 broker failover

kafka broker failover 序列图如下所示:

图.7

流程说明:

复制代码
复制代码
1. controller 在 zookeeper 的 /brokers/ids/[brokerId] 节点注册 Watcher,当 broker 宕机时 zookeeper 会 fire watch
2. controller 从 /brokers/ids 节点读取可用broker
3. controller决定set_p,该集合包含宕机 broker 上的所有 partition
4. 对 set_p 中的每一个 partition
    4.1 从/brokers/topics/[topic]/partitions/[partition]/state 节点读取 ISR
    4.2 决定新 leader(如4.3节所描述)
    4.3 将新 leader、ISR、controller_epoch 和 leader_epoch 等信息写入 state 节点
5. 通过 RPC 向相关 broker 发送 leaderAndISRRequest 命令
复制代码
复制代码

5.4 controller failover

当 controller 宕机时会触发 controller failover。每个 broker 都会在 zookeeper 的 “/controller” 节点注册 watcher,当 controller 宕机时 zookeeper 中的临时节点消失,所有存活的 broker 收到 fire 的通知,每个 broker 都尝试创建新的 controller path,只有一个竞选成功并当选为 controller。

当新的 controller 当选时,会触发 KafkaController.onControllerFailover 方法,在该方法中完成如下操作:

复制代码
复制代码
1. 读取并增加 Controller Epoch。
2. 在 reassignedPartitions Patch(/admin/reassign_partitions) 上注册 watcher。
3. 在 preferredReplicaElection Path(/admin/preferred_replica_election) 上注册 watcher。
4. 通过 partitionStateMachine 在 broker Topics Patch(/brokers/topics) 上注册 watcher。
5. 若 delete.topic.enable=true(默认值是 false),则 partitionStateMachine 在 Delete Topic Patch(/admin/delete_topics) 上注册 watcher。
6. 通过 replicaStateMachine在 Broker Ids Patch(/brokers/ids)上注册Watch。
7. 初始化 ControllerContext 对象,设置当前所有 topic,“活”着的 broker 列表,所有 partition 的 leader 及 ISR等。
8. 启动 replicaStateMachine 和 partitionStateMachine。
9. 将 brokerState 状态设置为 RunningAsController。
10. 将每个 partition 的 Leadership 信息发送给所有“活”着的 broker。
11. 若 auto.leader.rebalance.enable=true(默认值是true),则启动 partition-rebalance 线程。
12. 若 delete.topic.enable=true 且Delete Topic Patch(/admin/delete_topics)中有值,则删除相应的Topic。
复制代码
复制代码

 

6. consumer 消费消息

6.1 consumer API

kafka 提供了两套 consumer API:

1. The high-level Consumer API
2. The SimpleConsumer API

其中 high-level consumer API 提供了一个从 kafka 消费数据的高层抽象,而 SimpleConsumer API 则需要开发人员更多地关注细节。

6.1.1 The high-level consumer API

high-level consumer API 提供了 consumer group 的语义,一个消息只能被 group 内的一个 consumer 所消费,且 consumer 消费消息时不关注 offset,最后一个 offset 由 zookeeper 保存。

使用 high-level consumer API 可以是多线程的应用,应当注意:

1. 如果消费线程大于 patition 数量,则有些线程将收不到消息
2. 如果 patition 数量大于线程数,则有些线程多收到多个 patition 的消息
3. 如果一个线程消费多个 patition,则无法保证你收到的消息的顺序,而一个 patition 内的消息是有序的

6.1.2 The SimpleConsumer API

如果你想要对 patition 有更多的控制权,那就应该使用 SimpleConsumer API,比如:

1. 多次读取一个消息
2. 只消费一个 patition 中的部分消息
3. 使用事务来保证一个消息仅被消费一次

但是使用此 API 时,partition、offset、broker、leader 等对你不再透明,需要自己去管理。你需要做大量的额外工作:

1. 必须在应用程序中跟踪 offset,从而确定下一条应该消费哪条消息
2. 应用程序需要通过程序获知每个 Partition 的 leader 是谁
3. 需要处理 leader 的变更

使用 SimpleConsumer API 的一般流程如下:

复制代码
复制代码
1. 查找到一个“活着”的 broker,并且找出每个 partition 的 leader
2. 找出每个 partition 的 follower
3. 定义好请求,该请求应该能描述应用程序需要哪些数据
4. fetch 数据
5. 识别 leader 的变化,并对之作出必要的响应
复制代码
复制代码

以下针对 high-level Consumer API 进行说明。

6.2 consumer group

如 2.2 节所说, kafka 的分配单位是 patition。每个 consumer 都属于一个 group,一个 partition 只能被同一个 group 内的一个 consumer 所消费(也就保障了一个消息只能被 group 内的一个 consuemr 所消费),但是多个 group 可以同时消费这个 partition。

kafka 的设计目标之一就是同时实现离线处理和实时处理,根据这一特性,可以使用 spark/Storm 这些实时处理系统对消息在线处理,同时使用 Hadoop 批处理系统进行离线处理,还可以将数据备份到另一个数据中心,只需要保证这三者属于不同的 consumer group。如下图所示:

图.8

6.3 消费方式

consumer 采用 pull 模式从 broker 中读取数据。

push 模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成 consumer 来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而 pull 模式则可以根据 consumer 的消费能力以适当的速率消费消息。

对于 Kafka 而言,pull 模式更合适,它可简化 broker 的设计,consumer 可自主控制消费消息的速率,同时 consumer 可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。

6.4 consumer delivery guarantee

如果将 consumer 设置为 autocommit,consumer 一旦读到数据立即自动 commit。如果只讨论这一读取消息的过程,那 Kafka 确保了 Exactly once。

但实际使用中应用程序并非在 consumer 读取完数据就结束了,而是要进行进一步处理,而数据处理与 commit 的顺序在很大程度上决定了consumer delivery guarantee:

复制代码
复制代码
1.读完消息先 commit 再处理消息。
    这种模式下,如果 consumer 在 commit 后还没来得及处理消息就 crash 了,下次重新开始工作后就无法读到刚刚已提交而未处理的消息,这就对应于 At most once
2.读完消息先处理再 commit。
    这种模式下,如果在处理完消息之后 commit 之前 consumer crash 了,下次重新开始工作时还会处理刚刚未 commit 的消息,实际上该消息已经被处理过了。这就对应于 At least once。
3.如果一定要做到 Exactly once,就需要协调 offset 和实际操作的输出。
    精典的做法是引入两阶段提交。如果能让 offset 和操作输入存在同一个地方,会更简洁和通用。这种方式可能更好,因为许多输出系统可能不支持两阶段提交。比如,consumer 拿到数据后可能把数据放到 HDFS,如果把最新的 offset 和数据本身一起写到 HDFS,那就可以保证数据的输出和 offset 的更新要么都完成,要么都不完成,间接实现 Exactly once。(目前就 high-level API而言,offset 是存于Zookeeper 中的,无法存于HDFS,而SimpleConsuemr API的 offset 是由自己去维护的,可以将之存于 HDFS 中)
复制代码
复制代码

总之,Kafka 默认保证 At least once,并且允许通过设置 producer 异步提交来实现 At most once(见文章《kafka consumer防止数据丢失》)。而 Exactly once 要求与外部存储系统协作,幸运的是 kafka 提供的 offset 可以非常直接非常容易得使用这种方式。

更多关于 kafka 传输语义的信息请参考《Message Delivery Semantics》。

6.5 consumer rebalance

当有 consumer 加入或退出、以及 partition 的改变(如 broker 加入或退出)时会触发 rebalance。consumer rebalance算法如下:

复制代码
复制代码
1. 将目标 topic 下的所有 partirtion 排序,存于PT
2. 对某 consumer group 下所有 consumer 排序,存于 CG,第 i 个consumer 记为 Ci
3. N=size(PT)/size(CG),向上取整
4. 解除 Ci 对原来分配的 partition 的消费权(i从0开始)
5. 将第i*N到(i+1)*N-1个 partition 分配给 Ci
复制代码
复制代码

在 0.8.*版本,每个 consumer 都只负责调整自己所消费的 partition,为了保证整个consumer group 的一致性,当一个 consumer 触发了 rebalance 时,该 consumer group 内的其它所有其它 consumer 也应该同时触发 rebalance。这会导致以下几个问题:

复制代码
复制代码
1.Herd effect
  任何 broker 或者 consumer 的增减都会触发所有的 consumer 的 rebalance
2.Split Brain
  每个 consumer 分别单独通过 zookeeper 判断哪些 broker 和 consumer 宕机了,那么不同 consumer 在同一时刻从 zookeeper 看到的 view 就可能不一样,这是由 zookeeper 的特性决定的,这就会造成不正确的 reblance 尝试。
3. 调整结果不可控
  所有的 consumer 都并不知道其它 consumer 的 rebalance 是否成功,这可能会导致 kafka 工作在一个不正确的状态。
复制代码
复制代码

基于以上问题,kafka 设计者考虑在0.9.*版本开始使用中心 coordinator 来控制 consumer rebalance,然后又从简便性和验证要求两方面考虑,计划在 consumer 客户端实现分配方案。(见文章《Kafka Detailed Consumer Coordinator Design》和《Kafka Client-side Assignment Proposal》),此处不再赘述。

 

七、注意事项

7.1 producer 无法发送消息的问题

最开始在本机搭建了kafka伪集群,本地 producer 客户端成功发布消息至 broker。随后在服务器上搭建了 kafka 集群,在本机连接该集群,producer 却无法发布消息到 broker(奇怪也没有抛错)。最开始怀疑是 iptables 没开放,于是开放端口,结果还不行(又开始是代码问题、版本问题等等,倒腾了很久)。最后没办法,一项一项查看 server.properties 配置,发现以下两个配置:

复制代码
复制代码
# The address the socket server listens on. It will get the value returned from 
# java.net.InetAddress.getCanonicalHostName() if not configured.
#   FORMAT:
#     listeners = security_protocol://host_name:port
#   EXAMPLE:
#     listeners = PLAINTEXT://your.host.name:9092
listeners=PLAINTEXT://:9092

# Hostname and port the broker will advertise to producers and consumers. If not set,
# it uses the value for “listeners” if configured. Otherwise, it will use the value
# returned from java.net.InetAddress.getCanonicalHostName().
#advertised.listeners=PLAINTEXT://your.host.name:9092

复制代码
复制代码

以上说的就是 advertised.listeners 是 broker 给 producer 和 consumer 连接使用的,如果没有设置,就使用 listeners,而如果 host_name 没有设置的话,就使用 java.net.InetAddress.getCanonicalHostName() 方法返回的主机名。

修改方法:

1. listeners=PLAINTEXT://121.10.26.XXX:9092
2. advertised.listeners=PLAINTEXT://121.10.26.XXX:9092

修改后重启服务,正常工作。关于更多 kafka 配置说明,见文章《Kafka学习整理三(borker(0.9.0及0.10.0)配置)》。

 

八、参考文章

1. 《Kafka剖析(一):Kafka背景及架构介绍

2. 《Kafka设计解析(二):Kafka High Availability (上)

3. 《Kafka设计解析(二):Kafka High Availability (下)

4. 《Kafka设计解析(四):Kafka Consumer解析

5. 《Kafka设计解析(五):Kafka Benchmark

6. 《Kafka学习整理三(borker(0.9.0及0.10.0)配置)

7. 《Using the High Level Consumer

8. 《Using SimpleConsumer

9. 《Consumer Client Re-Design

10. 《Message Delivery Semantics

11. 《Kafka Detailed Consumer Coordinator Design

12. 《Kafka Client-side Assignment Proposal

13. 《Kafka和DistributedLog技术对比

14. 《kafka安装和启动

15. 《kafka consumer防止数据丢失

 

 

我喜欢程序员,他们单纯、固执、容易体会到成就感;面对压力,能够挑灯夜战不眠不休;面对困难,能够迎难而上挑战自我。他 们也会感到困惑与傍徨,但每个程序员的心中都有一个比尔盖茨或是乔布斯的梦想“用智慧开创属于自己的事业”。我想说的是,其 实我是一个程序员
http://www.cnblogs.com/kms1989/p/6943532.html

Kafka介绍

在流式计算中,Kafka一般用来缓存数据,Storm通过消费Kafka的数据进行计算。

KAFKA + STORM +REDIS

1、Apache Kafka是一个开源消息系统,用Scala写成。

2、Kafka是一个分布式消息队列:生产者、消费者的功能。它提供了类似于JMS的特性,但是在设计实现上完全不同,此外它并不是JMS规范的实现。

3、Kafka对消息保存时根据Topic进行归类,发送消息者称为Producer,消息接收者成为Consumer,此外Kafka集群由多个Kafka实例组成,每个实例(server)称为broker。

4、无论是kafka集群,还是Producer和Consumer都依赖于zookeeper集群保存一些meta信息,来保证系统可用性。

 

Kafka核心组件

Topic :消息根据Topic进行归类

Producer:发送消息者

Consumer:消息接受者

broker:每个kafka实例(server)

Zookeeper:依赖集群保存meta信息。

 

Kafka集群部署(前提:先部署好zookeeper集群)

1、下载安装包

http://kafka.apache.org/

2、上传并解压安装包

tar  -zxvf  /root/kafka_2.11-0.8.2.2.tgz  -C  /export/servers/

cd  /export/servers/

ln -s  kafka_2.11-0.8.2.2  kafka

3、修改配置文件

vi  /export/servers/kafka/config/server.properties

配置一下内容:

4、分发安装包

scp -r /export/servers/kafka_2.11-0.8.2.2  storm2:/export/servers

scp -r /export/servers/kafka_2.11-0.8.2.2  storm3:/export/servers

然后分别在各机器上创建软连

cd /export/servers/

ln -s kafka_2.11-0.8.2.2 kafka

5、再次修改配置文件(重要)

依次修改各服务器上配置文件的broker.id,分别是0,1,2,不得重复!

6、启动集群(先启动zookeeper集群)

依次在每个节点上的/kafka/路径下启动Kafka:

bin/kafka-server-start.sh  config/server.properties

 

Kafka常用操作命令

  • 查看当前服务器中的所有topic

bin/kafka-topics.sh –list –zookeeper  zk01:2181

  • 创建topic

./kafka-topics.sh –create –zookeeper mini1:2181 –replication-factor 1 –partitions 3 –topic first

  • 删除topic

sh bin/kafka-topics.sh –delete –zookeeper zk01:2181 –topic test

需要server.properties中设置delete.topic.enable=true否则只是标记删除或者直接重启。

  • 通过shell命令发送消息

kafka-console-producer.sh –broker-list kafka01:9092 –topic itheima

  • 通过shell消费消息

sh bin/kafka-console-consumer.sh –zookeeper zk01:2181 –from-beginning –topic test1

  • 查看消费位置

sh kafka-run-class.sh kafka.tools.ConsumerOffsetChecker –zookeeper zk01:2181 –group testGroup

  • 查看某个Topic的详情

sh kafka-topics.sh –topic test –describe –zookeeper zk01:2181

 

http://www.cnblogs.com/ahu-lichang/p/6906378.html

.NET全栈开发工程师

1.职位描述

独立负责至少一个产品的前后端开发工作
//注0:今年是博客园开发团队发展的关键一年,我们有两个重要目标——实践领域驱动设计与实现.NET应用的跨平台,我们期待有志开发者的加盟!
//注1:不要求立即全栈,但要在一定时间内成长为全栈。
//注2:投递简历时需要谈一下:你为什么想加入博客园团队。
//注3:除了面试,你还需要完成一份代码作业,我们要看你写的代码。漂亮代码是吸引我们的重要地方。

2.职位要求

* 喜欢编写代码。{
是发自内心的真爱——真正的爱好。在日复一日年复一年,看似枯燥的编码工作中,能够享受取之不尽的用代码进行创作的乐趣。
}

* 乐于解决问题。{
作为程序员,你有一个永远无法回避的问题——每天要面对各种各样的问题,而唯一的解决之道就是喜欢上解决问题。不仅要喜欢上解决问题后的兴奋感觉,更要喜欢上解决问题过程中的煎熬。
}

* 讨厌重复代码。{
将重复代码当作一种垃圾,自己不乱扔垃圾,也不能容忍别人乱扔垃圾。
}

* 讲究代码命名。{
代码如诗,命名如诗句。在代码的创作过程中,追求命名的优雅,不仅让自己也让别人读起代码来赏心悦目。

* 写过三年代码。{
编程语言不限。但从事这个职位,需要用C#,因为团队协作开发要使用统一的工具。

* 基础知识牢靠。{
比如知道三大原理(计算机原理、操作系统原理、编译原理)两个协议(TCP与HTTP协议)一种结构(数据结构)。不牢靠的地方,能及时补课,夯实基础。

* 基本功够扎实。{
比如正则表达式不在话下,Windows/Linux服务器玩得转,Git轻车熟路。如有基本功不扎实,能积极补练基本功。

* 能够面向对象。{
至少熟悉一门面向对象编程语言,有一定的面向对象设计基础,能够将业务逻辑转变为对象之间的交互。

* 英语水平不赖。{
能够流畅地阅读英文技术资料,能够顺畅地与国外开发者用英文进行文字交流,能够准确地用英文进行代码中的命名,能够用英文写代码注释与git提交说明。

* 自学能力很强。{
能够根据面临的实际问题,根据自己的不足,根据自己的兴趣,根据未来发展的需要,持续地进行学习。更要能在自己所犯的错误中学习。一边写代码,一边学习;一边学习,一边写代码,这就是你的程序人生。

* 自我驱动开发。{
在我们这里没有产品经理,没有项目经理,一切由程序员说了算。你要能直接面对用户,主动挖掘用户需求,将之变为自己的想法,然后用漂亮的代码实现。

扎实的基础知识之路

纵观博客园的招聘启事条目中,基础知识的牢靠一条中提出了“三大原理、两个协议和一种结构”,因此我开始补习我的计算机专业基础知识,下面列出我的补课成果:

1.一种结构:数据结构

参考书目:《大话数据结构》、《数据结构(C#语言描述)》、《剑指Offer》

  ①线性表部分:

线性表(上){ 数组、ArrayList }

线性表(中){ 单链表、双链表、LinkedList }

线性表(下){ 循环链表、约瑟夫问题 }

②栈与队列部分:

栈 { LIFO、Stack<T> }

队列 { FIFO、Queue<T> }

③树与二叉树部分:

树与二叉树(上){ 二叉树的创建与递归遍历 }

树与二叉树(中){ 二叉树的非递归遍历与二叉查找树 }

树与二叉树(下){ 二叉树的应用:求解四则运算 }

④图部分:

图(上){ 图的基本概念、存储结构与模拟实现 }

图(中){ 图的深度与广度优先遍历算法与实现 }

图(中){ 最小生成树算法介绍与实现 }

图(下){ 最短路径算法介绍与实现 }

⑤查找部分:

查找(上){ 二分查找、二叉查找树、平衡二叉树、SortedDictionary<TKey,TValue> }

查找(下){ 哈希表、Hashtable、Dictionary、三种查找表的对比测试 }

⑥排序部分:

   排序:{ 插入排序、交换排序、选择排序、归并排序 }

⑦剑指Offer算法题部分:

面试题1:{ 实现Singleton模式 }  面试题2:{ 二维数组中的查找 }

面试题3:{ 替换空格 }  面试题4:{ 从尾到头打印链表 }

面试题5:{ 重建二叉树 }   面试题6:{ 用两个栈实现队列 }

面试题7:{ 旋转数组的最小数字 }  面试题8:{ 斐波那契数列 }

面试题9:{ 二进制中1的个数 }  面试题10:{ 数值的整数次方 }

面试题11:{ 打印1到最大的n位数 }  面试题12:{ 在O(1)时间删除链表 }

面试题13:{ 调整整数数组使得奇数在偶数之前 }  面试题14:{ 链表的倒数第k个节点 }

面试题15:{ 反转链表 }  面试题16:{ 合并两个排序的链表 }

面试题17:{ 二叉树的子结构 }  面试题18:{ 二叉树的镜像 }

面试题19:{ 包含Min函数的栈 }  面试题20:{ 栈的压入、弹出序列 }

面试题21:{ 从上到下打印二叉树 }  面试题22:{ 二叉搜索树的后序遍历序列 }

面试题23:{ 二叉树中和为某一值的路径 }  面试题24:{ 复杂链表的复制 }

面试题25:{ 二叉搜索树与双向链表 }  面试题26:{ 字符串的排列 }

面试题27:{ 最小的k个数 }  面试题28:{ 连续子数组的最大和 }

面试题29:{ 丑数 }  面试题30:{ 第一次只出现一个的字符 }

面试题31:{ 两个链表的第一个公共节点 }  面试题32:{ 数字在排序数组中出现的次数 }

面试题33:{ 二叉树的深度 }  面试题34:{ 翻转单词顺序vs左旋转字符串 }

面试题35:{ 将字符串转换为数字 }

2.两个协议:TCP与HTTP协议

① TCP/IP协议

  参考书目:《图解TCP/IP协议》

网络基础、TCP/UDP、IP、应用层协议 }

  ② HTTP协议

  参考书目:《图解HTTP协议》

{ HTTP请求、HTTP报文、Web攻击技术 }

3.三大原理:计算机原理、操作系统原理与编译原理

① 计算机系统原理

  参考书目:《程序是怎样跑起来的》、《深入理解计算机系统》

② 操作系统原理

  参考书目:《计算机的心智-操作系统之哲学原理》

  Part 0.导论

操作系统导论 { 程序的演变过程、操作系统是什么、操作系统的角色和功能 }

操作系统基本概念 { 硬件基础知识、抽象、内核态与用户态、操作系统结构、系统调用 }

Part 1.CPU管理部分:

进程原理(上){ 进程模型、进程的层次结构、进程的状态、进程的缺陷 }

进程原理(中){ 进程调度的定义、各种调度算法、调度异常之优先级倒挂 }

进程原理(下){ 进程通信概要、各种通信方式 }

线程原理(上){ 线程基础、线程同步 }

线程原理(下){ 死锁描述、死锁必要条件、死锁应对方式、哲学家就餐问题、银行家算法 }

Part 2.内存管理部分:

内存管理(上){ 内存管理概念、基本内存管理 }

内存管理(中){ 页式内存管理、页面置换算法 }

内存管理(下){ 段式内存管理、段页式内存管理 }

Part 3.外存管理部分:

  外存管理(上){ 磁盘定义、磁盘结构、访问时间、磁盘调度算法 }

外存管理(下){ 文件系统、文件夹、文件系统的调用 }

Part 4.设备管理部分:

  设备管理 { I/O基本原理、I/O硬件与软件、I/O软件的分层 }

③ 编译原理

  参考书目:《编译原理(龙书)》

4.基本功:Windows Server/Linux/GitHub/正则表达式

① GitHub : { 基本概念、GitHub使用、Git操作、托管代码 }

Linux :  

  参考书目:《鸟哥的Linux私房菜》

熟练的面向对象之路

轻轻的一句熟悉面向对象,能够将业务逻辑转变为对象之间的交互,是一种大道至简的描述。至于面向对象,很多人都很熟悉,但却又不太熟悉。因此,我决定重新认知面向对象,下面列出我的补课成果:

1.面向对象分析之UML

① OOAD利器之UML基础

2.面向对象设计之设计模式

【创建型】

设计模式的征途-01.单例(Singleton)模式

设计模式的征途-02.简单工厂(Simple Factory)模式

设计模式的征途-03.工厂方法(Factory Method)模式

设计模式的征途-04.抽象工厂(Abstract Factory)模式

设计模式的征途-05.原型(Prototype)模式

设计模式的征途-06.建造者(Builder)模式

【结构型】

① 设计模式的征途-07.适配器(Adapter)模式

设计模式的征途-08.桥接(Bridge)模式

.NET核心原理理解之路

1. .NET核心基础拾遗

① 类型语法基础和内存管理基础

② 面向对象的实现和异常的处理基础

③ 字符串、集合与流的基础

委托、事件、反射与特性的基础

多线程开发基础

ADO.NET与数据库开发基础

⑦ Web Service的开发与应用基础

2.《CLR via C#》读书笔记

暂时就先列出这么多,后期持续更新!

.NET后端开发之路

1.ASP.NET MVC

自己动手写MVC框架:

自己动手写一个简单的ASP.NET MVC框架(第一版)

② 自己动手写一个简单的ASP.NET MVC框架(第二版)

2.ASP.NET WebAPI

3.WCF

数据库技术学习之路

1.MS SQL Server

  T-SQL语言基础:

  T-SQL查询:

2.Oracle

3.MySQL

Web前端开发之路

1.HTML5+CSS3

① Web前端温故知新-CSS基础

② Web前端温故知新-HTML5基础

③ Web前端温故知新-CSS3基础

2.JavaScript

3.Bootstrap

4.AngularJS

AngularJS基础入门初探

移动端开发之路

1.React Native

2.Cordova/PhoneGap

Hybrid App 移动应用开发初探

3.微信小程序

微信小程序开发初探

Agile-敏捷软件开发之路

1.Scrum Guide Scrum指南

① Scrum Guide 原版

② Scrum Guide 中文版

2.Agile 敏捷软件开发:原则、模式与实践

Agile Software Development 敏捷软件开发基础知识

DDD-领域驱动设计之路

TDD-测试驱动开发之路

1.单元测试的艺术

.NET单元测试的艺术:入门

② .NET单元测试的艺术:核心技术

③ .NET单元测试的艺术:测试代码

2.测试驱动开发

 

开源Asp.Net Core小型社区系统

源码地址:Github

前言

盼星星盼月亮,Asp.Net Core终于发布啦!!

Asp.Net发布时我还在上初中,没有赶上。但是Asp.Net Core我从beta版本便一直关注。最初项目名叫Asp.Net VNext,然后改名叫Asp.Net 5。最煎熬的是RC1发布后,官方继续发布了改名RC2延期的通告。这期间我已经做了一些demo项目,但是由于beta到RC2之间涉及到大量API的改动,包括dnx->dotnet cli,包括各种命名空间和工具名称的改动等等,因此这部分demo都已删掉。5月份,Github Asp.Net Core更新路线图,确定RC2于5月中旬发布,同时确定RC2会作为最终发布的版本基础。那段时间我疯狂的关注着Github,即使在国外度蜜月,也会在晚上蹭Wifi关注着动态(这里提一下,有空看一下各个项目的issue,可以积累很多知识。同时很多小道消息都可以在members的回复中看出来)。好在接下来没有再次跳票,开源、跨平台、高性能的Asp.Net Core终于来啦!

小型社区系统

首先看下项目截图:

项目布局参考了CNodeJS 前端采用了Bootstrap,数据库访问用了EntityFramework Core,同时自己用Middleware实现了一个简单的身份认证功能

目前完成的功能:注册,登录,发帖,回帖,收藏,置顶,精华等功能。

项目地址:GitHub

如何运行:

1. 首先安装基础环境

2. clone或者下载项目,先设置连接字符串,然后还原数据库,最后运行即可

详细流程请点击上方连接查看项目主页

开发感受

1. 对于初学者,Asp.Net Core的入门门槛还是挺高的。

没有了WebForm,无法再拖拖控件就完成一个Hello World Page。

MVC和WebApi合二为一,那么至少对这2种技术应该有些基础了解。

处理HTTP请求从传统的Handler、Page变成了Middleware,如果不熟悉nodejs(express)的话又是个新鲜事物。

搭建一个web项目,首先就用到依赖注入容器,又有多少初学者接触过依赖注入呢?

2. 对于.Net开发者,还有很多东西要学。

新的TagHelper和ViewComponent,看来是要培养起面向组建编程的习惯了。

前端可以方便的集成bower, gulp等,那么NodeJS, npm, bower, gulp等等都是需要学的。

project.json里面的东西涉及到编译、发布、部署等等一系列配置,再结合dotnet命令,可以很简单的实现自动化,想起来是不是很激动?

新的EntityFramework Core Migration,直接基于命令生成和更新数据库,看起来是不是很酷?

整个AspNet Core Framework都开源了,基础源码难道不想去看看?

最最最重要的是跨平台!现在我们再也没法逃避Linux啦,大家赶紧装虚拟机,从最基本的ls开始linux之旅吧!

3. 对于Asp.Net Core,还有很长的路要走

性能:从官方的性能测试看出,目前Asp.Net Core可以超过NodeJS,但是比JAVA的Netty还是差了太多(这个测试看起来还是RC1的版本)。首先我觉得大家应该培养起异步编程的好习惯,这篇文章讲述了异步编程是如何提升并发效率的;其次只能寄希望于微软继续提升性能,或者有第三方高性能web框架出现。

框架:Asp.Net Core从出生起就声明了只是.Net Framework的子集,但是部分基础框架的缺失还是带来了很大的不便。最最不方便的就是System.Drawing。

第三方库:作为一个婴儿,Asp.Net Core才刚出生,又经历跳票,因此这方面资源少得可怜。几大热门项目:Dapper,AutoMapper,Nlog等倒是很早就开始支持了。

开发人员流失:谁敢说身边没有从.Net转Java,转Android,转IOS的??

后记

昨天加班到3点,今天早上继续上班,头都是晕的。个人技术不好,见解不够,以上都是自己的想法,希望大家多多交流,一起为.Net社区出力!!

Delegates, Events, and Anonymous Methods 委托、事件与匿名方法

译者注:委托、事件和匿名方法等在C#编程中有广泛运用,也有很多资料和书籍对它们做过大量介绍,但在我接触的人群中仍有很多人对它们还不甚了解,甚至惧怕。我希望这篇博文能够把这些东西说清讲透,也希望有此需要的园友在阅读之后能够获得对它们的深刻理解,并在今后的编程生涯中熟练地运用它们。还希望这篇博文成为介绍委托的经典的技术文章。

本文的主体内容译自《Introducing Visual C# 2010》(Adam Freeman著,Apress出版)一书的第10章。在译文中用“译者注”对原文作了一些补充说明,以使读者对相关内容有更清醒的认识或更明确的概念。

这是一篇篇幅不短的文章,如能认真阅读,相信一定会有所收获,并从此不再惧怕!

Delegates are special types that encapsulate a method, similar to function pointers found in other programming languages. Delegates have a number of uses in C#, but you are most likely to encounter a special type of delegate—the event. Events make notifying interested parties simple and flexible, and I’ll explain the convention for their use later in the chapter.
委托是封装方法的特殊类型,它类似于其它编程语言中的函数指针。委托在C#中有大量运用,但你最可能遇到的是一种特殊的委托类型 — 事件。事件使得对相关部件的通知变得简单而灵活,本章后面将解释其使用约定。

I’ll also explain the Func and Action types that let you use delegates in a more convenient form and that are used extensively in some of the latest C# language features, such as parallel programming. We’ll finish up this chapter with a look at anonymous methods and lambda expressions, two C# features that let us implement delegates without having to define methods in our classes. Table 10-1 provides the summary for this chapter
我也会解释Func和Action类型,它们让你以更方便的形式使用委托,而且在一些最新的C#特性中也有广泛使用,如并行编程。本章最后考察匿名方法和lambda表达式,这是让我们不必在类中定义方法就可以使用委托的两个C#特性。表10-1提供了本章概要。

以上这两段文字告诉我们:委托是一种封装方法的特殊类型。事件是特殊形式的委托,用以实现对相关部件的通知。FuncAction是C#的两个特殊类型,使我们能够更方便地使用委托。匿名方法Lambda表达式是C#的两个特性,让我们不必定义方法就可以使用委托。 — 译者注

Using Delegates
使用委托

A delegate is a special C# type that represents a method signature. Methods are discussed in Chapter 9, and the signature is the combination of the return type and the type and order of the method parameters. Listing 10-1 contains an example of a delegate.
委托delegate)是表示方法签名的一种特殊的C#类型。方法在第9章讨论过,而方法签名是方法的返回类型和方法参数的类型及其顺序的组合。清单10-1是一个委托示例。

以上是委托的概念定义。很多人都知道委托是用来关联方法的,但未能强烈意识到,委托更主要的是定义和使用一种类型。既然是一种类型,委托的使用便与类的使用具有类似性。因此,关于委托的使用通常应当包含这样几个环节:定义委托、创建委托对象、委托对象实例化、执行或调用委托。

另外要特别注意的是,关于委托的名词是混淆的。委托定义、委托对象、委托实例、以及委托调用等,有时不作明确的区分,都笼统地叫做委托。因此,在委托的使用过程中,必须从概念上分清什么是委托定义、委托对象、委托实例、以及什么是执行或调用委托。 — 译者注

Listing 10-1. Defining a Delegate Type
清单10-1. 定义一个委托类型

public delegate int PerformCalc(int x, int y);

There are five parts to the delegate in Listing 10-1, and they are illustrated in Figure 10-1.
清单10-1所示的委托有五个部分,它们如图10-1所示。

IntrC10-11. Access Modifier — 访问修饰符 2. Delegate Keyword — delegate关键字 3. Result Type — 结果类型 4. Delegate Name — 委托名 5. Parameters — 参数

Figure 10-1. The anatomy of a delegate
图10-1. 一个委托的剖析

The first two parts of a delegate are simple. First, all delegates require the delegate keyword. Second, delegates, like all types, can have access modifiers. See Chapter 6 for a description of how these modifiers apply to classes; they have the same effect on delegates.
委托的前两个部分很简单。首先,所有委托都需要delegate关键字。其次,像所有类型一样,委托可以有访问修饰符。参见第6章如何把这些修饰符运用于类的描述,它们在委托上有同样的效果。

The delegate name is the name by which we will refer to the type we have created. This is equivalent to the class name. The name of the delegate type in Listing 10-1 is PerformCalc.
委托名是用来指向已创建的这个类型的名称。它等同于类名。清单10-1中的委托类型名是PerformCalc。

The remaining parts of the delegate specify the kind of method that instances of this delegate can represent. In Listing 10-1, instances of the delegate can represent methods that return an int and that have two int parameters.
该委托的其余部分指明了这个委托的实例可以代表的方法的种类。在清单10-1中,委托的实例可以代表返回一个int(整数)且有两个int参数的所有方法。

As we look at each part of the delegate in Listing 10-1, it is important to bear in mind that when we define a new delegate, we are defining a new type. What we are saying is, “Here is a new type that can be used to refer to a specific kind of method.” Delegates can be hard to understand, and if you find yourself getting lost in this chapter, you should come back to the previous sentence. You can define a new delegate type in the same places as you can create a new class—in a namespace, class, or struct. Once we have defined a new delegate type, we can create an instance of it and initialize it with a value. Listing 10-2 contains a demonstration.
在我们考察清单10-1中委托的各个部分时,重要的是记住:定义一个新委托时,实际是在定义一个新类型。就好像在说:“这是一个新类型,它可以用来指向一类特定的方法”。委托可能难以理解,但如果在本章中发现自己迷失了方向,你应该回想上面这句话。就像可以定义一个新类一样,你可以在定义类的那些地方定义一个新的委托类型 — 在命名空间、类、或结构中。一旦定义了一个新的委托类型,就可以创建它的实例,并用一个值对它初始化。清单10-2是一个演示。

Listing 10-2. Defining a Delegate Field
清单10-2. 定义一个委托字段

// 定义一个委托
public delegate int PerformCalc(int x, int y);
class Calculator {
    // 委托类型字段,用以创建委托对象
    PerformCalc perfCalc;
    // 无访问修饰符的字段意为private(私有)
    // 构造器
    public Calculator() {
        // 实例化委托对象。
        // 对委托对象进行实例化的办法是,用一个方法名对委托对象进行赋值。
        // 于是,以下语句的含义为,perfCalc委托是对CalculateProduct方法的引用
        perfCalc = CalculateProduct;
    }
    // 属性,用以暴露委托对象
    public PerformCalc CalcDelegate {
        get { return perfCalc; }
    }
    // 方法,与委托类型具有相同的方法签名,用以对委托对象实例化
    private int CalculateProduct(int num1, int num2) {
        return num1 * num2;
    }
}

The Calculator class in Listing 10-2 has a field called perfCalc that is of the type of delegate we defined in Listing 10-1. This has created a field that can be used to represent a method that matches the delegate, in other words, a method that returns an int and has two int parameters. The Calculator class contains a method called CalculateProduct that matches that description, and in the Calculator constructor, I assign a value to the delegate field by using the name of the matching method. The definition of the field and the assignment of a value are shown in bold.
清单10-2中的Calculator类有一个叫做perfCalc的字段,它的类型是清单10-1中所定义的委托类型。它创建了一个字段,可以用来表示与委托匹配的方法,即,一个返回int并有两个int参数的方法。Calculator类有一个叫做CalculateProduct的方法与这个描述相匹配,而且在Calculator构造器中,通过使用这个匹配的方法名给这个委托字段赋了一个值。这个字段的定义和赋值以黑体显示。

The Calculator class in Listing 10-2 also contains a public property that returns an instance of the delegate type. The accessor in the property returns the value assigned to the delegate field.
清单10-2中的Calculator类还有一个public属性,它返回委托类型的一个实例。这个属性的访问器(指属性的getter块 — 译者注)返回赋给委托字段的值。

Now we have a new delegate type, PerformCalc, instances of it can be used to represent methods that return an int and that have two int parameters. We have a Calculator class that has a private field of the PerformCalc type and that has been assigned the CalculateMethod and a public property that returns the value of the delegate field. Listing 10-3 demonstrates how to use the delegate.
现在,我们有了一个新的委托类型PerformCalc。它的实例可以用来表示返回一个int并有两个int参数的方法。有一个Calculator类,它有一个PerformCalc类型的private(私有)字段,并把CalculateMethod(应当是CalculateProduct — 译者注)赋给了它。还有一个public属性,它返回该委托字段的值。清单10-3演示了如何使用这个委托。

Listing 10-3. Using a Delegate Obtained Through a Property
清单10-3. 使用通过属性获得的委托

class Listing_03 {
    static void Main(string[] args) {
        Calculator calc = new Calculator();
        // get the delegate
        // 获取委托
        PerformCalc del = calc.CalcDelegate; 
        // invoke the delegate to get a result
        // 调用该委托以获得结果
        int result = del(10, 20); 
        // print out the result
        // 打印结果
        Console.WriteLine("Result: {0}", result); 
        // wait for input before exiting
        // 退出之前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

A new instance of the Calculator class is created, and the CalcDelegate property is used to assign a value to a local variable of the PerformCalc delegate type; this means that the del variable contains a reference to the CalculateProduct method in the Calculator object. I invoke the delegate with the following statement:
上述代码创建了Calculator类的一个新实例,用CalcDelegate属性把一个值赋给了PerformCalc委托类型的一个局部变量,这意味着del变量是对Calculator对象中CalculateProduct方法的引用。用以下语句调用这个委托:

int result = del(10, 20);

This statement passes the parameters 10 and 20 to the method assigned to the del variable, which means that the CalculateProduct method in the Calculator object is called. The result from the delegated method is assigned to the local result variable, just as would happen with a regular method call.
这条语句把参数10和20传递给赋给del变量的方法,这意味着调用Calculator对象中的CalculateProduct方法。从这个委托方法而来的结果被赋值给局部变量result,这就像调用一个常规方法所发生的情况一样。(可见,调用委托就像执行常规的方法一样 — 译者注)

总结上述委托编程过程,可以从概念上形成以下几个名词:

  1. 委托定义:委托定义的作用是创建一个能够封装一类方法的类型。
  2. 委托字段:在委托编程中需要有一个委托字段,该字段可以用来创建委托对象。也可以把这个委托字段看成为是一个委托类型的变量(简称为委托变量)。于是,随时可以用一个方法对这个委托变量进行赋值。
  3. 委托实例:这是对委托字段的实例化,以形成委托对象。实例化的办法是用一个具有相同签名的方法名对委托变量进行赋值。实例化的作用是把委托字段与实例化方法关联在一起,形成委托对象。因此,无论何时,调用委托就是执行其实例化方法。
  4. 委托属性:可以用委托属性对外暴露委托对象。
  5. 执行/调用委托:执行或调用委托实际上是执行委托对象的实例化方法,执行/调用委托与执行规则的方法一样(送入方法参数、接收返回结果)。 — 译者注

The reason that I created a new Calculator object is that I wanted to delegate an instance method, and you can do that only once you have an instance to work with. If you want to delegate a static method, then you can do so by using the class name; you can see an example of delegating a static method in Listing 10-4 later in the chapter.
创建一个新的Calculator对象的原因是想委托一个实例方法,而且你只要这样做一次,就有了一个用来进行工作的实例。如果想委托一个静态方法,那么可以用这个类名来做,本章稍后可以看到委托一个静态方法的示例。

You can also use generic types with delegates. If we wanted a generic version of the delegate type defined in Listing 10-1, we could define the following:
也可以使用委托的泛型类型。如果想定义类似于清单10-1所示的委托类型的泛型类型,可以这样定义:

public delegate T PerformCalc<T>(T x, T y);

Then to create the delegate field in the Calculator class, we would use the following:
然后创建Calculator类中的委托字段,像这样:

class Calculator {
    PerformCalc<int> perfCalc;
    ...

In this way, we can define a general-purpose delegate and provide the type parameters as needed when we define the delegate fields or variables. Generic types are described in Chapter 15.
通过这种方式,我们可以定义一个通用目的的委托,并在定义委托字段或变量时,提供必要的类型参数。泛型类型在第15章描述。

There are a couple of points to note about all the examples so far. The first is that we passed around a method as we would a regular variable, invoking it only when we needed. The other is that the class that called the delegated method had no direct relationship to the method being invoked. We delegated a private method hidden away inside the Calculator class that the Listing_03 class wouldn’t otherwise be able to access.
对于上述这些例子,有两点需要注意。第一点是我们像传递常规的变量一样来传递一个方法,只在需要的时候调用它(可见,我们可以像对变量赋值一样,把一个方法赋给一个委托,还可以像变量一样对委托进行传递。于是,通过委托使得对方法的传递变得十分灵活 — 译者注)。另一点是调用委托方法的类与被请求的方法没有直接的关系(从而实现了方法的使用与方法的具体实现之间的分离。于是,方法体可以被重构或被替换 — 译者注)。我们委托了在Calculator类中被隐藏起来的一个私有方法,这个方法因而在Listing_03类中是不能访问的。

The examples have shown how to use delegates but didn’t really explain why you might find them useful. In the following sections, I’ll show you ways to use delegates that simplify common coding patterns and demonstrate some useful C# features.
这些例子演示了如何使用委托,但并未解释为什么它们是有用的。在以下小节中,将演示使用委托的方式,以达到简化常规的编码模式,并演示一些有用的C#特性。

Using Delegates for Callbacks
用委托进行回调

You can use delegates to create callbacks, where one object is notified when something of interest happens in another object. Listing 10-4 contains an example of a simple callback to notify an interested class when a calculation is performed.
可以用委托来创建回调callback),即,当在一个对象中发生某个关心的事情时,通知另一个对象。清单10-4是一个简单回调的示例,以便在计算完成时通知一个感兴趣的类。

Listing 10-4. Using a Delegate for a Callback
清单10-4. 使用委托进行回调

using System; 
// 委托字义
delegate void NotifyCalculation(int x, int y, int result); 
class Calculator {
    // 委托字段
    NotifyCalculation calcListener; 
    // 构造器,用参数给委托字段赋值(委托实例化) — 译者注
    public Calculator(NotifyCalculation listener) {
        calcListener = listener;
    }
    // 方法,计算两数乘积,在其中通过委托向另一对象发送通知 — 译者注
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // notify the delegate that we have performed a calc
        // 向委托发送通知,说明已经执行了一个计算
        calcListener(num1, num2, result); 
        // return the result
        // 返回结果
        return result;
    }
}
// 接收通知的类 — 译者注
class CalculationListener {
    // 与委托的方法签名对应的方法。
    // 注意,这是一个静态方法,因此不需要在这个类中创建委托字段并实例化,
    // 可以直接通过类名返回该方法的一个实例,如Calculation.CalculationPrinter — 译者注
    public static void CalculationPrinter(int x, int y, int result) {
        Console.WriteLine("Calculation Notification: {0} x {1} = {2}",
                        x, y, result);
    }
}
// 主程序
class Listing_04 {
    static void Main(string[] args) {
        // create a new Calculator, passing in the printer method
        // 创建一个新的Calculator,在其中传递printer方法
        // 于是把一个方法对象注入到了Calculator对象的calc之中形成了一个委托实例 — 译者注
        Calculator calc = new Calculator(CalculationListener.CalculationPrinter);
        // perform some calculations
        // 执行一些计算
        // 注意,每执行一次计算,在计算体里都会回调委托向CalculationListener发送一个通知
        // 通知的作用导致执行CalculationListener中的printer方法 — 译者注
        calc.CalculateProduct(10, 20);
        calc.CalculateProduct(2, 3);
        calc.CalculateProduct(20, 1); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

回调是事件的基础,请通过上述清单真正理解委托回调:首先把一个接收通知的方法注入到一个对象(通知源对象,也即发出通知的对象)之中(上例中的黑体语句),以形成通知源中的委托实例。然后在通知源对象的某个方法中,通过执行这个委托实例,以便回过头来调用(回调)这个被委托方法,从而实现向目标对象(或目标方法)发送通知的目的。因此,所谓委托回调是指通过委托实现回调 — 译者注

The delegate type in this example is called NotifyCalculation and has parameters for the two numbers that have been used for the calculation and the result that was computed. The Calculator class in this example has a constructor argument that takes an instance of the delegate type, which is then invoked inside the CalculateProduct method.
此例中的委托类型叫做NotifyCalculation,其参数是用于计算的两个数字和计算所得的结果。这个例子中的Calculator类有一个构造器参数,它是委托类型的一个实例,在CalculatreProduct方法中对这个实例进行调用。

The Listing_04 class creates a new instance of Calculator and passes a reference to the static CalculationListener.CalculationPrinter method as the constructor parameter. The delegate is called each time the Calculator.CalculateProduct method is invoked, printing out a notification of the calculation that has been performed. Compiling and running the code in Listing 10-4 produces the following result:
Listing_04类创建了一个新的Calculator实例,并把对静态方法CalculationListener.CalculationPrinter的引用作为构造器参数进行传递。每次请求Calculator.CalculateProduct方法都会调用这个委托,打印出一个计算已被执行的通知,编译并运行清单10-4中的代码产生以下结果:

Calculation Notification: 10 x 20 = 200
Calculation Notification: 2 x 3 = 6
Calculation Notification: 20 x 1 = 20
Press enter to finish(按回车键结束)

Using delegates in callbacks means that the source of the notifications doesn’t need to know anything about the class that receives them, allowing the notification receiver to be refactored or replaced without the source having to be modified at all. As you’ll see in the “Delegating Selectively” section later in the chapter, we can select a delegate at runtime, which provides us with even greater flexibility.
在回调中使用委托,意味着通知源不需要知道通知接收者的任何信息,这允许通知接受者被重构或被替换,而源根本不需要做任何修改。正如你将在稍后的“选择性委托”小节中所看到的,可以在运行时选择一个委托,这为我们提供了更大的灵活性。

Multicasting with Delegates
委托多播

When performing callbacks, you will often need to cater for multiple listeners, rather than the single listener shown in Listing 10-4. The delegate type uses custom + and – operators that let you combine several method references together into a single delegate and invoke them in one go, known as multicasting. Custom operators are discussed in Chapter 8. Listing 10-5 contains an example of a multicasting delegate callback.
在执行回调时,通常需要迎合多个侦听器(通知接收者 — 译者注),而不是如清单10-4所示的一个单一的侦听器。委托类型采用 + 和 – 操作符,允许你把对几个方法的引用结合在一起,形成一个单一的委托,并一次性地调用它们,这称为多播multicasting)(因此,多播是一次性调用委托,达到向多个目标发送通知的目的 — 译者注)。自定义操作符在第8章讨论过。清单10-5是一个多播委托回调的例子。

Listing 10-5. Using Delegate Multicasting
清单10-5. 使用委托多播

using System; 
// 委托定义
delegate void NotifyCalculation(int x, int y, int result); 
class Calculator {
    // 委托字段
    NotifyCalculation calcListener; 
    // 方法,增加委托
    public void AddListener(NotifyCalculation listener) {
        calcListener += listener;
    }
    // 方法,去除委托
    public void RemoveListener(NotifyCalculation listener) {
        calcListener -= listener;
    }
    // 方法,执行乘积计算
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // notify the delegate that we have performed a calc
        // 通知委托,告知已执行了一个计算
        calcListener(num1, num2, result); 
        // return the result
        // 返回结果
        return result;
    }
}
// 侦听器类
class CalculationListener {
    // 字段,表示侦听器id
    private string idString; 
    // 构造器,设置侦听器id
    public CalculationListener(string id) {
        idString = id;
    }
    // 与委托签名对应的方法,调用委托时将执行此方法
    public void CalculationPrinter(int x, int y, int result) {
        Console.WriteLine("{0}: Notification: {1} x {2} = {3}",
                    idString, x, y, result);
    }
}
// 另一个侦听器类
class AlternateListener {
    public static void CalculationCallback(int x, int y, int result) {
        Console.WriteLine("Callback: {0} x {1} = {2}",
                    x, y, result);
    }
}
// 主程序
class Listing_05 {
    static void Main(string[] args) {
        // create a new Calculator
        // 创建一个新计算器
        Calculator calc = new Calculator();
        // create and add listeners
        // 创建并添加侦听器,这称为订阅通知 — 译者注
        calc.AddListener(new CalculationListener("List1").CalculationPrinter);
        calc.AddListener(new CalculationListener("List2").CalculationPrinter);
        calc.AddListener(AlternateListener.CalculationCallback); 
        // perform a calculation
        // 执行一个计算
        calc.CalculateProduct(10, 20); 
        // remove a listener
        // 移去一个侦听器,这称为退订通知 — 译者注
        calc.RemoveListener(AlternateListener.CalculationCallback); 
        // perform a calculation
        // 执行一个计算
        calc.CalculateProduct(10, 30); 
        // wait for input before exiting
        // 退出之前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The Calculator class in this example defines two methods that register and unregister callback delegates using the += and -= operators. There are two classes that contain methods that match the delegate signature, CalculationListener and AlternateListener, and the Listing_05 class registers and unregisters the methods as delegates with the Calculator object. You can see that you use a multicast delegate just as you would a single delegate. Compiling and running the code in Listing 10-5 produces the following results:
此例中的Calculator类定义了两个方法,用 += 和 -= 操作符注册和注销回调委托。有两个类含有与委托签名匹配的方法,CalculationListener和AlternateListener,而Listing_05类以Calculator对象来注册和注销委托方法。可以看出,使用多播委托就像使用单个委托一样。编译并运行清单10-5代码会产生如下结果:

List1: Notification: 10 x 20 = 200
List2: Notification: 10 x 20 = 200
Callback: 10 x 20 = 200
List1: Notification: 10 x 30 = 300
List2: Notification: 10 x 30 = 300
Press enter to finish

可以通过以下描述体会委托多播的价值:假设我们要做一个多国语言翻译机,把一国文字翻译成多个国家的文字。于是可以把英-汉、英-日、英-俄、英-德等多个翻译方法组合成一个委托多播,然后通过一次性委托调用得到全部翻译结果 — 译者注

Delegating Selectively
选择性委托

One of the benefits of being able to pass delegates around as variables is to apply delegates selectively, such that we create a delegate that is tailored to a given situation. Listing 10-6 contains a simple example.
能够把委托像变量一样传递的一个好处是可以有选择地运用委托,这样,我们可以针对一个给定的情况来创建委托。清单10-6是一个简单的例子。

Listing 10-6. Creating Anonymous Delegates Based on Parameter Value
清单10-6. 创建基于参数值的匿名委托

using System; 
// 委托定义
delegate int PerformCalc(int x, int y); 
// 计算器类
class Calculator {
    public enum Modes {
        Normal,
        Iterative
    };
    public PerformCalc GetDelegate(Modes mode) {
        if (mode == Modes.Normal) {
            return CalculateNormally;
        } else {
            return CalculateIteratively;
        }
    }
    // 与委托签名一致的方法,乘积
    private int CalculateNormally(int x, int y) {
        return x * y;
    }
    // 与委托签名一致的方法,累加
    private int CalculateIteratively(int x, int y) {
        int result = 0;
        for (int i = 0; i < x; i++) {
            result += y;
        }
        return result;
    }
}
// 主程序
class Listing_06 {
    static void Main(string[] args) {
        // create a new Calculator
        // 创建一个新Calculator
        Calculator calc = new Calculator();
        // get a delegate
        // 获取一个委托
        PerformCalc del = calc.GetDelegate(Calculator.Modes.Normal); 
        // use the delegate
        // 使用该委托
        Console.WriteLine("Normal product: {0}", del(10, 20)); 
        // get a delegate
        // 获取一个委托
        del = calc.GetDelegate(Calculator.Modes.Iterative); 
        // use the delegate
        // 使用该委托
        Console.WriteLine("Iterative product: {0}", del(10, 20)); 
        // wait for input before exiting
        // 退出之前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The Calculator class in Listing 10-6 has a GetDelegate method that returns an delegate based on the parameter value, selected from an enum. If the parameter is the Normal enum value, the delegate returned by the method uses the standard C# multiplication operator, but if the value is Iterative, then the method returns a delegate that performs multiplication as an iterative series of additions.
清单10-6中的Calculator类有一个GetDelegate方法,它返回一个基于参数值的、从一个enum(枚举)选择的委托。如果参数是Normal枚举值,由方法返回的委托使用标准的C#乘法运算符,但如果该值是Iterative,那么该方法返回另一种委托,它像反复累加那样执行乘法。

Interrogating Delegates
质询委托

The base type for all delegates is System.Delegate, and we can use the members of this class to find out which methods a delegate will invoke on our behalf. Listing 10-7 contains an example.
所有委托的基类型都是System.Delegate,而且可以使用该类的成员为我们找出一个委托所调用的方法。清单10-7是一个示例。

Listing 10-7. Interrogating Delegate Types
清单10-7. 质询委托类型

using System; 
delegate int PerformCalc(int x, int y); 
class Calculator {
    public int CalculateSum(int x, int y) {
        return x + y;
    }
}
class AlternateCalculator {
    public int CalculateProduct(int x, int y) {
        return x * y;
    }
}
class Listing_07 {
    static void Main(string[] args) {
        // create a delegate variable
        // 创建委托变量
        PerformCalc del = new Calculator().CalculateSum; 
        // combine with another method
        // 组合另一个方法
        del += new AlternateCalculator().CalculateProduct; 
        // Interrogate the delegate
        // 质询委托
        Delegate[] inlist = del.GetInvocationList();
        foreach (Delegate d in inlist) {
            Console.WriteLine("Target: {0}", d.Target);
            Console.WriteLine("Method: {0}", d.Method);
        }
        // wait for input before exiting
        // 退出之前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The Listing_07 class creates a new delegate variable and uses it to combine methods from the Calculator and AlternateCalculator classes. I use the GetInvocationList method on the delegate variable, which returns an array of System.Delegate objects. I enumerate the contents of the array with a foreach loop and print out the value of the Target and Method properties for each Delegate object. (C# arrays are described in Chapter 13.) Table 10-2 describes the Target and Method properties.
Listing_07类创建了一个新的委托变量,并用它把Calculator和AlternateCalculator的方法组合起来。这里使用了委托变量的GetInvocationList方法,它返回一个System.Delegate对象的数组。用foreach循环枚举了该数组的内容,并打印出每个Delegate对象的Target和Method属性(C#数组在第13章描述)。表10-2描述了Target和Method属性。

Table 10-2. The System.Delegate Properties
表10-2. System.Delegate属性
Property
属性
Description
描述
Target Returns the object that the delegate will use to invoke the method or null if the delegate method is static.
返回委托用来调用方法的对象(目标方法的父类 — 译者注),若委托方法是static(静态的),则返回null。
Method Returns a System.Reflection.MethodInfo that describes the method that will be invoked by the delegate.
返回一个System.Reflection.MethodInfo(方法信息),它描述由委托调用的方法(目标方法本身的信息 — 译者注)。

Compiling and running the code in Listing 10-7 produces the following results:
编译并运行清单10-7代码将产生以下结果:

Target: Calculator
Method: Int32 CalculateSum(Int32, Int32)
Target: AlternateCalculator
Method: Int32 CalculateProduct(Int32, Int32)
Press enter to finish

从程序设计角度讲,选择性委托与质询委托属于两个相反方向的委托处理。利用选择性委托,我们可以根据某些条件有选择地创建委托;而通过质询委托,我们可以根据委托的一些信息有选择地去做某些事情。 — 译者注

Using Events
使用事件

Events are specialized delegates designed to simplify the callback model we saw earlier in the chapter. There can be a problem when you use a delegate type as a field, where one object interferes with another. Listing 10-8 contains a demonstration.
事件是设计用来简化前述回调模型的特殊委托(这是事件的定义,即,事件是执行回调(或发送通知)的一种特殊形式的委托 — 译者注)。在将委托用作为字段时,可能存在一个问题:一个对象会干扰另一个对象。清单10-8是一个演示。

Listing 10-8. One Type Modifying a Delegate Supplied by Another Type
清单10-8. 一个类型修改了由另一个类型提供的委托

using System; 
// 委托
delegate void NotifyCalculation(int x, int y, int result); 
// 类,计算器
class Calculator {
    // 委托字段
    public static NotifyCalculation CalculationPerformed; 
    // 方法,计算乘积
    public static int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算,计算乘积
        int result = num1 * num2; 
        // notify any listeners
        // 通知各个侦听器
        CalculationPerformed(num1, num2, result); 
        // return the result
        // 返回结果
        return result;
    }
}
// 类,一个邪恶的类
class NefariousClass {
    // 委托字段
    private NotifyCalculation orig; 
    // 构造器
    public NefariousClass() {
        // get a reference to the existing listener
        // 获取对现有侦听器的一个引用
        orig = Calculator.CalculationPerformed; 
        // set a new listener for Calculator
        // 对Calculator设置一个新的侦听器(将其指向了下面的那个扭曲的方法 — 译者注)
        Calculator.CalculationPerformed = HandleNotifyCalculation;
    }
    // 方法,一个扭曲的方法。在这个方法中做了两件事:
    // (1)调用原委托谎报结果;(2)自己打印出正确结果 — 译者注
    public void HandleNotifyCalculation(int x, int y, int result) {
        // lie to the original listener
        // 谎报原侦听器
        // 把不良信息送入了原委托方法,这里送入的是加法结果 — 译者注
        orig(x, y, x + y); 
        // print out the details of the real calculation
        // 打印实际计算的细节
        Console.WriteLine("NefariousClass: {0} x {1} = {2}",
                x, y, result);
    }
}
class Listing_08 {
    static void Main(string[] args) {
        // set a listener for the Calculator class
        // 设置对Calculator类的一个侦听器
        Calculator.CalculationPerformed = StandardHandleResult; 
        // create an instance of the Nefarious class
        // 创建Nefarious类的一个实例
        // 注意,在创建这个实例时,原侦听器已经被修改,
        // 而指向了NefariousClass类的HandleNotifyCalculation方法 — 译者注
        NefariousClass nc = new NefariousClass();
        // perform a calculation
        // 执行一个计算。
        // 此时在CalculateProduct方法中执行委托回调时,
        // 实际执行的已经是HandleNotifyCalculation方法了 — 译者注
        Calculator.CalculateProduct(20, 72); 
        // wait for input before exiting
        // 退出之前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
    private static void StandardHandleResult(int x, int y, int result) {
        Console.WriteLine("Good Class: {0} x {1} = {2}", x, y, result);
    }
}

In this example, the Listing_08 class contains a method that matches the delegate type used for the Calculator.CalculationPerformed field. This method is used to process callbacks from the Calculator class. The idea is that the anonymous method will be called each time a calculation is performed by the Calculator class, just as in some of the earlier examples.
在这个例子中,Listing_08类含有一个用于Calculator.CalculationPerformed字段的委托类型相匹配的方法。该方法用于处理Calculator类的回调。其思想是Calculator类每执行一次计算,都调用这个方法(原文中说这是一个匿名方法,其实不是 — 译者注),就像前面那些例子一样。

The Listing_08 class also creates a new NefariousClass object, and the fun begins. The NefariousClass constructor assigns a new method to the Calculator delegate field, displacing the original. This method then feeds bad information to the original value of the delegate field. If we compile and run the code in Listing 10-9, we get the following results:
Listing_08类也创建了一个新的NefariousClass对象,于是开玩笑的事便开始了。NefariousrClass构造器把一个新方法赋给了Calcualtor的委托字段,替换了原有方法。然后这个方法(指已接管的方法HandleNotifyCalculation — 译者注)把不良信息送入了原委托字段。如果编译并运行清单10-9,会得到以下结果:

Good Class: 20 x 72 = 92
NefariousClass: 20 x 72 = 1440
Press enter to finish

The original method is invoked each time a calculation is performed, but NefariousClass has inserted itself in the way and changes the details of the calculation that is reported. And the problems don’t stop there—because the delegate field is public, any object can invoke the delegate as it wants, simulating callbacks even though no calculation has been performed.
每次执行一个计算时都会请求原方法,但NefariousClass阻挡了这一过程,并修改了所报告的计算细节。问题不仅于此 — 由于委托字段是public的,因此任何对象都可以在需要时请求这个委托,甚至在计算尚未执行时就模拟回调。

This example demonstrates deliberate interference, but most of the problems with public delegate fields arise because of sloppy programming, where an object makes an explicit assignment using = to set the value of the delegate field instead of using += to combine delegates. You could take steps to avoid this problem—make the delegate field private and implement methods that enforce checks to ensure that objects are not interfering with each other—but the C# event feature takes care of this for you. Defining an event is just like defining a delegate field, with the addition of the event keyword. So, here’s our delegate field from Listing 10-9:
这个示例演示了有意的干扰,但public委托字段的大多数问题都是由于草率编程所引发的(所以,在委托编程中,委托字段的访问修饰是重要的 — 译者注),在这些场合中,对象都是用 = 来设置委托字段的值,以形成一个明确的赋值,而不是用 += 来组合委托。可以采取一些步骤来避免这类问题 — 让委托字段为private的,并实现强制检查,以确保对象不会相互干扰 — 不过,C#的事件可以帮你照顾这些事。定义事件就像定义委托字段一样,只要附加一个event关键字即可。因此,在以下是清单10-9的委托字段:

class Calculator {
    public static NotifyCalculation CalculationPerformed;

becomes the following:
把它改成下面这样:

class Calculator {
    public static event NotifyCalculation CalculationPerformed;

注意,event关键字是附加在委托字段上的,因此,可以把事件看成是一种特殊形式的委托对象,是经过event修饰的一种委托对象,它让委托对象的使用和操作受到一定的限制和约束。通过使用事件,便不会出现上述一个对象干扰另一个对象之类的问题 — 译者注

When you make a delegate into an event, the class that contains the field can still invoke the delegate and make full use of the type members and operators. Every other object can use only the += and -= operators to add and subtract methods. It is no longer possible to interrogate the delegate or replace the methods assigned to the delegate as I did in Listing 10-8. Although you can make any delegate into an event, there is strong convention in C# to use a certain pattern for events known as the EventHandler pattern, which makes it easier for others to use your events in their code. I describe the pattern in the following sections.
当你把一个委托变成一个事件时,包含该字段的类仍然能够调用委托,并充分利用类型成员和操作符。每一个其它对象都只能用 += 和 -= 操作符来添加和移去方法。这就不再能像清单10-8所做的那样去质询委托,或替换赋给委托的方法。虽然,你可以让任何委托成为事件,但C#有强行的约定,它要求使用事件的特定模式,这称为EventHandler模式,它使其他人更易于在他们的代码中使用你的事件(可见,EventHandler模式是一种编写事件的约定模式,该模式使大家能以统一的方式编写和使用事件,便于事件编程,也便于事件互用 — 译者注)。以下几小节描述这个模式。

注:由于事件是用来实现委托回调的,为了更好地理解以下的事件编程模式,这里对委托回调作以下回顾和总结:

  1. 委托回调可以实现由一个对象(通知源)向外部发送通知的目的。
  2. 通知源需要有一个委托字段和一个发出通知的方法。委托字段用以接收外部方法的注入,形成一个委托实例。发出通知的方法通过执行委托实例,实现向外发送通知的目的。
  3. 在通知源的外部,需要有一个可以注入通知源的方法(接收通知的方法),以便把它注入到通知源形成委托实例,并在通知源发出通知时接收通知,而且在接收到通知时作相应的处理工作。接收通知的方法有时也叫侦听器,意即在侦听到(接收到)通知时,对通知做出响应。
  4. 在实现通知的编程中(主程序中),需要将接收通知的方法注入到通知源中,这称为通知的订阅。

如果把上述委托回调对应到事件场景,应当有以下环节:

  1. 事件是用来实现委托回调的。因此,事件的目的是为了从一个对象(事件源)向外发送事件通知,以便外部对象在接收到通知时进行事件处理。
  2. 事件源需要定义一个事件句柄(事件字段)和一个发送事件通知的方法。事件句柄用以接收事件侦听器(处理事件的方法)的注入,以形成事件委托对象。发送事件通知的方法通过执行事件委托,向外部发送事件通知,就好像向外部通告:“某个事件已经发生啦”。
  3. 事件侦听器是另一个实体中可以作为侦听器注入到事件源中的事件处理方法,以便在接收到事件通知时,对所发生的事件做出响应。
  4. 在实现事件的编程中(主程序中),需要将侦听器注入到事件源中,这通常称为事件订阅

由上可见,事件的本质与委托回调是一致的,但为了克服前述委托回调所具有的干扰,以及让事件有统一的编程和使用方式,对事件的编程需采用特定的编程模式,这个模式称为EventHandler事件编程模式。 — 译者注

Defining and Publishing EventHandler Pattern Events
定义并发布EventHandler模式的事件

The first step in defining an event is to derive a class from the System.EventArgs class, which contains properties and fields to contain any custom data that you want to pass when you invoke the event delegate; the name of this class should end with EventArgs. Listing 10-9 contains an example for the calculation notification from earlier examples.
定义事件的第一步是从System.EventArgs类(事件参数类)派生一个类,这个类需要包含在调用事件委托时希望传递的所有自定义数据的各种字段和属性,这个类的名称应当以EventArgs结尾。清单10-9是针对前面例子中计算通知的一个示例。

Listing 10-9. A Custom EventArgs Implementation
清单10-9. 一个自定义EventArgs实现

class CalculationEventArgs : EventArgs {
    // 定义事件委托的所有参数字段。
    // 注意,这些字段都是private的 — 译者注
    private int x, y, result; 
    // 构造器
    public CalculationEventArgs(int num1, int num2, int resultVal) {
        x = num1;
        y = num2;
        result = resultVal;
    }
    // 以下是与字段对应的属性。注意,这些属性都只有getter块,即都是只读的 — 译者注
    public int X {
        get { return x; }
    }
    public int Y {
        get { return y; }
    }
    public int Result {
        get { return result; }
    }
}

You can include any fields and properties that you need to express information about your event, but you should make sure that the fields cannot be modified. This is because the same EventArgs object will be passed to each of the subscribers of your event, and a poorly or maliciously coded recipient could change the information that subsequent listeners receive. The CalculationEventArgs class derives from EventArgs and defines fields for the details of our calculation and a set of read-only properties to provide access to the field values. You can get more information about properties in Chapter 8 and more information about fields in Chapter 7.
你可以包括需要表示事件信息的任何字段和属性,但要确保字段不能被修改(即,是只读的 — 译者注)。这是因为,同样的EventArgs对象(你所定义的这些字段和属性 — 译者注)将被传递给该事件的每个订户,无知或恶意的代码接收者可能会修改随后的侦听器所接收的信息。这个CalculationEventArgs类派生于EventArgs,且为计算细节定义了一组字段和只读属性,以提供对字段值的访问。你可以从第8章了解更多关于属性、从第7章了解更多关于字段的信息。

以上表明,事件编程的第一步需要定义一个派生于System.EventArgs类的事件参数类。这个类的名称按约定要以EventArgs结尾。在这个类中,应当为事件的所有参数定义相应的字段和属性 — 译者注

The next step is to define an event in your class. You don’t have to define a delegate for events (although as I showed earlier you certainly can), because you can use the generic EventHandler delegate, which is part of the System namespace. Listing 10-10 demonstrates the definition of an event.
下一步是在你的类(事件源类 — 译者注)中定义一个事件。你不必为事件定义一个委托(虽然我在前面已表明这是可以的),因为你可以使用泛型的EventHandler委托,它位于System命名空间中。清单10-10是一个事件定义。

Listing 10-10. Defining an Event Using the Genetic EventHandler Delegate
清单10-10. 用泛型EventHandler委托定义一个事件

class Calculator {
    public event EventHandler<CalculationEventArgs> CalculationPerformedEvent;
    ...
}

To define the event using the generic EventHandler delegate, you use your custom EventArgs class as the type parameter, as shown in the listing. The convention is that the name of the event should end with the word Event. You can learn more about generic types and generic type parameters in Chapter 15.
为了用泛型EventHandler委托来定义事件,你要以自定义的EventArgs类作为其类型参数,如清单所示。其约定是,事件的名称应当以单词Event结尾。第15章可以了解更多关于泛型类型及泛型类型参数的内容。

请把这个EventHandler理解为事件句柄,它是一种泛型委托类型,是用来定义事件(字段)的。用这个事件句柄定义事件可以将委托的定义和创建事件委托字段这两步工作合并成一步,即,此时不需要再定义事件的委托了。定义这个事件委托的作用是用它来接收外部注入的事件侦听器(事件处理方法) — 译者注

The convention dictates that you put the code to invoke your event delegate in a method whose name starts with On concatenated with the event name, less the word event. So, for example, since we have defined an event called CalculationPerformedEvent, the method would be called OnCalculationPerformed. This method should make a copy of the event to avoid a race condition (race conditions arise in parallel programming and are explained in later in this book) and ensure that the event has subscribers by ensuring that the event field is not null. Listing 10-11 shows the Calculator class updated to use the event pattern fully.
该约定指明,为了调用这个事件委托,你会把代码放到一个方法中,该方法的名称以On开头,后跟去掉单词event的事件名称。举例来说,由于我们定义了一个名称为CalculationPerformedEvent的事件,该方法要叫做OnCalculationPerformed。这个方法将形成该事件的一份拷贝,以避免竞争条件(竞争条件会出现在并行编程中,本书的后面会加以解释),并通过保证事件字段非空的办法来确保该事件已有订户。清单10-11展示了经过修改的Calculator类,以完全使用这种事件模式。

Listing 10-11. Implementing the EventArgs Pattern
清单10-11. 实现EventArgs模式

class Calculator {
    // 定义事件
    public event EventHandler<CalculationEventArgs> CalculationPerformedEvent; 
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // publish the event
        // 发布事件,意即执行事件委托,向外部发出事件通知 — 译者注
        OnCalculationPerformed(new CalculationEventArgs(num1, num2, result)); 
        // return the result
        // 返回结果
        return result;
    }
    // 在发布事件时调用的方法
    private void OnCalculationPerformed(CalculationEventArgs args) {
        // make a copy of the event
        // 形成事件的一份拷贝
        EventHandler<CalculationEventArgs> handler = CalculationPerformedEvent; 
        // check to see we have subscribers
        // 查看事件订户
        if (handler != null) {
            handler(this, args);
        }
    }
}

You can see that the CalculateProduct method creates a new instance of the CalculationEventArgs class and uses it to call the OnCalculationPerformed method, which then copies the event, checks to see that it isn’t null, and invokes it.
可以看出,CalculateProduct方法创建了CalculationEventArgs类的一个新实例,并用它来调用OnCalculationPerformed方法,该方法然后拷贝事件,检查如果不为null,便调用它。

事件编程的第二步是创建事件源类,这需要做三件事:

  1. 用EventHandler定义事件委托。EventHandler的参数是事件编程第一步中创建的事件参数类。该事件委托的名称按约定要以Event结尾。该事件委托的作用是用来接收外部注入的事件处理方法,以形成一个实例化的事件委托对象。
  2. 编写发送事件通知的方法。在这个方法中,通过执行OnXXX方法实现向外发送通知的目的。按约定,OnXXX方法名中的XXX是上一步的事件名称去掉单词Event的部分。送给OnXXX方法的参数是事件参数类的一个实例。
  3. 编写OnXXX方法。在这个方法中需形成事件的一份拷贝,并判断该事件有无订户,若有则执行事件。

以上三步工作如上述清单代码的黑体所示 — 译者注

Subscribing to events is just like using a delegate, with the exception that the subscriber is limited to using the += and -= operators. Listing 10-12 shows a class that uses the events defined in the previous examples.
订阅事件就像使用委托一样,只是把订阅者限制到只能使用 += 和 -= 操作符。清单10-12展示了使用前述示例所定义的事件的一个类。

Listing 10-12. Subscribing to Events
清单10-12. 订阅事件

class Listing_12 {
    static void Main(string[] args) {
        // create a new instance of the Calculator class
        // 创建Calculator类的新实例
        Calculator calc = new Calculator();
        // subscribe to the event in the Calculator class
        // 订阅Calculator类中的事件(将一个外部方法注入到事件源 — 译者注)
        calc.CalculationPerformedEvent += HandleEvent; 
        // perform a calculation
        // 执行计算
        calc.CalculateProduct(20, 72); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
    // 处理事件方法
    // 作为侦听器可以被注入到事件源中去,
    // 并在接收到事件源发出的通知时,对该事件进行处理(响应) — 译者注
    static void HandleEvent(object sender, CalculationEventArgs e) {
        Console.WriteLine("Good Class: {0} x {1} = {2}", e.X, e.Y, e.Result);
    }
}

由上可见,要实现事件的订阅与退订,需要做两件事:

  1. 编写事件处理方法:事件处理方法也叫侦听器。事件处理方法是用来注入到事件源、以接收事件通知、并对通知做出响应的方法。注意,事件处理方法的参数有特殊的要求。
  2. 订阅与退订事件:在应用程序(主程序)中,将事件处理方法注入到事件源对象中(订阅)。注意,只能以 += 或 -= 操作符进行事件的订阅或退订,否则会抛出异常。

事件处理方法的编程,以及事件的订阅与退订如上述清单的黑体所示 — 译者注

You must ensure that the method you are going to use to handle events is not publically accessible; otherwise, you are still liable to encounter problems with other classes, as demonstrated by Listing 10-13.
必须确保打算用来处理事件的方法不是公开可访问的(即,事件处理方法即上述说明中的事件侦听器方法,该方法的访问修饰符应当不是public的 — 译者注),否则,与其它类一起使用时,仍会遇到问题,如清单10-13所示。

Listing 10-13. Removing Another Delegate from an Event
清单10-13. 从一个事件中移去另一个委托

using System; 
class CalculationEventArgs : EventArgs {
    private int x, y, result; 
    public CalculationEventArgs(int num1, int num2, int resultVal) {
        x = num1;
        y = num2;
        result = resultVal;
    }
    public int X {
        get { return x; }
    }
    public int Y {
        get { return y; }
    }
    public int Result {
        get { return result; }
    }
}
class Calculator {
    public event EventHandler<CalculationEventArgs> CalculationPerformedEvent; 
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // publish the event
        // 公布事件
        OnCalculationPerformed(new CalculationEventArgs(num1, num2, result)); 
        // return the result
        // 返回结果
        return result;
    }
    private void OnCalculationPerformed(CalculationEventArgs args) {
        // make a copy of the event
        // 形成事件的一份拷贝
        EventHandler<CalculationEventArgs> handler = CalculationPerformedEvent; 
        // check to see we have subscribers
        // 检查是否有订户
        if (handler != null) {
            handler(this, args);
        }
    }
}
class NefariousClass {
    public NefariousClass(Calculator calc) {
        // add a new listener for Calculator
        // 添加一个新的Calculator侦听器
        calc.CalculationPerformedEvent += HandleNotifyCalculation; 
        // unsubscribe someone else's event handler
        // 退订其他人的事件处理器
        calc.CalculationPerformedEvent -= Listing_13.HandleEvent;
    }
    public void HandleNotifyCalculation(object sender, CalculationEventArgs e) {
        // print out the details of the real calculation
        // 打印出实际计算的细节
        Console.WriteLine("NefariousClass: {0} x {1} = {2}",
                e.X, e.Y, e.Result);
    }
}
class Listing_13 {
    static void Main(string[] args) {
        // create a new instance of the Calculator class
        // 创建Calculator类的新实例
        Calculator calc = new Calculator();
        // subscribe to the event in the calaculator class
        // 订阅Calculator类中的事件
        calc.CalculationPerformedEvent += HandleEvent; 
        // create an instance of NefariousClass
        // 创建NefariousClass实例
        NefariousClass nef = new NefariousClass(calc); 
        // perform a calculation
        // 执行计算
        calc.CalculateProduct(20, 72); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
    public static void HandleEvent(object sender, CalculationEventArgs e) {
        Console.WriteLine("Good Class: {0} x {1} = {2}", e.X, e.Y, e.Result);
    }
}

The NefariousClass constructor in the example adds a new listener to the Calculator event, but it also removes the listener added by the Listing_13 class. It can do this because the Listing_13.HandleEvent method is public and therefore can be accessed outside of its containing class. Changing the access modifier on the HandleEvent class to a more restrictive setting would prevent this from happening.
上例中NefariousClass的构造器把一个新的侦听器加到Calculator事件,但它也移去了Listing_13类加入的侦听器。这确实是能够做到的,因为Listing_13.HandleEvent方法是public的,因而可以在它的容器类的外部访问它。把HandleEvent类的访问修饰符改为更严格的设置将可以阻止这一情况的发生。

Creating Nongeneric Events
创建非泛型事件

Another approach to events is to use the nongeneric version of EventHandler. This is more like using a delegate in that you have to define the event/delegate type. This approach predates the introduction of generic types in C#, but I have included it because it is still widely used. Listing 10-14 shows the Calculator example implemented without generic support.
事件的另一种办法是使用EventHandler的非泛型版本(这是事件的另一种编程模式 — 译者注)。这更像以事件/委托类型的方式来使用委托。这种办法在C#中要早于泛型类型的引入,我在这里包含此内容是因为它仍有广泛的运用。清单10-14演示了不用泛型支持的Calculator示例实现。

Listing 10-14. Implementing Events Without Generic Types
清单10-14. 不用泛型类型实现事件

using System; 
delegate void CalculationPerformedEventHandler(object sender, CalculationEventArgs args); 
class CalculationEventArgs : EventArgs {
    private int x, y, result; 
    public CalculationEventArgs(int num1, int num2, int resultVal) {
        x = num1;
        y = num2;
        result = resultVal;
    }
    public int X {
        get { return x; }
    }
    public int Y {
        get { return y; }
    }
    public int Result {
        get { return result; }
    }
}
class Calculator {
    public event CalculationPerformedEventHandler CalculationPerformedEvent; 
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // publish the event
        // 公布事件
        OnCalculationPerformed(new CalculationEventArgs(num1, num2, result)); 
        // return the result
        // 返回结果
        return result;
    }
    private void OnCalculationPerformed(CalculationEventArgs args) {
        // make a copy of the event
        // 形成事件拷贝
        CalculationPerformedEventHandler handler = CalculationPerformedEvent; 
        // check to see we have subscribers
        // 检查是否已有订户
        if (handler != null) {
            handler(this, args);
        }
    }
}
class Listing_14 {
    static void Main(string[] args) {
        // create a new instance of the Calculator class
        // 创建Calculator类的新实例
        Calculator calc = new Calculator();
        // subscribe to the event in the calaculator class
        // 订阅Calculator类中的事件
        calc.CalculationPerformedEvent += HandleEvent; 
        // perform a calculation
        // 执行计算
        calc.CalculateProduct(20, 72); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
    static void HandleEvent(object sender, CalculationEventArgs e) {
        Console.WriteLine("Good Class: {0} x {1} = {2}", e.X, e.Y, e.Result);
    }
}

There isn’t much to say about this example; it is very similar to the generic event listings but has an additional delegate definition.
这个例子无需多说,除了有一个附加的委托定义之外,它与前述泛型事件完全类似。

Creating Events Without Custom Data
创建无自定义数据的事件

If you don’t need to pass custom data as part of your event, then you can use EventArgs directly, as shown in Listing 10-15.
如果不需要把自定义数据作为事件的一部分进行传递,那么可以直接使用EventArgs,如果清单10-15所示。

Listing 10-15. Creating and Using Events with No Custom Data
清单10-15. 创建和使用无自定义数据的事件

using System; 
class Calculator {
    public event EventHandler CalculationPerformedEvent;
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // publish the event
        // 公布事件
        OnCalculationPerformed();
        // return the result
        // 返回结果
        return result;
    }
    private void OnCalculationPerformed() {
        // make a copy of the event
        // 形成事件拷贝
        EventHandler handler = CalculationPerformedEvent; 
        // check to see we have subscribers
        // 检查是否已有订户
        if (handler != null) {
            handler(this, EventArgs.Empty); 
        }
    }
}
class Listing_15 {
    static void Main(string[] args) {
        // create a new instance of the Calculator class
        // 创建Calculator类新实例
        Calculator calc = new Calculator();
        // subscribe to the event in the calaculator class
        // 订阅Calculator类中的事件
        calc.CalculationPerformedEvent += HandleEvent; 
        // perform a calculation
        // 执行计算
        calc.CalculateProduct(20, 72); 
        // wait for input before exiting
        // 退出之前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
    static void HandleEvent(object sender, EventArgs e) {
        Console.WriteLine("Event Received");
    }
}

There is no need to define a custom delegate if you don’t need custom data. When defining the event, you simply use the EventHandler type, as follows:
如果不需要自定义数据,没必要定义一个自定义委托。当定义事件时,你只要简单地使用EventHandler类型,如下所示:

public event EventHandler CalculationPerformedEvent;

The event still requires two arguments to invoke it, but you can use the static EventArgs.Empty property to get a reference to a ready-made EventArgs instance that has no custom data. You can see this in the OnCalculationPerformed method of the Calculator class, which has been updated to remove the method parameters.
该事件仍然需要两个参数去调用它,但你可以使用静态的EventArgs.Empty属性,以获得对已形成的无自定义数据的EventArgs实例的引用。你可以在Calculator类的OnCalculationPerformed方法中看到这种情况,已对其作了修改,去除了方法参数。

由上可见,创建无自定义数据的事件,不需要创建事件参数类 — 译者注

Applying Modifiers to Events
对事件运用修饰符

You can control access to your events using the public, protected, internal, and private keywords. See Chapter 7 for details of these keywords and the effect they have on fields.
你可以用public、protected、internal和private关键字来控制对事件的访问。参阅第7章关于这些关键字的细节,以及它们运用于字段所具有的效应。

You should not use the virtual modifier on events. If you do, it is possible that your events will not be delivered properly. If you want to override an event from a base class, then you should mark the OnXXX method as virtual and override the method in the derived class. Listing 10-16 provides a demonstration. You can find more details and examples of overriding methods in Chapter 9.
你不应该在事件上使用virtual修饰符。如果这么做,你的事件可能不会被适当地投递。如果你想重写基类的事件,那么你应当将OnXXX方法标记为virtual,并在派生类中重写该方法。清单101-6提供了一个演示。在第9章中你会看到重写方法的更多细节和示例。

Listing 10-16. Deriving Event Implementations
清单10-16. 派生事件实现

// 基类
class Calculator {
    public event EventHandler<CalculationEventArgs> CalculationPerformedEvent; 
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // publish the event
        // 公布事件
        OnCalculationPerformed(new CalculationEventArgs(num1, num2, result)); 
        // return the result
        // 返回结果
        return result;
    }
    protected virtual void OnCalculationPerformed(CalculationEventArgs args) {
        // make a copy of the event
        // 形成事件拷贝
        EventHandler<CalculationEventArgs> handler = CalculationPerformedEvent; 
        // check to see we have subscribers
        // 确认已有订户
        if (handler != null) {
            handler(this, args);
        }
    }
}
// 派生类
class DerivedCalc : Calculator {
    protected override void OnCalculationPerformed(CalculationEventArgs args) {
        // perform custom logic here
        // 以下执行自定义逻辑
        // call the base method
        // 调用基方法
        base.OnCalculationPerformed(args);
    }
}

In this example, the DerivedCalc class overrides the OnCalculationPerformed method, which has been marked as virtual in the base Calculator class. The overridden method can perform modifications to the custom EventArgs implementation (or perform any other required task) before calling the base OnCalculationPerformed method to publish the event.
在这个例子中,DerivedCalc类重写了OnCalculationPerformed方法,它在基Calculator类中已被标记为virtual了。重写的方法可以在调用基OnCalculationPerformed方法来公布事件之前,对自定义的EventArgs实现进行修改(或执行任何其它所需的任务)。

Using Action and Func Delegates
使用Action和Func委托

The Func and Action classes are special types that allow you to use delegates without having to specify a custom delegate type. They are used throughout the .NET Framework. For example, you will see examples of both when we look at parallel programming.
Func和Action类是特殊的类型,它们允许你在不必指定自定义委托类型的情况下,去使用委托。在整个.NET框架中都可以使用它们。例如,在我们考察并行计算时,你也会看到这两个类的示例。

Func和Action是.NET的两个特殊类型,在整个.NET平台中都可以使用它们。这两个类型实际上是委托类型,可以用来创建委托对象。因此,由于它们是类型,我们便可以创建Func和Action类型的变量;又由于它们可以用来创建委托对象,于是直接用方法名对这些变量进行赋值,便可以形成实例化的委托对象,而不必进行委托定义 — 译者注

Using Action Delegates
使用Action委托

Action delegates encapsulate methods that do not return results. In other words, they can be used only with methods that are defined with the void keyword. The simplest Action delegate is the System.Action class, which is used for methods that have no parameters (and no results). Listing 10-17 contains an example.
Action委托封装不返回结果的方法(Action是可以用来委托无返回类型的方法,意即,可以用无返回类型的方法名对Action类型的变量进行赋值 — 译者注)。换句话说,这种委托只能用于以void关键字定义的那些方法。最简单的void委托是System.Action类,它用于无参数(且无结果)的方法。清单10-17含有一个例子。

Listing 10-17. Using the Basic Action Delegate
清单10-17. 使用基本的Action委托

using System; 
class Calculator {
    public void CalculateProduct() {
        // perform the calculation
        // 执行计算
        int result = 10 * 20; 
        // print out a message with the result
        // 用结果印出消息
        Console.WriteLine("Result: {0}", result);
    }
}
class Listing_17 {
    static void Main(string[] args) {
        // create a new instance of Calculator
        // 创建Calculator新实例
        Calculator calc = new Calculator();
        // create an action and assign a method
        // 创建一个Action并赋予一个方法
        // 以下语句创建了一个Action类型的变量,并用一个方法名对其实例化,形成了一个委托对象 — 译者注
        Action act = calc.CalculateProduct; 
        // invoke the method via the Action
        // 通过这个Action调用该方法
        act();
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The key statement in Listing 10-17 is shown in bold; it creates a new local Action variable and assigns the CalculateProduct method from an instance of Calculator as the value. The Action is then invoked, just as a regular delegate would be. Compiling and running the code in Listing 10-17 products the following results:
清单10-17中的关键语句以黑体显示,它创建了一个新的Action局部变量,并把Calculator实例的CalculateProduct方法作为值赋给了它。然后这个Action被调用,就像规则的委托那样。编译并运行清单10-17会产生以下结果:

Result: 200
Press enter to finish

We didn’t have to define a custom delegate in this example. The System.Action type handled everything for us. There are 17 different Action implementations available. Starting with the one used in Listing 10-17, each adds a new generic parameter. This is not as confusing as it may sound; you just create the generic implementation that matches the number of parameters the target method required. Listing 10-18 contains an example that uses two parameters.
在这个例子中,我们不必定义一个自定义委托(即,不必进行委托定义 — 译者注)。System.Action类型为我们处理所有事情。有17个不同的Action实现可用。从清单10-17所用的一个开始,每一个都添加了一个新的泛型参数。实际情况并不像听上去这样让人困扰,你只要创建与目标方法所需要的参数数目匹配的泛型实现。清单1-18包含了一个使用两个参数的示例。

Listing 10-18. Using a Generic Action Delegate
清单10-18. 使用泛型的Action委托

using System; 
class Calculator {
    public void CalculateProduct(int x, int y) {
        // perform the calculation
        // 执行计算
        int result = x * y; 
        // print out a message with the result
        // 用result打印出消息
        Console.WriteLine("Result: {0}", result);
    }
}
class Listing_18 {
    static void Main(string[] args) {
        // create a new instance of Calculator
        // 创建Calculator新实例
        Calculator calc = new Calculator();
        // create an action and assign a method
        // 创建一个action,并赋予一个方法
        Action<int, int> act = calc.CalculateProduct; 
        // invoke the method via the Action
        // 通过Action调用该方法
        act(10, 20);
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

In this example, the method I want to delegate has two int parameters, so I used the Action<int, int> delegate (the parameter types for an Action need not all be the same). If I had wanted to delegate a method with five parameters, then I would have used the Action<T1, T2, T3, T4, T5> type and filled in the type parameters of the Action type to match the parameter types of the delegated method.
在这个例子中,我想委托的方法有两个int参数,因此,我使用了Action<int, int>委托(Action的参数类型并不需要是相同的)。如果我想委托一个具有五个参数的方法,那么,我会使用Action<T1, T2, T3, T4, T5>类型,并填充这个Action类型的类型参数,以匹配被委托方法的参数类型。

Using Func Delegates
使用Func委托

System.Func delegates are just like Action delegates, except that they can return results. The simplest Func implementation has no parameters. Listing 10-19 contains an example; you can see the similarities to the Action examples.
System.Func委托除了可以返回结果以外,它与Action委托完全相同。最简单的Func实现没有参数。清单10-19包含一个示例,你可以看出它与Action示例的类似性。

Listing 10-19. A Simple Func Example
清单10-19. 一个简单的Func示例

using System; 
class Calculator {
    public int CalculateProduct() {
        // perform the calculation
        // 执行计算
        return 10 * 20;
    }
}
class Listing_19 {
    static void Main(string[] args) {
        // create a new instance of Calculator
        // 创建Calculator新实例
        Calculator calc = new Calculator();
        // create a Func and assign a method
        // 创建一个Func,并赋予一个方法
        Func<int> act = calc.CalculateProduct; 
        // invoke the method via the Action(应当是Func)
        // 通过Func调用方法
        int result = act();
        // print out the result
        // 印出结果
        Console.WriteLine("Result: {0}", result);
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The last generic type for System.Func is the result type for the delegate. So, in Listing 10-19, the Func<int> I used had no parameters but returned an int. Just like Action, there are 17 Func implementations with an increasing number of parameters, each of which can be of a different type. Listing 10-20 demonstrates using Func with two int parameters.
System.Func的最后一个泛型类型是委托的结果类型。因此,在清单10-19中,我所使用的Func<int>没有参数,但返回int。就像Action一样,也有17个带有不同参数数目的Func实现,每个参数都可以是不同的类型。清单10-20演示了使用两个int参数的Func。

Listing 10-20. Using a Func with Parameters
清单10-20. 使用带有参数的Func

using System; 
class Calculator {
    public int CalculateProduct(int x, int y) {
        // perform the calculation
        // 执行计算
        return x * y;
    }
}
class Listing_20 {
    static void Main(string[] args) {
        // create a new instance of Calculator
        // 创建Calculation新实例
        Calculator calc = new Calculator();
        // create a Func and assign a method
        // 创建Func并赋予一个方法
        Func<int, int, int> act = calc.CalculateProduct;
        // invoke the method via the Action Func
        // 通过这个Func调用该方法
        int result = act(10, 20);
        // print out the result
        // 印出结果
        Console.WriteLine("Result: {0}", result); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

感谢微软工程师,为我们提供了这么好用的Action和Func。有了它们,我们使用委托简直太方便了 — 译者注

Anonymous Methods
匿名方法

All of the examples so far in this chapter have used named methods, that is, methods that exist in classes and have a method identifier. C# also supports anonymous methods, which allow you to implement a delegate without defining a method. Listing 10-21 contains an anonymous method.
本章目前为止所演示的示例都使用了命名的方法,即,在类中存在且有方法修饰符的方法。C#也支持匿名方法,它让你能够不必定义方法就可以实现委托。清单10-21含有一个匿名方法。

Listing 10-21. An Anonymous Method
清单10-21. 匿名方法

using System; 
class Listing_21 {
    static void Main(string[] args) {
        // create a new Calculator
        // 创建一个新的Calculator
        Calculator calc = new Calculator();
        // create a delegate with an anonymous method
        // 用匿名方法创建一个委托
        calc.CalculationPerformedEvent += delegate(object sender, CalculationEventArgs e) {
            Console.WriteLine("Anonymous Calc: {0} x {1} = {2}", e.X, e.Y, e.Result);
        };
        // perform a calculation
        // 执行计算
        calc.CalculateProduct(20, 40); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The class in Listing 10-21 works with the Calculator and CalculationEventArgs classes defining in Listing 10-9. I have omitted them from this listing for brevity. You can see the anonymous method in bold; it is also illustrated in Figure 10-2.
清单10-21中的类使用了清单10-9中定义的Calculator和CalculationEventArgs类。出于简化,我忽略了它们。你可以看到以黑体表示的匿名方法,也如图10-2所示。

IntrC10-2图中:Delegate Keyword: 委托关键字;Parameters:参数;Code Statements:代码语句

Figure 10-2. The anatomy of an anonymous method
图10-2. 一个匿名方法的剖析

An anonymous method has a number of similarities to a named method. You can see the parameters and code block in Figure 10-2 are just like those you would get in a regular method. There is no method identifier (because there is no method name), and you must use the delegate keyword when defining an anonymous method.
匿名方法与命名方法有许多相似性。你可以看出,图10-2中的参数和代码块与你要在规则方法中得到的一样。没有方法修饰符(因为没有方法名),而且,在定义匿名方法时,你必须使用delegate关键字。

Using an anonymous method as a delegate is just like using a named method, as you can see from Listing 10-21. I used the += operator to add the anonymous method to the event in the Calculator class.
将匿名方法用作为委托与使用命名方法一样,正如你从清单10-21所看到的,我使用了+=操作符把匿名方法加到了Calculator类中的事件。

When the event invokes the delegate, the statements in the anonymous method body are executed, just as would happen for a regular method.
当事件调用委托时,匿名方法体中的语句被执行,就像规则方法所发生的情况一样。

If we compile and run the code in Listing 10-21, we get the following results:
如果编译并运行清单10-21代码,会得到以下结果:

Anonymous Calc: 20 x 40 = 800
Press enter to finish

Anonymous methods work very well with Func and Action delegates, allowing you to define a delegate without needing to create a delegate type or implement a named method. When implementing a delegate that returns a result, simply use the return keyword as you would for a named method, as demonstrated by Listing 10-22.
匿名方法可以与Func和Action委托很好地一起使用,这允许你定义一个委托,而不需要创建委托类型或实现命名方法。当实现返回一个结果的委托时,简单地使用return关键字,就像使用命名方法一样,如清单10-22所示。

Listing 10-22. An Anonymous Method That Returns a Result
清单10-22. 返回一个结果的匿名方法

using System; 
class Listing_22 {
    static void Main(string[] args) {
        // define a func and implement it with an anonymous method
        // 定义一个func,并用一个匿名方法实现它
        Func<int, int, int> productFunction = delegate(int x, int y) {
            return x * y;
        };
        // invoke the func and get a result
        // 调用这个func,并得到一个结果
        int result = productFunction(10, 20); 
        // print out the result
        // 印出结果
        Console.WriteLine("Result: {0}", result); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The bold statement in Listing 10-22 defines a Func that returns an int result and that has two int parameters. The implementation of this delegate is provided by an anonymous method that returns the product of the two parameters values. You invoke the delegate in the normal way, as though it were a method. Compiling and running the code in Listing 10-22 produces the following results:
清单10-22中的黑体语句定义了一个返回int结果且有两个int参数的Func。这个委托的实现是由一个匿名方法实现的,它返回两个参数值的积。用常规的方式调用这个委托,就像它是一个方法一样。编译并运行清单10-22代码产生以下结果:

Result: 200
Press enter to finish

Capturing Outer Variables
捕捉外部变量

Anonymous methods do more than reduce the number of methods in your class; they are also able to access the local variables that are defined in the containing methods, known as outer variables. Listing 10-23 provides a demonstration.
匿名方法的作用还不止能减少类中的方法数,它们也能访问容纳方法(指容纳该匿名方法的容器 — 译者注)中定义的局部变量,这称为外部变量(outer variables)。清单10-23提供了一个演示。

Listing 10-23. Accessing Outer Variables
清单10-23. 访问外部变量

using System; 
class Calculator {
    Func<int, int, int> calcFunction; 
    public Calculator(Func<int, int, int> function) {
        calcFunction = function;
    }
    public int PerformCalculation(int x, int y) {
        return calcFunction(x, y);
    }
}
class Listing_23 {
    static void Main(string[] args) {
        // define a local variable
        // 定义一个局部变量
        int calculationCount = 0;
        // define and implement a Func
        // 定义并实现一个Func
        Func<int, int, int> productFunc = delegate(int x, int y) {
            // increment the outer variables
            // 递增外部变量
            calculationCount++;
            // calculate and return the result
        // 计算并返回结果
            return x * y;
        };
        // create a new instance of Calculator
        // 创建Calculator新实例
        Calculator calc = new Calculator(productFunc);
        // perform several calculations
        // 执行一些计算
        for (int i = 0; i < 5; i++) {
            Console.WriteLine("Result {0} = {1}", i, calc.PerformCalculation(i, i));
        }
        // print out the value of the outer variable
        // 印出外部变量的值
        Console.WriteLine("calculationCount: {0}", calculationCount); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The Calculator class in this example takes a Func<int, int, int> as a constructor parameter and invokes the delegate when the PerformCalculation method is called. The Listing_23 class defines a matching Func using an anonymous method. This is passed to the constructor of the Calculator instance.
这个例子中的Calculator类以Func<int, int, int>作为构造器参数,并在调用PerformCalculation方法时调用该委托。Listing_23类用一个匿名方法定义了一个匹配的Func。它被传递给Calculator实例的构造器。

The anonymous method increments the calculationCount variable each time that it is invoked, even though the variable is defined outside of the anonymous method and even though the Func is invoked by an entirely different method in an entirely different object. Compiling and running the code in Listing 10-23 produces the following result:
匿名方法每次被调用时都递增了calculationCount变量,即使这个变量是在匿名方法的外部定义的,甚至Func也是由完全不同的对象中完全不同的方法来调用的。编译并运行清单10-23代码产生以下结果:

Result 0 = 0
Result 1 = 1
Result 2 = 4
Result 3 = 9
Result 4 = 16
calculationCount: 5
Press enter to finish

Variables that are accessed outside the anonymous method are called captured variables. The calculationCount variable in Listing 10-23 was captured by the anonymous method. An anonymous method that captures variables is called a closure.
匿名方法外部被访问的变量称为被捕捉captured)变量(因此,匿名方法体外部的那些变量称为外部变量,在匿名方法体中所访问的那个外部变量称为被捕捉变量 — 译者注)。清单10-23中的calculationCount变量是由匿名方法来捕捉的。一个要捕捉变量的匿名方法称为是一个闭包closure)。

Captured variables are evaluated when the delegate is invoked, which means that you should take care when making assumptions about the value of a variable. Listing 10-24 provides a demonstration.
在委托被调用时,会求取被捕捉变量的值,这意味着,在假设一个变量的值时要小心。清单10-24提供了一个演示。

Listing 10-24. Evaluating Captured Variables
清单10-24. 求取被捕捉变量的值

using System; 
class Listing_24 {
    static void Main(string[] args) {
        // define a variable that will be captured
        // 定义被捕捉变量
        string message = "Hello World";
        // define an anonymous method that will capture
        // the local variables
        // 定义一个将捕捉这个局部变量的匿名方法
        Action printMessage = delegate() {
            Console.WriteLine("Message: {0}", message);
        };
        // modify one of the local vaiables
        // 修改局部变量
        message = "Howdy!"; 
        // invoke the delegate
        // 调用委托
        printMessage();
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

When the anonymous method is defined, the value of the message variable is Hello World. But defining an anonymous method doesn’t capture the variables it references—that doesn’t happen until the delegate is invoked, by which time the value of the message variable has changed. Compiling and running the code in Listing 10-24 produces the following result:
在定义匿名方法时,message变量的值是Hello World。但在定义匿名方法时并不捕捉它所引用的变量 — 直到委托被调用时才会发生,此时message变量的值已经变化了。编译并运行清单10-24代码产生以下结果:

Message: Howdy!
Press enter to finish

Lambda Expressions
Lambda表达式

Lambda expressions have largely replaced anonymous methods since they were introduced in C# 3.0. They have much the same functionality as an anonymous method but are slightly more convenient to use. Listing 10-25 contains an anonymous method and an equivalent lambda expression.
Lambda表达式已经广泛代替了匿名方法,因为它是从C# 3.0开始引入的。Lambda表达式与匿名方法有很多同样的功能,但更便于使用。清单10-25包含了一个匿名方法和一个等效的Lambda表达式。

Listing 10-25. Comparing an Anonymous Method with a Lambda Expression
清单10-25. 匿名方法与Lambda表达式比较

using System; 
class Listing_25 {
    static void Main(string[] args) {
        // implement an anonymous method that multiplies ints
        // 实现一个int数相乘的匿名方法
        Func<int, int, int> anonFunc = delegate(int x, int y) {
            return x * y;
        };
        // do the same thing with a lambda expression
        // lambda表达式做同样的事情
        Func<int, int, int> lambaFunc = (x, y) => {
            return x * y;
        };
        // invoke the delegates
        // 调用委托
        Console.WriteLine("Anonymous Method Result: {0}", anonFunc(10, 10));
        Console.WriteLine("Lambda Expression Result: {0}", lambaFunc(10, 10)); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The lambda expression in Listing 10-25 is shown in bold. There are only three parts to a lambda expression, and they are illustrated in Figure 10-3.
清单10-25中的lambda表达式以黑体显示。一个lambda表达式只有三个部分,它们如图10-3所示。

IntrC10-3图中:Parameters:参数,Lambda Operator:Lambda操作符,Code Statements:代码语句

Figure 10-3. The anatomy of a lambda expression
图10-3. Lambda表达式剖析

Although the format of a lambda expression looks a little odd, the basic premise is the same as for anonymous methods. Parameters in a lambda expression are specified without their types—the types are inferred.
虽然lambda表达式看上去有点古怪,但基本前提与匿名方法是相同的。Lambda表达式的参数不指定类型 — 其类型通过推断。

The code statements are just like for a method, named or anonymous. You access the parameters by the names they have been specified by. In the case of Listing 10-25, the parameters x and y are multiplied together, and the result is returned.
其代码语句与命名或匿名方法同样。通过被指定的参数名称来访问参数。在清单10-25的示例中,参数x和y相乘,并返回结果。

You can omit the braces and the return keyword if there is a single operation in a lambda expression so that the lambda expression in Listing 10-25 can also be written as follows:
如果在lambda表达式中只有一个单一的操作,你可以忽略花括号和return关键字,因此,清单10-25的lambda表达式也可以写成如下形式:

(x, y) => x * y;

The lambda operator (=>) is, as you might expect, required for lambda operators. It is often described as the goes to operator. For the expression in Listing 10-25, we can say that the parameters x and y go to the product of x and y.
lambda操作符(=>),正如你可能想到的,是必需的。通常把它说成是“进入go to)”操作符(感觉把 => 操作符说成“送入”更恰当些,意即将参数送入方法体 — 译者注)。对于清单10-25中的表达式,我们可以说成,x和y参数进入x和y的乘积。

Compiling and running the code in Listing 10-25 produces the following results:
编译并运行清单10-25代码产生以下结果:

Anonymous Method Result: 100
Lambda Expression Result: 100
Press enter to finish

The C# compiler is pretty good at inferring the types of parameters for lambda expressions, but on occasion the compiler won’t be able to work it out, and you will you need to explicitly tell the compiler what they are. Here is an example:
C#编译器十分擅长推断lambda表达式的参数类型,但偶尔也会有编译器不能推断的情况,此时你需要明确地告诉编译器,它们是什么。以下是一个示例:

(int x, int y) => x * y;

I have used lambda expressions extensively in software projects and for other C# books, and I have found only a small number of instances where the compiler couldn’t figure things out implicitly, but when you do encounter one of those situations (or if you just prefer explicit typing), then it is good to know how to do it.
我在软件项目以及其它C#书籍中广泛使用了lambda表达式。而且我发现,只有很少情况下才会出现编译器不能推断的情况。当你遇到这种情况时(或者你更喜欢明确指定类型),那么,最好知道如何做这种事。

Summary
小结

In this chapter, you have how delegates can be used to encapsulate references to static and instance methods and passed around like regular types. You have seen the limitations of delegates when they are shared between objects and how events address this shortcoming.
在本章中,你知道了如何把委托用于封装对静态和实例化方法的引用,并像规则类型一样进行传递。你也看到了,对象之间共享委托所具有的局限性,以及事件是如何解决这一缺陷的。

The convention for using events is widely adopted, and I recommend that you follow it in your own code. Not only does it make your code easier for other programmers to use, but it also helps you create events that can be overridden reliably in derived classes.
使用事件的约定被广泛采纳,而且我建议你在代码中遵循它。不仅它使你的代码更易于被其他程序员使用,而且也有助于你创建能够在派生类中可靠地进行重写的事件。

We looked briefly at the System.Func and System.Action types, both of which allow us to work with delegates without creating custom delegate types, and we looked at anonymous methods and lambda expressions, which allow us to implement delegates without having to define methods. We learned how anonymous methods and lambda expressions capture local variables and when those variables are evaluated. We’ll see a lot more of Func, Action, and lambda expressions when we explore some of the advanced C# language features such as LINQ and parallel programming.
我们简要地考查了System.Func和System.Action类型,它们都能让我们不必创建自定义委托类型就可以使用委托。我们也考查了匿名方法和lambda表达式,它们让我们能够不必定义方法就可以实现委托。我们了解了匿名方法和lambda表达式如何捕捉局部变量,以及评估这些变量的时间。在我们考察C#的高级语言特性,如LINQ和并行编程时,会看到更多Func、Action和lambda表达式。

这是一篇有技术含量、甚至可以说是有学术价值的文章!请大家多给推荐

将ASP.NET Core应用程序部署至生产环境中(CentOS7)

这段时间在使用Rabbit RPC重构公司的一套系统(微信相关),而最近相关检验(逻辑测试、压力测试)已经完成,接近部署至线上生产环境从而捣鼓了ASP.NET Core应用程序在CentOS上的部署方案,今天就跟大家分享一下如何将ASP.NET Core应用程序以生产的标准部署在CentOS上。

环境说明

服务器系统:CentOS 7.2.1511

相关工具:Xshel、Xftp

服务器软件软件:.netcore、nginx、supervisor、policycoreutils-python

准备你的ASP.NET Core应用程序

首先将你的应用程序以便携的模式进行发布。

ps:这边我使用一个空的Web项目来进行演示,因为本篇主要介绍生产环境的部署,与应用无关。

命令为:dotnet publish –c release

具体的可以看:拥抱.NET Core,如何开发跨平台的应用并部署至Ubuntu运行,这篇博文介绍了以便携与自宿主方式发布web应用。

image

确保这份发布应用可以在windows上运行,以减少后续的问题。

image

为什么不用自宿主的方式进行部署?

自宿主的发布方式进行部署会简单很多,为什么生产环境要使用便携的方式进行发布呢?

原因1:性能比便携式的低(主)。

原因2:微软给出的建议(次)。

口说无凭,有图有真相。

image

image

参考地址:https://docs.microsoft.com/zh-cn/dotnet/articles/core/app-types

so,既然是用于生产环境的,当然我们要追求更高的性能。

安装CentOS7

这个就不细说了,网上教程很多,这边我使用了Hyper-V来虚拟化了CentOS7。

安装.NET Core SDK for CentOS7。

sudo yum install libunwind libicu(安装libicu依赖)

image

curl -sSL -o dotnet.tar.gz https://go.microsoft.com/fwlink/?LinkID=809131(下载sdk压缩包)

sudo mkdir -p /opt/dotnet && sudo tar zxf dotnet.tar.gz -C /opt/dotnet(解压缩)

sudo ln -s /opt/dotnet/dotnet /usr/local/bin(创建链接)

image

输入 dotnet –info 来查看是否安装成功

image

如果可以执行则表明.NET Core SDK安装成功。

参考资料:https://www.microsoft.com/net/core#centos

部署ASP.NET Core应用程序

上传之前发布的文件夹至/home/wwwroot/。

这边我使用了Xftp进行文件的上传。

image

image

检查是否能够运行

命令:dotnet /home/wwwroot/WebApplication1/WebApplication1.dll

image

如果出现这些信息则表示成功运行。

这时候我们是无法访问到这个页面的,这时候我们需要部署一个web容器来进行转发。

配置Nginx

安装Nginx

curl -o  nginx.rpm http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm

image

rpm -ivh nginx.rpm

yum install nginx

image

安装成功!

输入:systemctl start nginx 来启动nginx。

输入:systemctl enable nginx 来设置nginx的开机启动(linux宕机、重启会自动运行nginx不需要连上去输入命令)。

配置防火墙

命令:firewall-cmd –zone=public –add-port=80/tcp –permanent(开放80端口)

命令:systemctl restart firewalld(重启防火墙以使配置即时生效)

测试nginx是否可以访问。

image

配置nginx对ASP.NET Core应用的转发

修改 /etc/nginx/conf.d/default.conf 文件。

将文件内容替换为

server {
listen 80;
location / {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}

上传至CentOS进行覆盖。

执行:nginx –s reload 使其即时生效

运行ASP.NET Core应用程序

image

命令:dotnet /home/wwwroot/WebApplication1/WebApplication1.dll

这时候再次尝试访问。

image

想哭的心都有。。。经过后续了解,这个问题是由于SELinux保护机制所导致,我们需要将nginx添加至SELinux的白名单。

接下来我们通过一些命令解决这个问题。。

yum install policycoreutils-python

sudo cat /var/log/audit/audit.log | grep nginx | grep denied | audit2allow -M mynginx

sudo semodule -i mynginx.pp

image

再次尝试访问。

image

至此基本完成了部署。

配置守护服务(Supervisor)

目前存在三个问题

问题1:ASP.NET Core应用程序运行在shell之中,如果关闭shell则会发现ASP.NET Core应用被关闭,从而导致应用无法访问,这种情况当然是我们不想遇到的,而且生产环境对这种情况是零容忍的。

问题2:如果ASP.NET Core进程意外终止那么需要人为连进shell进行再次启动,往往这种操作都不够及时。

问题3:如果服务器宕机或需要重启我们则还是需要连入shell进行启动。

为了解决这个问题,我们需要有一个程序来监听ASP.NET Core 应用程序的状况。在应用程序停止运行的时候立即重新启动。这边我们用到了Supervisor这个工具,Supervisor使用Python开发的。

安装Supervisor

yum install python-setuptools

easy_install supervisor

配置Supervisor

mkdir /etc/supervisor

echo_supervisord_conf > /etc/supervisor/supervisord.conf

修改supervisord.conf文件,将文件尾部的配置

image

修改为

image

ps:如果服务已启动,修改配置文件可用“supervisorctl reload”命令来使其生效

配置对ASP.NET Core应用的守护

创建一个 WebApplication1.conf文件,内容大致如下

[program:WebApplication1]
command=dotnet WebApplication1.dll ; 运行程序的命令
directory=/home/wwwroot/WebApplication1/ ; 命令执行的目录
autorestart=true ; 程序意外退出是否自动重启
stderr_logfile=/var/log/WebApplication1.err.log ; 错误日志文件
stdout_logfile=/var/log/WebApplication1.out.log ; 输出日志文件
environment=ASPNETCORE_ENVIRONMENT=Production ; 进程环境变量
user=root ; 进程执行的用户身份
stopsignal=INT

将文件拷贝至:“/etc/supervisor/conf.d/WebApplication1.conf”下

运行supervisord,查看是否生效

supervisord -c /etc/supervisor/supervisord.conf

ps -ef | grep WebApplication1

image

如果存在dotnet WebApplication1.dll 进程则代表运行成功,这时候在使用浏览器进行访问。

image

至此关于ASP.NET Core应用程序的守护即配置完成。

配置Supervisor开机启动

新建一个“supervisord.service”文件

# dservice for systemd (CentOS 7.0+)
# by ET-CS (https://github.com/ET-CS)
[Unit]
Description=Supervisor daemon

[Service]
Type=forking
ExecStart=/usr/bin/supervisord -c /etc/supervisor/supervisord.conf
ExecStop=/usr/bin/supervisorctl shutdown
ExecReload=/usr/bin/supervisorctl reload
KillMode=process
Restart=on-failure
RestartSec=42s

[Install]
WantedBy=multi-user.target

将文件拷贝至:“/usr/lib/systemd/system/supervisord.service”

执行命令:systemctl enable supervisord

image

执行命令:systemctl is-enabled supervisord #来验证是否为开机启动

image

测试

GIF

 


如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,转载文章之后须在文章页面明显位置给出作者和原文连接,谢谢。

.Net Core[译文]

文章概览:

什么是.Net Core平台?

.Net Core是一个模块化的,跨平台的,开源实现的.Net Framework,它可以在Windows设备上运行,也可以在Linux和OS X上运行。不同于传统的.Net Framework(特点:庞大的,系统级的,是一个只能跑在Windows上的运行时环境),你可以用.Net Core创造以多平台为目标的组件化的库和应用程序,并且.Net Core可以与应用程序一起部署。

.Net Core特性

让我们从宏观上看看.Net Core的特性

跨平台支持

.NET Core 应用程序可以同时跑在32位和64位的Windows平台上,也可以跑在OS X和Linux上。

相反的,基于传统.Net Framework编写的应用程序只能跑在Windows上。注意的是UWP通用应用程序借助.Net Core的实现,也只能跑在Windows桌面程序,平板和Windows phone手机上。

开放源代码

.Net Core的RunTime(CoreCLR)和基础类库都是开放源代码的,另外.Net Core的源代码都是免费获得的,这些意味着:

  • 设计注释,功能规格,还有特定实现文档都是公开的
  • .Net Core的代码评审(Code review)是公开的
  • 您可以通过使用GitHub的提供的功能,将bug公开,对源代码提出您的建议,而且你可以提交新特性的请求,或者你自己写的代码都可以提交。
  • 你也可以下载所有的源代码在你的机器上编译。Runtime与基础类库,还有一些工具可以在任何平台上编译。

改善的Console App

传统的.Net Framework可以在Windows上创建运行创建控制台apps,.Net Core同样可以,但是.Net Core改善了控制应用程序:

  • 跨平台:控制台程序可以跑在Windows,OS X和Linux
  • 原生编译:将托管程序的好处与原生C/C++应用程序的性能相结合

安装简单

因为.Net Core不是一个操作系统的组件,现在安装:

  • 不需要管理员权限
  • 不需要触碰系统组件了,也就是说不需要向操作系统的系统目录和注册表中写文件了
  • 复制一些文件到目标计算机更简单了,或者将原生framework编译进你的app。

这样,新的开发者不到一分钟就可以入门.Net了,包括下载Framework和tools。可以从这里

部署简单

作为运维人员,去部署一个开发部门给你的.Net应用程序的时候,你首先要做的就是去服务器检查.Net framework版本是否符合要求,如果不符合就需要安装一个能运行这个程序的基础版本,这些会给运维人员带来很大的困扰
相比之下,。Net core有两种发布部署方式:

Portable Apps

在部署一个portable app的时候,你除了.Net Core库之外,只需要部署你的程序和它的依赖就可以。
在目标机器上为了将Portable Apps跑起来需要安装.Net Core。你不需要提前去考虑你的app支持哪个平台,因为.Net Core就是一个独立的组件。
在.Net Core里,Portable Apps是默认的程序类型。

Self-contained Apps

Self-contained apps包含了所有的依赖,.Net Core runtime也会作为软件的一部分.由于你的APP中内置了.Net Core,所以不管你要部署的那台机器上安装没安装.Net Core都可以运行,而且就算之前的机子上有人安装了.Net Core,你的.Net Core类库也是与别人的.Net Core类库隔离开的。
这种方式的前提得是你的程序里内置了对应平台的.Net Core,比如你要在OS X上部署,你的APP里需要内置OS X对应的.Net Core,如果你内置的.Net Core是Linux版本的,那你就不能在OS X上边部署,这就要求你必须先考虑好,你的应用程序面向的平台。

.Net Core组件(Components)

和传统的.Net Framework非常像,.Net Core由一个叫CoreCLR的公共语言运行时(Common language runtime)组成。和.Net Framework一样,.Net Core中关键的也是类库。.Net Core关键的是CoreFX,这是一个模块化的类库集,而非单一的.Net framework类库。这样就可以你的程序需要什么库就加载什么库,不需要的不会加载。

公共语言运行时(The Common Language Runtime)

.Net Core中的公共语言运行时——CoreCLR是一个轻量级的运行时,提供了好多和传统.Net Framework的运行时相同的服务。这些相同的服务包含:

  • 一个垃圾回收器,它提供了内存自动管理。垃圾回收器按需分配和释放内存;你不必通过程序去做这些。不像C++,需要自己去管理操作内存的分配和释放。.Net Core也用了和.Net Framework相同的垃圾回收器,更多信息请访问Garbage Collection
  • 一个just-in-time(JIT)编译器,编译IL或者.Net中间语言(intermediate language)到机器码。在某些架构中,JIT编译器支持SIMD硬件加速。
  • 一个异常处理机制,允许你通过try/catch语句处理异常。

类库(The Class Library)

.Net Core的类库与.Net Framework的类库除了有一处主要的不同点之外,其余的非常相似。
不同在于:
传统的.Net Framework有很多类库是属于操作系统的一部分,并且它是通过Windows 自带的Windows update更新。
在.Net Coe中,它是按照功能组织的模块的个人库。

Microsoft.NetCore.App 被包含在runtime里边了,它包含了开发所有APP基本的类型,这些类型包括:

  • 基础类型,比如bool类型,签名与不签名的整型,浮点型和char结构
  • String类型:在.Net Core中,一个字符串是UTF-16编码单元的序列。.Net Core还包括了许多编码类型,这些编码类型可以允许你将UTF-16编码字符串转换成其他编码的字节数组,例如:你可以用UTF8Encoding class将.Net Core的string字符串转换成utf-8编码的字节数组,用来表示Linux上的string字符串。
  • 任务类型,例如TaskTask,用来支持异步编程。
  • 基本的线程类型
  • Console Class,用来支持开发console apps.

另外的一些库,需要通过Nuget包来安装

.Net Core工具(.Net Core Tools)

.Net Core包含了一个跨平台的命令行SDK,名字叫做.Net Core CLI(Command-Line Interface).这个CLI是编写.Net Core应用程序的一组对Unix友好的工具。它让C#编译器和Nuget包管理工具变得抽象,变得抽象的意思是你感觉不到编译器和包管理工具的存在,应用程序就编写好了。他同样可以与.Net原生工具紧紧集成在一起来产生高性能的原生app和库。
CLI带来的好处是开发者可以不用安装大型的IDE就可以编译和测试他们的代码,这在不是自己的电脑或者生产服务器上是极好的。visual studio code与visual studio在底层都是用的CLI,你可以根据你的需要选择不同的IDE.比如你可以直接通过文本器来使用CLI,或者你可以用IDE开发,编辑器内部调用CLI.

大多数情况下,你直接使用.Net Core CLI就是通过给dotnet.exe 提供参数,下边是dotnet.exe可以使用的命令:

  • dotnet --help:显示关于.Net Core CLI命令行的信息
  • dotnet new:初始化一个C#项目
  • dotnet new --lang F#:初始化一个F#项目
  • dotnet restore:为你的app还原所有的依赖
  • dotnet build:编写一个.Net Core app
  • dotnet publish:发布一个portable或者self-contained app。(查看【部署简单】章节)
  • dotnet run:从源代码中运行app
  • dotnet pack:在你的app中创建一个Nuget包

    dotnet.exe有扩展模型,允许你添加额外的命令。

语言支持和开发环境(Language Support and Development Environments)

.Net Core是语言无关的:任何以.Net Core为目标的语言都是可以用来开发.Net Core应用程序的,通过不同的编程语言开发的app,用其中的一种语言即可无缝地访问类型与成员。

当前,你可以用下边的两种语言的任意一种开发:

我们打算在未来支持更多的语言。
你有多种开发环境可以选择用来编写app,包括:

.NET Core and the .NET Framework

为了更好的感知.Net Core是什么,将它与.Net framework相比较:

.Net Core

包含CoreCLR,一个可提供基础服务的轻量级运行时(runtime),尤其自动内存管理,垃圾回收器,还有一个基础的类型库。

包含了CoreFx,一套个人模块化组装,你可以安装需要将其添加到你的app中,与.Net Framework 4.x不同,.Net Framework 4.x总是使整个.Net Framework类库可用,.Net Core只需选择你想要的。例如,如果你正在开发一款基于矢量的应用,你可以下载System.Numerics.Vectors包,而不是需要一个很大类库的花销,这样可以显著的减少你app的体积和他的依赖项。

适用于各种各样的现代应用程序,对内存和储存有限制的小型设备起作用

可以利用若干的技术来开发应用,比如asp.net core
开发web应用,Windows communication Foundation(WCF)

开发与现有的WCF服务相关联的应用,workflow foundation(WF)构建工作流。

可以变成app本地。换句话说就是.Net Core版本可以紧紧与你的app相结合,这可以减轻好多版本问题。

.NET Framework 4.x

包含公共语言运行时(CLR),一个相当大的运行时,可以提供内存管理,隔离应用程序域(application domain),大量的应用程序服务。

包含了.Net Framework类库,这个类库包含了成千上万个的类与成员,不仅非常大而且又是一个整体,不论你的app用了单个类型或者他们的成员(或者大多数app利用了一小部分函数),他们都是始终加载并且可以随时访问的。

适用于传统的Windows桌面应用程序,包括Windows forms(winforms)和Windows Presentation Foundation(WPF)应用程序,可以运用许多技术来开发应用程序,例如,ASP.NET和ASP.NET Web Form构建web应用程序,Windows Communication Foundation (WCF)构建包含soap的服务,Workflow Foundation (WF), 构建工作流。

在一个给定系统中全局可用。换句话说,即使一个app中包含了一个特定版本的.Net Framework安装器,但是假如安装器发现它不存在还是会安装完整的.Net framework,并且会独立于app维护。这就会产生版本问题,特别地是一个app遭遇了一个没有预想的版本,或者一个app跑在了之前没有开发的.Net Framework版本上。
从Windows8开始,.Net Framework作为操作系统的一个组件安装,并且通过Windows update升级。对于不同的Windows里边内置不同的.Net Framework版本,更多的信息请访问.NET Framework System Requirements.

总结:

虽然.Net Framework 4.6.2预览版和.Net Core是面向不同的平台,代表着不同的app开发和部署方法,但是他们都遵守 .Net标准1.5。这意外着他们彼此能够提供一个高度的兼容性与统一行为。尤其:

  • 有经验的.Net 开发者想要开发不同设备和平台的应用程序的时候,可以很容易适应.Net Core的开发。
  • .Net Core开发者可以很容易的过渡到用.Net Framework开发Windows桌面程序、平板和手机的方式上。
  • 用.Net Framework或者.Net Core写的类库可以很容易的在另外一个平台工作。

.Net Core具体实现(implementations)

许多的开发技术依赖.Net Core的可定制实现。当你用这些技术开发Apps的时候,你也许不会意识你是在利用.Net Core的好处:

  • ASP.Net Core.ASP.Net Core是一个模块化的asp.Net版本,它结合了ASP.NET MVC and ASP.NET Web API.它可以同时运行在.Net Framework与.Net Core上边,它被设计用来构建高性能的云和微型服务;在.Net Framework中,它不是打算作为asp.net的替代者的。有关更多的ASP.Net Core信息,请访问Introduction to ASP.NET Core.
  • .NET Native。对于用C#和Visual Basic编写的Universal Windows Platform (UWP)应用程序,.NET Native是一个编译和部署技术。.Net Native将Apps编译成原生代码,静态资源文件放入应用程序集中,这些都是.Net Core和另外一些第三方的正在使用的代码。有关更多.Net Native的信息请访问Compiling Apps with .NET Native.
  • Universal Windows Platform (UWP) apps。Universal Windows Platform允许你构建一个运行在Windows桌面,Windows平板设备,Windows phone手机上的app。这些应用可以上传到Windows store中。UWP 应用程序通过.Net Native为他们各自的平台编译原生代码,有关更多的信息,请访问 Get started with Windows apps

原文:https://dotnet.github.io/docs/getting-started/what-is-dotnet.html

Signalr实现消息推送

一、前言

大多数系统里面好像都有获取消息的功能,但这些消息来源都不是实时的,比如你开两个浏览器,用两个不同的账号登录,用一个账号给另外一个账号发送消息,然而并不会实时收到消息,必须要自己手动F5刷新一下页面才会显示自己的消息,这样感觉用户体验不太好。之前看了Learning hard关于Signalr的文章,刚好自己项目中有用到获取实时消息的功能,然而我们项目中就是用js代码setinterval方法进行1秒刷新读取数据的,这样严重给服务器端添加负担,影响系统性能!所以自己稍微研究了一下,下面是自己的一些理解,如果有不对的地方,请大家加以斧正!

二、实现原理

下面谈一下自己对Signalr的理解,Signalr可以进行远程分布式实时通信,都是使用远程代理来实现,其中有两大内部对象,第一个是Persisten Connection,用于客户端和服务器端的持久连接,第二个是Hub(集线器)对象,主要用于信息交互,将服务器端的数据推送(push)至客户端,大致原理如下:

1、客户端建立与服务器端的连接

2、客户端调用服务器端的方法

3、服务器端通过客户端发送的请求,响应数据,再将数据推送至客户端

三、Signalr实现消息推送

具体操作实现如下:

1、创建一个应用程序,我这里创建的是MVC应用程序

2、引用相关组件,右键引用》选择管理Nuget程序包

3、搜索Signalr,如图所示:

点击安装,在应用程序的Scripts文件夹里面会自动生成两个js文件,如图所示:

4、添加集成器类

5、注册signalr/hubs,在Startup.cs里面添加如下代码

6、新建控制器MessageController,然后在控制器里面新建两个视图方法SendMessage和ReceiveMessage,为了让效果看起来更直观,一个页面用于发送消息,一个页面用于接收消息,如图所示:

7、在我们刚刚新建的集成器类MyHub类里面添加代码:

(特别说明一下,这里的InsertMsg方法主要是将客户端发送的消息信息保存到数据库里面,便于消息读取,为了快速创建数据库表,我采用的code first方法来创建的,至于你想用什么方式创建表,那都是可以的。)

 

复制代码
namespace Signalr.Models 
{
    [HubName("MyHub")]
    public class MyHub : Hub
    {
        MessageDbContext _db = new MessageDbContext();
        public void Send(string title, string message)
        {
            this.InsertMsg(title, message);
            // 调用所有客户端的sendMessage方法
            Clients.All.sendMessage(message);
        }

        private void InsertMsg(string title, string message)
        {
            Message msg = new Message();
            msg.Title = title;
            msg.MsgContent = message;
            _db.Messages.Add(msg);
            _db.SaveChanges();
        }
    }
}
复制代码

表结构如图所示:

8、控制器MessageController后台代码

复制代码
public class MessageController : Controller
    {
      private MessageDbContext _db = new MessageDbContext();
        public ActionResult SendMessage()
        {
            return View();
        }

        public ActionResult ReceiveMessage()
        {
            return View();
        }

        [HttpPost]
        public JsonResult MsgCount()  
        {
            var count = this._db.Messages.Where(p=>p.IsRead==0).Count();
          return Json(new {count=count},JsonRequestBehavior.AllowGet);
        }
    }
复制代码

 

9、前端页面代码(SendMessage.cshtml)

复制代码
@{
    ViewBag.Title = "发送消息";
}
<title>发送消息</title>
<script src="~/Scripts/jquery-1.10.2.js"></script>
<script src="~/Scripts/jquery.signalR-2.2.0.min.js"></script>
<script src="~/signalr/hubs"></script>
<script type="text/javascript">
    $(function () {
        // 引用自动生成的集线器代理
        var chat = $.connection.MyHub;
        // 定义服务器端调用的客户端sendMessage来显示新消息
        chat.client.sendMessage = function (title, message) {
            // 向页面发送接收的消息
            sendMsg();
        };
        // 集成器连接开始
        $.connection.hub.start().done(function () {
            sendMsg();
            // 服务连接完成,给发送按钮注册单击事件
            $('#sendmessage').click(function () {
                // 调用服务器端集线器的Send方法
                chat.server.send($("#title").val(), $('#message').val());
            });
        });
    });

    function sendMsg() {
        var options = {
            url: '/Message/MsgCount',
            type: 'post',
            success: function (data) {
                $("#count").html(data.count);
            }
        };
        $.ajax(options);
    }
</script>


<h2>
    发送消息
</h2>
<div>
    <label>我的消息:</label>
    <span style=" color:red; font-size:30px;" id="count"></span>条
</div>
<p>
    <div>
        标题:
        <input type="text" id="title" />
    </div>
    <br /><br />
    <div>
        内容:
        <textarea id="message" rows="4" cols="30"></textarea>
    </div>
    <br /><br />
    <div>
        <input type="button" id="sendmessage" value="发送" />
    </div>
</p>
复制代码

10、前端页面代码(ReceiveMessage.cshtml)

复制代码
@{
    ViewBag.Title = "接受消息";
}
<title>接受消息</title>
<script src="~/Scripts/jquery-1.10.2.js"></script>
<script src="~/Scripts/jquery.signalR-2.2.0.min.js"></script>
<script src="~/signalr/hubs"></script>
<script type="text/javascript">
    $(function () {
        // 引用自动生成的集线器代理
        var chat = $.connection.MyHub;
        // 定义服务器端调用的客户端sendMessage来显示新消息
        chat.client.sendMessage = function (title, message) {
            // 向页面发送接收的消息
            MsgCount();
            var html = "<div>标题:" + title + "消息内容:" + message + "</div>";
            $("#msgcontent").after(html);
        };
        // 集成器连接开始
        $.connection.hub.start().done(function () {
            MsgCount();
        });
    });
    function MsgCount() {
        var options = {
            url: '/Message/MsgCount',
            type: 'post',
            async:false,
            data: { title: $("#title").val(), msgcontent: $("#sendmessage").val() },
            success: function (data) {
                $("#count").html(data.count);
            }
        };
        $.ajax(options);
    }
</script>


<h2>
    接收消息
</h2>

<div>
    <label>我的消息:</label>
    <span style=" color: red; font-size: 30px;  margin-right:10px;" id="count"></span>条
    <br />
    <br />
    <div id="msgcontent"></div>
</div>
复制代码

 

好了,大功告成,可能你有点疑问的是这个js文件引用地方在哪里

不防我们运行页面,按F12查看一下,它会自动在这里生成一个js文件,我们只要在页面中引用这个路径即可

四、页面效果(见证奇迹的时刻到了,哈哈哈~~~)

为了让页面效果更为直观,我这里用IE打开SendMessage.cshtml页面,用Google打开ReceiveMessage.cshtml页面。

 

权责申明

作者:SportSky 出处: http://www.cnblogs.com/sportsky/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】,希望能够持续的为大家带来好的技术文章!想跟我一起进步么?那就【关注】我吧