百行go代碼構建p2p聊天室
只需百行代碼,就可以構建一個完整的p2p聊天室,並且消息加密,無法被追蹤;並且不需要服務器,永不停機,是不是很酷.
系統實際上基於以太坊的whisper,它本來是為以太坊上的DAPPS通信構建的,這里直接拿來做聊天室一點問題都沒有.
1. 上手使用
先說用法,來感受一下完全匿名的P2p聊天系統.
在終端運行p2pmessage.exe,然后等待出現.
Connected to peer,you can type message now.
,這時候你就已經連接到whisper的p2p網絡中了. 有可能你需要幾分鍾才能成功.
你可以收到來自別人的消息,也可以發送消息給別人.
你可以同時在不同的機器上啟動程序來感受一下結果.
2017-09-11 11:31:18 <mine>: hello
input ~Q to quit>
2017-09-11 11:33:10 [182cbbaac94313b3b96b25d9c9a0a1adea4519e9]: who am i
第一行是收到了我發送的消息,第二行是提示輸入,第三行是收到了182cbbaac94313b3b96b25d9c9a0a1adea4519e9發來的消息.
其中182cbbaac94313b3b96b25d9c9a0a1adea4519e9是結點標識.
2. whisper 原理
接入whisper網絡中的節點,在收到任何消息會首先驗證一下工作量(可以參考bitmessage),如果沒問題然后就轉發.
同時也會看看是不是發送給我的,如果是就告訴用戶.
至於怎么知道是不是發送給我的,有多種方式,這里只使用主題以及密碼匹配.
也就是說,必須是我感興趣的主題,同時加密的密碼也是我指定的.那就可以愉快的聊天了.
當然,我沒辦法知道對方是誰,除非他告訴我.
3. 源碼解讀
可以到github下載完整的源碼.
3.1 參數說明
總共有三個參數
-verbosity 用來打印調試信息
-topic 聊天室的主題(任意四個字節), 你必須事先知道主題才能加入,如果隨便寫一個,那就是你自己創建一個聊天室了.
-password 聊天室的密碼, 主題密碼都一致,才能進入同一個聊天室.
3.1 連接主節點
雖然說p2p網絡沒有服務器,但是必須存在知名節點,否則無從啟動網絡.
首先就是連接以太坊的主節點.
for _, node := range ethparams.MainnetBootnodes {
peer := discover.MustParseNode(node)
peers = append(peers, peer)
}
peer := discover.MustParseNode("enode://b89172e36cb79202dd0c0822d4238b7a7ddbefe8aa97489049c9afe68f71b10c5c9ce588ef9b5df58939f982c718c59243cc5add6cebf3321b88d752eac02626@182.254.155.208:33333")
peers = append(peers, peer)
后面這個節點是我搭建的.方便國內的用戶快速通信,因為基於主節點的通信可能會比較慢,延時比較長.
3.2 我的標識
每個節點都有自己的私鑰,標識就是自己的公鑰.
當然可以每次都使用相同的私鑰,這里簡單起見,每次都是自動生成了.
asymKeyID, err = shh.NewKeyPair()
if err != nil {
utils.Fatalf("Failed to generate a new key pair: %s", err)
}
asymKey, err = shh.GetPrivateKey(asymKeyID)
if err != nil {
utils.Fatalf("Failed to retrieve a new key pair: %s", err)
}
3.2 配置我的節點
一個節點就是不停的轉發符合Pow的消息,如果是我這個聊天室的消息,就告訴用戶.所以節點要和其他節點進行交互,交互的節點越多,消息傳播的越快.
當然這些節點數量要有一個上限,這里是80. 其中peers變量就是3.1 連接主節點的主節點.
maxPeers := 80
server = &p2p.Server{
Config: p2p.Config{
PrivateKey: asymKey,
MaxPeers: maxPeers,
Name: common.MakeName("p2p chat group", "5.0"),
Protocols: shh.Protocols(),
NAT: nat.Any(),
BootstrapNodes: peers,
StaticNodes: peers,
TrustedNodes: peers,
},
}
3.3 哪個聊天室
具有相同的主題和密碼的就是同一個聊天室.
symKey關聯到指定的密碼,topic保存四個字節的指定主題.
func configureNode() {
symKeyID, err := shh.AddSymKeyFromPassword(*argPass)
if err != nil {
utils.Fatalf("Failed to create symmetric key: %s", err)
}
symKey, err = shh.GetSymKey(symKeyID)
if err != nil {
utils.Fatalf("Failed to save symmetric key: %s", err)
}
copy(topic[:], common.FromHex(*argTopic))
fmt.Printf("Filter is configured for the topic: %x \n", topic)
}
3.3 加入聊天室
我的節點可能會收到千百條各種消息,有些我能解密,有些我不能解密,但是其中只有極少一部分是我想看到的.
所以要告訴我的節點我只對這個聊天室感興趣,如果有消息來就告訴我.
SubscribeMessage訂閱指定主題和密碼的消息,注意filterID,它相當於向系統訂閱特定消息的句柄,后面還會用到.
func SubscribeMessage() {
var err error
filter := whisper.Filter{
KeySym: symKey,
KeyAsym: asymKey,
Topics: [][]byte{topic[:]},
AllowP2P: true,
}
filterID, err = shh.Subscribe(&filter)
if err != nil {
utils.Fatalf("Failed to install filter: %s", err)
}
}
3.4 群發消息
在p2p網絡中群發消息反而是最簡單的,如果要點對點發消息,限制反而要多. whisper提供有發送消息的api.
主要就是構造一個合法的消息結構,主要是指定topic以及加密的秘鑰,還有就是消息體(payload)就可以了,asymKey主要是為了標識ID,不是用作非對稱加密.
發送消息主要是按照指定的PoW要求(比特幣,萊特幣,以太幣等等都是類似的思路),計算hash,然后把消息發送到網絡上.
func sendMsg(payload []byte) common.Hash {
params := whisper.MessageParams{
Src: asymKey,
KeySym: symKey,
Payload: payload,
Topic: topic,
TTL: whisper.DefaultTTL,
PoW: whisper.DefaultMinimumPoW,
WorkTime: 5,
}
msg, err := whisper.NewSentMessage(¶ms)
if err != nil {
utils.Fatalf("failed to create new message: %s", err)
}
envelope, err := msg.Wrap(¶ms)
if err != nil {
fmt.Printf("failed to seal message: %v \n", err)
return common.Hash{}
}
err = shh.Send(envelope)
if err != nil {
fmt.Printf("failed to send message: %v \n", err)
return common.Hash{}
}
return envelope.Hash()
}
3.5 接收消息
系統實際上一直在不停的接收消息並轉發,這里說的接收消息實際上就是把我們感興趣的消息提取出來,也就是我們這個聊天室的消息.
注意這里的filterID就是 3.3 哪個聊天室提到的,這里可以認作是聊天室的ID了.
可以看出messageLoop就是不停的輪詢有沒有相關聊天室的消息,目前whisper還沒有實現消息推送功能.
func messageLoop() {
f := shh.GetFilter(filterID)
if f == nil {
utils.Fatalf("filter is not installed")
}
ticker := time.NewTicker(time.Millisecond * 50)
for {
select {
case <-ticker.C:
messages := f.Retrieve()
for _, msg := range messages {
printMessageInfo(msg)
}
case <-done:
return
}
}
}
4. 再次使用p2pmessage
在主機1和主機2同時上運行p2pmessage -topic ffff0000 -password 7859931
,並等待Connected to peer,you can type message now.
可以看到如下截圖:
主機1:
主機2: