一、服务器
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);
}
}
参考连接:
运行结果:
客户端: