游戏开发实战Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)
Posted 林新发
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏开发实战Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)相关的知识,希望对你有一定的参考价值。
文章目录
- 一、前言
- 二、简单的Socket通信:多人聊天室
- 三、拓展:Mirror Networking
- 四、完毕
一、前言
嗨,大家好,我是新发。
事情是这样的,上次有同学问我能不能出一期 网络 相关的教程,
然而我眼花看错了,看成了 网格,我还专门写了一篇文章:《【游戏开发进阶】Unity网格探险之旅(Mesh | 动态合批 | 骨骼动画 | 蒙皮 )》
直到有同学在评论里提醒我,真是尴尬…
嘛,没事,今天就补上,写一篇 网络 相关文章。
我准备做个例子,使用.Net
原生的Socket
模块来实现简单的多人聊天室功能。
话不多说,我们开始吧~
二、简单的Socket通信:多人聊天室
Unity
中我们要实现网络通信,可以使用.Net
的Socket
模块来实现。
为了演示,我就用python
写个简单的服务端,用Unity
作为客户端。
先画个 流程图。
服务端(python
)流程图:
客户端(Unity
)流程图:
1、服务端:python代码
新建一个python
脚本:game_server.py
,如下
1.1、import socket
因为我们要使用socket
,所以先引入socket
模块:
import socket
1.2、构造socket对象
g_socket_server = None
g_socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
关于socket
的python
函数原型可以使用help(socket)
查看,
第一个参数是socket domains
(通信协议族),有两种类型:AF_UNIX
、AF_INET
,它们的区别:
通信协议族 | 说明 |
---|---|
AF_UNIX | 本机通信;另,它只能够用于单一的Unix 系统进程间通信,不能在Windows 系统中使用 |
AF_INET | TCP/IP 通信 |
第二个参数是socket type
(套接字类型),有SOCKET_STREAM
、SOCK_DGRAM
、SOCK_RAW
三种,
套接字类型 | 说明 |
---|---|
SOCKET_STREAM | 流式套接字,基于TCP 通信,数据有保障(即能保证数据正确传送到对方),多用于资料(如文件)传送 |
SOCK_DGRAM | 数据报套接字,基于UDP 通信,数据是无保障的 , 主要用于在网络上发广播信息 |
SOCK_RAW | 原始套接字,普通的套接字无法处理ICMP 、IGMP 等网络报文,而SOCK_RAW 可以;SOCK_RAW 也可以处理特殊的IPv4 报文;此外,利用原始套接字,可以通过IP_HDRINCL 套接字选项由用户构造IP头 |
1.3、绑定/监听端口
ADDRESS = ('127.0.0.1', 8712)
g_socket_server.bind(ADDRESS)
g_socket_server.listen(5)
1.3、监听客户端连接
client, info = g_socket_server.accept()
1.4、接收客户端socket消息
data = client.recv(1024)
msg = data.decode(encoding='utf8')
使用json
对消息字段进行解析:
import json
jd = json.loads(jsonstr)
protocol = jd['protocol']
uname = jd['uname']
msg = jd['msg']
1.5、多线程
由于监听客户端(socket.accept
)和接收消息(socket.recv
)都是 阻塞 的,为了不阻塞主线程,我们使用 子线程 来处理。
创建不带参数的线程:
thread = Thread(target=thread_func)
thread.start()
def thread_func():
pass
创建带参数的线程:
thread = Thread(target=thread_func, args=(p1, p2, p3))
thread.start()
def thread_func(p1, p2, p3):
pass
1.6、完整代码:game_server.py
最终,game_server.py
完整代码如下:
'''
作者:林新发,博客:https://blog.csdn.net/linxinfa
功能:简单的Socket通信,聊天室服务端
python版本:3.6.4
'''
import socket # 导入 socket 模块
from threading import Thread
import time
import json
ADDRESS = ('127.0.0.1', 8712) # 绑定地址
g_socket_server = None # 负责监听的socket
g_conn_pool = {} # 连接池
def accept_client():
global g_socket_server
g_socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
g_socket_server.bind(ADDRESS)
g_socket_server.listen(5) # 最大等待数(有很多人理解为最大连接数,其实是错误的)
print("server start,wait for client connecting...")
'''
接收新连接
'''
while True:
client, info = g_socket_server.accept() # 阻塞,等待客户端连接
# 给每个客户端创建一个独立的线程进行管理
thread = Thread(target=message_handle, args=(client, info))
thread.setDaemon(True)
thread.start()
def message_handle(client, info):
'''
消息处理
'''
handle_id = info[1]
# 缓存客户端socket对象
g_conn_pool[handle_id] = client
while True:
try:
data = client.recv(1024)
jsonstr = data.decode(encoding='utf8')
jd = json.loads(jsonstr)
protocol = jd['protocol']
uname = jd['uname']
if 'login' == protocol:
print('on client login, ' + uname)
# 转发给所有客户端
for u in g_conn_pool:
g_conn_pool[u].sendall((uname + " 进入了房间").encode(encoding='utf8'))
elif 'chat' == protocol:
# 收到客户端聊天消息
print(uname + ":" + jd['msg'])
# 转发给所有客户端
for key in g_conn_pool:
g_conn_pool[key].sendall((uname + " : " + jd['msg']).encode(encoding='utf8'))
except Exception as e:
remove_client(handle_id)
break
def remove_client(handle_id):
client = g_conn_pool[handle_id]
if None != client:
client.close()
g_conn_pool.pop(handle_id)
print("client offline: " + str(handle_id))
if __name__ == '__main__':
# 新开一个线程,用于接收新连接
thread = Thread(target=accept_client)
thread.setDaemon(True)
thread.start()
# 主线程逻辑
while True:
time.sleep(0.1)
2、客户端:Unity
2.1、创建工程,搭建场景
新建一个Unity
工程,
使用UGUI
简单搭建一下界面,如下
养成好习惯,界面保存为预设:TestPanel.prefab
,
2.2、Socket封装:ClientSocket.cs
我们先封装一个ClientSocket.cs
,实现Socket
的创建、连接和收发消息等功能。
2.2.1、构造Socket对象
// using System.Net.Sockets;
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
2.2.2、连接服务器
socket.Connect(host, port);
2.2.3、断开连接
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
2.2.4、发送消息
// byte[] bytes 你的消息的字节数组
NetworkStream netstream = new NetworkStream(socket);
netstream.Write(bytes, 0, bytes.Length);
2.2.5、接收服务端消息
// 回调函数对象
AsyncCallback recvCb = new AsyncCallback(RecvCallBack);
// 数据缓存
byte[] recvBuff = new byte[0x4000];
// 消息队列
Queue<string> msgQueue = new Queue<string>();
// 每帧调用此方法
socket.BeginReceive(recvBuff, 0, recvBuff.Length, SocketFlags.None, recvCb, this);
// 接收消息回调函数
private void RecvCallBack(IAsyncResult ar)
{
var len = socket.EndReceive(ar);
byte[] msg = new byte[len];
Array.Copy(m_recvBuff, msg, len);
var msgStr = System.Text.Encoding.UTF8.GetString(msg);
// 将消息塞入队列中
msgQueue.Enqueue(msgStr);
}
// 从消息队列中取出消息(供外部调用)
public string GetMsgFromQueue()
{
if (msgQueue.Count > 0)
return msgQueue.Dequeue();
return null;
}
2.2.6、完整代码:ClientSocket.cs
最终,ClientSocket.cs
完整代码如下:
/*
* Socket封装
* 作者:林新发 博客:https://blog.csdn.net/linxinfa
*/
using System;
using System.Net.Sockets;
using UnityEngine;
using System.Collections.Generic;
public class ClientSocket
{
private Socket init()
{
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 接收的消息数据包大小限制为 0x4000 byte, 即16KB
m_recvBuff = new byte[0x4000];
m_recvCb = new AsyncCallback(RecvCallBack);
return clientSocket;
}
/// <summary>
/// 连接服务器
/// </summary>
/// <param name="host">ip地址</param>
/// <param name="port">端口号</param>
public void Connect(string host, int port)
{
if (m_socket == null)
m_socket = init();
try
{
Debug.Log("connect: " + host + ":" + port);
m_socket.SendTimeout = 3;
m_socket.Connect(host, port);
connected = true;
}
catch (Exception ex)
{
Debug.LogError(ex);
}
}
/// <summary>
/// 发送消息
/// </summary>
public void SendData(byte[] bytes)
{
NetworkStream netstream = new NetworkStream(m_socket);
netstream.Write(bytes, 0, bytes.Length);
}
/// <summary>
/// 尝试接收消息(每帧调用)
/// </summary>
public void BeginReceive()
{
m_socket.BeginReceive(m_recvBuff, 0, m_recvBuff.Length, SocketFlags.None, m_recvCb, this);
}
/// <summary>
/// 当收到服务器的消息时会回调这个函数
/// </summary>
private void RecvCallBack(IAsyncResult ar)
{
var len = m_socket.EndReceive(ar);
byte[] msg = new byte[len];
Array.Copy(m_recvBuff, msg, len);
var msgStr = System.Text.Encoding.UTF8.GetString(msg);
// 将消息塞入队列中
m_msgQueue.Enqueue(msgStr);
// 将buffer清零
for (int i = 0; i < m_recvBuff.Length; ++i)
{
m_recvBuff[i] = 0;
}
}
/// <summary>
/// 从消息队列中取出消息
/// </summary>
/// <returns></returns>
public string GetMsgFromQueue()
{
if (m_msgQueue.Count > 0)
return m_msgQueue.Dequeue();
return null;
}
/// <summary>
/// 关闭Socket
/// </summary>
public void CloseSocket()
{
Debug.Log("close socket");
try
{
m_socket.Shutdown(SocketShutdown.Both);
m_socket.Close();
}
catch(Exception e)
{
//Debug.LogError(e);
}
finally
{
m_socket = null;
connected = false;
}
}
public bool connected = false;
private byte[] m_recvBuff;
private AsyncCallback m_recvCb;
private Queue<string> m_msgQueue = new Queue<string>();
private Socket m_socket;
}
2.3、UI交互:TestPanel.cs
然后再创建一个脚本:TestPanel.cs
,用于实现UI
部分的交互逻辑。
2.3.1、定义变量
先定义一些变量:
private const string IP = "127.0.0.1";
private const int PORT = 8712;
// 用户名输入
public InputField unameInput;
// 消息输入
public InputField msgInput;
// 登录按钮
public Button loginBtn;
// 发送按钮
public Button sendBtn;
// 连接状态文本
public Text stateTxt;
// 连接按钮文本
public Text connectBtnText;
// 聊天室聊天文本
public Text chatMsgTxt;
// 封装的ClientSocket对象
private ClientSocket clientSocket = new ClientSocket();
2.3.2、登录服务端
// 连接
clientSocket.Connect(IP, PORT);
stateTxt.text = clientSocket.connected ? "已连接" : "未连接";
connectBtnText.text = clientSocket.connected ? "断开" : "连接";
if (clientSocket.connected)
unameInput.enabled = false;
// 登录
Send("login");
2.3.3、断开连接
clientSocket.CloseSocket();
stateTxt.text = "已断开";
connectBtnText.text = "连接";
unameInput.enabled = true;
2.3.4、发送消息
这里用了一个迷你版的
json
库:JSONConvert
,源码可以参见我之前写的这篇文章:《用C#实现一个迷你json库,无需引入dll(可直接放到Unity中使用)》
private void Send(string protocol, string msg = "")
{
JSONObject jsonObj = new JSONObject();
jsonObj["protocol"] = protocol;
jsonObj["uname"] = unameInput.text;
jsonObj["msg"] = msg;
// JSONObject转string
string jsonStr = JSONConvert.SerializeObject(jsonObj);
// string转byte[]
byte[] data = System.Text.Encoding.UTF8.GetBytes(jsonStr);
// 发送消息给服务端
clientSocket.SendData(data);
}
2.3.5、接收消息
private void Update()
{
if (clientSocket.connected)
{
clientSocket.BeginReceive();
}
var msg = clientSocket.GetMsgFromQueue();
if (!string.IsNullOrEmpty(msg))
{
// 显示到聊天室文本中
chatMsgTxt.text += msg + "\\n";
Debug.Log("RecvCallBack: " + msg);
}
}
2.3.6、完整代码:TestPanel.cs
最终,TestPanel.cs
完整代码如下:
/*
* 聊天室客户端 UI交互
* 作者:林新发 博客:https://blog.csdn.net/linxinfa
*/
using UnityEngine;
using UnityEngine.UI;
public class TestPanel : MonoBehaviour
{
private const string IP = "127.0.0.1";
private const int PORT = 8712;
// 用户名输入
public InputField unameInput;
// 消息输入
public InputField msgInput;
// 登录按钮
public Button loginBtn;
// 发送按钮
public Button sendBtn;
// 连接状态文本
public Text stateTxt;
// 连接按钮文本
public Text connectBtnText;
// 聊天室聊天文本
public Text chatMsgTxt;
游戏开发实战用Go语言写一个服务器,实现与Unity客户端通信(Golang | Unity | Socket | 通信 | 教程 | 附工程源码)
游戏开发实战用Go语言写一个服务器,实现与Unity客户端通信(Golang | Unity | Socket | 通信 | 教程 | 附工程源码)
游戏开发实战教你Unity通过sproto协议与Skynet框架的服务端通信,附工程源码(Unity | Sproto | 协议 | Skynet)
游戏开发实战教你Unity通过sproto协议与Skynet框架的服务端通信,附工程源码(Unity | Sproto | 协议 | Skynet)