传送门: 柏链项目学院


就像1000个人眼中有1000个哈姆雷特一样,每个人眼中的区块链也是不一样的!作为技术人员眼中的区块链就是将各种技术的融合,包括密码学,p2p网络,分布式共识机制以及博弈论等。我们今天就来讨论一下区块链技术中的p2p网络,这是一种点到点的通信技术。

说到p2p通信,它并没有名字看上去那样简单,在网络世界里实现p2p还是需要一些手段的!很多朋友可能会说,实现一个c/s模式的点到点通信很简单呀,但是前提是彼此可以看见,比如服务器在公网,或者服务器和客户端都在同一个局域网内,我们要探讨的p2p通信是指通信的双方分别在两个局域网内部!

由于在两个局域网内部,两台设备并没有公网IP,彼此要通信需要借助路由器,但是路由器又会对不识别的ip进行过滤,也就是路由器有个陌生人排除机制!怎么办呢?类似于我们去一个安保较为严格的场所时,需要内部的工作人员接引才可入内,在网络编程中也是这样的原理!但和现实中不同的是,假设设备A想和另一个局域网的设备B通信,设备B是并不认识设备A的,设备A通过路由器NAT(Network Address Translation,网络地址转换)技术获得了一个公网映射IP,但是设备B并不认识,那么怎么样能让两者通信呢?所以这个时候需要一个介绍人,此时需要有一个公网的服务器作为媒介,介绍两个人介绍,当B设备对应路由器添加了A设备对应的公网IP后,A设备就可以与B设备建立连接了,这个时候就可以顺畅的通信了!

Server S
10.47.58.139:9527
|
|
+----------------------|----------------------+
| |
NAT A NAT B
122.27.219.161:10001 123.29.210.131:10002
| |
| |
Client A Client B
192.168.1.126:9901 192.168.1.102:9902

如上图所示,CLientA与ClientB想要通信,因为两个客户端都在各自的局域网内,都是通过NAT技术生成公网映射IP的,想要彼此访问必须通过一个中间服务器进行中介介绍,这样两个客户端才能彼此认识并建立连接,否则双方直接通信都会被路由器丢弃。那么为什么非要用NAT呢?直接为每个Client分配一个公网IP不可以吗?这是由于IPv4的限制,公网IP数量是有限的,我们国家拿到的公网IP段更是有限,甚至不及美国一所大学的IP段数量多。这样也就不可能为每个机器都分配一个公网IP,正因为此NAT技术才非常重要,它可以很好的帮我们解决公网IP不足的问题。

接下来我们还是介绍一下NAT的原理和类型:

NAT主要可以分为两类:

  • 基本NAT,这种要求NAT有多个公网IP,这样可以将公网IP和内网设备静态绑定
  • NAPT(Network Address Port Translation),更为常见的NAT,内网设备的网络请求通过不同端口加以映射

针对NAPT端口的映射方式,又可以分为四种形式:

  • 完全圆锥型NAT( Full Cone NAT ),将从一个内部IP地址和端口来的所有请求,都映射到相同的外部IP地址和端口。并且,任何外部主机通过向映射的外部地址发送报文,都可以实现和内部主机进行通信。
  • 地址限制圆锥型NAT( Address Restricted Cone NAT ),将从相同的内部IP地址和端口来的所有请求映射到相同的公网IP地址和端口。但是与完全圆锥型NAT不同,当且仅当内部主机之前已经向公网主机发送过报文,此时公网主机才能向内网主机发送报文。
  • 端口限制圆锥型NAT( Port Restricted Cone NAT ),端口受限圆锥型NAT增加了端口号的限制,当前仅当内网主机之前已经向公网主机发送了报文,公网主机才能和此内网主机通信。
  • 对称型NAT( Symmetric NAT),把从同一内网地址和端口到相同目的地址和端口的所有请求,都映射到同一个公网地址和端口。如果同一个内网主机,用相同的内网地址和端口向另外一个目的地址发送报文,则会用不同的映射。

本人经过检测发现,本机的NAT类型为上述第四种:Symmetric NAT

知识点普及后,我们继续来实践我们之前所说的p2p技术,也就是两个设备之间的通信问题,由于两个设备分别在各自的网络内部,我们也称这种行为为打洞!

接下来我们用go语言来实现这个打洞技术,主要使用UDP来实现,具体流程如下:

  1. 建立UDP服务器 S
  2. 建立A、B客户端,分别与S建立会话,SA与SB
  3. S当好中介人,将A的ip+端口通过SB告诉B,将B的ip+端口通过SA告诉A
  4. A向B公网地址发送一个UDP包,代表握手,打通A-B的路径
  5. B向A公网地址发送一个UDP包,A-B的会话建立成功

代码如下:

//server.go
"log"
"net"
"time"
) func main() {
listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 9527})
if err != nil {
fmt.Println(err)
return
}
log.Printf("本地地址: <%s> \n", listener.LocalAddr().String())
peers := make([]net.UDPAddr, 0, 2)
data := make([]byte, 1024)
for {
n, remoteAddr, err := listener.ReadFromUDP(data)
if err != nil {
fmt.Printf("error during read: %s", err)
}
log.Printf("<%s> %s\n", remoteAddr.String(), data[:n])
peers = append(peers, *remoteAddr)
if len(peers) == 2 {
log.Printf("进行UDP打洞,建立 %s <--> %s 的连接\n", peers[0].String(), peers[1].String())
listener.WriteToUDP([]byte(peers[1].String()), &peers[0])
listener.WriteToUDP([]byte(peers[0].String()), &peers[1])
time.Sleep(time.Second * 8)
log.Println("中转服务器退出,仍不影响peers间通信")
return
}
}
}

服务端显示如下:

ykdeMac-mini:study yekai$ ./server
2019/04/03 14:50:13 本地地址: <[::]:9527>
2019/04/03 14:51:48 <192.168.1.102:9901> hello, I'm new peer:yekai1
2019/04/03 14:52:57 <192.168.1.126:9902> hello, I'm new peer:yekai2
2019/04/03 14:52:57 进行UDP打洞,建立 192.168.1.102:9901 <--> 192.168.1.126:9902 的连接
2019/04/03 14:53:05 中转服务器退出,仍不影响peers间通信
//client.go
package main import (
"fmt"
"log"
"net"
"os"
"strconv"
"strings"
"time"
) var tag string const HAND_SHAKE_MSG = "我是打洞消息" func main() {
if len(os.Args) < 2 {
fmt.Println("请输入一个客户端标志")
os.Exit(0)
}
// 当前进程标记字符串,便于显示
tag = os.Args[1]
srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 9901} // 注意端口必须固定
dstAddr := &net.UDPAddr{IP: net.ParseIP("192.168.1.102"), Port: 9527}
conn, err := net.DialUDP("udp", srcAddr, dstAddr)
if err != nil {
fmt.Println(err)
}
if _, err = conn.Write([]byte("hello, I'm new peer:" + tag)); err != nil {
log.Panic(err)
}
data := make([]byte, 1024)
n, remoteAddr, err := conn.ReadFromUDP(data)
if err != nil {
fmt.Printf("error during read: %s", err)
}
conn.Close()
anotherPeer := parseAddr(string(data[:n]))
fmt.Printf("local:%s server:%s another:%s\n", srcAddr, remoteAddr, anotherPeer.String())
// 开始打洞
bidirectionHole(srcAddr, &anotherPeer)
}
func parseAddr(addr string) net.UDPAddr {
t := strings.Split(addr, ":")
port, _ := strconv.Atoi(t[1])
return net.UDPAddr{
IP: net.ParseIP(t[0]),
Port: port,
}
}
func bidirectionHole(srcAddr *net.UDPAddr, anotherAddr *net.UDPAddr) {
conn, err := net.DialUDP("udp", srcAddr, anotherAddr)
if err != nil {
fmt.Println(err)
}
defer conn.Close()
// 向另一个peer发送一条udp消息(对方peer的nat设备会丢弃该消息,非法来源),用意是在自身的nat设备打开一条可进入的通道,这样对方peer就可以发过来udp消息
if _, err = conn.Write([]byte(HAND_SHAKE_MSG)); err != nil {
log.Println("send handshake:", err)
}
go func() {
for {
time.Sleep(10 * time.Second)
if _, err = conn.Write([]byte("from [" + tag + "]")); err != nil {
log.Println("send msg fail", err)
}
}
}()
for {
data := make([]byte, 1024)
n, _, err := conn.ReadFromUDP(data)
if err != nil {
log.Printf("error during read: %s\n", err)
} else {
log.Printf("收到数据:%s\n", data[:n])
}
}
}

客户端1显示如下:

ykdeMac-mini:study yekai$ ./client yekai1
local:0.0.0.0:9901 server:192.168.1.102:9527 another:192.168.1.126:9902
2019/04/03 14:52:57 收到数据:我是打洞消息
2019/04/03 14:52:57 error during read: read udp 192.168.1.102:9901->192.168.1.126:9902: recvfrom: connection refused
2019/04/03 14:53:07 收到数据:from [yekai2]
2019/04/03 14:53:17 收到数据:from [yekai2]

客户端2显示如下:

localhost:zhuhai yk$ ./client yekai2
local:0.0.0.0:9902 server:192.168.1.102:9527 another:192.168.1.102:9901
2019/04/03 14:53:07 收到数据:from [yekai1]
2019/04/03 14:53:17 收到数据:from [yekai1]

备注:本文中的公网服务器使用的是192.168.1.102进行替代,测试时并没有实际走NAT映射,不过其他童鞋可以用代码在公网服务器进行验证!


go语言打造p2p网络的更多相关文章

  1. 使用Go语言编写区块链P2P网络(译)(转)

    转自:https://mp.weixin.qq.com/s/2daFH9Ej5fVlWmpsN5HZzw 外文链接: https://medium.com/coinmonks/code-a-simpl ...

  2. go语言打造个人博客系统(一)

    go语言打造个人博客系统(一) 为什么选择go语言?   听说go语言是在几年前,但真正深入了解他却是在2017年,因为当时作为讲师 ,需要准备go语言的课程,结果稍一接触立刻就喜欢上这门语言了,作为 ...

  3. 详解区块链P2P网络

    根据前一篇文章<从微观到宏观理解区块链>我们已经了解到,微观上,区块链本质就是一种不可篡改且可追踪溯源的哈希链条:宏观上,还具备了另外三个基本特征:分布式存储.P2P 网络和共识机制.分布 ...

  4. Derek解读Bytom源码-P2P网络 upnp端口映射

    作者:Derek 简介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom ...

  5. iOS—网络实用技术OC篇&网络爬虫-使用java语言抓取网络数据

    网络爬虫-使用java语言抓取网络数据 前提:熟悉java语法(能看懂就行) 准备阶段:从网页中获取html代码 实战阶段:将对应的html代码使用java语言解析出来,最后保存到plist文件 上一 ...

  6. 2015年p2p网络借贷平台的发展现状

    2015年春暖花开,莺飞草长,股市大涨大跌起起落落,P2P网络借贷收到越来越多的人关注,P2P网络借贷平台是p2p借贷与网络借贷相结合的金 融服务网站,这么多P2P网络借贷平台排我们应该如何选择呢?小 ...

  7. iOS开发——网络实用技术OC篇&网络爬虫-使用java语言抓取网络数据

    网络爬虫-使用java语言抓取网络数据 前提:熟悉java语法(能看懂就行) 准备阶段:从网页中获取html代码 实战阶段:将对应的html代码使用java语言解析出来,最后保存到plist文件 上一 ...

  8. 利用R语言打造量化分析平台

    利用R语言打造量化分析平台 具体利用quantmod包实现对股票的量化分析 1.#1.API读取在线行情2.#加载quantmod包3.if(!require(quantmod)){4. instal ...

  9. R语言︱贝叶斯网络语言实现及与朴素贝叶斯区别(笔记)

    每每以为攀得众山小,可.每每又切实来到起点,大牛们,缓缓脚步来俺笔记葩分享一下吧,please~ --------------------------- 一.贝叶斯网络与朴素贝叶斯的区别 朴素贝叶斯的 ...

随机推荐

  1. html&css学习二

    表格&b标签 表格 表格标签主要包含三个标签 <table> <tr> <td>单元格内的文字</td> ... </tr> ... ...

  2. Activity简介

    声明周期 4种状态 running / paused / stopped / killed 声明周期 activity启动  --->onCreate() -->onStart()---& ...

  3. 章节十、7-Xpath---Xpath中绝对路径相对路径的区别

    以下演示操作以该网址中的内容为例:https://learn.letskodeit.com/?_ga=2.143454972.85111248.1555037144-697706367.1554889 ...

  4. Git 中 .gitignore 的配置语法

    一.前言 在日常的开发中,当我们需要将一个项目提交到 Git 时,并不是所有的文件都需要提交,比如一些自动生成的文件,类似于 .idea 文件.class 文件等,这时候就可以使用.gitignore ...

  5. C 实现自己构建的数组

    #include<stdio.h>#include<malloc.h>#include<stdlib.h>#include<stdbool.h>stru ...

  6. Unity的Mesh压缩:为什么我的内存没有变化?

    0x00 前言 最近和朋友聊天,谈到了Mesh的内存优化问题,他发现开启Model Importer面板上的Mesh Compression选项之后,内存并没有什么变化.事实上,期望开启Mesh Co ...

  7. SpringBoot 项目在静态工具类中注入 RedisTemplate

    静态属性不能直接注入,可以通过其set方法进行注入.(注意生成的set方法需要去掉static). 在工具类里直接注入RedisTemplate,两种方法: (1)使用@Autowired priva ...

  8. Spring Cloud Eureka 常用配置详解,建议收藏!

    前几天,栈长分享了 <Spring Cloud Eureka 注册中心集群搭建,Greenwich 最新版!>,今天来分享下 Spring Cloud Eureka 常用的一些参数配置及说 ...

  9. 【Android Studio安装部署系列】十三、Android studio添加和删除Module

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 概述 新建.导入.删除Module是常见的操作,这里简单介绍下. 新建Module File——New——New Module... 选中 ...

  10. python学习第九讲,python中的数据类型,字符串的使用与介绍

    目录 python学习第九讲,python中的数据类型,字符串的使用与介绍 一丶字符串 1.字符串的定义 2.字符串的常见操作 3.字符串操作 len count index操作 4.判断空白字符,判 ...