Web server에 데이터를 전송 및 받는 것을 하고 싶다면, Web에 요청을 해야합니다.
저는 UnityWebRequest를 활용하여 Web에 요청하였습니다!
UnityWebRequest API를 활용하여 Datamodule를 설계하여 HTTP통신을 했습니다! Handler를 활용하여 다양한 네트워크 타입과 데이터 타입을 지정하고, 헤더는 커스텀을 하여 토큰과 함께 웹에 요청합니다. Web 통신에 대한 비동기 처리는 모바일 기기의 방대한 양의 데이터 처리를 대비하여 코루틴 사용대신 C#에서 제공하는 async-await를 활용했습니다. 여기서 async-await, Task는 유니티에 맞게 최적화한 UniTask를 사용하여 비동기처리 했습니다.
오늘의 요청은 WebRequestBuffer입니다
Buffer는 핸들러 중에 제일 단순하고 대부분의 사용 사례를 처리합니다.
이중에서 DownloadHandlerBuffer는 다운로드가 완료 되면 바이트 배열 또는 텍스트 문자열로 버퍼링된 데이터에 엑세스 할 수 있어 주로 Json 데이터를 주고받을 때 사용됩니다.
제가 작성한 DataModule 안의 WebRequestBuffer 함수를 차근차근 살펴봅시다!
private const string DOMAIN = "도메인 주소";
public static string REPLACE_BEARER_TOKEN = "임시 토큰";
/// <summary>
/// Network Type 설정
/// </summary>
public enum NetworkType
{
GET,
POST,
PUT,
DELETE
}
/// <summary>
/// 보낼 Data type 설정
/// </summary>
public enum DataType
{
BUFFER,
TEXTURE,
ASSETBUNDLE,
FILE
}
protected static double timeout = 180;
도메인 주소와 임시토큰을 전역에 임시로 넣어둡니다.
NetworkType과,
DataType은 enum형으로,
Request의 NetworkType을 넣거나,
특정 Handler를 request의 Handler에 넣을때 활용됩니다.
CheckNetwork 함수를 호출하여 네트워크 환경을 체킹합니다.
이 CheckNetwork함수는 위의 레퍼런스에 개발자분이 잘 구성하셔서 활용하였습니다!
Timeout 설정까지 하여 서버 요청 후 응답 시간이 길어지면 Timeout로그가 뜨게 되어 네트워크 환경 체킹에 도움이 많이 됐습니다!
중간에 끼워있는 requestURL은 WebRequestBuffer함수가 호출될때 전달받은 Server의 Rest API url과 도메인을 합쳐서 정의된다.
/// <summary>
/// Web에 Data를 Request할 수 있다.
/// </summary>
/// <typeparam name="T">Json 형식</typeparam>
/// <param name="_url">Json 올릴 url</param>
/// <param name="networkType">어떻게 Request 할 것인가</param>
/// <param name="data">보낼 데이터</param>
/// <returns></returns>
public static async UniTask<T> WebRequestBuffer<T>(string _url, NetworkType networkType, DataType dataType, string data = null, List<IMultipartFormSection> form = null, string filePath = null)
{
//네트워크 체킹
await CheckNetwork();
//API URL 생성
string requestURL = DOMAIN + _url;
//Timeout 설정
var cts = new CancellationTokenSource();
cts.CancelAfterSlim(TimeSpan.FromSeconds(timeout));
/// <summary>
/// Check Network
/// </summary>
/// <returns></returns>
private static async UniTask CheckNetwork()
{
if (Application.internetReachability == NetworkReachability.NotReachable)
{
Debug.LogError("The netwok is not connected");
await UniTask.WaitUntil(() => Application.internetReachability != NetworkReachability.NotReachable);
Debug.Log("The network is connected");
}
}
UnityWebRequest request;
//웹 요청 생성(Get,Post,Delete,Update)
//Texture인가 Buffer인가?
request = new UnityWebRequest(requestURL, networkType.ToString());
request.downloadHandler = DownHandlerFactory(dataType, filePath, requestURL);
if (data != null)
{
byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(data);
request.uploadHandler = new UploadHandlerRaw(jsonToSend);
}
Webserver에 Request를 보낼 UnityWebRequest를 생성해줍니다.
생성자 안의 인자값을 보면
앞에서 정의해준 requestURL을 넣고,
앞서 설명드렸던 enum형인 NewtorkType을 string형으로 하여 생성해줍니다.
또한 해당 request의 DownloadHandler는 제가 따로 만들어둔 DownHandlerFactory에서 dataType에 맞는 Handler를 return하여 넣어줍니다.
download가 아닌 upload도 Handler를 넣어줘야합니다.
그러나 Post와 Put이 아닌 Get같은 Network type은 데이터를 보낼 필요가 없기 때문에, data를 기본 매개변수로 null값을 넣어두고, null이 아니면 보낼 데이터가 있다는 것으로 간주하고, uploadHandler를 생성해줍니다.
/// <summary>
/// DownloadHandlerFactory
/// </summary>
/// <param name="dataType"></param>
/// <param name="filePath"></param>
/// <param name="url"></param>
/// <param name="crc"></param>
/// <returns></returns>
private static DownloadHandler DownHandlerFactory(DataType dataType, string filePath = null, string url = null, uint crc = 0)
{
switch (dataType)
{
case DataType.BUFFER:
return new DownloadHandlerBuffer();
case DataType.TEXTURE:
return new DownloadHandlerTexture();
case DataType.ASSETBUNDLE:
return new DownloadHandlerAssetBundle(url, crc);
case DataType.FILE:
return new DownloadHandlerFile(filePath);
default:
return null;
}
}
먼저 Header에 넣을 자신임을 증명할 토큰을 PlayerPrefs에서 가져옵니다!
여기서 만약에 IMultipartFormSection의 form 데이터가 들어올 경우 Header의 content type을 json으로 설정하면 안되기 때문에
form 데이터가 들어올 경우에는 form 데이터를 Post하여 request를 받고,
토큰을 Header에 넣어서 요청을 보냅니다.
form data가 없으면 Header에 토큰과, content type을 같이 넣어서 보내줍니다.
여기서 Header는 여러개가 올 수 있음을 가정해 SetHeader라는 함수에서 만들어서 request에 설정 해줍니다.
*참고로 여기서 form data는 zip파일을 server에 보낼 때 사용되었습니다! 그거에 대한 이야기도 후에 자세히 풀어드리겠습니다.
//Header 정보 입력
if (REPLACE_BEARER_TOKEN == "" && PlayerPrefs.GetString("Bearer") != "")
{
REPLACE_BEARER_TOKEN = PlayerPrefs.GetString("Bearer");
}
if (form != null)
{
request = UnityWebRequest.Post(requestURL, form);
SetHeaders(request, "Authorization", "Bearer " + REPLACE_BEARER_TOKEN);
}
else
{
SetHeaders(request, "Authorization", "Bearer " + REPLACE_BEARER_TOKEN);
SetHeaders(request, "Content-Type", "application/json");
}
/// <summary>
/// Setting Header
/// </summary>
/// <param name="request"></param>
/// <param name="value1"></param>
/// <param name="value2"></param>
private static void SetHeaders(UnityWebRequest request, string value1, string value2)
{
request.SetRequestHeader(value1, value2);
}
마지막으로 이렇게 완성된request에서 SendWebRequest를 호출하여 보냅니다.
그 후에 받은 response를 파싱하여 return해줍니다.
보내는 과정에서 server 문제라던지 클라이언트 문제가 생긴다면
try-catch문에서 잡혀 exception을 로그로 찍어줍니다.
try
{
var res = await request.SendWebRequest().WithCancellation(cts.Token);
T result = JsonUtility.FromJson<T>(res.downloadHandler.text);
return result;
}
catch (OperationCanceledException ex)
{
if (ex.CancellationToken == cts.Token)
{
Debug.Log("Timeout");
//TODO: 네트워크 재시도 팝업 호출.
//재시도
return await WebRequestBuffer<T>(_url, networkType, dataType, data);
}
}
catch (Exception e)
{
Debug.Log(e.Message);
return default;
}
return default;
public class DataModule
{
private const string DOMAIN = "도메인 주소";
public static string REPLACE_BEARER_TOKEN = "임시 토큰";
/// <summary>
/// Network Type 설정
/// </summary>
public enum NetworkType
{
GET,
POST,
PUT,
DELETE
}
/// <summary>
/// 보낼 Data type 설정
/// </summary>
public enum DataType
{
BUFFER,
TEXTURE,
ASSETBUNDLE,
FILE
}
protected static double timeout = 180;
/// <summary>
/// Web에 Data를 Request할 수 있다.
/// </summary>
/// <typeparam name="T">Json 형식</typeparam>
/// <param name="_url">Json 올릴 url</param>
/// <param name="networkType">어떻게 Request 할 것인가</param>
/// <param name="data">보낼 데이터</param>
/// <returns></returns>
public static async UniTask<T> WebRequestBuffer<T>(string _url, NetworkType networkType, DataType dataType, string data = null, List<IMultipartFormSection> form = null, string filePath = null)
{
//네트워크 체킹
await CheckNetwork();
//API URL 생성
string requestURL = DOMAIN + _url;
//Timeout 설정
var cts = new CancellationTokenSource();
cts.CancelAfterSlim(TimeSpan.FromSeconds(timeout));
UnityWebRequest request;
//웹 요청 생성(Get,Post,Delete,Update)
//Texture인가 Buffer인가?
request = new UnityWebRequest(requestURL, networkType.ToString());
request.downloadHandler = DownHandlerFactory(dataType, filePath, requestURL);
if (data != null)
{
byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(data);
request.uploadHandler = new UploadHandlerRaw(jsonToSend);
}
//Header 정보 입력
if (REPLACE_BEARER_TOKEN == "" && PlayerPrefs.GetString("Bearer") != "")
{
REPLACE_BEARER_TOKEN = PlayerPrefs.GetString("Bearer");
}
if (form != null)
{
request = UnityWebRequest.Post(requestURL, form);
SetHeaders(request, "Authorization", "Bearer " + REPLACE_BEARER_TOKEN);
}
else
{
SetHeaders(request, "Authorization", "Bearer " + REPLACE_BEARER_TOKEN);
SetHeaders(request, "Content-Type", "application/json");
}
try
{
var res = await request.SendWebRequest().WithCancellation(cts.Token);
T result = JsonUtility.FromJson<T>(res.downloadHandler.text);
return result;
}
catch (OperationCanceledException ex)
{
if (ex.CancellationToken == cts.Token)
{
Debug.Log("Timeout");
//TODO: 네트워크 재시도 팝업 호출.
//재시도
return await WebRequestBuffer<T>(_url, networkType, dataType, data);
}
}
catch (Exception e)
{
Debug.Log(e.Message);
return default;
}
return default;
}
/// <summary>
/// Check Network
/// </summary>
/// <returns></returns>
private static async UniTask CheckNetwork()
{
if (Application.internetReachability == NetworkReachability.NotReachable)
{
Debug.LogError("The netwok is not connected");
await UniTask.WaitUntil(() => Application.internetReachability != NetworkReachability.NotReachable);
Debug.Log("The network is connected");
}
}
/// <summary>
/// Setting Header
/// </summary>
/// <param name="request"></param>
/// <param name="value1"></param>
/// <param name="value2"></param>
private static void SetHeaders(UnityWebRequest request, string value1, string value2)
{
request.SetRequestHeader(value1, value2);
}
/// <summary>
/// DownloadHandlerFactory
/// </summary>
/// <param name="dataType"></param>
/// <param name="filePath"></param>
/// <param name="url"></param>
/// <param name="crc"></param>
/// <returns></returns>
private static DownloadHandler DownHandlerFactory(DataType dataType, string filePath = null, string url = null, uint crc = 0)
{
switch (dataType)
{
case DataType.BUFFER:
return new DownloadHandlerBuffer();
case DataType.TEXTURE:
return new DownloadHandlerTexture();
case DataType.ASSETBUNDLE:
return new DownloadHandlerAssetBundle(url, crc);
case DataType.FILE:
return new DownloadHandlerFile(filePath);
default:
return null;
}
}
}
예를 들어 하나만 살펴보자면,
User정보를 Web에서 받아오고 싶다면
먼저 User 정보를 Get하는 API URL을 넣고, Network Type은 Get타입으로 하고, Json형식으로 받아오는 것이기에 Buffer로 DataType을 지정해줍니다!
이외에 보낼 데이터는 없고, 폼데이터도 보내는 것이 아니기에 인자값을 기본
매개변수로 비워둡니다.
response body에서 result가 true라면 서버에서 성공했기에 서버에서 true와 Data를 보내줍니다!
[Serializable]
public class ResultGetId<T>
{
public bool result;
public T data;
}
[Serializable]
public class UserData
{
public int id;
public string profileImg;
public string type;
public string nickname;
public string avatar;
public int badgeCount;
public int productCount;
public string hp;
public int currentLandId;
public string createdAt;
public string updatedAt;
}
public class LoadingData : MonoBehaviourPunCallbacks
{
private async void Start()
{
ResultGetId <UserData> userData = await DataModule.WebRequestBuffer<ResultGetId<UserData>>(User정보를 받아오는 API url, DataModule.NetworkType.GET, DataModule.DataType.BUFFER);
if (userData.result)
{
Debug.Log("성공");
}
}
}