游戏开发实战用go语言写一个服务器,实现与unity客户端通信(golang|unity|socket|通信|教程|附工程源码)(代码片段)

林新发 林新发     2023-01-26     547

关键词:

一、前言

嗨,大家好,我是新发。
有老同事问我会不会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()
    
   

游戏脚本用啥语言写?

参考技术A谁能给我讲讲游戏脚本的事,用什么语言编写?是不是需要你所玩游戏的代码?10分目前游戏脚本用得多的是按键与TC简单开发,前者需要用到VB,学习起来比较吃力,后者是一个新推出的游戏脚本制作工具,可以说它在这... 查看详情

游戏脚本用啥语言写?

参考技术A问题一:谁能给我讲讲游戏脚本的事,用什么语言编写?是不是需要你所玩游戏的代码?10分目前游戏脚本用得多的是按键与TC简单开发,前者需要用到VB,学习起来比较吃力,后者是一个新推出的游戏脚本制作工具,可以... 查看详情

docker从入门到实战

...(二)一:什么是dockerDocker是一个开源的应用容器引擎,开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到主流的Linux、macOS、Windows机器上,实现虚拟化。Docker用Go语言写成,是一个重新定义了程序开发... 查看详情

go语言实战grpc实现一个简单微服务(代码片段)

文章目录写在前面1.安装1.1grpc1.2proto1.3protobuf2.编写代码2.1初始化项目2.2编写proto2.3编写服务端2.4实现客户端3.演示写在前面这一次我们用gRPC实现获取用户的信息1.安装1.1grpcgrpc的安装直接goget即可gogetgoogle.golang.org/grpc1.2proto下载proto... 查看详情

go语言项目开发实战极客时间百度网盘

...拥有多年Go项目开发经验,参与过腾讯云云函数SCF、腾讯游戏容器平台TenC、腾讯游戏微服务中台等大型企业项目的开发,目前负责腾讯云容器服务TKE的相关研发工作,专注于云原生混合云领域的基础架构开发。同时,拥有大规模... 查看详情

学习区块链开发是学习go语言、hyperledgerfabric比较好、还是以太坊智能合约比较好或者公链开发?

...发处理相对还是不错的,比如广告和搜索,那种高并发的服务器。Go语 查看详情

揭秘!用标准go语言能写脚本吗?

... | Go作为一种编译型语言,经常用于实现后台服务的开发。由于Go初始的开发大佬都是C的老牌使用者,因此Go中保留了不少C的编程习惯和思想,这对C/C++和PHP开发者来说非常有吸引力。作为编译型语言的特性... 查看详情

编程实践用go语言实现一个sqldsl(代码片段)

...一个SQLDSLGo语言是一种广泛使用的编程语言,特别是在Web开发领域。它的优势是高效、简单、易于编写、易于理解和学习,并且能够轻松实现高性能的应用程序。本文将演示如何使用Go语言来实现一个SQLDSL(数据定义语言),使得开... 查看详情

《unity2d与3d手机游戏开发实战》上架了。

...#xff0c;一个3DRPG的简单例子和一个尽可能用插件实现的射击游戏的例子。书很薄,不过因为是彩页印刷,价钱不是那么实惠。不过说实话,因为这类书里面有很多图片,彩页印刷了以后看着是要舒服很多。书早些时... 查看详情

13.go语言高并发与微服务实战---综合实战:秒杀系统的设计与实现

13.综合实战:秒杀系统的设计与实现 秒杀系统设计原则: 1.数据要尽量少 2.请求数尽量少 3.路径要尽量短 4.依赖要尽量少 5.尽量不要有单点       查看详情

13.go语言高并发与微服务实战---综合实战:秒杀系统的设计与实现

13.综合实战:秒杀系统的设计与实现 秒杀系统设计原则: 1.数据要尽量少 2.请求数尽量少 3.路径要尽量短 4.依赖要尽量少 5.尽量不要有单点       查看详情

leaf-一个由go语言编写的开发效率和执行效率并重的开源游戏服务器框架

转自:https://toutiao.io/posts/0l7l7n/previewLeaf游戏服务器框架简介Leaf是一个由Go语言(golang)编写的开发效率和执行效率并重的开源游戏服务器框架。Leaf适用于各类游戏服务器的开发,包括H5(HTML5)游戏服务器。Leaf的关注点:良好... 查看详情

html5游戏开发实战--注意点

...t是HTML5标准的一部分,Web页面可以用它来持久连接到socket服务器上。该接口提供了与服务器之间的事件驱动型连接,这意味着客户端不必再每隔一个时间段就需要向服务器发送一次新的数据请求。当有数据需要更新时,服务器就... 查看详情

docker入门与实战讲解

简述Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似iPhone的a... 查看详情

实战演示go反射的使用方法和应用场景(代码片段)

今天来聊一个平时用的不多,但是很多框架或者基础库会用到的语言特性--反射,反射并不是Go语言独有的能力,其他编程语言都有。这篇文章的目标是简单地给大家梳理一下反射的应用场景和使用方法。我们平时写... 查看详情

go语言接口(详解)(代码片段)

...章目录前言一、Go语言接口概述1.底层实现、如何定义及实战应用底层实现定义一个Go语言接口实战应用(利用接口实现多态)2.实现接口两种方式的区别①值接受者②指针接受者二、类型断言1.什么是空接口?2.类型断... 查看详情

《unity2d与3d手机游戏开发实战》上架了。

...#xff0c;一个3DRPG的简单例子和一个尽可能用插件实现的射击游戏的例子。书很薄,不过因为是彩页印刷,价钱不是那么实惠。不过说实话,因为这类书里面有很多图片,彩页印刷了以后看着是要舒服很多。书早些时... 查看详情

go语言实战go语言并发爬虫(代码片段)

文章目录写在前面1.单线程爬虫2.多线程爬虫2.1channel2.2sync.WaitGroup3.源码地址写在前面这篇文章主要让大家明白多线程爬虫,因为go语言实现并发是很容易的。这次的服务端,是我们之前搭建的电子商城平台,所以我们... 查看详情