一、前言

GoLet's GoGo语言基础

请添加图片描述

GoUnity

二、Go开发环境搭建(Windows系统)

Go.go.goWindows.exego buildgo buildGO.goIDEVSCodeGo

看起来好像有点小麻烦,不要怕,几分钟搞定,下面我就来教大家~

1、安装Go命令行工具

GoDownload GoWindowsmacOSLinuxWindowscmdwin + rcmdgo version

2、创建GoWorkspace目录

GoWorkSpacebinpkgsrc
.go.go

3、配置GOPATH环境变量

GOPATHgoGOPATH我的电脑属性高级系统设置环境变量
新建
GOPATHGoWorkSpace确定GOPATH

4、配置GOPROXY代理

goGOPROXYGOPROXY=https://proxy.golang.org,direct
go env -w GOPROXY=https://goproxy.cn,direct 

如下,
在这里插入图片描述

5、安装VSCode

IDEVSCodeVSCode

6、VSCode安装Go插件

VSCodegoGoinstall
Gogo

7、安装Go开发工具链

goVSCodeCtrl + Shift + Pgo:installGo: Install/Update ToolsOK
Go: Install/Update ToolsGo
1分钟VSCodeAll tools successfully installed. You are ready to Go. :)

三、HelloGo 工程

GoHelloWorldHelloGo

1、创建go脚本: main.go

GoWorkSpace/srcHelloGoVScodemain.go
mainmain

请添加图片描述

2、main.go代码

Hello Golang
// 包名,main包为入口包,main包中必须含有一个main方法
package main

import "fmt"

// 程序入口方法,必须叫main
func main() {
	// 输出日志
	fmt.Println("Hello Golang")
}

3、生成go.mod文件

go modgo modulesGolang 1.11goprotobufgoimport
import "github.com/micro/protobuf/proto"
githubprotobufGolang 1.11go modgo.modgo.modVSCodecdHelloGo
go mod init HelloGo
HelloGo
go.mod

4、编译生成可执行程序: go build命令

goHelloGogo buildHelloGo.exe
exe-ogo build -o MyTest.exeMyTest.exe

5、测试运行

HelloGo.exeHello Golanggo buildgogo run
go run main.go

如下,
请添加图片描述

四、用Go做个消息广播的服务端

GoSocket

1、思维导图

在开始写代码之前,我们先设计一下服务端的模块,画个图,如下,
在这里插入图片描述

2、脚本说明

main.goserver.gosocketuser.goserver.gosocketUsersocketuser.go

模块很简单,相信大家很容易看懂。

五、开始写服务端Go代码

1、创建项目文件夹和脚本

srcGoSocketServerGoSocketServermain.goserver.gouser.go

2、server.go脚本

ServerGostructpublicprivate
2.1、成员变量声明
// Server.go 脚本
package main

// import ...

type Server struct {
	Ip   string
	Port int

	// 在线用户容器
	OnlineMap map[string]*User
	// 用户列容器锁,对容器进行操作时进行加锁
	mapLock   sync.RWMutex

	// 消息广播的管道
	Message chan string
}
MessagechangoroutinegoroutineMessage
OnlineMapUseruser.goOnlineMapOnlineMapsync.RWMutexmapLock
2.2、全局方法,NewServer
NewServerServer
func NewServer(ip string, port int) *Server {
	server := &Server{
		Ip:        ip,
		Port:      port,
		OnlineMap: make(map[string]*User),
		Message:   make(chan string),
	}

	return server
}
2.3、Socket监听连接,Listen和Accept
SocketnetListen
// net 模块
func Listen(network, address string) (Listener, error)

例:

// import "net"

listener, err := net.Listen("tcp", "127.0.0.1:8888")
if err != nil {
	fmt.Println("net.Listen err:", err)
	return
}
ListenerAcceptsocket
// Listener接口
Accept() (Conn, error)

例:

conn, err := listener.Accept()
if err != nil {
	fmt.Println("listener accept err:", err)
}
Socket监听连接ServerStart
// Server.go 脚本

// 启动服务器的接口
func (this *Server) Start() {
	// socket监听
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.Ip, this.Port))
	if err != nil {
		fmt.Println("net.Listen err:", err)
		return
	}
	
	// 程序退出时,关闭监听,注意defer关键字的用途
	defer listener.Close()

	// 注意for循环不加条件,相当于while循环
	for {
		// Accept,此处会阻塞,当有客户端连接时才会往后执行
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener accept err:", err)
			continue
		}

		// TODO 启动一个协程去处理

	}
}
2.4、启动协程处理用户消息,Handler
StartListenerforHandler
// server.go 脚本

func (this *Server) Handler(conn net.Conn) {
	// ...
}
StartHandler
// server.go 脚本

func (this *Server) Start() {
	// ...
	
	for {
		// Accept,此处会阻塞,当有客户端连接时才会往后执行
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener accept err:", err)
			continue
		}
	
		// 启动一个协程去处理
		go this.Handler(conn)
	}
}
HandleUserConnUser
// server.go 脚本

func (this *Server) Handler(conn net.Conn) {
	// 构造User对象,NewUser全局方法在user.go脚本中
	user := NewUser(conn, this)
	
	// 用户上线
	user.Online()
	
	// 启动一个协程
	go func() {
		buf := make([]byte, 4096)
		for {
			// 从Conn中读取消息
			len, err := conn.Read(buf)
			if 0 == len {
				// 用户下线
				user.Offline()
				return
			}

			if err != nil && err != io.EOF {
				fmt.Println("Conn Read err:", err)
				return
			}

			// 用户针对msg进行消息处理
			user.DoMessage(buf, len)
		}
	}()
}
2.5、消息广播,通过管道同步
Message
// server.go 脚本

func (this *Server) BroadCast(user *User, msg string) {
	sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg

	this.Message <- sendMsg
}
ListenMessagerMessageMessage
// server.go 脚本

func (this *Server) ListenMessager() {
	for {
		// 从Message管道中读取消息
		msg := <-this.Message

		// 加锁
		this.mapLock.Lock()
		// 遍历在线用户,把广播消息同步给在线用户
		for _, user := range this.OnlineMap {
			// 把要广播的消息写到用户管道中
			user.Channel <- msg
		}
		// 解锁
		this.mapLock.Unlock()
	}
}
StartListenMessager
// server.go 脚本

func (this *Server) Start() {
	// ...
	
	// 启动一个协程来执行ListenMessager
	go this.ListenMessager()

	for {
		// Accept,此处会阻塞,当有客户端连接时才会往后执行
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener accept err:", err)
			continue
		}
	
		// 启动一个协程去处理
		go this.Handler(conn)
	}
}

3、user.go脚本

Userskynetagent
3.1、成员变量声明

我们先定义一些基础的成员变量,

// user.go 脚本

type User struct {
	Name string		// 昵称,默认与Addr相同
	Addr string		// 地址
	Channel chan string	// 消息管道
	conn net.Conn		// 连接
	server *Server		// 缓存Server的引用
}
3.2、全局方法,NewUser
NewUserUser
// user.go 脚本

func NewUser(conn net.Conn, server *Server) *User {
	userAddr := conn.RemoteAddr().String()

	user := &User{
		Name: userAddr,
		Addr: userAddr,
		Channel:    make(chan string),
		conn: conn,
		server: server,
	}

	return user
}
3.3、用户上线,Online
Online
// user.go 脚本

func (this *User) Online() {

	// 用户上线,将用户加入到OnlineMap中,注意加锁操作
	this.server.mapLock.Lock()
	this.server.OnlineMap[this.Name] = this
	this.server.mapLock.Unlock()

	// 广播当前用户上线消息
	this.server.BroadCast(this, "上线啦O(∩_∩)O")
}
3.4、用户下线,Offline
Offline
// user.go 脚本

func (this *User) Offline() {

	// 用户下线,将用户从OnlineMap中删除,注意加锁
	this.server.mapLock.Lock()
	delete(this.server.OnlineMap, this.Name)
	this.server.mapLock.Unlock()

	// 广播当前用户下线消息
	this.server.BroadCast(this, "下线了o(╥﹏╥)o")
}
3.5、消息处理,DoMessage
protobufsproto
// user.go 脚本

func (this *User) DoMessage(buf []byte, len int) {
	//提取用户的消息(去除'\n')
	msg := string(buf[:len-1])
	// 调用Server的BroadCast方法
	this.server.BroadCast(this, msg)
}
ServerBroadCastUserChannelUserChannelListenMessagebytebuf
func (this *User) ListenMessage() {
	for {
		msg := <-this.Channel
		fmt.Println("Send msg to client: ", msg, ", len: ", int16(len(msg)))
		bytebuf := bytes.NewBuffer([]byte{})
		// 前两个字节写入消息长度
		binary.Write(bytebuf, binary.BigEndian, int16(len(msg)))
		// 写入消息数据
		binary.Write(bytebuf, binary.BigEndian, []byte(msg))
		// 发送消息给客户端
		this.conn.Write(bytebuf.Bytes())
	}
}
NewUser
func NewUser(conn net.Conn, server *Server) *User {
	// ...

	// 启动协程,监听Channel管道消息
	go user.ListenMessage()

	return user
}

4、main.go脚本

main.gomainStartServerNewServerServerStart
// main.go 脚本

func StartServer() {
	server := NewServer("127.0.0.1", 8888)
	server.Start()
}
mainStartServer
// main.go 脚本

func main() {
	// 启动Server
	go StartServer()

	// TODO 你可以写其他逻辑
	fmt.Println("这是一个Go服务端,实现了Socket消息广播功能")

	// 防止主线程退出
	for {
		time.Sleep(1 * time.Second)
	}
}

5、编译运行

VSCodeGoSocketServergo mod init GoSocketServergo.modgo buildgo.exeWindowsGoSocketServer.exeUnity

六、Unity客户端

1、创建工程,UnitySocketClient

UnityUnitySocketClient

2、UGUI制作界面

UGUI

3、C#脚本

C#ClientNet.csMain.cs
3.1、ClientNet.cs脚本
ClientNet.cs
using System;
using UnityEngine;

using System.Net.Sockets;

public class ClientNet : MonoBehaviour
{
    private void Awake()
    {
        m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        m_readOffset = 0;
        m_recvOffset = 0;
        // 16KB
        m_recvBuf = new byte[0x4000];
    }

    private void Update()
    {
        if (null == m_socket) return;
        if (m_connectState == ConnectState.Ing && m_connectAsync.IsCompleted)
        {
            // 连接服务器失败
            if (!m_socket.Connected)
            {
                m_connectState = ConnectState.None;
                if (null != m_connectCb)
                    m_connectCb(false);
            }
        }

        if (m_connectState == ConnectState.Ok)
        {
            TryRecvMsg();
        }
    }

    private void TryRecvMsg()
    {
        // 开始接收消息
        m_socket.BeginReceive(m_recvBuf, m_recvOffset, m_recvBuf.Length - m_recvOffset, SocketFlags.None, (result) =>
        {
            // 如果有消息,会进入这个回调

            // 这个len是读取到的长度,它不一定是一个完整的消息的长度,我们下面需要解析头部两个字节作为真实的消息长度
            var len = m_socket.EndReceive(result);

            if (len > 0)
            {
                m_recvOffset += len;
                m_readOffset = 0;

                if (m_recvOffset - m_readOffset >= 2)
                {
                    // 头两个字节是真实消息长度,注意字节顺序是大端
                    int msgLen = m_recvBuf[m_readOffset + 1] | (m_recvBuf[m_readOffset] << 8);

                    if (m_recvOffset >= (m_readOffset + 2 + msgLen))
                    {
                        // 解析消息
                        string msg = System.Text.Encoding.UTF8.GetString(m_recvBuf, m_readOffset + 2, msgLen);
                        Debug.Log("Recv msgLen: " + msgLen + ", msg: " + msg);
                        if (null != m_recvMsgCb)
                            m_recvMsgCb(msg);

                        m_readOffset += 2 + msgLen;
                    }
                }

                // buf移位
                if (m_readOffset > 0)
                {
                    for (int i = m_readOffset; i < m_recvOffset; ++i)
                    {
                        m_recvBuf[i - m_readOffset] = m_recvBuf[i];
                    }
                    m_recvOffset -= m_readOffset;
                }
            }
        }, this);
    }

    /// <summary>
    /// 连接服务端
    /// </summary>
    /// <param name="host">IP地址</param>
    /// <param name="port">端口</param>
    /// <param name="cb">回调</param>
    public void Connect(string host, int port, Action<bool> cb)
    {
        m_connectCb = cb;
        m_connectState = ConnectState.Ing;
        m_socket.SendTimeout = 100;
        m_connectAsync = m_socket.BeginConnect(host, port, (IAsyncResult result) =>
        {
            // 连接成功会进入这里,连接失败不会进入这里
            var socket = result.AsyncState as Socket;
            socket.EndConnect(result);
            m_connectState = ConnectState.Ok;
            m_networkStream = new NetworkStream(m_socket);
            Debug.Log("Connect Ok");
            if (null != m_connectCb) m_connectCb(true);
        }, m_socket);

        Debug.Log("BeginConnect, Host: " + host + ", Port: " + port);
    }

    /// <summary>
    /// 注册消息接收回调函数
    /// </summary>
    /// <param name="cb">回调函数</param>
    public void RegistRecvMsgCb(Action<string> cb)
    {
        m_recvMsgCb = cb;
    }

    /// <summary>
    /// 发送消息
    /// </summary>
    /// <param name="bytes">消息的字节流</param>
    public void SendData(byte[] bytes)
    {
        m_networkStream.Write(bytes, 0, bytes.Length);
    }

    /// <summary>
    /// 关闭Sockete
    /// </summary>
    public void CloseSocket()
    {
        m_socket.Shutdown(SocketShutdown.Both);
        m_socket.Close();
    }

    /// <summary>
    /// 判断Socket是否连接状态
    /// </summary>
    /// <returns></returns>
    public bool IsConnected()
    {
        return m_socket.Connected;
    }

    private enum ConnectState
    {
        None,
        Ing,
        Ok,
    }

    private Action<bool> m_connectCb;
    private Action<string> m_recvMsgCb;
    private ConnectState m_connectState = ConnectState.None;
    private IAsyncResult m_connectAsync;

    private byte[] m_recvBuf;
    private int m_readOffset;
    private int m_recvOffset;
    private Socket m_socket;
    private NetworkStream m_networkStream;

    private static ClientNet s_instance;
    public static ClientNet instance
    {
        get
        {
            if (null == s_instance)
            {
                var go = new GameObject("ClientNet");
                s_instance = go.AddComponent<ClientNet>();
            }

            return s_instance;
        }
    }
}

3.2、Main.cs脚本
Main.csUI
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Main : MonoBehaviour
{
    public Button sendBtn;
    public InputField inputField;
    public Text chatText;

    private Queue<string> m_msgQueue;

    private void Awake()
    {
        m_msgQueue = new Queue<string>();
    }

    void Start()
    {
        // 注册消息回调
        ClientNet.instance.RegistRecvMsgCb((msg) =>
        {
            // 把消息缓存到队列中,注意不要在这里直接操作UI对象
            m_msgQueue.Enqueue(msg);
        });

        // 连接服务端
        ClientNet.instance.Connect("127.0.0.1", 8888, (ok) =>
         {
             Debug.Log("连接服务器, ok: " + ok);
         });

        sendBtn.onClick.AddListener(SendMsg);

    }

    /// <summary>
    /// 发送消息
    /// </summary>
    private void SendMsg()
    {
        if (ClientNet.instance.IsConnected())
        {
            // 把字符串转成字节流
            byte[] data = System.Text.Encoding.UTF8.GetBytes(inputField.text + "\n");
            // 发送给服务端
            ClientNet.instance.SendData(data);
            // 清空输入框文本
            inputField.text = "";
        }
        else
        {
            Debug.LogError("你还没连接服务器");
        }
    }

    private void Update()
    {
        if (m_msgQueue.Count > 0)
        {
            // 从消息队列中取消息,并更新到聊天文本中
            chatText.text += m_msgQueue.Dequeue() + "\n";
        }

        // 按回车键,发送消息
        if(Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))
        {
            SendMsg();
        }
    }

    private void OnDestroy() {
        ClientNet.instance.CloseSocket();
    }
}
Main.csCanvasUI

4、打包客户端

File / Build Settings...Scenes in BuildPCPlayer SettingsFullscreen ModeWindowedBuildexe

七、运行测试

1、启动Go服务端

GoGoSocketServer.exe

2、启动Unity客户端

VeryGood

八、工程源码

CODE CHINAgo1.17.2Unity2021.1.9f1c1

九、完毕