5.8 Go语言项目实战:点对点聊天

Amiee7 · 2019-03-17 17:39:29 · 3572 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2019-03-17 17:39:29 的主题,其中的信息可能已经有所发展或是发生改变。

本篇文章IT兄弟连GO语言学院小美

给读者们分享一下

对GO语言感兴趣想要学习Golang开发技术的小伙伴就随小编来了解一下吧。 需求摘要

  • 实现一个分布式点对点的聊天系统,所有节点都是对等的,不需要中央服务器
  • 实现注册节点名称,节点之间通过节点名称发起会话

    思路分析

    • 节点同时具备服务端和客户端的职能
    • 服务端只负责接收其它节点主动发送过来的消息
    • 客户端只负责主动向其它节点发送消息
    • 通信都用短连接,服务端收完消息/客户端发完消息都断开conn——一方面是节约IO资源,另一方面是为了使逻辑清晰
    • 节点名称注册到【注册服务器】(很像DNS),以便根据节点名称访问节点而不是监听端口

节点代码实现

peer.go代码实现如下

package main

import (
    "fmt"
    "net"
    "os"
    "time"
)

/*
·用一个可执行程序实现相互聊天
·实现注册节点名称,并通过名称发起会话
·实现群发消息
*/

/*
思路概要:
·节点同时具备服务端和客户端的职能
·服务端只负责接收其它节点主动发送过来的消息
·客户端只负责主动向其它节点发送消息
·通信都用短连接,服务端收完消息/客户端发完消息都断开conn——一方面是节约IO资源,另一方面是为了使逻辑清晰
·节点名称注册到【注册服务器】(很像DNS),以便根据节点名称访问节点而不是监听端口
*/

/*
节点注册服务器地址
提供节点注册和查询功能
*/
const registerAddress = "127.0.0.1:8888"

/*
节点的主业务逻辑
*/
func main() {

    //初始化缓存表
    cacheMap = make(map[string]string)

    /*从命令行接收监听端口和节点名称*/
    //从命令行上接收一个用于监听的端口:peer pa 1234
    peerName = os.Args[1]
    peerListeningPort = os.Args[2]
    fmt.Println(peerListeningPort, peerName)

    /*
    向注册器注册自己
    reg pa 1234
    */
    peerAddress := RegOrGetPeerListeningAddress("reg " + peerName + " " + peerListeningPort)
    fmt.Println("节点注册成功", peerName, peerAddress)

    /*在独立并发任务中接收其它节点的消息*/
    go StartServe()

    /*在独立并发任务中向其它节点发送消息*/
    go StartRequest()

    //不能主协程会在此挂掉(如果主协程挂掉,子协程就跟着挂掉了)
    for {
        time.Sleep(1 * time.Second)
    }
    fmt.Println("GAME OVER")

}

/*错误处理*/
func HandleErr(err error, when string) {
    if err != nil {
        fmt.Println("err=", err, when)
        os.Exit(1)
    }
}

//节点监听端口,节点名称
var peerListeningPort, peerName string

//缓存其它通信节点的监听地址(如果已经查询过一回,就没必要每次都查询)
var cacheMap map[string]string

/*
向【注册器】注册/获取【节点的监听地址】
request 请求命令:
    reg pa 1234 向注册机注册名为pa的节点,监听在1234端口
    get pa        向注册机获取名为pa的节点的监听地址
返回值    pa节点的监听地址
*/
func RegOrGetPeerListeningAddress(request string) string {

    //拨号【注册服务器】
    conn, e := net.Dial("tcp", registerAddress)
    HandleErr(e, "RegOrGetPeerListeningAddress")

    //发送注册/查询命令
    conn.Write([]byte(request))

    //得到要注册/查询的节点的监听地址
    buffer := make([]byte, 1024)
    n, e := conn.Read(buffer)
    HandleErr(e, "RegGetPeerAddressconn.Read(buffer)")
    peerAddress := string(buffer[:n])

    //返回这个监听地址
    return peerAddress
}

/*
监听并接收其它节点发送过来的消息
这是节点【服务端】的一面
*/
func StartServe() {
    //在配置和注册好的端口建立TCP监听
    listener, e := net.Listen("tcp", ":"+peerListeningPort)
    HandleErr(e, "net.Listen")

    /*循环接入其它节点*/
    for {
        conn, e := listener.Accept()
        HandleErr(e, "listener.Accept()")

        //接收远程节点的消息
        buffer := make([]byte, 1024)
        n, err := conn.Read(buffer)
        HandleErr(err, "conn.Read(buffer)")
        msg := string(buffer[:n])
        fmt.Println(conn.RemoteAddr(), ":", msg)

        //接收完毕立即断开
        conn.Close()
    }
}

/*
主动向其它节点发起会话
这是节点【客户端】的一面
*/
func StartRequest() {

    //目标节点名称,要发送的消息
    var targetName, msg string

    for {

        //从控制台输入信息
        fmt.Println("请输入对方名称:消息内容")
        fmt.Scan(&targetName, &msg)

        //看看缓存中是否有节点信息
        var targetAddress string
        if temp, ok := cacheMap[targetName]; !ok {
            //向注册器查询节点的监听地址
            fmt.Println("从注册服务器获得节点监听地址")
            targetAddress = RegOrGetPeerListeningAddress("get " + targetName)

            //将查询结果写入缓存
            cacheMap[targetName] = targetAddress

        } else {

            //使用缓存中的监听地址
            fmt.Println("从缓存获得节点监听地址")
            targetAddress = temp
        }

        //向目标地址发送消息
        conn, e := net.Dial("tcp", targetAddress)
        HandleErr(e, "net.Dial")
        conn.Write([]byte(msg))

        //消息发送完毕,断开连接
        conn.Close()
    }

}

节点注册服务器

  • 节点注册服务器作为基础设施,提供节点的注册和查询功能

registerer.go 代码实现如下

package main

import (
    "fmt"
    "net"
    "os"
    "strings"
)

/*
负责注册节点名称:节点运行端口
*/
func RHandleErr(err error, when string)  {
    if err != nil{
        fmt.Println("注册器err=",err,when)
        os.Exit(1)
    }
}

//所有节点【名称-监听地址】映射表
var peerNameListeningAddressMap map[string]string

/*注册机的主业务*/
func main() {

    //初始化注册表
    peerNameListeningAddressMap = make(map[string]string)

    //开启注册服务
    listener, e := net.Listen("tcp", ":8888")
    RHandleErr(e,"net.Listen")

    buffer := make([]byte, 1024)

    /*循环接受节点的注册和查询服务*/
    for  {
        conn, e := listener.Accept()
        RHandleErr(e,"listener.Accept()")

        /*
        接收节点消息
        reg pa 1234     注册:节点名称pa,监听端口1234
        get pa             查询:节点名称为pa的节点的监听地址
        */
        n, e := conn.Read(buffer)
        RHandleErr(e,"conn.Read(buffer)")
        msg := string(buffer[:n])

        //将消息炸碎为字符串,获取命令和节点名称
        strs := strings.Split(msg, " ")
        cmd := strs[0]
        peerName := strs[1]

        if cmd == "reg"{
            //将节点名称和【节点-地址】写入全局映射表 reg pa 1234
            runningAddress := conn.RemoteAddr().String()

            //拼接节点的IP和监听端口,得到节点的监听地址
            peerIP := strings.Split(runningAddress, ":")[0]
            peerListeningPort := strs[2]
            listenAddress := peerIP +":" + peerListeningPort

            //节点名称为键,监听地址为值,写入map(下次就可以供别人查询了)
            peerNameListeningAddressMap[peerName] = listenAddress

            //将节点的名称和监听地址写入全局映射表
            conn.Write([]byte(listenAddress))

        } else if cmd=="get"{

            //根据节点名称查询节点监听地址
            listeningAddress := peerNameListeningAddressMap[peerName]
            conn.Write([]byte(listeningAddress))

        }

        //断开会话,继续接受其他节点的注册或查询请求
        conn.Close()
    }

}

想要了解更多关于GO语言开发方面内容的小伙伴,

请关注IT兄弟连官网、公众号:GO语言研习社,

IT兄弟连教育有专业的微软、谷歌讲师为您指导,

此外IT兄弟连老师精心推出的GO语言教程定能让你快速掌握GO语言从入门到精通开发实战技能。


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

3572 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传