目录
2.3.1、异步Accept,BeginAccept函数原型
第一部分:“扎基础”导读目标:
1、TCP网络游戏开发的必备知识有哪些?
2、TCP异步连接怎么样实现?
3、多路复用的处理是怎么处理?
4、什么是粘包分包以及怎么处理?
5、怎么样发送完整的网络数据?
6、怎么样设置正确的网络参数
第一章节:网络游戏的开端
1、Socket(套接字):
网络上的两个程序通过一个双向的通信连接实现数据交换、这个连接的一端成为一个socket。socket一般包含了进行网络通信必需的五大信息:连接使用的协议、本地主机ip地址、本地的协议端口、远程主机的ip地址、远程协议端口
2、Socket通信流程:
1、创建一个Socket对象(使用API Socket),绑定端口(使用API Bind),对于客户端而言,连接时(使用API Connect)会由系统分配端口;
2、服务端开始监听,等待客户端接入
3、客户端连接服务器
4、服务端接受客户端连接,可以开始通信
通过以上4步可以成功建立连接,可以收发数据
5、客户端和服务端收发数据,传输信息
6、某一方关闭连接,通信结束
3、TCP和UDP协议:
TCP:是一种面向连接的,可靠安全的,基于字节流的,传输层通信协议
UDP:无连接的,不安全的,传输效率高的,传输层协议
4、TCP/IP协议模型
5、编写客户端程序:
1.5.1、Echo客户端1.0
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;
public class EchoClient : MonoBehaviour
{
Socket socket;
string sendMsg;
public GameObject InputGameObject;
private InputField inputField;
public GameObject showGameObject;
public Text ShowText;
private void Awake()
{
inputField = InputGameObject.transform.GetComponent<InputField>();
ShowText = showGameObject.GetComponent<Text>();
}
// Start is called before the first frame update
void Start()
{
}
public void Connet()
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
socket.Connect(endPoint);
Debug.Log("连接成功");
}
public void Send()
{
sendMsg = inputField.text;
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendMsg);
socket.Send(sendBytes);
byte[] receiveBytes = new byte[1024];
int count = socket.Receive(receiveBytes, SocketFlags.None);
string receiveStr = System.Text.Encoding.Default.GetString(receiveBytes, 0, count);
System.Console.WriteLine("接受的数据为:" + receiveStr);
ShowText.text = receiveStr;
socket.Close();
}
}
6、编写服务端程序
1.6.1、服务端EchoServer1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace EchoServer
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("HelloWorld!");
//Socket
Socket listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//Bind
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
listenfd.Bind(ipEp);
//Listen
listenfd.Listen(10);
Console.WriteLine("服务器已经启动成功了");
while (true)
{
//Accept
Socket connfd = listenfd.Accept();
//receive
byte[] readBuff = new byte[1024];
int count = connfd.Receive(readBuff) ;
string readStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
Console.WriteLine("接受数据为:"+readStr);
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(readStr);
connfd.Send(sendBytes);
}
}
}
}
7、思考
1、上面的实现有什么优缺点?
程序集使用的API都是阻塞API(Connect,Send,Receive等),又称同步程序,会造成程序卡顿的致命缺点,同时服务端每次只能处理一个客户端的请求消息,不具有实用性
改进:使用异步方式和多路复用技术
2、怎么样实现同时处理多个客户端的请求?
使用异步方式和多路复用技术
第二章:分手有术:异步和多路复用
2.1、Unity中的异步
异步的实现依赖于多线程,在unity中的执行生命周期函数的线程是主线程。
2.2、异步客户端
2.2.1、BeginConnect 函数原型:
public IAsyncResult BeginConnect(
string host,
int port,
AsyncCallback requestCallback,
object state
)
参数 | 说明 |
host | 远程主机的名称(IP) |
port | 远程主机的端口号 |
requestCallback | 一个AsyncCallback委托,即回调函数,回函函数必须是这样的形式:void ConnectCallback(IAsyncResult ar) |
state | 一个用户自定义的对象,可包含连接操作的相关信息。此对象会被传递给回调函数 |
知识点:IAsyncResult是.NET提供的一种异步操作,通过名为BeginXXX和EndXXX的两个方法来实现原同步方法的异步调用。BeginXXX方法包含同步方法中所需要的参数,此外还包含另外两个参数:一个AsyncCallback委托和一个用户自定义的状态对象。委托用来调用回调方法,状态对象用来向回调方法传递状态信息。
2.2.2、EndConnect函数原型
public void EndConnect(IAsyncResult asyncResult)
2.2.3、使用异步改进客户端代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;
public class EchoClient : MonoBehaviour
{
Socket socket;
string sendMsg;
public GameObject InputGameObject;
private InputField inputField;
public GameObject showGameObject;
public Text ShowText;
private void Awake()
{
inputField = InputGameObject.transform.GetComponent<InputField>();
ShowText = showGameObject.GetComponent<Text>();
}
// Start is called before the first frame update
void Start()
{
}
public void Connet()
{
//socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
//IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
//socket.Connect(endPoint);
//Debug.Log("连接成功");
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
socket.BeginConnect(endPoint, AsyncConnetCallback, socket);
}
private void AsyncConnetCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
socket.EndConnect(ar);
Debug.Log("connect Succ");
}
catch (Exception e)
{
Debug.Log("connect Fail"+e.ToString());
throw;
}
}
public void Send()
{
sendMsg = inputField.text;
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendMsg);
socket.Send(sendBytes);
byte[] receiveBytes = new byte[1024];
int count = socket.Receive(receiveBytes, SocketFlags.None);
string receiveStr = System.Text.Encoding.Default.GetString(receiveBytes, 0, count);
System.Console.WriteLine("接受的数据为:" + receiveStr);
ShowText.text = receiveStr;
socket.Close();
}
}
2.2.4、BeginReceive 函数原型
//用于实现异步接收数据
public IAsyncResult BeginReceive(
byte[] buffer,
int offset,
int size,
SocketFlags socketFlags,
AsyncCallback callback,
object state
)
参数 | 说明 |
buffer | Byte类型的数组 |
offset | buffer中存储数据的位置,该位置是从0开始计数的 |
size | 最多接受的字节数 |
socketFlags |
socketFlags值的按位组合,这里设置为0 |
callback |
回调函数,一个AsyncCallback委托 |
state | 一个用户自定义的对象,其中包含接受操作的相关信息。当操作完成时,此对象会被传递给EndReceive委托 |
2.2.5、EndReceive 函数原型
public int EndReceive(ASyncResult asyncResult)
2.2.6、使用异步改进客户端代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;
public class EchoClient : MonoBehaviour
{
Socket socket;
string sendMsg;
public GameObject InputGameObject;
private InputField inputField;
public GameObject showGameObject;
public Text ShowText;
string receiveStr;
private void Awake()
{
inputField = InputGameObject.transform.GetComponent<InputField>();
ShowText = showGameObject.GetComponent<Text>();
}
// Start is called before the first frame update
void Start()
{
}
public void Connet()
{
//socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
//IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
//socket.Connect(endPoint);
//Debug.Log("连接成功");
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
socket.BeginConnect(endPoint, AsyncConnetCallback, socket);
byte[] receiveBytes = new byte[1024];
socket.BeginReceive(receiveBytes, 0, receiveBytes.Length, SocketFlags.None, AsyncReceiveCallback, socket);
}
private void AsyncConnetCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
socket.EndConnect(ar);
Debug.Log("connect Succ");
}
catch (Exception e)
{
Debug.Log("connect Fail"+e.ToString());
throw;
}
}
public void Send()
{
sendMsg = inputField.text;
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendMsg);
socket.Send(sendBytes);
}
private void AsyncReceiveCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
int count = socket.EndReceive(ar);
byte[] receiveBytes = new byte[1024];
receiveStr = System.Text.Encoding.UTF8.GetString(receiveBytes, 0, count);
socket.BeginReceive(receiveBytes, 0, receiveBytes.Length, SocketFlags.None, AsyncReceiveCallback, socket);
}
catch (Exception e)
{
Debug.Log(e.ToString());
throw;
}
}
// Update is called once per frame
void Update()
{
ShowText.text = receiveStr;
}
}
注意:
2.2.6.1、BeginReceive的调用位置:
两次调用BeginReceive,在ConnetCallback,在连接成功后就开始调用接受数据,接受到数据后,回调函数AsyncReceiveCallback被调用,接受玩一串数据后,等待接受下一串数据。
2.2.6.2、Update和ReceiveStr
在unity中只有主线程可以操作ui组件。由于异步回调是在其它的线程中执行的,如果在BeginReceive或者AsyncReceiveCallback中给UnityEngine.UI中的Text.text赋值,此程序会报错
2.2.7 异步Send,BeginSend函数原型
public IAsyncResult BeginSend(
byte[] buffer,
int offset,
int size,
SocketFlags socketFlags,
AsyncCallback callback,
object state
)
参数 | 说明 |
buffer | Byte类型的数组 |
offset | buffer中存储数据的位置,该位置是从0开始计数的 |
size | 最多接受的字节数 |
socketFlags |
socketFlags值的按位组合,这里设置为0 |
callback |
回调函数,一个AsyncCallback委托 |
state | 一个用户自定义的对象,其中包含接受操作的相关信息。当操作完成时,此对象会被传递给EndSend委托 |
2.2.8、EndSend函数原型
public int EndSend(IAsyncResult asyncResult)
2.2.9、修改客户端程
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;
public class EchoClient : MonoBehaviour
{
Socket socket;
string sendMsg;
public GameObject InputGameObject;
private InputField inputField;
public GameObject showGameObject;
public Text ShowText;
string receiveStr;
byte[] readbuff = new byte[1024];
private void Awake()
{
inputField = InputGameObject.transform.GetComponent<InputField>();
ShowText = showGameObject.GetComponent<Text>();
}
// Start is called before the first frame update
void Start()
{
}
public void Connet()
{
//socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
//IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
//socket.Connect(endPoint);
//Debug.Log("连接成功");
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
socket.BeginConnect(endPoint, AsyncConnetCallback, socket);
}
private void AsyncConnetCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
socket.EndConnect(ar);
Debug.Log("connect Succ");
socket.BeginReceive(readbuff, 0, 1024, 0, AsyncReceiveCallback, socket);
}
catch (Exception e)
{
Debug.Log("connect Fail"+e.ToString());
throw;
}
}
public void Send()
{
sendMsg = inputField.text;
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendMsg);
//socket.Send(sendBytes);
socket.BeginSend(sendBytes, 0, sendBytes.Length, SocketFlags.None, AsyncBeginSendCallback, socket);
}
private void AsyncBeginSendCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
socket.EndSend(ar);
}
catch (Exception e)
{
Debug.Log(e.ToString());
throw;
}
}
private void AsyncReceiveCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
int count = socket.EndReceive(ar);
receiveStr = System.Text.Encoding.UTF8.GetString(readbuff, 0, count);
socket.BeginReceive(readbuff, 0, readbuff.Length, SocketFlags.None, AsyncReceiveCallback, socket);
}
catch (Exception e)
{
Debug.Log(e.ToString());
throw;
}
}
// Update is called once per frame
void Update()
{
ShowText.text = receiveStr;
}
}
2.3、异步服务端
同步服务端程序,同一时间只能处理一个客户端的请求,因为它会一直阻塞,等待某一个客户端的数据。而使用异步方法,可以让服务端同时处理多个客户端的数据,及时响应。
2.3.1、异步Accept,BeginAccept函数原型
pubic IAsyncResult BeginAccept(
AsynncCallback asyncCallback,
object state
)
2.3.2、EndAccept函数原型
public IAsyncResult EndAccept(AsyncResult asyncResult)
2.3.3、异步服务端改进代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace EchoServer
{
class Program
{
static Socket listenSocket;
static Dictionary<Socket, ClientState> ClientSocketDic = new Dictionary<Socket, ClientState>();
static void Main(string[] args)
{
Console.WriteLine("HelloWorld!");
//Socket
listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//Bind
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
listenSocket.Bind(ipEp);
//Listen
listenSocket.Listen(10);
Console.WriteLine("服务器已经启动成功了");
listenSocket.BeginAccept(AsyncBeginAceptCallback, listenSocket);
Console.ReadLine();
}
private static void AsyncBeginAceptCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
Socket clientSocket = socket.EndAccept(ar);
Console.WriteLine(clientSocket.ToString());
Console.WriteLine(clientSocket.RemoteEndPoint.ToString());
ClientState state = new ClientState();
state.clientSocket = clientSocket;
ClientSocketDic.Add(clientSocket, state);
clientSocket.BeginReceive(state.readBuff, 0, state.readBuff.Length, SocketFlags.None, AsyReceiveCallback, state);
//clientSocket.BeginAccept(AsyncBeginAceptCallback, clientSocket);//System.InvalidOperationException: 在执行此操作前必须先调用 Listen 方法。
socket.BeginAccept(AsyncBeginAceptCallback, socket);
}
catch (SocketException ex)
{
Console.WriteLine("Socket Aceept fail" + ex.ToString());
}
}
private static void AsyReceiveCallback(IAsyncResult ar)
{
try
{
ClientState clientState = (ClientState)ar.AsyncState;
Socket clientSocket = clientState.clientSocket;
int count = clientSocket.EndReceive(ar);
if (count == 0)
{
clientSocket.Close();
ClientSocketDic.Remove(clientSocket);
Console.WriteLine("Socket Close");
return;
}
string receveStr = System.Text.Encoding.UTF8.GetString(clientState.readBuff, 0, count);
Console.WriteLine("接受的数据为:"+receveStr);
byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes("ServerSend:" + receveStr);
clientSocket.Send(sendBytes);
clientSocket.BeginReceive(clientState.readBuff, 0, clientState.readBuff.Length, SocketFlags.None, AsyReceiveCallback, clientState);
}
catch (Exception e)
{
Console.WriteLine("Socket Receive Fail "+e.ToString());
throw;
}
}
}
//新建一个类,包含客户端的socket的信息,
//因为每个客户端和服务器通信都需要一个自己的readBuff,不然容易造成数据丢失
public class ClientState
{
public Socket clientSocket;
public byte[] readBuff = new byte[1024];
}
}
2.4、做一个聊天室
2.4.1、需要改进的地方
在前面2.3.3中异步服务端的代码可以接受多个客户端的连接请求,但是处理消失时给客户端发送的数据是最近一个客户端发来的数据,因此这种在聊天室中是不符合的,需要遍历在线的客户端,然后向每一个客户端推送消息。改进后的AsyncReceiveCallback函数如下:
private static void AsyReceiveCallback(IAsyncResult ar)
{
try
{
ClientState clientState = (ClientState)ar.AsyncState;
Socket clientSocket = clientState.clientSocket;
int count = clientSocket.EndReceive(ar);
if (count == 0)
{
clientSocket.Close();
ClientSocketDic.Remove(clientSocket);
Console.WriteLine("Socket Close");
return;
}
string receveStr = System.Text.Encoding.UTF8.GetString(clientState.readBuff, 0, count);
Console.WriteLine("接受的数据为:"+receveStr);
string sendStr = clientSocket.RemoteEndPoint.ToString() + ":" + receveStr;
byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr);
foreach (var item in ClientSocketDic.Values)
{
item.clientSocket.Send(sendBytes);
}
//clientSocket.Send(sendBytes);
clientSocket.BeginReceive(clientState.readBuff, 0, clientState.readBuff.Length, SocketFlags.None, AsyReceiveCallback, clientState);
}
catch (Exception e)
{
Console.WriteLine("Socket Receive Fail "+e.ToString());
throw;
}
}
2.4.2、聊天室完整版异步客户端
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;
public class EchoClient : MonoBehaviour
{
Socket socket;
string sendMsg;
public GameObject InputGameObject;
private InputField inputField;
public GameObject showGameObject;
public Text ShowText;
string receiveStr;
byte[] readbuff = new byte[1024];
private void Awake()
{
inputField = InputGameObject.transform.GetComponent<InputField>();
ShowText = showGameObject.GetComponent<Text>();
}
// Start is called before the first frame update
void Start()
{
}
public void Connet()
{
//socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
//IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
//socket.Connect(endPoint);
//Debug.Log("连接成功");
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
socket.BeginConnect(endPoint, AsyncConnetCallback, socket);
}
private void AsyncConnetCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
socket.EndConnect(ar);
Debug.Log("connect Succ");
socket.BeginReceive(readbuff, 0, 1024, 0, AsyncReceiveCallback, socket);
}
catch (Exception e)
{
Debug.Log("connect Fail"+e.ToString());
throw;
}
}
public void Send()
{
sendMsg = inputField.text;
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendMsg);
//socket.Send(sendBytes);
socket.BeginSend(sendBytes, 0, sendBytes.Length, SocketFlags.None, AsyncBeginSendCallback, socket);
}
private void AsyncBeginSendCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
socket.EndSend(ar);
}
catch (Exception e)
{
Debug.Log(e.ToString());
throw;
}
}
private void AsyncReceiveCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
int count = socket.EndReceive(ar);
string s = System.Text.Encoding.UTF8.GetString(readbuff, 0, count);
receiveStr = s + "\n" + receiveStr;
socket.BeginReceive(readbuff, 0, readbuff.Length, SocketFlags.None, AsyncReceiveCallback, socket);
}
catch (Exception e)
{
Debug.Log(e.ToString());
throw;
}
}
// Update is called once per frame
void Update()
{
ShowText.text = receiveStr;
}
}
客户端界面:
2.4.3、聊天室完整版异步服务端
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace EchoServer
{
class Program
{
static Socket listenSocket;
static Dictionary<Socket, ClientState> ClientSocketDic = new Dictionary<Socket, ClientState>();
static void Main(string[] args)
{
Console.WriteLine("HelloWorld!");
//Socket
listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//Bind
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
listenSocket.Bind(ipEp);
//Listen
listenSocket.Listen(10);
Console.WriteLine("服务器已经启动成功了");
listenSocket.BeginAccept(AsyncBeginAceptCallback, listenSocket);
Console.ReadLine();
}
private static void AsyncBeginAceptCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
Socket clientSocket = socket.EndAccept(ar);
Console.WriteLine(clientSocket.ToString());
Console.WriteLine(clientSocket.RemoteEndPoint.ToString());
ClientState state = new ClientState();
state.clientSocket = clientSocket;
ClientSocketDic.Add(clientSocket, state);
clientSocket.BeginReceive(state.readBuff, 0, state.readBuff.Length, SocketFlags.None, AsyReceiveCallback, state);
//clientSocket.BeginAccept(AsyncBeginAceptCallback, clientSocket);//System.InvalidOperationException: 在执行此操作前必须先调用 Listen 方法。
socket.BeginAccept(AsyncBeginAceptCallback, socket);
}
catch (SocketException ex)
{
Console.WriteLine("Socket Aceept fail" + ex.ToString());
}
}
private static void AsyReceiveCallback(IAsyncResult ar)
{
try
{
ClientState clientState = (ClientState)ar.AsyncState;
Socket clientSocket = clientState.clientSocket;
int count = clientSocket.EndReceive(ar);
if (count == 0)
{
clientSocket.Close();
ClientSocketDic.Remove(clientSocket);
Console.WriteLine("Socket Close");
return;
}
string receveStr = System.Text.Encoding.UTF8.GetString(clientState.readBuff, 0, count);
Console.WriteLine("接受的数据为:"+receveStr);
string sendStr = clientSocket.RemoteEndPoint.ToString() + ":" + receveStr;
byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr);
foreach (var item in ClientSocketDic.Values)
{
item.clientSocket.Send(sendBytes);
}
//clientSocket.Send(sendBytes);
clientSocket.BeginReceive(clientState.readBuff, 0, clientState.readBuff.Length, SocketFlags.None, AsyReceiveCallback, clientState);
}
catch (Exception e)
{
Console.WriteLine("Socket Receive Fail "+e.ToString());
throw;
}
}
}
//新建一个类,包含客户端的socket的信息,
//因为每个客户端和服务器通信都需要一个自己的readBuff,不然容易造成数据丢失
public class ClientState
{
public Socket clientSocket;
public byte[] readBuff = new byte[1024];
}
}
聊天室效果图:
2.5、状态检测Poll
2.5.1、什么是Poll?
比起异步程序,同步程序更加的简单明了,而且不会引发线程安全问题,只需要在阻塞方法前加上一层判断,当有数据可读才调用Receive,有数据可写才调用Send,这样就既能够实现功能,又不会卡住程序。于是,微软给Socket类提供了Poll方法。
2.5.2、Poll方法原型
public bool Poll(
int microSecends,
SelectMode mode
)
参数 |
说明 |
microSecends | 等待回应的时间,以微秒为单位,如果参数为-1,表示一直等待,如果该参数为0,表示非阻塞 |
mode |
有三种可选的模式,分别如下: SelectRead:如果Socket可读(可以接收数据),返回true,否则返回false; SelectWrite:如果Socket可写,返回true,否则返回false; SelectError:如果连接失败,返回true,否则返回false; |
2.5.3、使用Poll方法改写客户端:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;
public class PollClient : MonoBehaviour
{
Socket PollClientSocket;
public InputField inputField;
public Text text;
byte[] buffer;
private bool isSend = false;
void Awake()
{
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (PollClientSocket == null) return;
if (PollClientSocket.Poll(0, SelectMode.SelectError)) return;
else if (PollClientSocket.Poll(0, SelectMode.SelectRead))
{
byte[] receBuffers = new byte[1024];
int count = PollClientSocket.Receive(receBuffers, 0, 1024, 0);
string recString = System.Text.Encoding.UTF8.GetString(receBuffers, 0, count);
text.text = text.text +"\n"+ recString;
}else if(PollClientSocket.Poll(0, SelectMode.SelectWrite)&&isSend){
buffer = System.Text.Encoding.UTF8.GetBytes(inputField.text);
PollClientSocket.Send(buffer, 0, buffer.Length, SocketFlags.None);
isSend = false;
}
}
public void Connect()
{
PollClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAdress = IPAddress.Parse("127.0.0.1");
PollClientSocket.Connect(ipAdress, 8888);
Debug.Log("连接成功!");
}
public void Send()
{
//buffer = System.Text.Encoding.UTF8.GetBytes(inputField.text);
//PollClientSocket.Send(buffer, 0, buffer.Length, SocketFlags.None);
//Receive();
isSend = true;
}
void Receive()
{
//byte[] receBuffers = new byte[1024];
//int count = PollClientSocket.Receive(receBuffers, 0, 1024, 0);
//string recString = System.Text.Encoding.UTF8.GetString(receBuffers, 0, count);
//text.text = text.text + recString;
}
}
运行效果:
2.5.4、使用Poll方法改写服务端:
在服务端需要不断重复两件事情,第一个,监听客户端Socke是否可读,如果可读意味着客户端连接上来了,就需要Accept客户端连接,然后把连接加入到客户端信息列表里;第二个,遍历客户端的信息列表,判断每一个客户端是否可读,如果可读,就处理消息数据。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace PollServerTest
{
class Program
{
static Socket ServerSocket;//listenfd
static Dictionary<Socket, ClientState> ClientSocketDic = new Dictionary<Socket, ClientState>();
static void Main(string[] args)
{
ServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
IPEndPoint iPEndPoint = new IPEndPoint(iPAddress, 8888);
ServerSocket.Bind(iPEndPoint);
ServerSocket.Listen(10);
Console.WriteLine("服务器已经启动了");
while (true)
{
if (ServerSocket.Poll(0, SelectMode.SelectRead))
{
ReadListListenfd(ServerSocket);
}
foreach (var item in ClientSocketDic.Values)
{
if (item.ClientSocket.Poll(0, SelectMode.SelectRead))
{
if (!ReadListClientfd(item))
break;
}
}
//防止CPU占用过高
System.Threading.Thread.Sleep(1);
}
}
private static bool ReadListClientfd(ClientState clientState)
{
Socket ClientSocket = clientState.ClientSocket;
byte[] receiiveBuffer = clientState.readBuffer;
int count = 0;
try
{
count = ClientSocket.Receive(receiiveBuffer, 0, clientState.readBuffer.Length, SocketFlags.None);
}
catch (Exception ex)
{
ClientSocket.Close();
ClientSocketDic.Remove(ClientSocket);
Console.WriteLine("Exception:" + ex.ToString() + "\n");
return false;
}
if (count == 0)
{
ClientSocket.Close();
ClientSocketDic.Remove(ClientSocket);
Console.WriteLine("ClientSocket Close");
return false;
}
string ReceiveStr = System.Text.Encoding.UTF8.GetString(receiiveBuffer,0,count);
Console.WriteLine("接受到的数据:" + ReceiveStr);
byte[] sendBuffer = System.Text.Encoding.UTF8.GetBytes((ClientSocket.RemoteEndPoint.ToString() +":"+ReceiveStr));
foreach (var item in ClientSocketDic.Values)
{
item.ClientSocket.Send(sendBuffer);
}
return true;
}
private static void ReadListListenfd(Socket serverSocket)
{
Socket ClientSocket = serverSocket.Accept();
ClientState clientState = new ClientState();
clientState.ClientSocket =ClientSocket;
ClientSocketDic.Add(ClientSocket,clientState);
Console.WriteLine(ClientSocket.RemoteEndPoint.ToString());
}
}
class ClientState
{
public Socket ClientSocket;
public byte[] readBuffer = new byte[1024];
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;
public class PollClient : MonoBehaviour
{
Socket PollClientSocket;
public InputField inputField;
public Text text;
byte[] buffer;
private bool isSend = false;
void Awake()
{
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (PollClientSocket == null) return;
if (PollClientSocket.Poll(0, SelectMode.SelectError)) return;
else if (PollClientSocket.Poll(0, SelectMode.SelectRead))
{
byte[] receBuffers = new byte[1024];
int count = PollClientSocket.Receive(receBuffers, 0, 1024, 0);
string recString = System.Text.Encoding.UTF8.GetString(receBuffers, 0, count);
text.text = text.text +"\n"+ recString;
}else if(PollClientSocket.Poll(0, SelectMode.SelectWrite)&&isSend){
buffer = System.Text.Encoding.UTF8.GetBytes(inputField.text);
PollClientSocket.Send(buffer, 0, buffer.Length, SocketFlags.None);
isSend = false;
}
}
public void Connect()
{
PollClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAdress = IPAddress.Parse("127.0.0.1");
PollClientSocket.Connect(ipAdress, 8888);
Debug.Log("连接成功!");
}
public void Send()
{
//buffer = System.Text.Encoding.UTF8.GetBytes(inputField.text);
//PollClientSocket.Send(buffer, 0, buffer.Length, SocketFlags.None);
//Receive();
isSend = true;
}
void Receive()
{
//byte[] receBuffers = new byte[1024];
//int count = PollClientSocket.Receive(receBuffers, 0, 1024, 0);
//string recString = System.Text.Encoding.UTF8.GetString(receBuffers, 0, count);
//text.text = text.text + recString;
}
}
这段代码注意点:第一个:主循环之后调用了System.Threading.Thread.Sleep(1);让程序挂起1秒,这样做的目的是避免死循环,让CPU有个短暂喘息的机会。第二个:ReadClient会返回true或者false,返回false表示客户端断开连接,由于客户端断开后,ReadClientfd会删除ClientSocketDic列表中对应的客户端信息,导致ClientSocketDic列表发生改变,然而ReadClientfd又是在foreach的循环中被调用的,ClientSocketDic列表变化,会导致遍历失败,因此程序在检测到客户端关闭后将退出foreach循环,第三个,是将Poll的超时时间设置为0,程序不会有任何等待。如果设置较长的超时时间,服务端将无法及时处理多个客户端同时连接的情况。
2.6、多路复用
2.6.1、什么是多路复用?
多路复用就是同时处理多路信号,比如同时检测多个Socket的状态。在PollClient中update()每帧判断Socket是否可读可写,服务端也一直在循环,这样会浪费CPU。如果同时检测多个Socket的状态。在设置要监听的Socket列表后,如果有一个(或者多个)Socket可读(或者可写,或发生错误信息),那就返回这些可读的Socket,如果没有可读的,那就阻塞。这样即可优化。
2.6.2、Select方法原型
public static void Select(
IList checkRead,
IList checkWrite,
IList checkError,
IList microSeconds
)
参数 | 说明 |
checkRead | 检测是否有可读的Socket列表 |
checkWrite | 检测是否有可写的Socket列表 |
CheckError | 检测是否有出错的Socket列表 |
microSeconds | 等待回应的时间,以微秒为单位,如果该参数为-1,表示一直等待,如果为0,表示非阻塞。 |
2.6.3、使用Select改进服务端
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace SelectServerTest
{
class ClientState
{
public Socket socket;
public byte[] readBuffer = new byte[1024];
}
class Program
{
static Socket listenfd;//listenfd
static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
static void Main(string[] args)
{
listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
IPEndPoint iPEndPoint = new IPEndPoint(iPAddress, 8888);
listenfd.Bind(iPEndPoint);
listenfd.Listen(10);
Console.WriteLine("服务器已经启动了");
//checkRead
List<Socket> checkRead = new List<Socket>();
//主循环
while (true)
{
//填充Socktet列表
checkRead.Clear();
checkRead.Add(listenfd);
foreach (var item in clients.Values)
{
checkRead.Add(item.socket);
}
//Select
Socket.Select(checkRead, null, null, 0);
foreach (Socket s in checkRead)
{
if (s == listenfd)
{
ReadListenfd(s);
}
else
{
ReadClientfd(s);
}
}
}
}
private static void ReadListenfd(Socket listenfd)
{
Console.WriteLine("Accept");
Socket clientfd = listenfd.Accept();
ClientState clientState = new ClientState();
clientState.socket = clientfd;
clients.Add(clientfd, clientState);
Console.WriteLine(clientfd.RemoteEndPoint.ToString());
}
private static void ReadClientfd(Socket clientfd)
{
ClientState state = clients[clientfd];
//接收
int count = 0;
try
{
count = clientfd.Receive(state.readBuffer);
}
catch (Exception e)
{
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("Receive SocketException:"+e.ToString());
}
if(count == 0)
{
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("socket close");
}
string ReceiveStr = System.Text.Encoding.UTF8.GetString(state.readBuffer, 0, count);
Console.WriteLine("接受到的数据:" + ReceiveStr);
byte[] sendBuffer = System.Text.Encoding.UTF8.GetBytes((state.socket.RemoteEndPoint.ToString() + ":" + ReceiveStr));
foreach (ClientState cs in clients.Values)
{
cs.socket.Send(sendBuffer);
}
}
}
}
2.6.4、Select客户端
与Poll客户端相似,不赘述,都需要在Update里面不停的检测数据,性能较差。商业上为了做到性能的极致,大多使用异步的客户端,Select服务端(或者异步服务端,之后的项目展示我们使用Select服务端)
文章评论