0%

JasonChatroom学习笔记

今天跟着博主 科技7C100 做一个简单的 chatroom.
在 Unity 前端页面,设置了 Scroll View 来显示聊天内容,一个 InputField 来输入信息,还有两个按钮用于链接和发送。
在设计脚本之前,我们需要了解网络编程的基础知识


以下内容摘自 http://www.cnblogs.com/skynet/

网络中进程之间如何通信

网络通信首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程 PID 来唯一标识一个进程,但是在网络中这是行不通的。其实 TCP/IP 协议族已经帮我们解决了这个问题,网络层的 “ip 地址” 可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组( ip 地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

使用 TCP/IP 协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和 UNIX System V 的 TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用 socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆 socket ”。

什么是Socket

socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写 write/read –> 关闭 close”模式来操作。我的理解就是 Socket 就是该模式的一个实现,socket 即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写 IO、打开、关闭)


前端设计

在Connect()方法中,我们这样设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public static void Connect()
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//使用指定的地址族、套接字类型和协议初始化 Socket 类的新实例。
//AddressFamily指定 Socket 类的实例可以使用的寻址方案;InterNetwork 是 IPV4 的地址
//SocketType指定 Socket 类的实例表示的套接字类型;Stream支持可靠、双向、基于连接的字节流,而不重复数据,也不保留边界。 此类型的 Socket 与单个对方主机通信,并且在通信开始之前需要建立远程主机连接。 Stream 使用传输控制协议 (ProtocolType.Tcp) 和 AddressFamily。InterNetwork 地址族。
//ProtocolType是指定 Socket 类支持的协议。

socket.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 10010));
//socket.Connect()与远程主机建立连接。
//Connect(IPAddress, Int32)与远程主机建立连接。 主机由 IP 地址和端口号指定。
//IPEndPoint提供 Internet 协议 (IP) 地址。这里是远程主机的 IP 地址。
//10010是 port 远程主机的端口号。

socket.BeginReceive(buffer, 0, buffer.Length,0, _ =>{}, socket);
//BeginReceive(Byte[], Int32, Int32, SocketFlags, AsyncCallback, Object)开始从连接的 Socket 中异步接收数据。
// buffer
// Byte[]
// Byte 类型的数组,它是存储接收到的数据的位置。

// offset
// Int32
// buffer 参数中存储所接收数据的位置,该位置从零开始计数。

// size
// Int32
// 要接收的字节数。

// socketFlags
// SocketFlags
// SocketFlags 值的按位组合。

// callback
// AsyncCallback
// 一个 AsyncCallback 委托,它引用操作完成时要调用的方法。

// state
// Object
// 一个用户定义的对象,其中包含接收操作的相关信息。 当操作完成时,此对象会被传递给 EndReceive(IAsyncResult) 委托。
}

Recevie()是(同步阻塞方式), 注意使用同步方法时,需要使用线程来开始方法,不然服务器不发送任何信息的话,Unity界面会卡死
这里使用使用BeginReceive(异步)

我们继续看Send的内部实现

1
2
3
4
5
6
7
8
9
public static void Send(string data)
{
socket.Send(System.Text.Encoding.UTF8.GetBytes(data));
//Socket.Send 方法将数据发送到连接的 Socket。
//buffer
//Byte[]
//类型 Byte 的数组,其中包含要发送的数据。
//Encoding表示字符编码
}

这里写一下回调方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void ReceiveCallback(IAsyncResult ar)//接收数据回调
{
Socket socket = (Socket)ar.AsyncState;//获取当前的socket
int length = socket.EndReceive(ar);//获取接收到的数据长度

//错误示范:UIManager.Instance.connectText.text += System.Text.Encoding.UTF8.GetString(buffer, 0, length);//显示接收到的数据
//原因:UIManager.Instance.connectText.text是在主线程中调用的,而socket.BeginReceive不会在Unity主线程中执行
//下面是正确示范
GameManager.Instance.messageQueue.Enqueue(() =>//将接收到的数据放入消息队列中
{
UIManager.Instance.connectText.text += System.Text.Encoding.UTF8.GetString(buffer, 0, length);//显示接收到的数据
});
socket.BeginReceive(buffer, 0, buffer.Length, 0, ReceiveCallback, socket);//再次接收数据
}

重要的知识点来了!
Unity里的UI组件或者是其他仅仅存活于Unity生命周期中的组件,只在Unity主线程内执行
由此引出了消息队列的妙用,请看GameManager.cs

1
2
3
4
5
6
7
8
9
public Queue<Action> messageQueue = new Queue<Action>();//消息队列
private void Update() {
if(messageQueue.Count>0)//如果消息队列中有消息
{
messageQueue//消息队列
.Dequeue()//从队列中取出消息
.Invoke();//执行队列中的消息
}
}

到这里,我们的前端设计就基本完成了,来看看后端设计吧

后端设计

后端设计就是服务器端的设计,这里我们使用C#的Socket类来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Net.Sockets;
using System.Net;
public class ChattingServer
{
public static Socket socket;
public static void Main(string[] args)
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(new IPEndPoint(IPAddress.Any, 10010));//IPAddress.Any代表本机上的所有IP地址;socket.Bind(EndPoint) 方法使 Socket 与一个本地终结点相关联。
socket.Listen(0);//监听任意多个客户端
socket.BeginAccept(AcceptCallback, socket);
System.Console.WriteLine("Server is running...");
System.Console.ReadLine();//阻塞主线程
}
}

还有我们需要用到的回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void AcceptCallback(System.IAsyncResult ar)
{
Socket socket = ar.AsyncState as Socket;//获取传入的socket
Socket client = socket.EndAccept(ar);//获取连接的客户端socket
System.Console.WriteLine("Client connected");
ClientInfo clientInfo = new ClientInfo(client);//创建客户端信息
clientList.Add(clientInfo);//添加到客户端列表
client.BeginReceive(clientInfo.readBuff, 0, 1024, 0, ReceiveCallback, clientInfo);//开始接收数据
socket.BeginAccept(AcceptCallback, socket);//再次监听
}
public static void ReceiveCallback(System.IAsyncResult ar)
{
ClientInfo info = ar.AsyncState as ClientInfo;//获取传入的客户端信息
int count = info.socket.EndReceive(ar);//获取接收到的数据长度
foreach (ClientInfo c in clientList)//遍历客户端列表
{
c.socket.Send(info.readBuff,0,count,0);//转发给所有客户端
}
info.socket.BeginReceive(info.readBuff, 0, 1024, 0, ReceiveCallback, info);//再次接收数据
}

这里建立了一个 ClientInfo 类用于管理数据和客户端 socket. 非常方便

1
2
3
4
5
6
7
8
9
public class ClientInfo//客户端信息
{
public Socket socket;//客户端socket
public byte[] readBuff = new byte[1024];//接收缓冲区
public ClientInfo(Socket socket)
{
this.socket = socket;
}
}

当然,聊天室不是一个人的网络备忘录,必然会出现多个客户端链接的情况,所以有必要new 一个 List 存储客户端信息。

1
public static List<ClientInfo> clientList = new List<ClientInfo>();//客户端列表

到这里,Jason网络聊天室的基本功能就实现了。


本文作于2023-08-12,首发于个人博客https://rdququ.top/

欢迎关注我的其它发布渠道