主頁 >  其他 > 【游戲開發實戰】用Go語言寫一個服務器,實作與Unity客戶端通信(Golang | Unity | Socket | 通信 | 教程 | 附工程原始碼)

【游戲開發實戰】用Go語言寫一個服務器,實作與Unity客戶端通信(Golang | Unity | Socket | 通信 | 教程 | 附工程原始碼)

2021-11-03 09:21:04 其他

文章目錄

      • 一、前言
      • 二、Go開發環境搭建(Windows系統)
        • 1、安裝Go命令列工具
        • 2、創建GoWorkspace目錄
        • 3、配置GOPATH環境變數
        • 4、配置GOPROXY代理
        • 5、安裝VSCode
        • 6、VSCode安裝Go插件
        • 7、安裝Go開發工具鏈
      • 三、HelloGo 工程
        • 1、創建go腳本: main.go
        • 2、main.go代碼
        • 3、生成go.mod檔案
        • 4、編譯生成可執行程式: go build命令
        • 5、測驗運行
      • 四、用Go做個訊息廣播的服務端
        • 1、思維導圖
        • 2、腳本說明
      • 五、開始寫服務端Go代碼
        • 1、創建專案檔案夾和腳本
        • 2、server.go腳本
          • 2.1、成員變數宣告
          • 2.2、全域方法,NewServer
          • 2.3、Socket監聽連接,Listen和Accept
          • 2.4、啟動協程處理用戶訊息,Handler
          • 2.5、訊息廣播,通過管道同步
        • 3、user.go腳本
          • 3.1、成員變數宣告
          • 3.2、全域方法,NewUser
          • 3.3、用戶上線,Online
          • 3.4、用戶下線,Offline
          • 3.5、訊息處理,DoMessage
        • 4、main.go腳本
        • 5、編譯運行
      • 六、Unity客戶端
        • 1、創建工程,UnitySocketClient
        • 2、UGUI制作界面
        • 3、C#腳本
          • 3.1、ClientNet.cs腳本
          • 3.2、Main.cs腳本
        • 4、打包客戶端
      • 七、運行測驗
        • 1、啟動Go服務端
        • 2、啟動Unity客戶端
      • 八、工程原始碼
      • 九、完畢

一、前言

嗨,大家好,我是新發,
有老同事問我會不會Go語言,人生苦短,Let's Go,我做了一個Go語言基礎的思維導圖,福利給大家~

請添加圖片描述

嘛,今天就做一個Go語言服務端與Unity通信的小案例吧,效果如下,
請添加圖片描述
工程原始碼見文章末尾~

二、Go開發環境搭建(Windows系統)

Go語言是一門編譯型語言,代碼檔案以.go為后綴,我們寫的.go代碼最終要編譯為可執行檔案(在Windows平臺下就是.exe檔案),編譯需要用到go build命令,go build命令哪來的呢,這就需要我們在系統中安裝GO命令列工具,
另外,寫.go代碼需要一個IDE,推薦使用VSCode,需要而外安裝Go插件和工具鏈,
畫個圖,方便大家理解~
在這里插入圖片描述

看起來好像有點小麻煩,不要怕,幾分鐘搞定,下面我就來教大家~

1、安裝Go命令列工具

進入Go官網:https://golang.google.cn/,點擊Download Go
在這里插入圖片描述
然后根據你的作業系統選擇對應的檔案,它支持WindowsmacOSLinux三個平臺,我以Windows為例,點擊第一個,如下,
在這里插入圖片描述
下載完畢后直接雙擊執行安裝即可,
在這里插入圖片描述
安裝完畢后,打開cmd命令列(步驟: 按win + r鍵,輸入cmd按回車),然后執行go version,如果能正常輸出版本號,則說明安裝成功了,如下,
在這里插入圖片描述

2、創建GoWorkspace目錄

在任意磁盤中創建一個檔案夾作為工程 作業空間,建議命名為GoWorkSpace,然后再分別創建binpkgsrc三個檔案夾,
在這里插入圖片描述
三個檔案夾的用途如下:

檔案夾用途
bin用來存放編譯后的可執行檔案
pkg用于存放編譯后的包檔案(一些第三方包檔案)
src是用來存放.go原始碼檔案(就是自己寫的.go代碼)

3、配置GOPATH環境變數

GOPATH是一個環境變數,用來表明你寫的go專案的存放路徑,現在我們來設定一下GOPATH環境變數,
我的電腦上滑鼠右鍵,點擊屬性,然后點擊高級系統設定
在這里插入圖片描述
再點擊環境變數
在這里插入圖片描述

在系統變數下點擊新建按鈕,

在這里插入圖片描述
變數名為GOPATH,變數值為剛剛創建的GoWorkSpace路徑,然后點擊確定
在這里插入圖片描述
這樣,我們的GOPATH環境變數就配置完成了~

4、配置GOPROXY代理

我們在執行go編譯時,會自動去下載依賴包,GOPROXY默認配置是:GOPROXY=https://proxy.golang.org,direct,由于國內訪問不到,編譯時會報錯超時,我們需要改成國內的源,打開命令列,執行下面的命令:

go env -w GOPROXY=https://goproxy.cn,direct 

如下,
在這里插入圖片描述

5、安裝VSCode

接下來是IDE的安裝,建議用VSCode,安裝程序很簡單,這里不贅述~
VSCode官網:https://code.visualstudio.com/
在這里插入圖片描述

6、VSCode安裝Go插件

VSCode安裝完畢后,點擊插件安裝按鈕,搜索go,選擇Go插件,點擊install按鈕,如下,
在這里插入圖片描述

注:這個Go插件提供了go代碼的智能感知、提示、語法高亮、語法檢測等功能,

7、安裝Go開發工具鏈

進行go開發還需要下載配套的開發工具鏈(比如除錯器、代碼風格格式化等),
我們打開VSCode,按Ctrl + Shift + P,輸入go:install,選擇Go: Install/Update Tools,然后全選,最后點擊OK按鈕,如下,

注:如果你沒有Go: Install/Update Tools這個選項,請檢查第6步的Go插件是否已正常安裝,

請添加圖片描述
耐心等待(大約1分鐘左右),下載完畢后可以在VSCode的日志輸出中看到All tools successfully installed. You are ready to Go. :),如下,
在這里插入圖片描述

三、HelloGo 工程

以上,我們的Go開發環境就搭建好了,現在我們來寫一個HelloWorld,不,是HelloGo測驗一下吧~

1、創建go腳本: main.go

GoWorkSpace/src目錄中新建一個HelloGo檔案夾,如下,
在這里插入圖片描述
回到VScode,創建一個main.go腳本,

注:檔案名不叫main也可以,不過一般作為程式入口腳本,建議叫main

請添加圖片描述

2、main.go代碼

好了,現在我們開始寫代碼,功能就是列印一句日志:Hello Golang,代碼如下:

// 包名,main包為入口包,main包中必須含有一個main方法
package main

import "fmt"

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

3、生成go.mod檔案

go mod全稱go modules,在Golang 1.11版本之前,go代碼的包依賴沒有版本控制的概念,比如你依賴了一個protobuf庫,你在go腳本中通過import引入包,如下

import "github.com/micro/protobuf/proto"

它只會從github中下載最新版本的protobuf,可想而知,這對于團隊協作是很不友好的,不同人電腦上不同時期引入的第三方包坑內版本存在差異,可能導致程式無法正常作業,
于是呢,在Golang 1.11版本開始,就引入了go mod,由一個go.mod檔案來記錄依賴包的版本資訊,
現在,我們就來生成這個go.mod檔案,在VSCode終端中,cd進入HelloGo目錄,然后執行命令

go mod init HelloGo

注: 上面的命令的HelloGo是模塊名

它會生成一個go.mod檔案,如下,
請添加圖片描述

4、編譯生成可執行程式: go build命令

go代碼最終要生成成可執行程式才能運行,現在我們在HelloGo目錄下,執行go build命令,最終它生成了一個HelloGo.exe,如下,
請添加圖片描述

注: 如果要指定生成的exe名字,則可以加上-o引數,例:go build -o MyTest.exe,它就會生成一個MyTest.exe啦~

5、測驗運行

現在我們去執行這個HelloGo.exe,可以看到,成功輸出了Hello Golang,如下,
請添加圖片描述
如果我們想跳過go build命令,直接測驗go腳本,可以使用go run命令,例:

go run main.go

如下,
請添加圖片描述

四、用Go做個訊息廣播的服務端

接下來,我們用Go來開發一個Socket通信的服務端,實作訊息廣播的功能吧~

1、思維導圖

在開始寫代碼之前,我們先設計一下服務端的模塊,畫個圖,如下,
在這里插入圖片描述

2、腳本說明

main.go為程式入口腳本;
server.go負責socket監聽和管理;
user.go是用戶類腳本,當server.gosocket監聽到有客戶端連接時,構造一個User物件,后續的socket通信交由user.go腳本代理,

模塊很簡單,相信大家很容易看懂,

五、開始寫服務端Go代碼

1、創建專案檔案夾和腳本

我們在src目錄中創建一個GoSocketServer檔案夾,作為專案檔案夾,
在這里插入圖片描述
接著我們在GoSocketServer檔案夾中創建main.goserver.gouser.go三個腳本,
在這里插入圖片描述

2、server.go腳本

我們先封裝一下Server類,注意在Go語言中,定義類用的是struct關鍵字,成員變數名如果是首字母大寫,則表示是public的,如果是小寫,則表示是private的,

2.1、成員變數宣告
// Server.go 腳本
package main

// import ...

type Server struct {
	Ip   string
	Port int

	// 在線用戶容器
	OnlineMap map[string]*User
	// 用戶列容器鎖,對容器進行操作時進行加鎖
	mapLock   sync.RWMutex

	// 訊息廣播的管道
	Message chan string
}

講解:
Message成員是一個chan型別,即管道型別,用于goroutine之間的訊息同步,當客戶端連接服務端時,服務端開啟一個goroutine來處理后續的用戶訊息,訊息需要廣播給所有在線的客戶端,所以這里我們通過Message管道來做一層訊息傳遞,

OnlineMap是在線用戶容器(注意User類在user.go腳本中定義,下文會講),OnlineMap存盤當前連接到服務端的用戶,OnlineMap的操作存在多執行緒并行處理的情況,所以我們需要使用一個sync.RWMutex讀寫鎖對它進行加鎖處理,宣告一個mapLock成員,

2.2、全域方法,NewServer

我們定義一個NewServer全域方法,構造Server物件,提供給外部呼叫,

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

監聽Socket,我們可以使用net模塊的Listen方法,函式原型如下,

// 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
}

要監聽客戶端連接,則用到的是ListenerAccept介面,該方法會阻塞,當接收到socket連接時才會繼續往下執行,介面原型如下,

// 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

上面Start函式中,當Listener接收到連接后,為了不阻塞for回圈,我們啟動協程去處理用戶行為,封裝一個Handler方法,

// server.go 腳本

func (this *Server) Handler(conn net.Conn) {
	// ...
}

在上面的Start方法中添加Handler呼叫,

// 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)
	}
}

Handle方法里面主要做三件事情:
1、構建User物件;
2、啟動一個新的協程從Conn中讀取訊息;
3、通過User物件執行訊息處理,

// 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
}

接著我們定義一個ListenMessager方法,去監聽Message管道,當Message管道中有訊息時,把訊息寫到用戶管道中,

// 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()
	}
}

我們在Start方法中去啟動一個協程來執行ListenMessager

// 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腳本

User類,主要做的就是訊息處理,即用戶行為的代理,如果是在skynet中,就是一個用戶agent服務,

注:關于skynet,我之前寫過幾篇文章,感興趣的同學也可以看看,
【游戲開發實戰】手把手教你從零跑一個Skynet,詳細教程,含案例講解(服務端 | Skynet | Ubuntu)
【游戲開發實戰】手把手教你在Windows上通過WSL運行Skynet,不用安裝虛擬機,方便快捷(WSL | Linux | Ubuntu | Skynet | VSCode)
【游戲開發實戰】教你Unity通過sproto協議與Skynet框架的服務端通信,附工程原始碼(Unity | Sproto | 協議 | Skynet)

3.1、成員變數宣告

我們先定義一些基礎的成員變數,

// user.go 腳本

type User struct {
	Name string		// 昵稱,默認與Addr相同
	Addr string		// 地址
	Channel chan string	// 訊息管道
	conn net.Conn		// 連接
	server *Server		// 快取Server的參考
}
3.2、全域方法,NewUser

我們定義一個NewUser全域方法,構造User物件,提供給外部呼叫,

// 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)
}

上面Server類中的BroadCast方法,會把訊息同步回每個User物件的Channel管道,所以我們需要在User中去監聽Channel管道訊息,封裝個ListenMessage方法,我們先構造一個bytebuf,在頭部兩個位元組寫入訊息長度,然后再寫入訊息內容,如下,

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.go腳本是程式入口腳本,我們要定義一個main方法作為入口函式,
我們封裝一個StartServer方法,通過NewServer全域方法構造一個Server物件,然后執行Start成員方法,如下,

// main.go 腳本

func StartServer() {
	server := NewServer("127.0.0.1", 8888)
	server.Start()
}

然后在main方法中啟動一個協程去執行StartServer,如下,

// main.go 腳本

func main() {
	// 啟動Server
	go StartServer()

	// TODO 你可以寫其他邏輯
	fmt.Println("這是一個Go服務端,實作了Socket訊息廣播功能")

	// 防止主執行緒退出
	for {
		time.Sleep(1 * time.Second)
	}
}

5、編譯運行

VSCode的終端中,進入GoSocketServer目錄,然后執行go mod init GoSocketServer,生成go.mod檔案,如下,
請添加圖片描述
執行go build命令,將go腳本編譯為.exe可執行程式(Windows平臺),如下,
請添加圖片描述
運行GoSocketServer.exe,如下,可以看到,服務端啟動起來了,
請添加圖片描述
下面,我們用Unity實作客戶端部分的功能吧~

六、Unity客戶端

1、創建工程,UnitySocketClient

創建Unity工程,專案名稱叫UnitySocketClient吧,如下,
在這里插入圖片描述

2、UGUI制作界面

使用UGUI制作一個界面,如下,
在這里插入圖片描述
節點層級結構如下,
在這里插入圖片描述

3、C#腳本

C#腳本只有兩個,一個ClientNet.cs,一個Main.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.cs腳本作為入口腳本,同時作為UI互動的腳本,
代碼如下,

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.cs腳本掛到Canvas節點上,并賦值腳本的UI成員物件,如下,
在這里插入圖片描述

4、打包客戶端

點擊選單File / Build Settings...,然后添加場景到Scenes in Build串列中,選擇平臺為PC平臺,如下,
在這里插入圖片描述
接著點擊Player Settings,設定Fullscreen ModeWindowed模式,即視窗模式,設定一下視窗大小,如下,
在這里插入圖片描述
最終,點擊Build,執行打包,
在這里插入圖片描述
打包完畢,生成了exe檔案,如下,
在這里插入圖片描述

七、運行測驗

1、啟動Go服務端

回到我們的Go服務端工程,在終端執行我們上面生成的GoSocketServer.exe,如下,
請添加圖片描述

2、啟動Unity客戶端

啟動多個客戶端,測驗聊天,如下,
請添加圖片描述
功能正常,VeryGood,收拾吃飯去~

八、工程原始碼

本文服務端+客戶端工程原始碼已上傳到CODE CHINA,感興趣的同學可自行下載學習,
地址:https://codechina.csdn.net/linxinfa/golangserverandunityclientdemo
注:我使用的go版本是1.17.2,使用的Unity版本是2021.1.9f1c1
在這里插入圖片描述

九、完畢

好啦,就先到這里吧~
我是林新發:https://blog.csdn.net/linxinfa
原創不易,若轉載請注明出處,感謝大家~
喜歡我的可以點贊、關注、收藏,如果有什么技術上的疑問,歡迎留言或私信~

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/345862.html

標籤:其他

上一篇:C/C++游戲專案完整教程:《推箱子》

下一篇:ELasticSearch——ElasticScarch 概述及安裝 v7.8

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more