HTML5 + CSS + JavaScript 로 멀티 플랫폼 Application 을 만들기

Desktop 용 Application 을 만든다면 [Electron]
예) Skype, Visual Studio Code, Atom, Slack, …

Mobile 용 App 을 만든다면 [Apache Cordova]
Cordova 로 직접 App 을 만드는 건 마치 Java 로 Andriod 앱을 만드는 것과 다름이 없을 거 같고…
Cordova 를 다시 특정 용도에 맞게 좀더 다듬은 것을 사용하지 않을까 생각되네요.
예) Adobe PhoneGap, Ionnic, InstaBug, …

HyperLogLog 에 대해 잠깐 테스트를 해 보았습니다.

BS가 근무하는 팀에서 이런 저런 스터디를 하는데 Redis 에 대해서 스터디 중입니다.
이번 스터디에 Redis 의 PFADD, PFCOUNT, PFMERGE 를 다루게 되어 조금 찾아 보았습니다.

참고자료 #1: [성태의 닷넷 이야기]
참고자료 #2: [Naver D2]
참고자료 #3: [Adnan.Korkmaz Blog]
참고자료 #4: [Microsoft CardinalityEstimation]
참고자료 #5: [Wikipedia]

위 자료에서 3, 4번의 소스와 라이브러리를 받아 간단하게 테스트 프로그램을 작성하였습니다.

using CardinalityEstimation;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;

namespace HyperLogLogTest {

    // from http://adnan-korkmaz.blogspot.com/2012/06/hyperloglog-c-implementation.html

    public class HyperLogLog {
        private readonly double mapSize, alpha_m, k;
        private readonly int kComplement;
        private readonly Dictionary<int, int> Lookup = new Dictionary<int, int>();
        private const double pow_2_32 = 4294967297;
        private readonly Func<object, uint> hashFunc;

        public HyperLogLog(double stdError, Func<object, uint> hashFunc) {
            this.hashFunc = hashFunc;

            mapSize = 1.04 / stdError;
            k = (long)Math.Ceiling(Log2(mapSize * mapSize));

            kComplement = 32 - (int)k;
            mapSize = (long)Math.Pow(2, k);

            alpha_m = mapSize == 16 ? 0.673
                  : mapSize == 32 ? 0.697
                  : mapSize == 64 ? 0.709
                  : 0.7213 / (1 + 1.079 / mapSize);
            for (int i = 0; i < mapSize; i++)
                Lookup[i] = 0;
        }

        private static double Log2(double x) {
            return Math.Log(x) / 0.69314718055994530941723212145818;//Ln2 
        }
        private static int GetRank(uint hash, int max) {
            int r = 1;
            uint one = 1;
            while ((hash & one) == 0 && r <= max) { ++r; hash >>= 1;
            }
            return r;
        }

        public int Count() {
            double c = 0, E;

            for (var i = 0; i < mapSize; i++)
                c += 1d / Math.Pow(2, Lookup[i]);

            E = alpha_m * mapSize * mapSize / c;

            // Make corrections & smoothen things. 
            if (E <= (5 / 2) * mapSize) {
                double V = 0;
                for (var i = 0; i < mapSize; i++) if (Lookup[i] == 0) V++; if (V > 0)
                    E = mapSize * Math.Log(mapSize / V);
            }
            else
                if (E > (1 / 30) * pow_2_32)
                E = -pow_2_32 * Math.Log(1 - E / pow_2_32);
            // Made corrections & smoothen things, or not. 

            return (int)E;
        }

        public void Add(object val) {
            uint hashCode = hashFunc(val);
            int j = (int)(hashCode >> kComplement);

            Lookup[j] = Math.Max(Lookup[j], GetRank(hashCode, kComplement));
        }
    }

    class Program {

        static MD5 md5 = MD5.Create();
        static readonly string outputFormat = "{0,-40}: {1,10}: {2,10}";

        static uint CustomHash(object val) {
            string text = val.ToString();
            uint hash = 0;

            for (int i = 0, l = text.Length; i < l; i++) {
                hash += text[i];
                hash += hash << 10; hash ^= hash >> 6;
            }
            hash += hash << 3; hash ^= hash >> 6;
            hash += hash << 16;

            return hash;
        }

        static uint DefaultHash(object val) {
            return (uint)val.GetHashCode();
        }

        static uint MD5Hash(object val) {
            var bt = md5.ComputeHash(Encoding.UTF8.GetBytes(val.ToString()));
            return BitConverter.ToUInt32(bt, 0) ^ BitConverter.ToUInt32(bt, 4) ^ BitConverter.ToUInt32(bt, 8) ^ BitConverter.ToUInt32(bt, 12);
        }

        static void HyperLogLogTest(Func<object, uint> hashFunc, string name, int testCount) {
            var hll = new HyperLogLog(0.01, hashFunc);
            for (var i = 0; i < testCount; i++)
                hll.Add(i);
            Console.WriteLine(outputFormat, name, testCount, hll.Count());
        }

        // from https://github.com/Microsoft/CardinalityEstimation
        static void MSHyperLogLogTestFnv1A(Func<object, uint> hashFunc, string name, int testCount) {
            var hllMS = new CardinalityEstimator(hashFunctionId: CardinalityEstimation.Hash.HashFunctionId.Fnv1A);
            for (var i = 0; i < testCount; i++)
                hllMS.Add(i);
            Console.WriteLine(outputFormat, name, testCount, hllMS.Count());
        }

        // from https://github.com/Microsoft/CardinalityEstimation
        static void MSHyperLogLogTestMurmur3(Func<object, uint> hashFunc, string name, int testCount) {
            var hllMS = new CardinalityEstimator(hashFunctionId: CardinalityEstimation.Hash.HashFunctionId.Murmur3);
            for (var i = 0; i < testCount; i++)
                hllMS.Add(i);
            Console.WriteLine(outputFormat, name, testCount, hllMS.Count());
        }

        static void Main(string[] args) {
            HyperLogLogTest(DefaultHash, "Adnan.Korkmaz: DefaultHash", 10);
            HyperLogLogTest(DefaultHash, "Adnan.Korkmaz: DefaultHash", 10000);
            HyperLogLogTest(MD5Hash, "Adnan.Korkmaz: MD5 + XOR", 10);
            HyperLogLogTest(MD5Hash, "Adnan.Korkmaz: MD5 + XOR", 10000);
            HyperLogLogTest(CustomHash, "Adnan.Korkmaz: Adnan.Korkmaz", 10);
            HyperLogLogTest(CustomHash, "Adnan.Korkmaz: Adnan.Korkmaz", 10000);
            MSHyperLogLogTestFnv1A(CustomHash, "CardinalityEstimation with Fnv1A", 10);
            MSHyperLogLogTestFnv1A(CustomHash, "CardinalityEstimation with Fnv1A", 10000);
            MSHyperLogLogTestMurmur3(CustomHash, "CardinalityEstimation with Murmur3", 10);
            MSHyperLogLogTestMurmur3(CustomHash, "CardinalityEstimation with Murmur3", 10000);
        }
    }
}

최대한 비슷한 결과를 얻기 위해 두 라이브러리 모두 레지스터 수량을 16K 개(pow(2, 14)) 로 하였습니다.
그리고 결과는 아래와 같이 나왔습니다.

Adnan.Korkmaz: DefaultHash              :         10:          1
Adnan.Korkmaz: DefaultHash              :      10000:          1
Adnan.Korkmaz: MD5 + XOR                :         10:         10
Adnan.Korkmaz: MD5 + XOR                :      10000:      10068
Adnan.Korkmaz: Adnan.Korkmaz            :         10:         10
Adnan.Korkmaz: Adnan.Korkmaz            :      10000:       9853
CardinalityEstimation with Fnv1A        :         10:         10
CardinalityEstimation with Fnv1A        :      10000:      11419
CardinalityEstimation with Murmur3      :         10:         10
CardinalityEstimation with Murmur3      :      10000:       9980

여기에서 확인되는 것은 동일한 알고리즘으로 구현된 것이라도 어떤 Hash 함수를 넣는가에 따라서 굉장히 다른 결과를 얻게 된다는 것입니다.
C#의 GetHashCode() 의 경우에는… 쩝…

Redis 의 PFADD, PFCOUNT 를 쓰면

var redis = require('redis');
var client = redis.createClient();

function test(n) {
  client.flushall();
  for (i = 0; i < n; i++)
    client.pfadd("hll", i);
  client.pfcount("hll", function(e, r) {
    console.log(n, r);
  });
}

test(10);
test(10000);

client.flushall();
client.quit();
10 10
10000 9999

이렇게 나오네요.

metadata-extractor – EXIF 정보를 읽을 수 있는 .NET Framework 용 라이브러리

EXIF 정보를 읽어야 할 일이 생겨서 찾다보니 이것!

metadata-extractor

EXIF란 Exchangeable Image File Format 의 약어로, JPEG, TIFF 등 이미지 파일에 각종 메타정보를 저장하는 포맷입니다.

쌩으로 .NET Framework 에 있는 기본 라이브러리로 읽을 수도 있겠지만… 엄청 고난의 길…

metadata-extractor 는 Java, C# 을 지원하기 때문에 웹 기반 + 사진 관련된 어플리케이션을 개발한다면 유용하게 사용할 수 있습니다.

물론 정말 하드코어하게

Image.PropertyItems 으로 이런 식으로 직접 만들 수 있습니다.

C언어 요걸 요새 어디에다가 써먹지?

C++말고 걍 C 이거 쓸데 없는 녀석 아닌가 싶은 생각이 들었는데

잘 생각해보면

C++의 표준 라이브러리인 STL은 컴파일 러의 버전에 너무 민감하여 다양한 컴파일러를 지원해야 하는 라이브러리를 만들 때에는 문제가 될 수 있다.

정적 라이브러리를 컴파일러 버전 무관하게 사용할 수 있도록 개발해야 한다면 C 말고는 노답

banker’s rounding

[System.Math.Round 설명 페이지]

위 링크를 들어가면 “Rounding to nearest, or banker’s rounding” 라는 항목이 있습니다.

대략 설명을 보면 가까운 짝수를 향해 반올림을 한다 입니다.

기존의 반올림에서 2.5 를 소수점 바로 아래에서 반올림하면 당연히 3이 되어야 하는데…

ToEven이 기본이라서 2가 되어 버립니다.

반올림 함수를 사용할 때에는 반드시 “Away from zero” 반올림인지 “ToEven” 반올림인지 확인하고 사용하세요.

엑셀의 경우 VBA에서 Round 함수를 사용하면 .NET과 같이 ToEven을
Application.WorksheetFunction.Round() 을 사용하면 AwayFromZero를 사용하는 반올림이 수행된다고 합니다.

근데 왜? ToEven을 사용할까?
음수에 대한 처리 또는 평균 값에 대한 처리 등에서 AwayToZero보다 ToEven이 장점이 있다고 설명하네요.

여러분은 코딩용 폰트로 무얼 사용하시나요?

BS가 기분 전환을 위해 개발 환경에서 쓰는 코딩용 폰트… 고정폭 폰트를 바꿨습니다.

VS 6부터 쓰던 Fixedsys

그 뒤 VS.NET의 기본이었던 돋움체

얼마전 잠시 사용한 Bitstream Vera Sans Mono… 하지만 한글 문제로 포기하고…

나눔고딕코딩

개인적으로는

영문 폰트는 Fixedsys, Bitstream Vera Sans Mono, 나눔고딕코딩, 돋움체 순으로 좋은 것 같고
한글 폰트는 돋움체, 나눔고딕코딩 … 나머지 두 폰트는 영문 전용이라…

물론 구글신께 물어보면 Bitstream Vera Sans Mono에 맑은 고딕을 뜯어붙인 폰트를 구할 수 있긴 합니다만…
BS는 왠만하면 그런 저작권 위반하는 것을 선호하지 않기에
Windows의 FontLink 기능을 사용해 보려 했습니다.
그러나… 실패… Windows 8이후의 Windows에서는 안먹히더군요.

그래서… 지금은? “나눔고딕코딩”을 사용 중입니다.

[Fixedsys] – 공개 버전

[Fixedsys Excelsior] – Fixedsys에 유니코드 버전

[Bitstream Vera Sans Mono]

[나눔고딕코딩]

[폰트 리뷰 사이트] – 좀 오래된…

[폰트 리뷰 다른 것] – 약간 덜 오래된…

Unity 에서 장치 ID, MAC 주소 가져오기

Unity에서 장치의 고유한 식별 번호를 얻고 싶은데…


아이폰으로는 테스트를 못해 봤습니다만… 잘 되지 않을까 싶네요.


안드로이드에서 장치의 식별자로 쓸만한 것이 3개 있는데



  1. Settings.Secure 의 ANDROID_ID

  2. WiFi의 MAC 주소

  3. 전화 모듈의 식별자

입니다.
아래 코드에서는 위 순서대로 가져오기를 시도해서 잘 가져오는 것을 반환하게 했습니다.


당연히… 별다른 이상한 오류 상황이 아니면 ANDROID_ID를 가져 오겠지요?

	public string GetDeviceID ()
{
if (Application.platform == RuntimePlatform.Android)
{
try
{
using (AndroidJavaObject activity = new AndroidJavaClass (“com.unity3d.player.UnityPlayer”).GetStatic<AndroidJavaObject> (“currentActivity”))
{
// ANDROID_ID
try
{
using (AndroidJavaObject resolver = activity.Call<AndroidJavaObject> (“getContentResolver”))
{
using (AndroidJavaObject settingsSecure = new AndroidJavaObject (“android.provider.Settings.Secure”))
{
string deviceID = settingsSecure.CallStatic<string> (“getString”, resolver, settingsSecure.GetStatic<string> (“ANDROID_ID”));
if (!string.IsNullOrEmpty (deviceID))
{
return deviceID;
}
}
}
}
catch (System.Exception)
{
}

// WiFi MAC
try
{
using (AndroidJavaObject wifiManager = activity.Call<AndroidJavaObject> (“getSystemService”, activity.GetStatic<string>(“WIFI_SERVICE”)))
{
string macAddr = wifiManager.Call<AndroidJavaObject> (“getConnectionInfo”).Call<string> (“getMacAddress”);
if (!string.IsNullOrEmpty (macAddr))
{
return macAddr;
}
}
}
catch (System.Exception)
{
}

// IMEI/MEID code
try
{
using (AndroidJavaObject telephonyManager = activity.Call<AndroidJavaObject> (“getSystemService”, activity.GetStatic<string>(“TELEPHONY_SERVICE”)))
{
string imeiCode = telephonyManager.Call<string> (“getDeviceId”);
if (!string.IsNullOrEmpty (imeiCode))
{
return imeiCode;
}
}
}
catch (System.Exception)
{
}

}
}
catch (System.Exception)
{
}
}
else
{
// 이 방법은 안드로이드에서는 안된다. – Unity 4.3.4f1
try
{
var nics = NetworkInterface.GetAllNetworkInterfaces ();
if (nics.Length > 0)
{
return nics[0].GetPhysicalAddress ().ToString ();
}
}
catch (System.Exception)
{
}
}
return “”;
}


WiFi MAC 주소를 가져오려면 ACCESS_WIFI_STATE 권한이 필요합니다.


IMEI code 를 가져오려면 READ_PHONE_STATE 권한이 필요합니다.


그럼 이 권한은 어떻게 설정할까요?
만약 프로젝트 경로 아래에
Assets/Plugins/Android/AndroidManifest.xml 파일이 있다면 해당 파일을 수정하시면 됩니다.
이 파일이 없으시다면
(Unity 설치 경로)/Editor/Data/PlaybackEngines 폴더 밑에 있는
androidplayer 또는 androiddevelopmentplayer 폴더에 있는 파일을 복사해서 사용하시면 됩니다.


아래 내용을 추가하시면 되겠죠?



<uses-permission
android:name="android.permission.READ_PHONE_STATE"
>
</uses-permission>
<uses-permission
android:name="android.permission.ACCESS_WIFI_STATE"
>
</uses-permission>


Unity3D에서 쿠키로 전달된 세션 유지하는 방법

많이들 쓰기는 Unity입니다.


그리고 최근 모바일 게임에서 많이 쓰이는 게 웹 응용 서버이죠.


그리고 웹 응용 프로그램에서 많이 쓰이는 쿠키와 세션…


Unity의 WWW, WWWForm 으로 세션을 유지하는 방법을 소개합니다.


[원본 링크]


아래처럼 세션을 유지하기 위한 쿠키 정보를 저장할 해시 테이블을 만들고

// will be set to some session id after login
private Hashtable session_ident = new Hashtable();
 
// and some helper functions and properties
public void ClearSessionCookie(){
    session_ident["Cookie"] = null;
}
 
public void SetSessionCookie(string s){
    session_ident["Cookie"] = s;
}
 
public Hashtable SessionCookie{
    get { return session_ident; }
}
 
public string GetSessionCookie(){
    return session_ident["Cookie"] as string;
}
 
public bool SessionCookieIsSet{
    get { return session_ident["Cookie"] != null; }
}

그리고 로그인 응답으로부터 파싱해서 저장해 두었다가

// Failed=Bad server error, Error=server did reply with an error, OK=all fine
public enum ReturnCode : int { Failed=-1, Error=0, OK=1 }
public delegate void OnNetResult(ReturnCode code, string result);
public IEnumerator Login(OnNetResult callback, string nm, string pw)
{
    // create params to send
    WWWForm form = new WWWForm();
    form.AddField("nm", nm);
    form.AddField("pw", pw);
 
    // let www do its thing
    WWW www = new WWW("http://path_to/login/", form);
    yield return www;
 
    // the following code can be used to see what the SET-COOKIE contains
    // Have a look at http://en.wikipedia.org/wiki/HTTP_cookie to see what Set-Cookie is all about
    // It is bascially how the server will tell you what it expects you to be doing with the cookie
    // The name you are looking for will be the first characters followed be "=", after that follows
    // the value of the cookie. There could also be other entries on the same line like 'Expires'
    // but they will all be seperated by ';'
    //if (www.responseHeaders.ContainsKey("SET-COOKIE")){
    //  Debug.Log(www.responseHeaders["SET-COOKIE"]);
    //}
 
    // handle the data from www, but first check if there where errors
    if (!string.IsNullOrEmpty(www.error) || string.IsNullOrEmpty(www.text))
    {
        errmsg = "Network communication error.";
        if (callback != null) callback(ReturnCode.Failed, errmsg);
    }
    else
    {
        errmsg = "Network communication error.";
 
        // like I mentioned in description, this code
        // expects "1player_name" on success, else "0"
        if (www.text[0] == '1')
        {
            try
            {
                // extract the public name of player
                Game.Instance.name = www.text.Substring(1);
                Game.Instance.ClearSessionCookie();
 
                // check if session cookie was send, if not, well, no use to continue then
                if (www.responseHeaders.ContainsKey("SET-COOKIE"))
                {
                    // extract the session identifier cookie and save it
                    // the cookie will be named, "auth" (this could be something else in your case)
                    char[] splitter = { ';' };
                    string[] v = www.responseHeaders["SET-COOKIE"].Split(splitter);
                    foreach (string s in v)
                    {
                        if (string.IsNullOrEmpty(s)) continue;
                        if (s.Substring(0, 4).ToLower().Equals("auth"))
                        {   // found it
                            Game.Instance.SetSessionCookie(s);
                            break;
                        }
                    }
                }
            }
            catch {
                // this should only possibly happen during development
                if (callback != null) callback(ReturnCode.Failed, "Network communication error.");
            }
        }
 
        // let whomever is interrested know that the login succeeded or failed
        if (callback != null)
        {
            if (www.text[0] == '1' && Game.Instance.SessionCookieIsSet)
            {
                callback(ReturnCode.OK, "Login ok");
            }
            else if (www.text[0] == '0' || !Game.Instance.SessionCookieIsSet)
            {
                errmsg = "Invalid login name or password.";
                // my server sometimes sends "0some_explenation", therefore this next line
                if (www.text.Length > 1) errmsg = www.text.Substring(1);
                callback(ReturnCode.Error, errmsg);
            }
            else
            {
                // this should only happen during development since there was an unexpected
                // value at [0], not 0 or 1 as expected, so probably some script error
                errmsg = "Network communication error.";
                callback(ReturnCode.Failed, errmsg);
            }
        }
    }
}

다른 요청에서 헤더에 넣으면 됩니다

public IEnumerator NormalRequest(string url, OnNetResult callback, Dictionary p)
{
    // p: is a set of keys and values where the key is the name and value the value for the post field
    // create form to send with request
    WWWForm form = new WWWForm();
    if (p != null) {
        foreach (KeyValuePair kv in p) form.AddField(kv.Key, kv.Value);
    }
 
    // let www do its thing, note that you send the SessionCookie along
    WWW www = new WWW(url, form.data, Game.Instance.SessionCookie);
    yield return www;
    // ...
}