Unity联网之使用Socket简单实现多人在线聊天室(一)

「前言」
之前在一直在写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

视屏地址:UnitySocket异步聊天室

努力积才能,壹叶便成名,喜欢我关注我!

原创出品,转载请注明出处。

  • 19
    点赞
  • 116
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 27
    评论
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铸梦xy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值