[begin] 在 [/begin]第一章的学习内容中我们已经完成了基本的 Socket 通信,但是在第一章中我们只能处理一个客户端的消息。
在实际游戏中,我们服务器需要对多个客户端进行监听和处理。
Begin XXX
每一个同步 API 对应着两个异步 API,分别是在原名称前面加上 Begin 和 End(如 BeginConnect 和 EndConnect)。使用异步程序可以防止程序卡住 。
例如:BenginConnect
参数 | 说明 |
---|---|
host | 远程主机ip地址 |
port | 端口号 |
requestCallback | 异步回调函数,其函数必须包含一个实现 IAsynResult的对象 |
state | 一个自定义对象,此对象会被传递给回调函数 |
EndConnect
参数 | 说明 |
---|---|
asyncResult | 接收一个异步结果对象 |
其他的都大致相同
[toc]
客户端
客户端使用 begin 和 end,修改为:
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
public class Echo : MonoBehaviour
{
public Button linkBtn;
public Button sendBtn;
public InputField inputField;
public Text infoText;
//套接字
private Socket _socket;
//接收缓冲区
private byte[] _readBuff=new byte[1024];
private string _recvStr=string.Empty;
private void Start()
{
linkBtn.onClick.AddListener(Connection);
sendBtn.onClick.AddListener(SendMessage);
//Timer线程计时器
var timer = new Timer((state) => { print("时间到"); },null,5000,0);
}
private void Update()
{
infoText.text = "接收到来自服务器的数据:"+_recvStr;
}
///
/// 连接
///
void Connection()
{
_socket=new Socket(SocketType.Stream,ProtocolType.Tcp);
//异步进行连接
_socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, _socket);
Debug.Log("开始连接...");
// _socket.Connect("127.0.0.1",8888);
}
///
/// 连接回调
///
///
void ConnectCallback(IAsyncResult ar)
{
try
{
var socket = ar.AsyncState as Socket;
socket?.EndConnect(ar); //连接完毕
Debug.Log("连接成功!");
//开始监听服务器端回复
socket?.BeginReceive(_readBuff, 0, _readBuff.Length, 0, ReceiveCallback, socket);
}
catch (SocketException e)
{
Debug.Log("Socket连接错误:"+e);
throw;
}
}
///
/// 接收回调
///
///
void ReceiveCallback(IAsyncResult ar)
{
//ps:作者在这里提到,假如在send缓冲区的数据量太多,并且删除服务端receive相关的内容,使这些数据不能得到及时的释放,这时候客户端就会卡住。
//所以我们也需要对send进行异步处理,防止堵塞。
try
{
var socket = ar.AsyncState as Socket;
var count = socket.EndReceive(ar);
//注意在unity中,UI对象只能在主线程中更新,由于异步回调是由其他线程更新的,所以我只在这儿对_recvStr进行记录
//并在主线程update中对UI对象进行更新。
_recvStr = Encoding.UTF8.GetString(_readBuff, 0, count);
socket.BeginReceive(_readBuff, 0, _readBuff.Length, 0, ReceiveCallback, socket);
}
catch (SocketException e)
{
Debug.Log("Socket错误:"+e);
}
}
///
/// 发送消息
///
void SendMessage()
{
var sendStr = inputField.text;
var sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr);
// _socket.Send(sendBytes);
/*
* 值得注意的是,send方法的过程只是将数据写入到缓冲区,由操作系统进行重传和确认。
* send成功只表示,数据成功吸入缓冲区,并不代表对方已经收到数据。
*/
_socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, _socket);
}
///
/// Send回调
///
///
void SendCallback(IAsyncResult ar)
{
try
{
var socket = ar.AsyncState as Socket;
var count = socket?.EndSend(ar);
/*
* 在这里作者提到,一般来说收发的数据长度应当保持一致,但是当我们的数据大小超过缓冲区的大小时,我们需要再次调用相关的send或reivce方法
* 发送或接收剩余的数据。(在后面的章节再介绍)
*/
Debug.Log("socket 发送成功 发送字节数:"+count);
}
catch (SocketException e)
{
Debug.Log("Socket Send 错误:"+e);
}
}
}
服务器
在上一章中,我们的服务器使用单线程的阻塞,也就是说每次只能处理一个客户端的请求,现在我们也将服务器改为多线程模式,让他可以同时处理多个线程。(多个客户端请求)
其代码实现如下:
创建一个clientState类存储客户端状态
using System.Net.Sockets;
namespace netWork_Server
{
///
/// 连接状态
///
public class ClientState
{
public Socket Socket { get; set; }
public byte[] ReadBuff { get; set; }
public ClientState()
{
ReadBuff=new byte[1024];
}
}
}
主代码
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace netWork_Server
{
internal class Program
{
private static Socket socket;
static Dictionary _clientStates=new Dictionary();
public static void Main(string[] args)
{
Console.WriteLine("Hi");
socket=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEndPoint=new IPEndPoint(ipAddress,8888);
socket.Bind(ipEndPoint);
//listen
socket.Listen(0);
Console.WriteLine("[服务器] 启动成功");
//Accept
//当客户端连接上服务器时,调用AcceptCallback
socket.BeginAccept(AcceptCallback,socket);
Console.ReadLine();
}
///
/// Accept回调
///
///
private static void AcceptCallback(IAsyncResult ar)
{
try
{
Console.WriteLine("[服务器] 连接到新客户端");
Socket listenfd=ar.AsyncState as Socket;
Socket clientfd = listenfd.EndAccept(ar);
ClientState state=new ClientState();
state.Socket = clientfd;
_clientStates.Add(clientfd,state);
//进行Receive
clientfd.BeginReceive(state.ReadBuff, 0, 1024, 0,ReceiveCallback, state);
//继续Accept
listenfd.BeginAccept(AcceptCallback, listenfd);
}
catch (SocketException e)
{
Console.WriteLine("Socket Accept错误:"+e);
throw;
}
}
///
/// Receive回调
///
///
static void ReceiveCallback(IAsyncResult ar)
{
try
{
var state = ar.AsyncState as ClientState;
var clientfd = state.Socket;
int count = clientfd.EndReceive(ar);
//关闭客户端
if (count==0)
{
clientfd.Close();
_clientStates.Remove(clientfd);
Console.WriteLine("Socket Close!");
return;
}
string recvStr = Encoding.UTF8.GetString(state.ReadBuff,0,count);
Console.WriteLine("[客户端]"+recvStr);
byte[] sendBytes = Encoding.UTF8.GetBytes("echo" + recvStr);
clientfd.Send(sendBytes);
//继续获取数据
clientfd.BeginReceive(state.ReadBuff, 0, 1024, 0, ReceiveCallback, state);
}
catch (SocketException e)
{
Console.WriteLine("Socket Receive 错误:"+e);
}
}
}
}
进行会话
我们将客户端生成一份,让生成的客户端和编辑器客户端同时与服务器连接并进行会话操作。
其效果如图所示:
!{服务器端}(https://i.loli.net/2020/01/14/kVrQIWpejXBRman.png)
!{客户端}(https://i.loli.net/2020/01/14/JdbaRLpt5k1hogs.png)
结语
这是这一章的前半部分,完成了在 socket 下的多线程模式,其中在字符编码部分,原书使用了 Default,这导致可能没法解析中文字符,所以我将其改为了固定的 UTF8 做解析。
感谢您读完这篇文章,在本章的下半部分将实现一个聊天室。