【Unity】Socket网络通信(TCP) - 实现简单的多人聊天功能

这篇具有很好参考价值的文章主要介绍了【Unity】Socket网络通信(TCP) - 实现简单的多人聊天功能。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

多客户端连接服务器其原理是在服务端保存客户端连入后与客户端通信的socket,由于等待客户端连接会阻塞主线程,所以结合多线程就能实现多客户端连入功能。多人聊天只需要将A客户端发来的消息,转发给除A客户端外的其他客户端,即可实现。如果你还不怎么熟悉服务端和客户端的通信流程,可以看一下我的这两篇文章。
【Unity】Socket网络通信(TCP) - 最基础的C#服务端通信流程
【Unity】Socket网络通信(TCP) - 最基础的客户端通信流程
这篇文章只实现了简单的发送String类型的消息,发送复杂的消息根据需求封装一个消息类,再把消息类对象序列化成对应的字节数组进行发送,接收方收到字节数组再根据对应的方法反序列化成消息类对象就行。
效果如下
unity socket,Unity,unity,tcp/ip,c#

服务端程序逻辑

封装客户端连入时返回的Socket

新建一个C#的控制台项目,添加一个类封装一下客户端连入时返回的socket,让其拥有自己的发送消息、接收消息、释放连接的方法,由于我们还需要实现消息的转发功能,所以我们还需要定义一个客户端的ID,让每个连入服务器的客户端都拥有唯一的ID。
需要注意的是客户端连入时返回的Socket是服务端用于和客户端通信的socket,并不是客户端的socket。
代码如下:

using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace TCPServer
{
    public class ClientSocket
    {
    	//定义一个静态的变量用于赋予客户端ID
        public static int CLIENT_BEGIN_ID = 1;
        //客户端ID
        public int clientID;
        //客户端连入返回的socket
        private Socket socket;
        //封装好的服务端的socket,调用其封装好的转发消息功能(后面再封装这个ServerSocket类)
        private ServerSocket serverSocket;

		//构造函数中传入客户端连入返回的socket和服务端的ServerSocket对象
        public ClientSocket(Socket clientSocket, ServerSocket serverSocket)
        {
        	//记录一下socket
            socket = clientSocket;
            this.serverSocket = serverSocket;
			//记录ID
            clientID = CLIENT_BEGIN_ID;
            ++CLIENT_BEGIN_ID;
        }

        //发送消息,这里的发送消息是指服务端给客户端发送消息
        public void SendMsg(string msg)
        {
        	//将string类型消息序列化成字节数组并发送
            if(socket != null)
                socket.Send(Encoding.UTF8.GetBytes(msg));
        }

        //接收消息
        public void ReceiveClientMsg()
        {
            if (socket == null)
                return;

			//判断一下客户端即将收到消息的数量,如果没有消息,就不需要接收处理
            if(socket.Available > 0)
            {
            	//定义一个字节数组来装载收到的消息
                byte[] msgBytes = new byte[1024];
                //接收消息,返回的是消息长度,反序列化时需要用到
                int msgLength = socket.Receive(msgBytes);
                //BroadcastMsg这个是服务端socket封装的广播消息方法,将消息和客户端ID传进去进行消息转发
                serverSocket.BroadcastMsg(Encoding.UTF8.GetString(msgBytes, 0, msgLength), clientID);
            }
        }
        
        //释放连接
        public void Close()
        {
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
            socket = null;
        }
    }
}

封装服务端Socket

服务端的socket需要有以下几个功能,保存客户端连入后返回的和客户端通信的socket、持续等待客户端连接、持续接收客户端发来的消息、广播消息等功能。
因为等待客户端连入的方法Accept是阻塞式的,不开新线程的话,会阻塞主线程进行,而且无法持续不断地监听客户端连接,所以需要开启一个新的线程。
广播消息不用转发给发送消息的客户端,也就是例如有A、B、C客户端连入了服务器,A客户端发送了消息,服务端进行转发则不需要转发给A客户端。
代码如下:

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace TCPServer
{
    public class ServerSocket
    {
    	//服务端socket
        private Socket socket;
        //是否已经关闭socket释放连接
        private bool isClose;
        //保存所有连入客户端与其通信的socket
        private List<ClientSocket> clientList = new List<ClientSocket>();

		//传入IP地址,端口号和最大连入客户端数量
        public void Start(string ip, int port, int clientNum)
        {
        	//建立连接开始,改变状态
            isClose = false;
            
            //创建socket
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            
            //绑定IP地址和端口号
            IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);
            socket.Bind(iPEndPoint);
            
            Console.WriteLine("服务器启动成功...IP:{0},端口:{1}", ip, port);
            Console.WriteLine("开始监听客户端连接...");
            //设置监听数量
            socket.Listen(clientNum);

			//开启线程,持续监听客户端连接
            ThreadPool.QueueUserWorkItem(AcceptClientConnect);
            //开启线程,持续监听连入客户端是否发送了消息
            ThreadPool.QueueUserWorkItem(ReceiveMsg);
        }

        //等待客户端连接(新线程)
        private void AcceptClientConnect(object obj)
        {
            Console.WriteLine("等待客户端连入...");
            //死循环,持续不断地监听客户端连接
            while (!isClose)
            {
            	//这里会阻塞线程,当有客户端连入时,返回一个socket与客户端通信,才会执行接下来的逻辑
                Socket clientSocket = socket.Accept();
                //有客户端连入了,创建一个我们上面封装好了的ClientSocket对象
                ClientSocket client = new ClientSocket(clientSocket, this);
                Console.WriteLine("客户端{0}连入...", clientSocket.RemoteEndPoint.ToString());
                //向客户端发送一条欢迎消息
                client.SendMsg("欢迎连入服务端...");
                //将客户端添加到List,后续和客户端通信可以从这个List里面取
                clientList.Add(client);
            }
        }

        //接收消息(新线程)
        private void ReceiveMsg(object obj)
        {
            int i;
            //持续不断地监听客户端有没有发送消息
            while (!isClose)
            {
            	//当连入客户端数量大于0的时候才去接收消息
                if (clientList.Count > 0)
                {
                	//遍历所有连入的客户端
                    for(i = 0; i < clientList.Count; i++)
                    {
                        try
                        {
                        	//调用上面代码封装好的接收消息方法
                            clientList[i].ReceiveClientMsg();
                        }
                        catch(Exception e)
                        {
                            Console.WriteLine(e.Message);
                        }
                    }
                }
            }
        }

        //广播消息,传入消息,和发送消息的客户端的ID
        public void BroadcastMsg(string msg, int clientID)
        {
            if (isClose)
                return;
            //遍历所有连入的客户端
            for(int i = 0; i < clientList.Count; i++)
            {
            	//如果不是发送消息的客户端则转发消息,不用转发给发送消息的客户端
                if(clientList[i].clientID != clientID)
                    clientList[i].SendMsg(msg);
            }
        }

        //释放连接
        public void Close()
        {
            isClose = true;
            for(int i = 0; i < clientList.Count; i++)
            {
                clientList[i].Close();
            }

            clientList.Clear();

            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
            socket = null;
        }
    }
}

封装好了这两个类,只需要在入口函数创建ServerSocket对象并调用Start方法就可以了

using System;

namespace TCPServer
{
    class Program
    {
        private static ServerSocket serverSocket;
        static void Main(string[] args)
        {
            serverSocket = new ServerSocket();
            serverSocket.Start("127.0.0.1", 8080, 1024);

            while (true)
            {
                string inputStr = Console.ReadLine();
                if(inputStr == "Quit")
                {
                    serverSocket.Close();
                    break;
                }
                else if (inputStr.Substring(0, 2) == "B:")
                {
                    serverSocket.BroadcastMsg(inputStr.Substring(2), -1);
                }
            }
        }
    }
}

服务端的代码就写好了,以上代码没有实现客户端主动断开连接服务端的逻辑,还不是很完善,后续有时间还需要再完善一下。

Unity客户端逻辑

Unity客户端实现一个聊天面板,可以主动发送消息,封装一个网络模块用来连接服务器、收发消息。
unity socket,Unity,unity,tcp/ip,c#

网络模块封装

由于网络模块一般都是唯一的,我这里做成单例模式,方便使用和管理,因为要用到Unity的生命周期函数,所以继承了MonoBehaviour。
客户端的网络模块需要

  • 与服务器通信的socket
  • 发送消息队列
  • 接收消息队列
  • 存放收到的消息的字节数组
  • 连接服务器方法
  • 发送消息方法
  • 接收消息方法

当点击发送按钮时,将消息放到消息队列中,开启一个线程去发送消息,避免了网络不好或者是消息太大发送缓慢,一个消息没发完又有新消息要发送的问题,
代码如下:

//NetManager.cs

using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;

public class NetManager : MonoBehaviour
{
    public static NetManager Instance => instance;
    private static NetManager instance;

	//客户端socket
    private Socket socket;
	//是否连接状态
    private bool isConnect;
	//发送消息队列
    private Queue<string> sendQueue = new Queue<string>();
    //接收消息队列
    private Queue<string> receiveQueue = new Queue<string>();
	//存放收到消息的字节数组
    private byte[] receiveBytes = new byte[1024 * 1024];
    
	//UI面板(这里违背了单例模式的设计思想,由于项目比较简单,图方便就直接在这里得到UI面板了)
	//实际项目可以封装一个UI管理类(单例模式)管理UI更新,收到消息通过UI管理类去更新对应的UI面板
    private ChatPanel chatPanel;

    private void Awake()
    {
    	//初始化单例对象
        if(instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
    }

    private void Start()
    {
    	//得到UI面板(这里违背了单例模式的设计思想,由于项目比较简单,图方便就直接在这里得到UI面板了)
    	//实际项目可以封装一个UI管理类(单例模式)管理UI更新,收到消息通过UI管理类去更新对应的UI面板
        GameObject chatPanelObj = GameObject.Find("ChatPanel");
        chatPanel = chatPanelObj.GetComponent<ChatPanel>();
    }

    // Update is called once per frame
    void Update()
    {
    	//如果收到消息队列中存在消息,则让UI更新面板
        if(receiveQueue.Count > 0)
        {
	        //这里图方便,实际项目可以封装一个UI管理类(单例模式)管理UI更新,收到消息通过UI管理类去更新对应的UI面板
            chatPanel.UpdateChatInfo("他人:" + receiveQueue.Dequeue());
        }
    }

    /// <summary>
    /// 连接服务器
    /// </summary>
    /// <param name="ip">服务器IP地址</param>
    /// <param name="port">服务器程序端口号</param>
    public void ConnectServer(string ip, int port)
    {
        //如果在连接状态,就不执行连接逻辑了
        if (isConnect)
            return;

        //避免重复创建socket
        if(socket == null)
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        //连接服务器
        IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);

        try
        {
            socket.Connect(ipEndPoint);
        }
        catch (SocketException e)
        {
            print(e.ErrorCode + e.Message);
            return;
        }
        
        isConnect = true;

        //开启发送消息线程
        ThreadPool.QueueUserWorkItem(SendMsg_Thread);
        //开启接收消息线程
        ThreadPool.QueueUserWorkItem(ReceiveMsg_Thread);
    }

    //发送消息
    public void Send(string msg)
    {
        //将消息放入到消息队列中
        sendQueue.Enqueue(msg);
    }

    private void SendMsg_Thread(object obj)
    {
        while (isConnect)
        {
            //如果消息队列中有消息,则发送消息
            if(sendQueue.Count > 0)
            {
                socket.Send(Encoding.UTF8.GetBytes(sendQueue.Dequeue()));
            }
        }
    }

    //接收消息
    private void ReceiveMsg_Thread(object obj)
    {
        print("持续监听是否收到消息");
        int msgLength;
        while (isConnect)
        {
        	//判断有没有收到消息
            if(socket.Available > 0)
            {
                msgLength = socket.Receive(receiveBytes);
                print("接收到消息,长度为" + msgLength);
                //收到消息,反序列化后放入收到消息队列中,在Update中不停检测有没有收到的消息,收到了就让UI更新面板
                receiveQueue.Enqueue(Encoding.UTF8.GetString(receiveBytes, 0, msgLength));
            }
        }
    }

	//释放连接
    public void Close()
    {
        if (socket != null && isConnect)
        {
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();

            isConnect = false;
        }
    }

    private void OnDestroy()
    {
        Close();
    }
}

避免忘记挂单例对象的脚本,我这里直接新建一个Main的空物体挂载Main.cs脚本,后续所有继承MonoBehaviour的单例模式都可以通过这个脚本自动挂载。

//Main.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Main : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
    	//如果网络模块单例对象为空
        if (NetManager.Instance == null)
        {
        	//创建一个空物体
            GameObject netGameObj = new GameObject("Net");
            //挂载NetManager脚本
            netGameObj.AddComponent<NetManager>();
        }
		//连接服务器
        NetManager.Instance.ConnectServer("127.0.0.1", 8080);
    }
}

unity socket,Unity,unity,tcp/ip,c#

UI面板逻辑

UI面板很简单,就一个输入框,一个按钮,一个Scroll View来展示聊天记录,把这三个创建出来,摆好位置就行,我这里用了一个Panel来包裹住上面的三个UI控件。
unity socket,Unity,unity,tcp/ip,c#
Scroll View去掉横向的滚动条,Scroll View下面的Content添加一个竖向自动布局的组件Vertical Layout Group用来自动布局,增加Content Size Fitter组件控制Content大小自适应,将Vertical Fit调整为Preferred Size,将Content的锚点改为底部对齐。
unity socket,Unity,unity,tcp/ip,c#
创建一个Text预制体,用来在Scroll View中显示聊天内容
unity socket,Unity,unity,tcp/ip,c#
创建ChatPanel.cs挂在场景ChatPanel上

//ChatPanel.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class ChatPanel : MonoBehaviour
{
	//发送按钮
    public Button sendBtn;
    //输入框
    public InputField inputField;
    //Scroll View
    public ScrollRect sr;

    // Start is called before the first frame update
    void Start()
    {
    	//为按钮添加点击事件监听函数
        sendBtn.onClick.AddListener(() =>
        {
        	//当输入框的内容不是空时,发送消息到服务器,并更新Scroll View的聊天内容
            if(inputField.text != "")
            {
                NetManager.Instance.Send(inputField.text);
                UpdateChatInfo("我:" + inputField.text);
                //发送完将输入框的内容清空
                inputField.text = "";
            }
        });
    }

	//更新聊天内容
    public void UpdateChatInfo(string msgInfo)
    {
    	//在Scroll View中的Content中动态创建Text预制体
        Text chatInfoText = Instantiate(Resources.Load<Text>("UI/MsgInfoText"), sr.content);
        //修改Text的内容
        chatInfoText.text = msgInfo;
    }
}

客户端代码就完成了

测试

将Unity项目打包一个可执行文件,先运行服务器,依次打开客户端程序,就能完成多个客户端互相通信聊天!
unity socket,Unity,unity,tcp/ip,c#

总结

这次的客户端和服务端通信部分代码并不完善,比如服务端没做客户端主动断开的一些处理以及分包黏包的处理等等,只是TCP长连接的一个简单的雏形,如有哪些地方写的不够好的地方请多多包容,也欢迎指出,一起探讨、共同进步、无限进步!

工程文件我已上传到csdn资源,无需积分,需要点击自取!
⬇️⬇️⬇️
点击下载项目工程文件

以上就是这篇文章的所有内容了,此为个人学习记录,如有哪个地方写的有误,劳烦大佬指出,感谢,希望对各位看官有所帮助!文章来源地址https://www.toymoban.com/news/detail-778470.html

到了这里,关于【Unity】Socket网络通信(TCP) - 实现简单的多人聊天功能的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Socket网络编程(TCP/IP)实现服务器/客户端通信。

    一.前言 回顾之前进程间通信(无名管道,有名管道,消息队列,共享内存,信号,信号量),都是在同一主机由内核来完成的通信。 那不同主机间该怎么通信呢? 可以使用Socket编程来实现。 Socket编程可以通过网络来实现实现不同主机之间的通讯。 二.Socket编程的网络模型如

    2024年02月08日
    浏览(64)
  • Qt 实现简单的tcp网络通信

    背景: 最近计网要结课了,匆忙之间有个计网实验还没做。 上网这里查查那里查查,随便搞搞 然后在 这篇文章里找到了能够实现的代码 自己想着把它图形化一下,最后在超级棒棒糖的帮助下实现了。 工具头文件tool.h 该头文件用于添加一些要用到的库,直接引用这个库,比

    2024年02月01日
    浏览(26)
  • 网络通信(Socket/TCP/UDP)

    Socket(又叫套接字)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接协议,客户端的IP地址,客户端的端口,服务器的IP地址,服务器的端口。 一个Socket是一对IP地址和端口。 Socket可以看

    2024年01月22日
    浏览(43)
  • 【网络】socket——TCP网络通信 | 日志功能 | 守护进程

    🐱作者:一只大喵咪1201 🐱专栏:《网络》 🔥格言: 你只管努力,剩下的交给时间! 上篇文章中本喵介绍了UDP网络通信的socket代码,今天介绍TCP网络通信的socket代码。 和 udp 的网络通信一样, tcp 通信也需要服务器指定端口号,IP地址同样使用 0.0.0.0 ,以便客户端所有对服

    2024年02月16日
    浏览(28)
  • 【网络通信】socket编程——TCP套接字

    TCP依旧使用代码来熟悉对应的套接字,很多接口都是在udp中使用过的 所以就不会单独把他们拿出来作为标题了,只会把第一次出现的接口作为标题 通过TCP的套接字 ,来把数据交付给对方的应用层,完成双方进程的通信 在 tcpServer.hpp 中,创建一个命名空间 yzq 用于封装 在命名

    2024年02月13日
    浏览(29)
  • 【Java】网络编程与Socket套接字、UDP编程和TCP编程实现客户端和服务端通信

    为什么需要网络编程? 现在网络普及程序越来越高,网络上保存着我们日常生活中需要的各种资源,使用程序通过网络来获取这些资源的过程就需要网络编程来实现。 什么是网络编程? 网络编程,指网络上的主机,通过不同的进程以程序的方式实现网络通信(网络数据传输)

    2024年02月17日
    浏览(48)
  • 网络编程3——TCP Socket实现的客户端服务器通信完整代码(详细注释帮你快速理解)

    本人是一个刚刚上路的IT新兵,菜鸟!分享一点自己的见解,如果有错误的地方欢迎各位大佬莅临指导,如果这篇文章可以帮助到你,劳请大家点赞转发支持一下! 今天分享的内容是TCP流套接字实现的客户端与服务器的通信,一定要理解 DatagramSocket,DatagramPacket 这两个类的作用以及方法

    2024年02月12日
    浏览(27)
  • Java【网络编程2】使用 TCP 的 Socket API 实现客户端服务器通信(保姆级教学, 附代码)

    📕各位读者好, 我是小陈, 这是我的个人主页 📗小陈还在持续努力学习编程, 努力通过博客输出所学知识 📘如果本篇对你有帮助, 烦请点赞关注支持一波, 感激不尽 📙 希望我的专栏能够帮助到你: JavaSE基础: 基础语法, 类和对象, 封装继承多态, 接口, 综合小练习图书管理系统

    2024年02月05日
    浏览(41)
  • tcp/udp socket 网络通信中超时时间的设置

    1.connect函数的超时时间设置只对TCP有效 UDP由于是无连接的connect都会返回success 有两种方法: 第一种方法 默认的socket是阻塞模式 我们只需要设置其为非阻塞模式,然后调用select去查询其状态 代码如下:  第二种是 默认其为阻塞模式  通过setsockopt 函数设置TCP_SYNCNT 值 头文件

    2024年02月15日
    浏览(25)
  • 网络编程之简单socket通信

    Socket,又叫套接字,是在应用层和传输层的一个抽象层。它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。  socket分为流socket和数据报socket,分别基于tcp和udp实现。 SOCK_STREAM 有以下几个特征: 数据在传输过程中不会消失; 数据是按照顺序

    2024年02月01日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包