using System; using System.Net; using System.Threading; using com.fpnn.proto; /* * TCPConnection 需要在连接事件完成后,抛弃连接事件代理,避免长时间持有client的引用,导致无法释放资源 * TCPClient 需要在状态转换后,判断是否放弃当前的TCPConnection对象,避免资源持续相互引用,无法释放 */ namespace com.fpnn { /* * Connection events. */ public delegate void ConnectionConnectedDelegate(Int64 connectionId, string endpoint, bool connected); public delegate void ConnectionCloseDelegate(Int64 connectionId, string endpoint, bool causedByError); /* * Process server pushed quests and return answers if necessary. */ public delegate Answer QuestProcessDelegate(Int64 connectionId, string endpoint, Quest quest); public interface IQuestProcessor { QuestProcessDelegate GetQuestProcessDelegate(string method); } /* * Recommend that call Close() method when client instance is no longer used. * If this method haven't been called, the client instance will be existed until which connection is broken. */ public class TCPClient { public enum ClientStatus { Closed, Connecting, Connected } //----------------[ fields ]-----------------------// private object interLocker; private readonly DnsEndPoint dnsEndpoint; public volatile int ConnectTimeout; public volatile int QuestTimeout; public volatile bool AutoConnect; private ClientStatus status; private ManualResetEvent syncConnectingEvent; private TCPConnection connection; private ConnectionConnectedDelegate connectConnectedDelegate; private ConnectionCloseDelegate connectionCloseDelegate; private IQuestProcessor questProcessor; private common.ErrorRecorder errorRecorder; //----------------[ Constructor ]-----------------------// public TCPClient(string host, int port, bool autoConnect = true) { interLocker = new object(); dnsEndpoint = new DnsEndPoint(host, port); ConnectTimeout = 0; QuestTimeout = 0; AutoConnect = autoConnect; status = ClientStatus.Closed; syncConnectingEvent = new ManualResetEvent(false); errorRecorder = ClientEngine.errorRecorder; } public static TCPClient Create(string host, int port, bool autoConnect = true) { return new TCPClient(host, port, autoConnect); } public static TCPClient Create(string endpoint, bool autoConnect = true) { int idx = endpoint.LastIndexOf(':'); if (idx == -1) throw new ArgumentException("Invalid endpoint: " + endpoint); string host = endpoint.Substring(0, idx); string portString = endpoint.Substring(idx + 1); int port = Convert.ToInt32(portString, 10); return new TCPClient(host, port, autoConnect); } //----------------[ Properties methods ]-----------------------// public string Endpoint() { return dnsEndpoint.ToString(); } public ClientStatus Status() { lock (interLocker) { return status; } } public bool IsConnected() { lock (interLocker) { return status == ClientStatus.Connected; } } //----------------[ Configure Operations ]-----------------------// public void SetConnectionConnectedDelegate(ConnectionConnectedDelegate ccd) { lock (interLocker) { connectConnectedDelegate = ccd; } } public void SetConnectionCloseDelegate(ConnectionCloseDelegate cwcd) { lock (interLocker) { connectionCloseDelegate = cwcd; } } public void SetErrorRecorder(common.ErrorRecorder recorder) { lock (interLocker) { errorRecorder = recorder; } } public void SetQuestProcessor(IQuestProcessor processor) { lock (interLocker) { questProcessor = processor; } } //----------------[ Internal Configure Operations ]-----------------------// private void SetClientStatus(TCPConnection conn, ClientStatus newStatus) { lock (interLocker) { if (connection == conn) { status = newStatus; if (status == ClientStatus.Closed) connection = null; } } } private void ConfigConnectedDelegate(TCPConnection conn, ConnectionConnectedDelegate cb, ManualResetEvent finishEvent) { conn.SetConnectedDelegate((Int64 connectionId, string endpoint, bool connected) => { if (!connected) SetClientStatus(conn, ClientStatus.Closed); if (cb != null) try { cb(connectionId, endpoint, connected); } catch (Exception ex) { if (errorRecorder != null) errorRecorder.RecordError("Connected event exception. Remote endpoint: " + endpoint + ".", ex); } if (connected) SetClientStatus(conn, ClientStatus.Connected); finishEvent.Set(); }); } private void ConfigWillCloseDelegate(TCPConnection conn, ConnectionCloseDelegate cb) { conn.SetCloseDelegate((Int64 connectionId, string endpoint, bool causedByError) => { SetClientStatus(conn, ClientStatus.Closed); cb?.Invoke(connectionId, endpoint, causedByError); }); } //----------------[ Connect Operations ]-----------------------// private void RealConnect() { TCPConnection conn; lock (interLocker) { if (status != ClientStatus.Closed) return; connection = new TCPConnection(dnsEndpoint); ConfigConnectedDelegate(connection, connectConnectedDelegate, syncConnectingEvent); ConfigWillCloseDelegate(connection, connectionCloseDelegate); if (questProcessor != null) connection.SetQuestProcessor(questProcessor); if (errorRecorder != null) connection.SetErrorRecorder(errorRecorder); status = ClientStatus.Connecting; syncConnectingEvent.Reset(); conn = connection; } conn.AsyncConnect(ConnectTimeout); } public void AsyncConnect() { RealConnect(); } public bool SyncConnect() { RealConnect(); syncConnectingEvent.WaitOne(); lock (interLocker) { return (status == ClientStatus.Connected); } } public void AsyncReconnect() { Close(); AsyncConnect(); } public bool SyncReconnect() { Close(); return SyncConnect(); } public void Close(bool callCloseEvent = true) { TCPConnection conn; lock (interLocker) { conn = connection; connection = null; status = ClientStatus.Closed; syncConnectingEvent.Set(); //-- If some threads are waiting for sync connecting finished. } if (conn != null) conn.Close(callCloseEvent); } //----------------[ Operations ]-----------------------// public bool SendQuest(Quest quest, IAnswerCallback callback, int timeout = 0) { if (AutoConnect) AsyncConnect(); //-- Auto check and reconnect if necessary. TCPConnection conn = null; lock (interLocker) { conn = connection; } if (conn != null) { if (timeout == 0) timeout = QuestTimeout; if (timeout == 0) timeout = ClientEngine.globalQuestTimeoutSeconds; conn.SendQuest(quest, callback, timeout); return true; } else return false; } public bool SendQuest(Quest quest, AnswerDelegate callback, int timeout = 0) { AnswerDelegateCallback cb = new AnswerDelegateCallback(callback); return SendQuest(quest, cb, timeout); } public Answer SendQuest(Quest quest, int timeout = 0) { if (quest.IsOneWay()) { SendQuest(quest, (IAnswerCallback)null, timeout); return null; } SyncAnswerCallback callback = new SyncAnswerCallback(quest); if (SendQuest(quest, callback, timeout)) { return callback.GetAnswer(); } Answer answer = new Answer(quest); answer.FillErrorCode(ErrorCode.FPNN_EC_CORE_INVALID_CONNECTION); return answer; } public void SendAnswer(Answer answer) { TCPConnection conn = null; lock (interLocker) { conn = connection; } if (conn != null) conn.SendAnswer(answer); } } }