「前言」
之前在一直在写lua联网等一些知识,虽然lua重要,但C#联网也必不可少是吧。所以呢,本篇博客就主要介绍如何使用Unity和C#在实现多人在线聊天室。
服务器 客户端工作原理:(通过消息类型控制)
●登陆消息:Login
客户端向服务器发起请求加入消息,服务器收到后回给客户端一个允许加入的消息。同时,在发给房间的所有人一条某某人加入房间的消息,客户端收到后,把收到的信息显示出来。
●聊天消息:Chat
客户端把聊天的信息发给服务器,服务器收到后转发给所有人。
●登出消息:LogOut
客户端向服务器发送一条请求退出的消息,服务器收到后回复一条允许退出的消息,同时在告诉所有人这个人退出了房间。
工程流程:
● 搭建UI界面
● 制定消息协议
● 编写服务器
● 编写客户端
注意事项:Unity虽然支持线程的使用,但是我们要尽量避免在Unity中去使用线程。因为这对于手机配置低的机器来说运行起来会很卡。当然,如果你希望手机变成暖手宝,可以多开几个线程试一下。
效果如下:
● 搭建UI界面
参考上图,随意搭建,快乐就好。
● 制定消息协议
每一条消息都是通过创建消息对象,设置消息类型,和消息内容组成。服务器和客户端都必须拥有这个消息协议。
参考如下:
/// <summary>
/// 简单的协议类型
/// </summary>
public enum MessageType
{
Chat = 0,//聊天
Login = 1,//登陆
LogOut = 2,//登出
}
/// <summary>
/// 消息体
/// </summary>
public class MessageData
{
/// <summary>
/// 消息类型
/// </summary>
public MessageType msgType;
/// <summary>
/// 消息内容
/// </summary>
public string msg;
}
● 编写服务器
在有用户连接成功时,服务器会自动创建该用户的客户端管理器,并记录用户的客户端管理器。
参考如下:
//N0.0-----------------------
// 用户连接控制器
//N0.0-----------------------
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Net;
namespace Server
{
class Program
{
/// <summary>
/// 客户端管理列表
/// </summary>
public static List<ClientController> clientControllerList = new List<ClientController>();
static void Main(string[] args)
{
//定义socket
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint ipendPoint = new IPEndPoint(IPAddress.Parse("192.168.213.50"), 10004);
Console.WriteLine("开始绑定端口号....");
//将ip地址和端口号绑定
serverSocket.Bind(ipendPoint);
Console.WriteLine("绑定端口号成功,开启服务器....");
//开启服务器
serverSocket.Listen(100);
Console.WriteLine("启动服成功!");
while(true)
{
Console.WriteLine("等待链接.....");
Socket clinetSocket = serverSocket.Accept();
ClientController controller = new ClientController(clinetSocket);
//添加到列表中
clientControllerList.Add(controller);
Console.WriteLine("当前有" + clientControllerList.Count + "个用户");
Console.WriteLine("有一个用户链接....");
}
}
}
}
服务器在接收到信息时会根据消息类型而定做出回应,或转发他人,或回给客户端。
//N0.1-----------------------
// 客户端控制器
//N0.1-----------------------
using System;
using System.Net.Sockets;
using System.Threading;
namespace Server
{
class ClientController
{
/// <summary>
/// 用户链接的通道
/// </summary>
private Socket clientSocket;
/// <summary>
/// 昵称
/// </summary>
public string nickName;
//接收的线程
Thread receiveThread;
public ClientController(Socket socket)
{
clientSocket = socket;
//启动接收的方法
//开始收的线程
receiveThread = new Thread(ReceiveFromClient);
//启动收的线程
receiveThread.Start();
}
/// <summary>
///
/// </summary>
void ReceiveFromClient()
{
while (true)
{
byte[] buffer = new byte[512];
int lenght = clientSocket.Receive(buffer, 0, buffer.Length, SocketFlags.None);
Console.WriteLine("接收长度=" + lenght);
string json = System.Text.Encoding.UTF8.GetString(buffer,0, lenght);
json.TrimEnd();
if (json.Length>0)
{
Console.WriteLine("json=" + json);
MessageData data = LitJson.JsonMapper.ToObject<MessageData>(json);
switch (data.msgType)
{
case MessageType.Login://如果是登陆消息
nickName = data.msg;
//1、告诉这个客户端,你登陆成功了!
MessageData backData = new MessageData();
backData.msgType = MessageType.Login;
backData.msg = "";
SendToClient(backData);
//2、需要告诉所有客户端,***加入了房间
MessageData chatData = new MessageData();
chatData.msgType = MessageType.Chat;
chatData.msg = nickName + " 进入了房间";
SendMessageDataToAllClientWithOutSelf(chatData);
break;
case MessageType.Chat://如果是聊天消息
//转发聊天信息
MessageData chatMessageData = new MessageData();
chatMessageData.msgType = MessageType.Chat;
chatMessageData.msg = nickName + ":" + data.msg;
SendMessageDataToAllClientWithOutSelf(chatMessageData);
break;
case MessageType.LogOut://客户端请求退出
//1、回给这个用户一个消息,告诉用你你可以退出了
MessageData logOutData = new MessageData();
logOutData.msgType = MessageType.LogOut;
SendToClient(logOutData);
//2、告诉所有的其他用户,**退出了房间
MessageData logOutChatData = new MessageData();
logOutChatData.msgType = MessageType.Chat;
logOutChatData.msg = nickName + " 退出了房间";
SendMessageDataToAllClientWithOutSelf(logOutChatData);
break;
}
}
}
}
/// <summary>
/// 广播信息,告诉所有用户有什么信息过来了!
/// </summary>
/// <param name="data"></param>
void SendMessageDataToAllClient(MessageData data)
{
for(int i = 0;i<Program.clientControllerList.Count;i++)
{
try
{
Program.clientControllerList[i].SendToClient(data);
}
catch(Exception e)
{
i--;
}
}
}
/// <summary>
/// 广播消息,排除掉自己
/// </summary>
/// <param name="data"></param>
void SendMessageDataToAllClientWithOutSelf(MessageData data)
{
//5 i=3
for (int i = 0; i < Program.clientControllerList.Count; i++)
{
if(Program.clientControllerList[i] != this)
{
Program.clientControllerList[i].SendToClient(data);
}
}
}
void SendToClient(MessageData data)
{
//把对象转换为json字符串
string msg = LitJson.JsonMapper.ToJson(data);
//把json字符串转换byte数组
byte[] msgBytes = System.Text.Encoding.UTF8.GetBytes(msg);
//调用发送字节的方法
SendToClient(msgBytes);
}
/// <summary>
/// 发消息给客户端
/// </summary>
/// <param name="msgByte">需要发送的内容</param>
void SendToClient(byte[] msgBytes)
{
//确保发送的是拼接后的信息
int sendLength = clientSocket.Send(msgBytes);
Console.WriteLine("服务器发送信息结束,成功发送:" + sendLength);
Thread.Sleep(50);
}
/// <summary>
/// 销毁自己,也就是释放资源
/// </summary>
public void DestroySelf()
{
try
{
receiveThread.Abort();
}
catch(Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
}
● 编写客户端
客户端在进行发送消息时会生成消息对象,发送之前会把消息对象换成Json字符串,在把Json转换为Byte字节流进行最终的发送。当然服务器也是同理。服务器接收到字节流时会将字节流转为字符串(Json),并且对字符串进行解析,最终得到的将是一个对象。该对象里包含客户端发来的消息内容,和消息类型。同时,这也属于一个简单的深度克隆。
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System;
using LitJson;
/// <summary>
/// 声明一个委托对象
/// </summary>
/// <param name="data">接收到的数据</param>
public delegate void ReceiveMessageData(byte[] buffer,int offset,int size);
/// <summary>
/// 当连接改变
/// </summary>
public delegate void OnConnectChange();
public class ClientSocket : MonoBehaviour {
/// <summary>
/// 客户端Socket
/// </summary>
Socket clientSocket;
/// <summary>
/// 数据缓冲池
/// </summary>
private byte[] buffer = new byte[10000];
/// <summary>
/// 委托变量
/// </summary>
public ReceiveMessageData receiveMessageData;
/// <summary>
/// 链接成功
/// </summary>
public OnConnectChange onConnectSuccess;
/// <summary>
/// 链接异常/链接断开
/// </summary>
public OnConnectChange onConnectExcept;
void Start () {
//创建socket对象
clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
ConnectToServer();
}
public void ConnectToServer()
{
IPEndPoint ipendPoint = new IPEndPoint(IPAddress.Parse("172.0.0.1"), 10004);
Debug.Log("开始链接服务器!!!");
//请求链接
clientSocket.BeginConnect(ipendPoint, ConnectCallback, "");
}
/// <summary>
/// 链接的回调
/// </summary>
/// <param name="ar"></param>
public void ConnectCallback(IAsyncResult ar)
{
Debug.Log("ar.AsyncState=" + ar.AsyncState + ",clientSocket.Connected=" + clientSocket.Connected);
try
{
clientSocket.EndConnect(ar);
}
catch(Exception e)
{
Debug.Log(e.ToString());
}
//判断到底链接成功了还是没有
Debug.Log("链接回调!!!!!!!!!!");
if(clientSocket.Connected == true)
{
//调用链接成功的回调
onConnectSuccess();
//链接成功
Debug.Log("链接成功");
//开启接收消息
ReceiveMessageFromServer();
}
else
{
//链接失败
Debug.Log("链接失败");
}
}
/// <summary>
/// 发送消息的方法
/// </summary>
/// <param name="sendMsgContent">消息内容</param>
/// <param name="offset">从消息内容第几个开始发送</param>
/// <param name="size">发送的长度</param>
public void SendBytesMessageToServer(byte[] sendMsgContent, int offset, int size)
{
Debug.Log("开始发送!!准备发送长度=" + size);
clientSocket.BeginSend(sendMsgContent, offset, size, SocketFlags.None, SendMessageCallback, "");
}
/// <summary>
/// 发送一g给服务器
/// </summary>
/// <param name="msg"></param>
public void PutMessageToQueue(MessageData data)
{
//将对象序列化发过去
byte[] msgBytes = System.Text.Encoding.UTF8.GetBytes(JsonMapper.ToJson(data));
SendBytesMessageToServer(msgBytes, 0, msgBytes.Length);
Debug.Log("开始发送的字节为:"+ msgBytes);
}
/// <summary>
/// 发送一条文字信息给服务器
/// </summary>
/// <param name="msg"></param>
public void PutMessageToQueue(string msg)
{
MessageData msgdata = new MessageData();
msgdata.msgType = MessageType.Chat;
msgdata.msg = msg;
//将对象序列化发过去
byte[] msgBytes = System.Text.Encoding.UTF8.GetBytes(JsonMapper.ToJson(msgdata));
SendBytesMessageToServer(msgBytes, 0, msgBytes.Length);
}
/// <summary>
/// 发送消息的回调
/// </summary>
/// <param name="ar"></param>
public void SendMessageCallback(IAsyncResult ar)
{
Debug.Log("发送结束!!!");
//停止发送
int length = clientSocket.EndSend(ar);
Debug.Log("发送的长度:" + length);
}
/// <summary>
/// 从服务器开始接收信息
/// </summary>
public void ReceiveMessageFromServer()
{
Debug.Log("开始接收数据");
clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveMessageCallback, "");
}
/// <summary>
/// 接收回调
/// </summary>
/// <param name="ar"></param>
public void ReceiveMessageCallback(IAsyncResult ar)
{
Debug.Log("接收结束!!");
//结束接收
int length = clientSocket.EndReceive(ar);
Debug.Log("接收的长度是:" + length);
string msg = System.Text.Encoding.UTF8.GetString(buffer, 0, length);
Debug.Log("服务器发过来的消息是:" + msg);
if(receiveMessageData != null)
{
receiveMessageData(buffer, 0, length);
}
//开启下一次消息的接收
ReceiveMessageFromServer();
}
public void OnDestroy()
{
//该释放的内容释放掉
Debug.Log("释放链接资源");
//clientSocket.Disconnect(true);
//clientSocket.Dispose();
}
}
在上面的客户端代码中我使用了委托让ClientSocket脱离了出来,方式就是设计模式中的单一原则。降低了耦合度。这样增加了代码的可移植性。下面是UI聊天控制源码。
参考如下:
//N0.1-----------------------
// 聊天控制器
//N0.1-----------------------
using UnityEngine;
using UnityEngine.UI;
public class ChatUIController : MonoBehaviour {
/// <summary>
/// 要发送的内容
/// </summary>
public InputField sendMsgInputField;
/// <summary>
/// 昵称
/// </summary>
public InputField nickNameInputField;
/// <summary>
/// socket控制对象
/// </summary>
public ClientSocket clientSocket;
//显示消息的文本
public Text text;
//接收的消息
public string receiveMsg;
/// <summary>
/// 界面 0==loading 1==登陆 2==聊天
/// </summary>
public GameObject[] panels;
/// <summary>
/// 界面状态 0==loading 1==登陆 2==聊天
/// </summary>
public int panelState = 0;
void Start () {
//委托和具体方法关联
clientSocket.receiveMessageData += ReceiveMsgData;
clientSocket.onConnectSuccess += OnSocketConnectSuccess;
}
void Update () {
//聊天信息的显示
text.text = receiveMsg;
//首先把所有的界面关闭掉
panels[0].SetActive(panelState == 0);
panels[1].SetActive(panelState == 1);
panels[2].SetActive(panelState == 2);
}
/// <summary>
/// 发送按钮的点击
/// </summary>
public void SendBtnClick()
{
if(sendMsgInputField != null && sendMsgInputField.text != "")
{
//发送
clientSocket.PutMessageToQueue(sendMsgInputField.text);
receiveMsg += "我:" + sendMsgInputField.text + "\n";
//清理一下输入框内容
sendMsgInputField.text = "";
}
}
/// <summary>
/// 接收消息的方法
/// </summary>
/// <param name="byteArray"></param>
/// <param name="offset"></param>
/// <param name="length"></param>
public void ReceiveMsgData(byte[] byteArray, int offset, int length)
{
Debug.Log("!!!!!!!!!!!!!!!!!!!!!!");
string msg = ToolsUtil.ByteArrayToString(byteArray, offset, length);
Debug.Log("收到信息:" + msg);//xml
//receiveMsg += "服务器发来信息:" + msg + "\n";
//对信息进行处理
MessageData data = LitJson.JsonMapper.ToObject<MessageData>(msg);
switch (data.msgType)
{
case MessageType.Login://如果是登陆,代表界面可以切换了
receiveMsg = "";
panelState = 2;
break;
case MessageType.Chat://如果是聊天,代表进行聊天的显示
receiveMsg += data.msg + "\n";
break;
case MessageType.LogOut://退出消息
panelState = 1;
break;
}
}
/// <summary>
/// 链接成功的回调
/// </summary>
public void OnSocketConnectSuccess()
{
//进入登陆界面
panelState = 1;
}
/// <summary>
/// 加入房间按钮点击
/// </summary>
public void JoinInBtnClick()
{
if(nickNameInputField != null && nickNameInputField.text != "")
{
//创建数据对象
MessageData data = new MessageData();
data.msgType = MessageType.Login;
data.msg = nickNameInputField.text;
//发送数据对象
clientSocket.PutMessageToQueue(data);
}
else
{
//提示
Debug.Log("昵称不能为空!");
}
}
/// <summary>
/// 退出房间点击事件
/// </summary>
public void LogOutBtnClick()
{
//消息数据
MessageData data = new MessageData();
data.msgType = MessageType.LogOut;
//把消息传进去
clientSocket.PutMessageToQueue(data);
}
}
回顾一下:我们从搭建UI界面,制定消息协议,编写服务器,编写客户端,至此,实现了简单的多人联网聊天。其中不掺杂粘包,拆包,和队列收发。当然这些会在后面的博客中陆续更新出来。
Demo:聊天demo
努力积才能,壹叶便成名,喜欢我关注我!
原创出品,转载请注明出处。