Unity使用Socket进行服务器与客户端通讯

一、服务器

1.服务器场景布局

由 显示提示信息、保存信息、清空消息按钮和信息提示Text、Scroll View组成。

信息提示:添加Content Size Fitter

 Scroll View下的Content:添加Text 和Content Size Fitter

 2.Loom脚本:

因为GetCompent组件必须在unity主线程下运行,使用线程进行Socket连接,返回的数据要在Text组件中显示,所以需要使用Loom脚本。

Loom脚本使用

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
using System.Threading;
using System.Linq;

public class Loom : MonoBehaviour
{
	public static int maxThreads = 8;//最大线程数量
	static int numThreads;

	private static Loom _current;
	private int _count;
	//获取Loom
	public static Loom Current
	{
		get
		{
			//初始化 返回_current(loom)
			Initialize();
			return _current;
		}
	}

	void Awake()
	{
		//初始化(已拖拽脚本)
		_current = this;
		initialized = true;
	}

	static bool initialized;//true 已初始化

	//在程序运行时,创建一个单例的loom组件(防止忘记拖拽脚本)
	static void Initialize()
	{
		//单例
		if (!initialized)
		{
			//程序运行时执行,创建一个Loom组件
			if (Application.isPlaying) 
			{
				initialized = true;
				var g = new GameObject("Loom");
				_current = g.AddComponent<Loom>();
			}
		}
	}

	private List<Action> _actions = new List<Action>();//行为列表
	//延时排队
	public struct DelayedQueueItem
	{
		public float time;//延时
		public Action action;//行为
	}
	private List<DelayedQueueItem> _delayed = new List<DelayedQueueItem>();//延时行为列表

	List<DelayedQueueItem> _currentDelayed = new List<DelayedQueueItem>();


	#region 在主线程调用方法
	public static void QueueOnMainThread(Action action)
	{
		QueueOnMainThread(action, 0f);
	}
	public static void QueueOnMainThread(Action action, float time)
	{
		//当前时间不为0 写入延时列表中
		if (time != 0)
		{
			//防止锁死(防止_delayed集合在写入的时候,其他进程进行其他操作导致错误)
			lock (Current._delayed)
			{
				Current._delayed.Add(new DelayedQueueItem { time = Time.time + time, action = action });
			}
		}
		else
		{
			//不为0 写入行为列表中
			lock (Current._actions)
			{
				Current._actions.Add(action);
			}
		}
	}

	#endregion

	#region 在线程调用的方法
	public static Thread RunAsync(Action a)
	{
		Initialize();
		while (numThreads >= maxThreads)
		{
			Thread.Sleep(1);
		}
		Interlocked.Increment(ref numThreads);
		ThreadPool.QueueUserWorkItem(RunAction, a);
		return null;
	}

	private static void RunAction(object action)
	{
		try
		{
			((Action)action)();
		}
		catch
		{
		}
		finally
		{
			Interlocked.Decrement(ref numThreads);
		}

	}
	#endregion


	void OnDisable()
	{
		//当前loom清空
		if (_current == this)
		{
			_current = null;
		}
	}



	// Use this for initialization
	void Start()
	{

	}

	List<Action> _currentActions = new List<Action>();//当前行为列表

	// Update is called once per frame
	void Update()
	{
		//行为列表 防锁死
		lock (_actions)
		{
			_currentActions.Clear();//当前行为列表 清空
			_currentActions.AddRange(_actions);//将_actions列表全部添加到_currentActions列表中
			_actions.Clear();//_actions列表清空
		}
		foreach (var a in _currentActions)
		{
			a();//循环执行 a 行为
		}
		lock (_delayed)
		{
			_currentDelayed.Clear();
			_currentDelayed.AddRange(_delayed.Where(d => d.time <= Time.time));
			foreach (var item in _currentDelayed)
				_delayed.Remove(item);
		}
		foreach (var delayed in _currentDelayed)
		{
			delayed.action();
		}



	}
}

服务器脚本SocketServer.cs:

using UnityEngine.UI;
using UnityEngine;
using System.Collections;

using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System;
using System.Collections.Generic;
using System.Net.NetworkInformation;
/// <summary>  
/// scoket服务器监听端口脚本  
/// </summary>  
public class SocketServer : MonoBehaviour
{
    //public static int msgIndex=0;
    [SerializeField] Text txt;
    private Thread thStartServer;//定义启动socket的线程  
    void Start()
    {
        //thStartServer = new Thread(StartServer);
        //thStartServer.Start();//启动该线程  

        //Loom线程中启动方法
        Loom.RunAsync(() =>
        {
            StartServer();
        });

    }

    void Update()
    {
    }

    private void StartServer()
    {
        //const int bufferSize = 8792;//缓存大小,8192字节  
        
        IPAddress ip = IPAddress.Parse(GetIP(ADDRESSFAM.IPv4));

        TcpListener tlistener = new TcpListener(ip, 9999);
        tlistener.Start();

        //执行unity主线程方法,GetComponent需要在主线程运行
        Loom.QueueOnMainThread(() => { txt.text += "  服务器"+ ip.ToString() + "监听启动......     " + DateTime.Now + "\n"; });
        //Debug.Log("Socket服务器监听启动......");

        do
        {
            GameClient server = new GameClient(tlistener.AcceptTcpClient(), txt);
            Loom.QueueOnMainThread(() => { txt.text += "  " + server._clientIP + "   客户端连接     " + DateTime.Now + "\n"; });
        }
        while (true);
    }

    void OnApplicationQuit()
    {
        //线程在Loom中关闭
        Loom.RunAsync(() =>
        {
            thStartServer.Abort();//在程序结束时杀掉线程 
        });
    }


    #region 获取本地IP地址
    public enum ADDRESSFAM
    {
        IPv4, IPv6
    }

    /// <summary>
    /// 获取本机IP
    /// </summary>
    /// <param name="Addfam">要获取的IP类型</param>
    /// <returns></returns>
    private string GetIP(ADDRESSFAM Addfam)
    {
        if (Addfam == ADDRESSFAM.IPv6 && !Socket.OSSupportsIPv6)
        {
            return null;
        }

        string output = "";

        foreach (NetworkInterface item in NetworkInterface.GetAllNetworkInterfaces())
        {
#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
            NetworkInterfaceType _type1 = NetworkInterfaceType.Wireless80211;
            NetworkInterfaceType _type2 = NetworkInterfaceType.Ethernet;

            if ((item.NetworkInterfaceType == _type1 || item.NetworkInterfaceType == _type2) && item.OperationalStatus == OperationalStatus.Up)
#endif 
            {
                foreach (UnicastIPAddressInformation ip in item.GetIPProperties().UnicastAddresses)
                {
                    //IPv4
                    if (Addfam == ADDRESSFAM.IPv4)
                    {
                        if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
                        {
                            output = ip.Address.ToString();
                            //Debug.Log("IP:" + output);
                        }
                    }

                    //IPv6
                    else if (Addfam == ADDRESSFAM.IPv6)
                    {
                        if (ip.Address.AddressFamily == AddressFamily.InterNetworkV6)
                        {
                            output = ip.Address.ToString();
                        }
                    }
                }
            }
        }
        return output;
    }

    #endregion
}

public class GameClient
{
    public static Hashtable allClient = new Hashtable();
    public static List<string> ipList = new List<string>();
    private TcpClient _client;
    public string _clientIP;
    public string _clientNick;
    private byte[] data;

    Text txt;


    public GameClient(TcpClient client, Text _txt)
    {
        txt = _txt;
        _client = client;
        _clientIP = client.Client.RemoteEndPoint.ToString();
        if (allClient.Count <= 2)
        {
            allClient.Add(_clientIP, this);
            ipList.Add(_clientIP);
            data = new byte[_client.ReceiveBufferSize];
            client.GetStream().BeginRead(data, 0, Convert.ToInt32(_client.ReceiveBufferSize), RceiveMessage, null);

            Loom.QueueOnMainThread(() => { txt.text += "   登录成功      " + DateTime.Now + "\n"; });
            //sendMessage("登录成功");
        }
        else
        {
            sendMessage("连接数量为最大,连接失败");
        }
    }
    public void RceiveMessage(IAsyncResult ar)
    {
        int bytesread;
        try
        {
            lock (_client.GetStream())
            {
                bytesread = _client.GetStream().EndRead(ar);
            }
            if (bytesread < 1)
            {
                allClient.Remove(_clientIP);
                Guangbo("服务器错误");
                return;
            }
            else
            {
                string messageReceived = Encoding.UTF8.GetString(data, 0, bytesread);
                Loom.QueueOnMainThread(() => { txt.text += "  服务器得到消息 :" + messageReceived + "     " + DateTime.Now + "\n"; });
                //Debug.Log("服务器得到消息 :" + messageReceived);

                if (!messageReceived.Contains("+"))
                {
                    _clientNick = messageReceived;
                    //Debug.Log(this._clientIP + this._clientNick);
                }
                else
                {
                    string[] strVect = messageReceived.Split('+');
                }
                lock (_client.GetStream())
                {

                    Loom.QueueOnMainThread(() => { txt.text += "lock (this._client.GetStream()):        " + DateTime.Now + "\n"; });

                    //Debug.Log("lock (this._client.GetStream()):");
                    _client.GetStream().BeginRead(data, 0, Convert.ToInt32(_client.ReceiveBufferSize),
                        RceiveMessage, null);
                }
            }
            sendMessage("服务器发送时间:" + DateTime.Now);
        }
        catch (Exception ex)
        {
            Loom.QueueOnMainThread(() => { txt.text += "something wrong     " + DateTime.Now + "\n"; });

            //Debug.Log("something wrong");
            allClient.Remove(_clientIP);
            Guangbo(_clientNick + " leave");
        }
    }

    public void Guangbo(string message)
    {
        Loom.QueueOnMainThread(() => { txt.text += "Guangbo:" + message + "       " + DateTime.Now + "\n"; });
        //Debug.Log("Guangbo:" + message);

        foreach (DictionaryEntry c in allClient)
        {
            ((GameClient)(c.Value)).sendMessage(message);
        }
    }

    public void sendMessage(string message)
    {
        Loom.QueueOnMainThread(() => { txt.text += "服务器获取的消息:" + _clientNick + " ,发送的消息: " + message + "      " + DateTime.Now + "\n"; });
        //Debug.Log("服务器获取的消息:" +_clientNick + " ,发送的消息: " + message);

        try
        {
            NetworkStream ns;
            lock (_client.GetStream())
            {
                ns = _client.GetStream();
            }
            byte[] bytestosend = Encoding.UTF8.GetBytes(message);
            ns.Write(bytestosend, 0, bytestosend.Length);
            ns.Flush();
        }
        catch (Exception ex)
        {
            Loom.QueueOnMainThread(() => { txt.text += "sendMessage_ex:" + ex.Message + "     " + DateTime.Now + "\n"; });

            //Debug.Log("sendMessage_ex:" + ex.Message);
        }
    }
}

服务器界面管理脚本MessageManager.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using System.IO;

public class MessageManager : MonoBehaviour
{
    string filePath;//文件路径

    [SerializeField] Text hideTxt;//提示信息
    [SerializeField] Text contentTxt;

    [SerializeField] Button isShowBtn;//显示隐藏提示信息
    [SerializeField] Button saveBtn;
    [SerializeField] Button clearBtn;

    void Start()
    {
        filePath = Application.streamingAssetsPath + "/serverMessage.txt";

        isShowBtn.onClick.AddListener(IsShowHideEvent);
        saveBtn.onClick.AddListener(SaveEvent);
        clearBtn.onClick.AddListener(ClearEvent);
    }

    void Update()
    {
        //if (Input.GetKeyDown(KeyCode.Escape))
        //    Application.Quit();//退出
    }

    bool isShow = true;
    private void IsShowHideEvent()
    {
        isShowBtn.transform.GetChild(0).GetComponent<Text>().text = isShow ?"隐藏提示":"显示提示";
        hideTxt.gameObject.SetActive(isShow);
        isShow = !isShow;
    }

    private void SaveEvent()
    {
        //不存在创建
        if (!File.Exists(filePath))
        {
            FileStream fileStream = File.Create(filePath);
            //释放文件 否则出现程序被占用
            fileStream.Dispose();
        }

        File.WriteAllText(filePath, contentTxt.text);//写入信息
        hideTxt.text = "信息保存在:" + filePath + " 路径下";

        //刷新文件 在project目录下显示
        #if UNITY_EDITOR
        UnityEditor.AssetDatabase.Refresh();
        #endif

    }

    private void ClearEvent()
    {
        contentTxt.text = string.Empty;
    }
}

 直接将两个脚本挂接到相机 或空物体上,将需要的组件拖入就好,Loom组件可以不用挂载,在使用时可以自动生成。

二、客户端

1.客户端布局

 2.客户端脚本SocketClient.cs:

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

public class SocketClient : MonoBehaviour
{
    public int portNo = 9999;
    private TcpClient _client;//TcpClient 是一个独立通信线程
    byte[] data;
    public InputField ip;

    //获取账号 消息内容
    public InputField user;
    public InputField messager;
    public Text server_Msg;//服务器返回信息

    [SerializeField] Button login;
    [SerializeField] Button send;
    [SerializeField] Button clearBtn;

    public void Awake()
    {
        //instance = this;
        login.onClick.AddListener(Btn_Login);
        send.onClick.AddListener(Btn_Msg);
        clearBtn.onClick.AddListener(ClearMessage);
    }

    private void ClearMessage() 
    {
        server_Msg.text = string.Empty;
    }

    string idtext = "";
    bool connect = false;  //判断是否登录 

    /*登录方法
     * 1.判断输入账号是否为空 此设备是否未登录
     * 2.建立连接
     * 3.调用发送消息方法
     */
    public void Login()
    {
        //获取账号
        idtext = user.text;
        //判断账号不为空并且未登录
        if (idtext != "" && !connect)
        {
            connect = true;//已登陆

            /*TcpClient作用:为 TCP 网络服务提供客户端连接 采用Web服务器
             * 提供了一些简单的方法,用于在同步阻塞模式下通过网络来连接、发送和接收流数据
             */
            _client = new TcpClient();

            /*TcpClient.Connect与Socket.Bind作用相同结合IP和端口号
             * 区别:1.TcpClient.Connect(string IP地址,int 端口号)
             * 2.Socket.Bind(EndPoint endpoint)需要EndPoint结合使用
             */
            _client.Connect(ip.text, portNo);//整合IP和端口

            data = new byte[_client.ReceiveBufferSize];//初始化Byte[]数组 长度等于_client数据长度
            idtext = "账号" + idtext;
            SocketSendMessage(idtext);//调用SendMessage方法 将账号信息发送

            /*BeginRead 开始始读,并立即返回,不会等待执行完
             * 参数:(1.Byte[] 信息 2.下标 从数组那个部分开始 3.数据总长度,4.AsyncCallback异步委托 用于回调数值,5.object start 自己定义的函数 )
             */
            _client.GetStream().BeginRead(data, 0, Convert.ToInt32(_client.ReceiveBufferSize),
            ReceiveMessage, null);//作用:读取数据流 调用ReceiveMessage(IAsyncResult ar)方法
        }
        else
        {
            //Server_Msg.text = "连接失败或已连接"+"\n";
            //Debug.Log("连接失败或已连接");
            //server_Msg.text = "连接失败或已连接\n";
        }
    }

    // 接收服务器返回值
    public void ReceiveMessage(IAsyncResult ar)
    {
        try
        {
            int bytesRead;//比特数组存储的数据长度
            bytesRead = _client.GetStream().EndRead(ar);//将获取的信息长度存入bytesRead EndRead(ar)获取异步访问结束后的返回值 ar的值是BeginRead回调数值
            if (bytesRead < 1)//数据小于1
            {
                // Server_Msg.text +="数据小于1\n" ;
                Debug.Log("数据小于1");
                return;
            }
            else
            {
                //Server_Msg.text += Encoding.UTF8.GetString(data, 0, bytesRead)+"\n";
                //UTF8数值
                //Debug.Log("服务器返回值:" + Encoding.UTF8.GetString(data, 0, bytesRead));
                string message = Encoding.UTF8.GetString(data, 0, bytesRead);
                //SocketSendMessage(message);
            }
        }
        catch (Exception ex)
        {
            // Server_Msg.text += "发送失败"+ex+"\n";
            Debug.Log("发送失败:" + ex);
        }

    }

    //发送消息
    public void SocketSendMessage(string message)
    {
        if (connect)
        {
            try
            {
                NetworkStream ns = _client.GetStream();
                //将发送的消息转换成byte存入dataMsg中
                byte[] dataMsg = Encoding.UTF8.GetBytes(message);
                //write信息写入(1.byte数据信息 2.下标 3.长度)
                ns.Write(dataMsg, 0, dataMsg.Length);

                //用户登录发送消息
                server_Msg.text += user.text + ":" + message + "\n";

                //ns.Read();

                //Encoding.UTF8.GetString(data, 0, bytesRead)

                //Debug.Log("客户端发送的消息:" + message);
                //发送完毕 清楚数据
                ns.Flush();
            }
            catch (Exception ex)
            {
                server_Msg.text = "信息发送错误: " + ex.Message + "\n";
                //Debug.Log("信息发送错误:" + ex.Message);
            }
        }
        else
        {
            server_Msg.text = "无连接或账号已经登陆\n";
            //Debug.Log("无连接或账号已经登陆");
        }
    }

    public void Btn_Login()
    {
        Login();
    }
    public void Btn_Msg()
    {
        //Btn_Login();
        SocketSendMessage(messager.text);
    }
}

参考连接:https://blog.csdn.net/u014732824/article/details/80665786 

运行结果:

客户端:

 

猜你喜欢

转载自blog.csdn.net/qq_42345116/article/details/121537290