談談Unity游戲TCP連接和網絡重連
Unity中通常使用TcpClient
來進行Tcp連接,TcpClient
支持異步讀寫,避免了我們需要另外開辟線程管理網絡數據發送。
當異步讀寫經常會讓人摸不著頭腦,比較困惑。
1. 建立連接
/// <summary>
/// 連接服務器
/// </summary>
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<TimerManager> (ManagerName.Timer);
timer.AddTask (OnConnectTimeout, CONN_TIMEOUT);
} catch (Exception e) {
Log.Instance.error ("connect server error", e);
// 通知連接失敗
NetworkManager.AddEvent (Protocal.ConnectFail, null);
}
}
}
2. 異步處理連接結果
/// <summary>
/// 連接上服務器
/// </summary>
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. 處理連接超時
/// <summary>
/// 連接超時
/// </summary>
void OnConnectTimeout ()
{
lock (lockObj) {
if (connectingFlag) {
Log.Instance.error ("connect server timeout");
// 通知連接失敗
NetworkManager.AddEvent (Protocal.ConnectFail, null);
}
}
}
4. 異步讀取數據
/// <summary>
/// 讀取消息
/// </summary>
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. 發送消息
/// <summary>
/// 發送消息
/// </summary>
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;
}
/// <summary>
/// 寫數據
/// </summary>
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;
}
/// <summary>
/// 向鏈接寫入數據流
/// </summary>
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字節的包,代碼如下:
/// <summary>
/// 檢查socket狀態
/// </summary>
/// <returns><c>true</c>, if socket was checked, <c>false</c> otherwise.</returns>
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;
}