Hello,
This post is continuation of this. Many times, in our games we have logic which continuously interact with back-end logic (can be in php, java or C#). In order to minimize number of requests which client makes to server, we can send web request in batches. In this post I will explain how to implement a Request Manager to manage web request in batches.
This post assumes that you have idea of WWW and Co routines.
Let us start...
In this post we will create:
This post is continuation of this. Many times, in our games we have logic which continuously interact with back-end logic (can be in php, java or C#). In order to minimize number of requests which client makes to server, we can send web request in batches. In this post I will explain how to implement a Request Manager to manage web request in batches.
This post assumes that you have idea of WWW and Co routines.
Let us start...
In this post we will create:
- BaseRequest: Class which will be parent of all the requests
- IRequest: Interface with basic functionality of request
- IRequestContext: Interface having method for processing response
- Request: Class which extends IRequest
- RequestBatch: Class to contain our batch of requests
- Response: Class to contain response from server
- RequestQueueManager: Class which has implementation to interact with server and process the response
Create following classes in unity project
- Base Request
public class BaseRequest { [JsonIgnore] public IRequestContext RequestContext { get; set; } public BaseRequest() { } }
- IRequest
public interface IRequest { [JsonName("time")] long Time { get; set; } [JsonName("expectedStatus")] int ExpectedStatus { get; set; } [JsonName("requestId")] int RequestId { get; set; } [JsonName("token")] string Token { get; set; } void IncrementRetryCount(); int RetryCount { get; } void Execute(string response); }
- IRequestContext
public interface IRequestContext { void OnSuccess <T>(T response); void OnFailure<T>(T response); void Execute(string response); }
- Request
public class Request <T> : IRequest { public Request(T args) { Arguments = args; Time = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; // seconds since Jan 1st 1970 ExpectedStatus = 0; RequestId = 1; Token = Time.ToString() + RequestId.ToString(); } [JsonName("time")] public long Time { get; set; } [JsonName("expectedStatus")] public int ExpectedStatus { get; set; } [JsonName("requestId")] public int RequestId { get; set; } [JsonName("token")] public string Token { get; set; } [JsonName("args")] public T Arguments { get; set; } int retryCount; public int RetryCount { get { return retryCount; } } public void IncrementRetryCount() { retryCount++; } public void Execute(string response) { (Arguments as BaseRequest).RequestContext.Execute(response); } }
- RequestBatch
public class RequestBatch { private static int nextBatchId = 100; private int id; public int Id { get { return id; } } int retryCount; public int RetryCount { get { return retryCount; } } [JsonName("requests")] public List <IRequest> Requests { get; set; } public void IncrementRetryCount() { ++retryCount; } [JsonName("sentclienttime")] public long SentClientTime { get; set; } public RequestBatch() { id = nextBatchId++; Requests = new List<IRequest>(); } }
- Response
public class BatchResponse { [JsonName("data")] public Response[] Data { get; set; } [JsonName("servertime")] public long ServerTime { get; set; } } public class Response { [JsonName("requestId")] public int RequestId { get; set; } [JsonName("requestdata")] public object RequestData { get; set; } } public class SampleResponse { [JsonName("status")] public int Status { get; set; } }
- RequestQueueManager
public class RequestQueueManager : MonoBehaviour { public static RequestQueueManager Instance; private static int INSTANCECOUNT = 0; // Self tick interval. private const int TIMER_MS = 500; public int batchAutoFlushSize = 5; public int batchAutoFlushSec = 15; public int batchTimeoutSec = 30; public int maxBatchRetryCount = 3; public bool verbose = true; // ------------------------------ // PRIVATE VARIABLES // ------------------------------ static int RequestCount = 1; // Commands waiting to be batched and sent. List <IRequest> queue = new List<IRequest>(); List<RequestBatch> asyncBatchesInFlight = new List<RequestBatch>(); // The utc time when the first element was added to the current queue. private long queueFirstElemTime = -1; // Set by call or flush. private bool queueReadyToBeSent = false; // The synchronous batch currently in flight, or null. private RequestBatch activeBatch; // The timer we use to tick ourselves. private Timer processTimer; bool isProcessing; private static readonly System.DateTime Jan1st1970 = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); void Awake() { Instance = this; INSTANCECOUNT++; } public RequestQueueManager() { } public void Initialize() { if (INSTANCECOUNT > 1) { Debug.LogError("More than 1 intance of RequestQueueManager"); } processTimer = new Timer(TIMER_MS); processTimer.addListener(() => { Process(); }); processTimer.start(); } public bool IsRunning { get { return null != processTimer; } } public void Add(IRequest request) { if (0 == queue.Count) { queueFirstElemTime = Helpers.CurrentTime(Jan1st1970); } request.RequestId = RequestCount++; request.Token = GetTimestamp(DateTime.UtcNow); queue.Add(request); isProcessing = false; queueReadyToBeSent = true; } public void AsyncAdd<T>(Request<T> request) { List<IRequest> asyncQueue = new List<IRequest>(); request.RequestId = RequestCount++; request.Token = GetTimestamp(DateTime.UtcNow); asyncQueue.Add(request); RequestBatch batch = PostRequests(asyncQueue); asyncBatchesInFlight.Add(batch); } void RemoveRequestsFromQueueWhichHaveTimedOut(List<IRequest> localQueue) { localQueue.RemoveAll(x => x.RetryCount >= maxBatchRetryCount); } protected void Process() { int index = 0; // Check async batches for timeouts. for (index = asyncBatchesInFlight.Count - 1; index >= 0; index--) { RequestBatch batch = asyncBatchesInFlight[index]; if (hasBatchTimedOut(batch)) { if (incrementRetriesAndIsTooMany(batch)) { if (verbose) { Debug.Log("Removing async batch, as max retry count exceeded, batch Retry count: " + batch.RetryCount); } asyncBatchesInFlight.Remove(batch); batch = null; } else { if (verbose) { Debug.Log("Resending async batch with retry count: " + batch.RetryCount); } resendBatch(batch); } } } // If batch in flight... if (null != activeBatch) { // Check to see if it has timed out. if (hasBatchTimedOut(activeBatch)) { if (verbose) { Debug.Log("Time out occured retrying.."); } if (incrementRetriesAndIsTooMany(activeBatch)) { if (verbose) { Debug.Log("Removing active batch, as max retry count exceeded, batch Retry count: " + activeBatch.RetryCount); } RemoveRequestsFromQueueWhichHaveTimedOut(queue); activeBatch = null; } else { if (verbose) { Debug.Log("Resending acive batch with retry count: " + activeBatch.RetryCount); } resendBatch(activeBatch); } } // Done: wait for batch to return. return; } // If queue is empty, we have nothing to do. if (0 == queue.Count || isProcessing) { return; } double queueAgeSec = (Helpers.CurrentTime(Jan1st1970) - queueFirstElemTime) * 0.001; // If queue is long enough, or old enough, or has been called, // dispatch it. if (queue.Count >= batchAutoFlushSize || queueAgeSec > batchTimeoutSec || queueReadyToBeSent) { if (verbose) { Debug.Log("Checking and sending commands to post"); } activeBatch = PostRequests(queue); isProcessing = true; queueFirstElemTime = -1; queueReadyToBeSent = false; } } private System.Collections.IEnumerator ExecuteBatch(RequestBatch batch) { string jsonRequest = JsonWriter.Serialize(batch); WWWForm form = new WWWForm(); form.AddField("batch", jsonRequest); if (verbose) { Debug.Log("jsonRequest: " + jsonRequest); } WWW www = new WWW("http://www.ashwanik.in/blog/process", form); while (!www.isDone) { if (hasBatchTimedOut(batch)) { if (verbose) { Debug.Log("ExecuteBatch: Batch timed out"); } www.Dispose(); www = null; break; } yield return null; } if (www != null && www.error == null && www.isDone && www.text != null) { if (verbose) { Debug.Log("Starting processing " + www.text); } ProcessResponse<int>(batch, www.text); } } private void ProcessResponse<Result>(RequestBatch batch, string batchResponse) { try { if (verbose) { Debug.Log("Processing response " + batchResponse); } Dictionary<int, IRequest> requestDictionary = new Dictionary<int, IRequest>(); foreach (IRequest request in batch.Requests) { requestDictionary.Add(request.RequestId, request); } JsonObject obj = new JsonObject(batchResponse); JsonArray data = obj["data"] as JsonArray; int requestId = 0; IRequest localRequest = null; foreach (JsonObject item in data) { requestId = Convert.ToInt32(item["requestId"]); localRequest = requestDictionary[requestId]; localRequest.Execute(item.ToString()); if (requestDictionary.ContainsKey(requestId)) { queue.Remove(requestDictionary[requestId]); } else { if (verbose) { Debug.Log("Received response for request which is not in queue: " + requestId + " Response: " + item.ToString()); } } if (verbose) { Debug.Log("Each request response: " + item.ToString()); } } if (activeBatch == batch) { activeBatch = null; } else { int index = 0; for (index = 0; index < asyncBatchesInFlight.Count; ++index) { if (batch == asyncBatchesInFlight[index]) { if (verbose) { Debug.Log("Removed batch from aync batches" + index); } asyncBatchesInFlight.Remove(batch); break; } } } batch = null; } catch (Exception ex) { Debug.Log(ex.Message + ex.StackTrace); } } private RequestBatch PostRequests(List<IRequest> requests, RequestBatch batch = null) { if (null == batch) { batch = new RequestBatch(); } batch.SentClientTime = Helpers.CurrentTime(Jan1st1970); if (requests != null && requests.Count > 0) { foreach (IRequest request in requests) { if (!batch.Requests.Where(x => x.RequestId == request.RequestId).Any()) { batch.Requests.Add(request); } } ///Removing requests from queue only for active batch when retry count is equal to (Max retry count -1) if (batch == activeBatch && batch.RetryCount >= (maxBatchRetryCount - 1)) { RemoveRequestsFromQueueWhichHaveTimedOut(requests); } ///Remove request from batch requests list else if (batch.RetryCount >= (maxBatchRetryCount - 1)) { RemoveRequestsFromQueueWhichHaveTimedOut(batch.Requests); } } if (batch.Requests.Count > 0) { StartCoroutine(ExecuteBatch(batch)); } else { batch = null; } return batch; } private String GetTimestamp(DateTime value) { return value.ToString("yyyyMMddHHmmssffff"); } private bool hasBatchTimedOut(RequestBatch batch) { // Check for timeout long batchAge = (Helpers.CurrentTime(Jan1st1970) - batch.SentClientTime) / 1000; if (batchAge > batchTimeoutSec) { return true; } return false; } /** * Increments the batch's retry count and the returns true if * the batch has reached the retry limit. */ private bool incrementRetriesAndIsTooMany(RequestBatch batch) { batch.IncrementRetryCount(); foreach (IRequest request in batch.Requests) { request.IncrementRetryCount(); } if (batch.RetryCount >= maxBatchRetryCount) { return true; } return false; } private void resendBatch(RequestBatch batch) { if (activeBatch == batch) { // Append any new queued commands to this batch and resend. PostRequests(queue, batch); isProcessing = true; } else { // Async, so nothing to append. PostRequests(null, batch); } } }
In order to test the request manager, create
- SampleRequest
public class SampleRequest : BaseRequest { #region Methods public SampleRequest(IRequestContext context) { this.RequestContext = context; } [JsonName("type")] public RequestType Type { get; set; } #endregion }
- SampleRequestContext
public class SampleRequestContext : IRequestContext { public void OnSuccess <T>(T response) { Debug.Log("Sample Request successful!"); } public void OnFailure<T>(T response) { Debug.Log("Sample Request failed!"); } public void Execute(string response) { var serializedResponse = (Helpers.Serialize<SampleResponse>(response)); if (serializedResponse.Status == 0) { OnSuccess<SampleResponse>(serializedResponse); } else { OnFailure<SampleResponse>(serializedResponse); } } }
- RequestManagerTester
public class RequestManagerTester : MonoBehaviour { #region Methods void Awake() { } void Start() { RequestQueueManager.Instance.Initialize(); } void OnGUI() { GUILayout.BeginArea(new Rect((Screen.width - 310) / 2, 200, 310, 1000)); if (GUILayout.Button("Send Sync Sample Request", GUILayout.Width(300), GUILayout.Height(20))) { SampleRequest sampleRequest = new SampleRequest(new SampleRequestContext()); IRequest request = new Request <samplerequest>(sampleRequest); RequestQueueManager.Instance.Add(request); } if (GUILayout.Button("Send Aync Sample Request", GUILayout.Width(300), GUILayout.Height(20))) { SampleRequest sampleRequest = new SampleRequest(new SampleRequestContext()); Request<SampleRequest> request = new Request<SampleRequest>(sampleRequest); RequestQueueManager.Instance.AsyncAdd<SampleRequest>(request); } GUILayout.EndArea(); } #endregion }
Attach RequestQueueManager and RequestManagerTester to Main Camera
of the scene and save the scene. Now play the scene in the editor.
You can get the sample project here.
Hope this helps.
Thanks for printing this post. Hope you liked it.
Keep visiting and sharing.
Thanks,
Ashwani.
Keep visiting and sharing.
Thanks,
Ashwani.
0 comments :
Post a Comment