谈谈Unity游戏TCP连接和网络重连
Unity中通常使用TcpClient
来进行Tcp连接,TcpClient
支持异步读写,避免了我们需要另外开辟线程管理网络数据发送。
当异步读写经常会让人摸不着头脑,比较困惑。
1. 建立连接
///
/// 连接服务器
///
public void ConnectServer (string host, int port)
{
Log.Instance.infoFormat ("start connect server host:{0}, port:{1}", host, port);
lock (lockObj) {
// 关闭老的连接
if (null != client) {
Close ();
}
// 建立新的连接
client = new TcpClient ();
client.SendTimeout = 1000;
client.ReceiveTimeout = 1000;
client.NoDelay = true;
IsConnected = false;
connectingFlag = true;
try {
client.BeginConnect (host, port, new AsyncCallback (OnConnect), client);
// 这里是一个任务管理器,可以用来执行定时任务。连接时候添加一个超时检查的定时任务。
TimerManager timer = AppFacade.Instance.GetManager (ManagerName.Timer);
timer.AddTask (OnConnectTimeout, CONN_TIMEOUT);
} catch (Exception e) {
Log.Instance.error ("connect server error", e);
// 通知连接失败
NetworkManager.AddEvent (Protocal.ConnectFail, null);
}
}
}
2. 异步处理连接结果
///
/// 连接上服务器
///
void OnConnect (IAsyncResult asr)
{
lock (lockObj) {
TcpClient client = (TcpClient)asr.AsyncState;
bool validConn = (client == this.client);
connectingFlag = false;
try {
// 结束异步连接
client.EndConnect (asr);
// 非当前连接
if (!validConn) {
client.Close ();
}
if (client.Connected) {
Log.Instance.info ("connect server succ");
// 异步读socket数据
socketStream = client.GetStream ();
socketStream.BeginRead (byteBuffer, 0, MAX_READ, new AsyncCallback (OnRead), new SocketState (client, socketStream));
// 通知连接成功
IsConnected = true;
NetworkManager.AddEvent (Protocal.Connect, null);
} else {
// 通知连接失败
Log.Instance.info ("connect server failed");
NetworkManager.AddEvent (Protocal.ConnectFail, null);
}
} catch (SocketException e) {
Log.Instance.error ("connect error", e);
if (validConn) {
// 通知连接失败
NetworkManager.AddEvent (Protocal.ConnectFail, null);
} else {
client.Close ();
}
}
}
}
3. 处理连接超时
///
/// 连接超时
///
void OnConnectTimeout ()
{
lock (lockObj) {
if (connectingFlag) {
Log.Instance.error ("connect server timeout");
// 通知连接失败
NetworkManager.AddEvent (Protocal.ConnectFail, null);
}
}
}
4. 异步读取数据
///
/// 读取消息
///
void OnRead (IAsyncResult asr)
{
int bytesRead = 0; // 读取到的字节
bool validConn = false; // 是否是合法的连接
SocketState socketState = (SocketState)asr.AsyncState;
TcpClient client = socketState.client;
if (client == null || !client.Connected) {
return;
}
lock (lockObj) {
try {
validConn = (client == this.client);
NetworkStream socketStream = socketState.socketStream;
// 读取字节流到缓冲区
bytesRead = socketStream.EndRead (asr);
if (bytesRead < 1) {
if (!validConn) {
// 已经重新连接过了
socketStream.Close ();
client.Close ();
} else {
// 被动断开时
// 通知连接被断开
OnDisconnected (DisType.Disconnect, "bytesRead < 1");
}
return;
}
// 接受数据包,写入缓冲区
OnReceive (byteBuffer, bytesRead);
// 再次监听服务器发过来的新消息
Array.Clear (byteBuffer, 0, byteBuffer.Length); //清空数组
socketStream.BeginRead (byteBuffer, 0, MAX_READ, new AsyncCallback (OnRead), socketState);
} catch (Exception e) {
Log.Instance.errorFormat ("read data error, connect valid:{0}", e, validConn);
if (validConn) {
// 通知连接被断开
OnDisconnected (DisType.Exception, e);
} else {
socketStream.Close ();
client.Close ();
}
}
}
// 对消息进行解码
if (bytesRead > 0) {
OnDecodeMessage ();
}
}
对于数据的解包和封包,推荐MiscUtil
这个库十分好用,大端小端模式都能很好处理。
5. 发送消息
///
/// 发送消息
///
public bool SendMessage (Request request)
{
try {
bool ret = WriteMessage (request.ToBytes ());
request.Clear ();
return ret;
} catch (Exception e) {
Log.Instance.errorFormat ("write message error, requestId:{0}", e, request.GetRequestId ());
}
return false;
}
///
/// 写数据
///
bool WriteMessage (byte[] message)
{
bool ret = true;
using (MemoryStream ms = new MemoryStream ()) {
ms.Position = 0;
EndianBinaryWriter writer = new EndianBinaryWriter (EndianBitConverter.Big, ms);
int msglen = message.Length;
writer.Write (msglen);
writer.Write (message);
writer.Flush ();
lock (lockObj) {
if (null != socketStream) {
byte[] bytes = ms.ToArray ();
socketStream.BeginWrite (bytes, 0, bytes.Length, new AsyncCallback (OnWrite), socketStream);
ret = true;
} else {
Log.Instance.warn ("write data, but socket not connected");
ret = false;
}
}
}
return ret;
}
///
/// 向链接写入数据流
///
void OnWrite (IAsyncResult r)
{
lock (lockObj) {
try {
NetworkStream socketStream = (NetworkStream)r.AsyncState;
socketStream.EndWrite (r);
} catch (Exception e) {
Log.Instance.error ("write data error", e);
if ((e is IOException) && socketStream == this.socketStream) {
// IO 异常并且还是当前连接
OnDisconnected (DisType.Exception, e);
}
}
}
}
6. 总结
为了防止并发,这里使用lock
对于共享变量client
、socketStream
是使用都加了锁。
在出现异常,连接断开的时候都通过事件机制抛给上层使用者,由上层使用者决定如何
处理这个异常。
7. 断线重连处理
断线重连第一步监听TcpClient
使用的过程中,对于异常发生之后触发重连逻辑。
但在移动端比较重要的一点还要做好从后台切回前台过程中及时检查网络连接状态
及时重连。
Android后台切回前台的事件流
onPause(切回后台之前) -> onResume -> focusChanged(false) -> focusChanged(true) (后面3个都是要在前台才能收到)
不切出游戏暂停游戏 focusChanged(false) -> focusChanged(true) // 如呼出键盘,或者下拉通知栏
IOS后台切回前台的事件流
IOS的消息顺序 resignActive(切回后台之前) -> enterBackground -> enterForeground -> becomeActive (后面3个都是要在前台才能收到)
不切出游戏暂停游戏 resignctive -> becomeActive
由上不难看出:
- Android可以监听focusChanged(false) -> focusChanged(true) ,注意onPause要当做一次focusChanged(false)。记录两次事件的间隔,比如间隔时间过长直接重新建立连接,比较短的话立即做一次
网络检查。 - IOS可以监听resignctive -> becomeActive
TcpClient做网络检查可以发送一个0字节的包,代码如下:
///
/// 检查socket状态
///
/// true , if socket was checked, false otherwise.
public bool CheckSocketState ()
{
Log.Instance.info ("check socket state start");
// socket流为空
if (client == null) {
return true;
}
// 不在连接状态
if (!client.Connected) {
Log.Instance.info ("check socket state end, socket is not connected");
return false;
}
// 判断连接状态
bool connectState = true;
Socket socket = client.Client;
bool blockingState = socket.Blocking;
try {
byte[] tmp = new byte[1];
socket.Blocking = false;
socket.Send (tmp, 0, 0);
connectState = true; // 若Send错误会跳去执行catch体,而不会执行其try体里其之后的代码
Log.Instance.info("check socket state succ");
} catch (SocketException e) {
Log.Instance.warnFormat ("check socket error, errorCode:{0}", e.NativeErrorCode);
// 10035 == WSAEWOULDBLOCK
if (e.NativeErrorCode.Equals (10035)) {
// Still Connected, but the Send would block
connectState = true;
} else {
// Disconnected
connectState = false;
}
} finally {
socket.Blocking = blockingState;
}
return connectState;
}