就像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 S10.47.58.139:9527||+----------------------|----------------------+|                                             |NAT A                                         NAT B
122.27.219.161:10001                            123.29.210.131:10002|                                             ||                                             |Client A                                      Client B192.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端口的映射方式,又可以分为四种形式:

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

package mainimport ("fmt""log""net""time"
)func main() {//ListenUDP创建一个接收目的地是本地地址laddr的UDP数据包的网络连接。net必须是"udp"、"udp4"、"udp6";如果laddr端口为0,函数将选择一个当前可用的端口,可以用Listener的Addr方法获得该端口。返回的*UDPConn的ReadFrom和WriteTo方法可以用来发送和接收UDP数据包(每个包都可获得来源地址或设置目标地址)。//IPv4zero:本地地址,只能作为源地址(曾用作广播地址)listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 9527})if err != nil {fmt.Println(err)}//LocalAddr返回本地网络地址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.Println("err 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())//WriteToUDP通过c向地址addr发送一个数据包,b为包的有效负载,返回写入的字节。//WriteToUDP方***在超过一个固定的时间点之后超时,并返回一个错误。在面向数据包的连接上,写入超时是十分罕见的。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 mainimport ("fmt""log""net""os""strconv""strings""time"
)var tag stringconst HAND_SHAKE_MSG = "我是打洞消息"func main() {if len(os.Args) < 2 {//Args保管了命令行参数,第一个是程序名。fmt.Println("请输入一个客户端标志")os.Exit(0)   //Exit让当前程序以给出的状态码code退出。一般来说,状态码0表示成功,非0表示出错。程序会立刻终止,defer的函数不会被执行}//当前进程标记字符串,便于显示tag = os.Args[1]//UDPAddr代表一个UDP终端地址//IPv4zero本地地址,只能作为源地址(曾用作广播地址)srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 9901} //注意端口必须固定//ParseIP将s解析为IP地址,并返回该地址。如果s不是合法的IP地址文本表示,ParseIP会返回nil。//字符串可以是小数点分隔的IPv4格式(如"74.125.19.99")或IPv6格式(如"2001:4860:0:2001::68")格式。dstAddr := &net.UDPAddr{IP: net.ParseIP("192.168.3.251"), Port: 9527}//DialTCP在网络协议net上连接本地地址laddr和远端地址raddr。net必须是"udp"、"udp4"、"udp6";如果laddr不是nil,将使用它作为本地地址,否则自动选择一个本地地址。//(conn)UDPConn代表一个UDP网络连接,实现了Conn和PacketConn接口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)//ReadFromUDP从c读取一个UDP数据包,将有效负载拷贝到b,返回拷贝字节数和数据包来源地址。//ReadFromUDP方***在超过一个固定的时间点之后超时,并返回一个错误。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)//开始打洞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("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])}}
}

客户端显示

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]

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

golang打造p2p网络相关推荐

  1. Derek解读Bytom源码-P2P网络 地址簿

    作者:Derek 简介 Github地址:github.com/Bytom/bytom Gitee地址:gitee.com/BytomBlockc- 本章介绍bytom代码P2P网络中addrbook ...

  2. 【区块链技术工坊27期实录】李庆华:HPB底层P2P网络实践

    1,活动基本信息 1)题目: [区块链技术工坊27期]HPB底层P2P网络实践 2)议题: HPB芯链是融入硬件加速引擎的全新体系架构,打造基于BOE硬件加速芯片驱动的高性能公链.作为开放式公链,任何 ...

  3. P2P网络的历史、现在和未来

    互联网能够发展至今,根本原因就是其每一种技术思想的出现都是为了人与人之间的交流而产生的.而现在能够引起互联网震动的,无非只有交流方式的改变.互联网技术充斥在我们周围之时,恐怕只有少数人知道P2P的概念 ...

  4. 发布一个嘿嘿嘿的技术方案 —— 商用群发p2p网络

    目前反群发的主要技术措施有: (1)       帐号控制:有帐号才能发,同时限制帐号的发送频率 (2)       IP控制:限制指定IP的发送频率 (3)       协议控制:采用非开放协议 ( ...

  5. P2P网络“自由”穿越NAT的“秘密”

    P2P网络"自由"穿越NAT的"秘密"<?xml:namespace prefix = o ns = "urn:schemas-microsof ...

  6. P2P网络中DHT算法分析

    结构化与非结构化P2P 依照节点信息存储与搜索方式的不同,诸多P2P协议可以分为2大类:结构化(Structured)的与非结构化(Unstructured)的系统. 非结构化P2P系统 在非结构化的 ...

  7. 区块链BaaS云服务(28)TOP Network 之P2P 网络

    1. 总结 P2P网络"不是一个单片网络,而是许多以分层方式组成的P2P网络,类似于互联网的设计. 开发了优化的数据传输和Gossip协议,使带宽消耗最小化,提高节点发现的效率. 将地理信息 ...

  8. 百度超级链XChain(2)p2p网络

    1. 定义 非结构化p2p网络 结构化p2p网络:结构化p2p最普遍的实现方案是使用分布式哈希表(DHT),eg. 以太坊网络. 1.1 NAT技术 通过将局域网内的主机地址映射为互联网上的有效ip地 ...

  9. IPFS网络是如何运行的(p2p网络)

    图片来自wiki IPFS是一个p2p网络,先来看看BitTorrent的p2p网络是如何工作的? 想要bt下载一个文件,首先你需要一个种子文件torrent,种子文件包含至少一个 Tracker(一 ...

最新文章

  1. html设置根rem,经过js动态设置根元素的rem方案
  2. linux高编线程-------线程的创建,终止
  3. SuperMap iClient for Leaflet入门学习
  4. EOS开发步骤(2) 钱包操作
  5. java 热补丁_Android热补丁之AndFix原理解析
  6. 接收请求参数及数据回显 2021-04-26
  7. MySQL使用什么关键字添加唯一约束_mysql怎么添加唯一约束?
  8. Windows系统管理和网络服务笔记生涯 源于BENET2.0课程(S2)
  9. [nRF51822] 1、一个简单的nRF51822驱动的天马4线SPI-1.77寸LCD彩屏DEMO
  10. Multi-level learning based memetic algorithm for community detection笔记
  11. android关于16进制转字符串的问题
  12. webservice框架jersey简单总结
  13. 点击微信公众号菜单发送图片或文本
  14. 如果已经安装过个人版Delphi2007,如何安装Delphi2007企业版
  15. OpenJudge - 红与黑(DFS)
  16. day25/RegexDemo1.java
  17. Jetpack:Lifecycle 和 LiveData
  18. 计算机算法设计与分析第五章思维导图知识点总结 ( 初稿 )
  19. 多线程MT和多线程MD的区别
  20. java-php-python-ssm如家酒店管理系统计算机毕业设计

热门文章

  1. ThreadX(三)------线程thread
  2. 幽默搞笑:我赶紧把手抽开,这死胖子暗恋我十年,死心不改啊
  3. FPGA到底是什么?
  4. python 邮件_Python发送邮件(常见四种邮件内容)
  5. 选择IDC机房的心得
  6. 魔坊APP项目-19-种植园,我的背包、道具购买
  7. 北漂家乡买房记:6年至少亏了50% 还无法脱手!
  8. ORACLE PL/SQL编程之六: 把过程与函数说透
  9. 案例理解LEFT JOIN、RIGHT JOIN、INNER JOIN、OUTER JOIN 相关的 7 种用法
  10. 尚学堂Java第五章所有题目