1.unity 同步socket 改异步
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net.Sockets;
using UnityEngine.UI;
using System.Threading;
using System;
public class Echo : MonoBehaviour
{
//定义套接字
Socket socket;
//UGUI
public InputField InputField;
public Text text;
//接收缓冲区
byte[] readBuff = new byte[1024];
string recvStr = "";
//点击连接按钮
public void Connection()
{
Debug.Log("点击了按钮");
//Socket
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//Connect 这个是同步方法
//socket.Connect("127.0.0.1", 8888);
socket.BeginConnect("127.0.0.1", 8888, ConnectionCallback, socket);
}
//Connect 回调
public void ConnectionCallback(IAsyncResult ar) {
try{
Socket socket = (Socket) ar.AsyncState;
socket.EndConnect(ar);
Debug.Log("Socket Connect Succ");
socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket);
} catch (SocketException ex) {
Debug.Log("Socket Connect fail " + ex.ToString());
}
}
//Receive回调
public void ReceiveCallback(IAsyncResult ar) {
try {
Socket socket = (Socket) ar.AsyncState;
int count = socket.EndReceive(ar);
recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket);
} catch(SocketException ex) {
Debug.Log("Socket Receive fail " + ex.ToString());
}
}
//点击发送按钮
public void Send()
{
//Send
string sendStr = InputField.text;
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
//测试卡住的问题 同步发送
// for(int i=0; i<10000; i++) {
// socket.Send(sendBytes);
// }
//改用异步发送
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
//Recv 这是原来同步的处理 现在改成异步处理
// byte[] readBuff = new byte[1024];
// int count = socket.Receive(readBuff);
// string recvStr = System.Text.Encoding.UTF8.GetString(readBuff, 0, count);
// text.text = recvStr;
//Close
//socket.Close();
//
}
//Send回调
public void SendCallback(IAsyncResult ar) {
try {
Debug.Log("异步发送测试");
Socket socket = (Socket) ar.AsyncState;
int count = socket.EndSend(ar);
Debug.Log("Socket Send succ" + count);
// 一般情况下EndSend的返回值count与要发送数据的长度相同,代表数据全部发出
// 但也不绝对,如果EndSend的返回值指示未全部发完,需要再次调用BeginSend方法,以便发送未发送的数据
} catch (SocketException ex) {
Debug.Log("Socket Send fail " + ex.ToString());
}
}
public void Update() {
text.text = recvStr;
}
}
每个同步API(Connect)对应着两个异步API,分别是在原名称前面加上Begin和End(如BeginConnect 和 EndConnect)。客户端发起连接时,如果网络不好或服务的没有回应,客户端会被卡住一段时间。而这卡住的十几秒,用户不能做任何操作,游戏体验很差。
由BeginConnect最后一个参数传入的socket,可由ar.AsyncState获取到
下面 对值 得 注 意的 地 方进 行 进 一 步 解 释。
(1)BeginReceive 的参数
上述程序 中,BeginReceive 的 参数为(readBuff, 0, 1024, 0, ReceiveCallback, socket )。 第一个参数readBuff 表示接收缓冲区; 第二个参数。表示从readBuff第。位开始接收数据, 这 个 参 数 和 T C P 粘 包 问 题 有 关 , 后 续 章 节 再 详 细 介 绍; 第 三 个 参 数 1 0 2 4 代 表 每 次 最 多 接 收 1024 个字节的数据,假如服务端回应一串长长的数据,那一次也只会收到1024 个字节。
(2 ) BeginReceive 的调用位置
程序在两个地方调用了BeginReceive: 一 个是ConnectCallback,在连接成功后,就开 始 接 收 数 据 , 接 收 到数 据 后 , 回 调 两 数 R e c e i v e C a l l b a c k 被 调 用 。 另 一个 是 B e g i n R e c e i v e 内 部,接收完一串数据后, 等待下一串数据的到来,如图2- 4所示。
( 3 ) Update 和 recvStr
在Unity中,只有主线程可以操作UI 组件。由于异步回调是在其他线程执行的,如 果在BeginReceive给text.text赋值,Unity会 弹出“get isActiveAndEnabled can only be called from the main thread”的异常信息,所以程序只给变量recvStr赋值 , 在主线程执行的Update 中再给text.text 赋值(如图2-5所示)。
- 上面new Socket 参数说明
socketType的含义
SocketType的值 | 含义 |
---|---|
Dgram | 支 持 数 据 报 , 即 最 大 长 度 固 定 ( 通 常 很 小 ) 的 无 连 接 、 不 可 靠 消 息 。, 消 息 可 能 会 天 失 或重复并可能在到达时不按顺序排列。Dgram 类型的Socket 在发送和接收 数据之前不 需要任何连接,并且可以与多个对方主机进行通信。Dgram使用数据报协议(UDP) 和 InterNetwork AddressFamily |
Raw | 支持对基础传输协议的访问。通过使用Socket TypeRaw,可以使用Inter et控制消息协议 ( ICMP) 和Internet 组管理协议(Igmp)这样的协议来进行通信。在发送时,您的应 用程序必 须 提 供 完整 的 1 P 标 头。 所接 收 的 数 据 报 在 返 回 时会 保 持 其 1 P 标 头和 选 项 不 变 |
RDM | 支持无连接、面向消息、以可靠方式发送的消息,并保留数据中的消息边界。RDM( 以 可靠方式发送的消息)消息会依次到达,不会重复。此外,如果消息丢失, 将会通知发送 方。 如 果 使 用 R D M 初 始 化 S o c k e t , 则 在 发 送 和 接 收 数 据 之 前 无 须 建 立 远 程 主 机 连 接 。 利 用 RDM,可以 与多个对方主机 进行通信 |
Seqpacket | 在网络上提供排序字节流的面 向连接且可靠的双向传输。Seqpacket 不重复数据, 它在数 据流中保留边界。Seqpacket 类型的Socket与单个对方主机通信,并且在通信开始之前需要 建立 远程主机连接 |
Stream | 支持可靠、双向、基于连接的字节流,而不重复数据,也不保留边界。此类型的Socket 与 单个对方主机通信,并且在通信开始 之前需要建 立远程主机连接。Strca m 使用传输控制协议 ( TCP)和InterNetworkAddressFamily |
Unknown | 指定未知的Socket 类型 |
常用的协议
常用的协议 | 含义 |
---|---|
GGP | 网 关 到 网 关 协 议 |
ICMP | 网际 消息控制协议 |
ICMPv6 | 用于IPv6 的Internet 控制消息协议 |
IDP | I n t e r n e t 数 据报 协 议 |
IGMP | 网 际组 管理 协议 |
IP | 网际 协议 |
Internet | 数 据 包 交 换 协议 |
PARC | 通用 数 据 包 协 议 |
RAW | 原始IP 数据包 协议 |
Т С Р | 传输控制协议 |
U D P | 用 户 数 据 包 协 议 |
U n k n o w n | 未知 协 议 |
Unspecified | 未指定 的协议 |
- 异步Connect函数说明
首先同步方法这样 socket.Connect(“127.0.0.1”, 8888);
public IAsyncResult BeginConnect ( string host,
int port,
AsyncCallback requestCallback,
object state
)
参数 | 说明 |
---|---|
host | 远 程 主机 的名 称 ( I P ) , 如 “ 1 2 7 . 0 . 0 . 1 ” |
port | 远程主机的端又号,如“8888” |
requestCallback | 一个AsyncCallback 委托,即回调两数,回调两数的参数必须是这样的形式:void requestCallbackConnectCallback(IAsyncResultar) |
state | 一个用户 定义对象,可包含连接操作的相关信 息。此对象会被传递给回调函数 |
public void EndConnect (
IAsyncResult asyncResult
)
- 异步Receive函数方法
Receive 是个阻塞方法,会让客户端一直卡着,直至收到服务端的数据为止。如果服务 端不回应(试试注释掉Echo服务端的Send 方法!),客户端就算等到海枯石烂,也只能继 续等着。异步Receive 方法BeginReceive 和EndReceive 正是解决这个问题的关键。 与BeginConnect 相似,BeginReceive 用于实现异步数据的接收,它的原型如下所示。
public IAsyncResult BeginReceive ( byte[] buffer,
int offset,
int size,
SocketFlags socketFlags,
AsyncCallback callback,
object state
)
表 2- 2 对Be gi n Re c e i ve 的 参数进行 了说明。
参 数 | 说明 |
---|---|
buffer | Byte 类型的数组,它存储接收到的数据 |
offset | buffer中存储数据的位置,该位置从0开始计数 |
size | 最 多接收的 字节数 |
SocketFlags | SocketFlags 值的按位组合,这里设置为0 |
callback | 回调函数,一 个AsyncCallback 委托 |
state | 一 个用户定义对象,其中包含接收操作的相关信息。 当操作完成时,此对象会被传递 state给EndReceive 委托 |
虽然参数比较多,但我们先重点关注buffer、callback 和state 三个即可。对应的End- R e c e i v e 的原 型 如 下, 它 的返 回 值 代 表 了 接 收 到 的 字 节 数 。
public int EndReceive (
IAsyncResult asyncResult
)
- 异步send
尽管不容易察觉,Send也是个阻塞方法, 可能导致客户端在发送数据的一瞬间卡住。 T C P 是 可 靠 连 接 , 当 接 收 方 没 有 收 到 数 据 时 , 发 送 方 会 重 新 发 送 数 据 , 直 至 确 认 接收 方 收 到数据为止。
在操作系统内部,每个Socket 都会有一个发送缓冲区,用于保存那些接收方还没有确 认的数据。图2-6指示了一个Socket涉及的属性,它分为“用户层面” 和“操作系统层面” 两大部分。Socket 使用的协议、IP、端又属于用户层面的属性,可以直接修改;操作系统 层面拥有“发送” 和“接收” 两个缓冲区,当调用Send 方法时,程序将要发送的字节流写 人到发送缓冲区中,再由操作系统完成数据的发送和确认。由于这些步骤是操作系统 自动 处理的,不对用户开放,因此称为“操作系统层面” 上的属性。
发 送 缓 冲 区 的 长 度 是 有 限 的 ( 默 认 值 约 为 8 K B ), 如 果 缓 冲 区 满 , 那 么 S e n d 就 会 阻 塞 ,直到 缓冲区 的数据被确认 腾出 空间。
可以 做 一个 这 样 的 实 验 : 删 去 服 务 端 Receive 相 关 的 内 容 , 使 客 户 端 的 S o c k e t 缓 冲 区 不 能 释 放 , 然 后 发 送 很 多 数 据 ( 如 下 代 码 所 示 ), 这 时 就 能 够 把 客 户 端 卡 住 。
// 点击发送按钮
public void Send ( ) {
// Send
string sendStr = InputFeld.text;
byte [ ] sendBytes = System. Text. Encoding. Default.GetBytes (sendStr);
for (int i=0;i<10000;1++) {
socket.Send( sendBytes ) ;
}
}
值得注意的是,send 过程只是把数据写人到发送缓冲区,然后由操作系统负责重传、确 认等步骤。Send 方法返 回只代表成功将数据放到发送缓存区中,对方可能还没有收到数据。
异步Send 不会卡住程序, 当数据成功写人输人缓冲区(或发生错误)时会调用回调两 数。异步Send 方法BeginSend的原型如下。
public IAsyncResult BeginSend(
byte[] buffer,
int offset,
int size,
SocketFlags socketFlags,
AsyncCallback callback,
object state
)
表2- 3对BeginSend的参数进行了说明。
参数 | 说明 |
---|---|
buffer | Byte类型的数组, 包含要发送的数据 |
offset | 从buffer中的offset位置开始发送 |
socketFlags | SocketFlags值的按位组合,这里设置为0 |
callback | 回调函数,一个AsyncCallback委托 |
state | 一个用户定义对象,其中包含发送操作的相关信息. 当操作完成时,此对象会被传递给EndSend委托 |
EndSend 函数原 型 如 下。 它 的 返 回值 代 表 发 送 的 字 节 数 , 如 果 发 送失 败 会 抛 出 异 常
public int EndSend (
IAsyncResult asyncResult
)
============================================================
c#建议服务端
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
namespace EchoServer
{
class ClientState
{
public Socket socket;
public byte[] readBuff = new byte[1024];
}
class MainClass
{
// 监听Socket
static Socket listenfd;
// 客户端Socket及状态信息
static Dictionary<Socket, ClientState> clients =
new Dictionary<Socket, ClientState>();
public static void Main (string[] args)
{
Console.WriteLine ("Hello World!");
//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(0);
Console.WriteLine("[服务器]启动成功");
// Accept
listenfd.BeginAccept(AcceptCallBack, listenfd);
// 等待
Console.ReadLine();
// accept 这部分改异步
// while (true) {
// //Accept
// Socket connfd = listenfd.Accept ();
// Console.WriteLine ("[服务器]Accept");
// //Receive
// byte[] readBuff = new byte[1024];
// int count = connfd.Receive (readBuff);
// string readStr = System.Text.Encoding.UTF8.GetString (readBuff, 0, count);
// Console.WriteLine ("[服务器接收]" + readStr);
// //Send
// string sendStr = System.DateTime.Now.ToString();
// byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
// connfd.Send(sendBytes);
// }
}
//Accept 回调
// 1. 给新的连接分配clientState,并把它添加到clients列表中 2. 异步接收客户端数据 3.再次调用BeginAccept实现循环
public static void AcceptCallBack(IAsyncResult ar) {
try {
Console.WriteLine("[服务器]Accept");
Socket listenfd = (Socket)ar.AsyncState;
Socket clientfd = listenfd.EndAccept(ar);
//clients列表
ClientState state = new ClientState();
state.socket = clientfd;
clients.Add(clientfd, state);
// 接收数据BeginReceive
clientfd.BeginReceive(state.readBuff, 0, 1024, 0, ReceiveCallback, state);
//继续Accept
listenfd.BeginAccept(AcceptCallBack, listenfd);
} catch (SocketException ex) {
Console.WriteLine("Socket Accept fail" + ex.ToString());
}
}
//Receive 回调
public static void ReceiveCallback(IAsyncResult ar) {
try {
ClientState state = (ClientState) ar.AsyncState;
Socket clientfd = state.socket;
int count = clientfd.EndReceive(ar);
// 客户端关闭
if (count == 0) {
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("Socket Close");
return;
}
string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 0, count);
byte[] sendBytes = System.Text.Encoding.Default.GetBytes("echo" + recvStr);
clientfd.Send(sendBytes);//减少代码量, 不用异步
clientfd.BeginReceive(state.readBuff, 0, 1024, 0, ReceiveCallback, state);
} catch (SocketException ex) {
Console.WriteLine("Socket Receive fail" + ex.ToString());
}
}
}
}