📢前言
Socket 作为网络编程超级重要的一个知识点,对于程序员自然是一个必备的东西。- 正好最近在看一些Socket套接字的内容,就想着能不能使用Socket做一个简单的通信功能,来加深对Socket的认知。
- 说到Socket那自然少不了TCP/IP协议,想了解这块可以参考另一篇文章:Socket基本概念简单理解
- 所以这篇文章就来使用Socket来简单制作一个多人聊天室,看看Socket到底怎样进行使用。
- 本文使用
Unity引擎 + C# 实现,制作过程还是挺简单的,下面一起来看看吧!
🎬使用Socket通信,做一个简单的多人聊天室
套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。 一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。 从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。 要使用Socket制作一个简单的多人聊天室,需要有 服务端 和 客户端。
一个简单的 基于 TCP 协议的 客户端 和 服务端 工作流程如下:
- 服务端 和 客户端初始化
socket ,得到文件描述符 - 服务端 调用
bind ,绑定 IP 地址和端口号 - 服务端 调用
listen ,进行监听 并 设置最大连接数量 - 服务端 调用
accept ,等待接收 客户端 连接 - 客户端 调用
connect ,向服务器端的地址和端口发起连接请求 - 服务端
accept 返回用于传输的 socket 的文件描述符 - 客户端 调用
send 写入数据 - 服务端调用
recv 读取数据 - 客户端 断开连接时,会调用
close ,那么 服务端 读取数据的时候,就会读取到了 EOF ,待处理完数据后,服务端 调用 close ,表示连接关闭
先来说一下这个 多人聊天室 要满足的基本功能
- 有一个服务端 和 多个 客户端,服务端 建立监听,客户端 进行连接。
- 当有 新的客户端 连接成功时,会给 其他客户端 发送一条消息。
- 客户端 给 服务端 发消息时,服务端 把收到的消息转发给 除客户端本身(发消息的客户端) 外的 所有客户端。
这样就实现了一个 简单的多人即时通信聊天室 的功能。 下面就开始编写 服务端 和 客户端 的代码,使其建立连接实现通信。
🏳??🌈一个简单的UI
按照自己的需求喜好制作一个简易的聊天室。
必要条件:
- 一个服务端开启的Button按钮
- 客户端连接到服务器的按钮(自己几个随意)
- 消息输入框InputField 和 内容显示框Text(跟随客户端数量添加)
🏳??🌈服务端 部分
服务端 部分大致步骤操作如下:
- 第一步:创建一个服务器Socket对象。
- 第二步:等待客户端的连接 并且创建与之通信的Socket
- 第三步:服务器端不停的接收客户端发来的消息
- 第四步:服务器向客户端发送消息
先在服务器创建一个Socket对象,绑定IP和端口号,设置最大连接数量,创建一个线程进行监听。 然后等待客户端的连接,并将已连接的客户端存取起来,有客户端加入连接时广播消息。 服务器端不停的接收客户端发来的消息,将消息字节流转化为字符串并群发消息。
新建脚本SocketServer.cs ,完整代码如下:
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
public class SocketServer : MonoBehaviour
{
private List<string> _ClientList;
private List<Socket> _SocketsList;
private Dictionary<string, string> _ClientName;
private void Awake()
{
_ClientList = new List<string>();
_SocketsList = new List<Socket>();
_ClientName = new Dictionary<string, string>();
}
public void StartServer()
{
bt_connnect_Click();
}
private void bt_connnect_Click()
{
try
{
int _port = 6000;
string _ip = "127.0.0.1";
Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ip = IPAddress.Parse(_ip);
IPEndPoint point = new IPEndPoint(ip, _port);
socketWatch.Bind(point);
Debug.Log("监听成功!");
socketWatch.Listen(10);
Thread thread = new Thread(Listen);
thread.IsBackground = true;
thread.Start(socketWatch);
}
catch { }
}
Socket socketSend;
void Listen(object o)
{
try
{
int client=1;
Socket socketWatch = o as Socket;
while (true)
{
socketSend = socketWatch.Accept();
Debug.Log(socketSend.RemoteEndPoint.ToString() + ":" + "连接成功!");
_ClientList.Add(socketSend.RemoteEndPoint.ToString());
_SocketsList.Add(socketSend);
_ClientName.Add(socketSend.RemoteEndPoint.ToString(), client+"号");
client++;
for (int i = 0; i <= _ClientList.Count - 1; i++)
{
if (socketSend.RemoteEndPoint.ToString() != _ClientList[i])
{
Sends(_ClientName[socketSend.RemoteEndPoint.ToString()]+ "加入房间\n" , i);
}
}
Thread r_thread = new Thread(Received);
r_thread.IsBackground = true;
r_thread.Start(socketSend);
}
}
catch(Exception e)
{
Debug.Log(e);
}
}
void Received(object o)
{
try
{
Socket socketSend = o as Socket;
while (true)
{
byte[] buffer = new byte[1024 * 1024 * 3];
int len = socketSend.Receive(buffer);
if (len == 0)
{
break;
}
string str = Encoding.UTF8.GetString(buffer, 0, len);
Debug.Log("服务器打印:" + socketSend.RemoteEndPoint + ":" + str);
for (int i=0;i<= _ClientList.Count-1;i++)
{
if (socketSend.RemoteEndPoint.ToString() != _ClientList[i])
{
Sends(_ClientName[socketSend.RemoteEndPoint.ToString()] + ":" + str,i);
}
}
}
}
catch (Exception e)
{
Debug.Log(e);
}
}
void Sends(string msg,int socket)
{
byte[] buffer = Encoding.UTF8.GetBytes(msg);
_SocketsList[socket].Send(buffer);
}
}
🏳??🌈客户端部分
客户端部分 比 服务端 简单,大致步骤如下:
- 第一步:客户端连接到服务器
- 第二步:接收服务端返回的消息
- 第三步:向服务器发送消息
这部分和服务端其实差不多,就是从监听改为了连接到服务端。 然后使用了Loom插件 从多线程中给主线程中的UI添加内容,对Loom插件使用不熟悉的可以参考这篇文章:
Unity零基础到进阶 | Unity中的多线程的使用,普通创建Thread + 使用Loom插件创建
当然也可以不使用Loom插件来进行Socket通信,我这里只是简单利用Loom插件来将服务端发来的消息直接绘制在聊天框里了。
也可以自己将服务端发来的内容通过别的方式发送到主线程的UI中,避免增加项目的复杂度。
新建脚本SocketClient.cs ,完整代码如下:
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
public class SocketClient : MonoBehaviour
{
public InputField input;
public Text text;
public void StartClient()
{
bt_connect_Click();
}
public void SendMsg()
{
bt_send_Click(input.text);
}
Socket socketSend;
private void bt_connect_Click()
{
try
{
int _port = 6000;
string _ip = "127.0.0.1";
socketSend = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ip = IPAddress.Parse(_ip);
IPEndPoint point = new IPEndPoint(ip, _port);
socketSend.Connect(point);
Loom.RunAsync(() =>
{
Thread c_thread = new Thread(Received);
c_thread.IsBackground = true;
c_thread.Start();
});
}
catch (Exception)
{
Debug.Log("IP或者端口号错误...");
}
}
void Received()
{
while (true)
{
try
{
byte[] buffer = new byte[1024 * 1024 * 3];
int len = socketSend.Receive(buffer);
if (len == 0)
{
break;
}
string str = Encoding.UTF8.GetString(buffer, 0, len);
Debug.Log("客户端打印:" + socketSend.RemoteEndPoint + ":" + str);
Loom.QueueOnMainThread((param) =>
{
text.text += str + "\n";
}, null);
}
catch (Exception e)
{
Debug.Log("错误..."+e);
}
}
}
private void bt_send_Click(string str)
{
try
{
string msg = str;
byte[] buffer = new byte[1024 * 1024 * 3];
buffer = Encoding.UTF8.GetBytes(msg);
socketSend.Send(buffer);
text.text += "我:" + str + "\n";
input.text = "";
}
catch { }
}
}
🏳??🌈Loom插件的使用
Loom插件 就一个脚本,目的是可以从多线程中调用Unity的UI部分。 在上面的客户端中用到,直接将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;
public static Loom Current
{
get
{
Initialize();
return _current;
}
}
void Awake()
{
_current = this;
initialized = true;
}
static bool initialized;
public static void Initialize()
{
if (!initialized)
{
if (!Application.isPlaying)
return;
initialized = true;
var g = new GameObject("Loom");
_current = g.AddComponent<Loom>();
#if !ARTIST_BUILD
UnityEngine.Object.DontDestroyOnLoad(g);
#endif
}
}
public struct NoDelayedQueueItem
{
public Action<object> action;
public object param;
}
private List<NoDelayedQueueItem> _actions = new List<NoDelayedQueueItem>();
public struct DelayedQueueItem
{
public float time;
public Action<object> action;
public object param;
}
private List<DelayedQueueItem> _delayed = new List<DelayedQueueItem>();
List<DelayedQueueItem> _currentDelayed = new List<DelayedQueueItem>();
public static void QueueOnMainThread(Action<object> taction, object tparam)
{
QueueOnMainThread(taction, tparam, 0f);
}
public static void QueueOnMainThread(Action<object> taction, object tparam, float time)
{
if (time != 0)
{
lock (Current._delayed)
{
Current._delayed.Add(new DelayedQueueItem { time = Time.time + time, action = taction, param = tparam });
}
}
else
{
lock (Current._actions)
{
Current._actions.Add(new NoDelayedQueueItem { action = taction, param = tparam });
}
}
}
public static Thread RunAsync(Action a)
{
Initialize();
while (numThreads >= maxThreads)
{
Thread.Sleep(100);
}
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);
}
}
void OnDisable()
{
if (_current == this)
{
_current = null;
}
}
void Start()
{
}
List<NoDelayedQueueItem> _currentActions = new List<NoDelayedQueueItem>();
void Update()
{
if (_actions.Count > 0)
{
lock (_actions)
{
_currentActions.Clear();
_currentActions.AddRange(_actions);
_actions.Clear();
}
for (int i = 0; i < _currentActions.Count; i++)
{
_currentActions[i].action(_currentActions[i].param);
}
}
if (_delayed.Count > 0)
{
lock (_delayed)
{
_currentDelayed.Clear();
_currentDelayed.AddRange(_delayed.Where(d => d.time <= Time.time));
for (int i = 0; i < _currentDelayed.Count; i++)
{
_delayed.Remove(_currentDelayed[i]);
}
}
for (int i = 0; i < _currentDelayed.Count; i++)
{
_currentDelayed[i].action(_currentDelayed[i].param);
}
}
}
}
🎁效果展示
写完脚本之后将其挂在场景中,把Button按钮的监听事件设置成对应的方法就可以了。
我这是的设置是服务端自动将加入的客户端命名为1号、2号。 还可以在客户端进行连接成功的时候,再给服务端发送一个自定义的昵称,这样效果会更好一下。 大家也可以自己去改改代码尝试一下。
下面是直接在Unity编辑器运行的效果,有两个客户端。
再来看一下四个客户端的效果,编辑器加上打包出来的应用一起连接:
💬总结
- 本文使用
Socket通信 做了一个多人聊天的简单实例。 - 客户端一个脚本,服务端一个脚本,再加一个工具脚本Loom就完成了这个简单的
Socket 多人聊天实例。 - 主要是了解Socket通信的基本流程 和 几个步骤,让我们更好的认识Socket。
- 整体执行代码还是很好理解的,但是真正的项目中Socket使用要比这复杂的多。
- 所以想学会一个知识点,还是要多学多练习呀~
温馨提示: 点击下面卡片可以获取更多编程知识,包括各种语言学习资料,上千套PPT模板和各种游戏源码素材等等资料。更多内容可自行查看哦!
|