본문 바로가기

C#/근웹 연대기

c#으로 근본 없는 웹서버 개발기 11 : 세션마다 닉네임 부여

⚠WARNING
ASP.NET에 대한 포스팅이 아닙니다
나가실 문은 오른쪽 하단입니다

 

 

크롬 데브툴 세션 id

 

기존에 세션 값은 위 사진처럼 헥사 값으로 되어있어서 사람 눈으로는 분간하기가 힘들다.

그래서 나중에 Debug 할 때, 분간하기 쉽도록 닉네임을 주기로 하였다.

 

무엇으로 할지 고민하다가 그냥 무난한 동물 이름으로 하기로 하고 구글에 검색했더니

적당한 사이트가 나왔다.

http://animal.memozee.com/animal/Dic/

이 사이트의 좋은 점은 디자인이 좀 후 달려 보이나, 복사 붙여 넣기를 하면 동물명만 카피되고 다른정보는 안 나와서 추가로 파싱 작업을 할 필요가 없다는 것이다.

마치.. 그것을 의식하고 만들었을까 하는 생각마저 들게 하는 심플함이다.

가나다라순으로 잘 되어있다.

파일 이름은 우선순위를 낮추기 위해서 Z를 박아주었다.

2437마리의 종 이름을 아주 간단하기 긁어왔다. So simple~😁

 

그러고 나서 위 스샷의 Util.cs에서 파일을 로드하고 인출하는 코드를 작성하였다.

    public static readonly string[] animals = File.ReadAllLines(sPath + "/Z_animals").Shuffle();
    public static string getNickName(int nth) => animals[nth % animals.Length];

뒷부분의 shuffle()이라는 함수는... 사실 기본적 제공하지 않는다.

파이쎤을 만져본 사람은 있었야 될 것이 없어서 당황할 수 있다. 

그렇다

c#에서는 클래스나 인터페이스에 해당기능을 직접 추가하여야한다.

static class ExMethod{
    public static T[] Shuffle<T> (this IEnumerable<T> en)
    {
        Random rng = new Random(Environment.TickCount);
        var array = en.ToArray();
        int n = array.Count();
        while (n > 1)
        {
            int k = rng.Next(n--);
            T temp = array[n];
            array[n] = array[k];
            array[k] = temp;
        }
        return array;
    }
}
 

코드설명을 하자면: 정적클래스 내에서 정적메서드의 파라미터에 this를 기입해주면 해당메서드를 전역적으로 사용 할 수 있다. (용법이 상당히 직관적이지 않다.)
그리고 위의 로직은 해당 키워드로 스택오바플로에서 상단에 검색되는 무슨 알고리즘인데, 까먹었다.😅
어째든 셔플이 된다는 것이 검증이 된 알고리즘이라는 것을 기억해둔다.
참고로 자바스크립트의 prototype과 비슷하다. 

 

그 다음은 Request 클래스 내에서 사용되도록 하였다

    public string newSessionID()
    {
        sessionID = Util.newSessionID(client);
        sessionDic[sessionID] = new(){[nameof(this.nickname)] =  Util.getNickName(sessionDic.Count)};
        return sessionID;
    }
더보기
using System.Collections.Specialized;
using System.Net.Sockets;
using System.Web;
public enum Method { GET, POST, UNKNOW }
public class Request : Dictionary<string, string>
{
    public readonly Method method = Method.UNKNOW;
    public string url;
    public NameValueCollection param = new();
    public readonly Socket client;
    public readonly string err = null;
    public Dictionary<string, string> cookie = new();
    public string  sessionID;
    private readonly static Dictionary<string, Dictionary<string, string>> sessionDic = new();
    public Dictionary<string, string> Sess => sessionDic.GetD(sessionID);
    public string nickname => Sess?.First().Value;

    public Request(Socket client)
    {
        this.client = client;
        byte[] data = new byte[8192];
        int cnt = client.Receive(data);
        if (cnt == 0) return;

        var strs = Encoding.UTF8.GetString(data, 0, cnt);
        String[] split = strs.Split("\r\n");

        #region top-line parse        
        var topLine = split.FirstOrDefault();
        this[nameof(topLine)] = topLine;
        var dic = new[]{"method", "url", "http"}
        .Zip(topLine.Split(' '), (a, b) => new { a, b })
        .ToDictionary(x => x.a.ToString(), x => x.b.Trim());

        foreach (Method m in Enum.GetValues(typeof(Method)))
        {
            if (dic["method"] != m.ToString()) continue;
            this.method = m;
            break;
        }

        foreach (var item in dic) this.Add(item.Key, item.Value);

        this.err = (this.method, this.Has("url")) switch
        {
            (Method.UNKNOW, _) => "Invaild method",
            (_, false) => "Can't find url",
            _ => null
        };
        if (this.err != null) return;


        url = this["url"];
        var startIdx = url.IndexOf('?');
        if (startIdx > -1)  //GET 파라미터
        {
            this.param = HttpUtility.ParseQueryString(url[startIdx..]);
            url = url[..startIdx];
        }
        #endregion

        #region rest-header parse
        var infos = split.Skip(1).GetEnumerator();
        while (infos.MoveNext())
        {
            var val = infos.Current.Trim();
            if (val.Length == 0) break;
            var sp = val.Split(':', 2);
            this.Add(sp[0].Trim(), sp[^1].Trim());
        }
        #endregion

        #region Getting body data
        infos.MoveNext();
        var body = infos.Current;
        var bodyLen = int.Parse("0" + this.GetD("Content-Length"));
        if (bodyLen > body.Length)
        {
            cnt = client.Receive(data);
            body += Encoding.UTF8.GetString(data, 0, cnt);
        }
        this["body"] = body;
        if (this.GetD("Content-Type") == "application/x-www-form-urlencoded")
        {
            this.param = HttpUtility.ParseQueryString(body); //POST 파라미터
        };
        #endregion

        #region console.writeLine for checking
        WriteLine($"[req]-------------[{DateTime.Now}]--------------");
        //foreach (var t in this) WriteLine($"{t.Key}: {t.Value}");
        foreach (var t in param.Keys) WriteLine($"{t}: {param.Get(t.ToString())}");
        #endregion
       
        cookie = getCookie(this.GetD("Cookie"));
        var reqID = cookie.GetD(nameof(sessionID));
        sessionID = sessionDic.GetD(reqID) != null ? reqID : null;
    }

    public string newSessionID()
    {
        sessionID = Util.newSessionID(client);
        sessionDic[sessionID] = new(){[nameof(this.nickname)] =  Util.getNickName(sessionDic.Count)};
        return sessionID;
    }

    public static Dictionary<string, string> getCookie(string value)
    {
        var dic = (value ?? "").Split(';').Select(x => x.Split('='))
        .ToDictionary(x => x[0].Trim(), x => x[^1].Trim());
        return dic;
    }
}
 

설명: 전역 맴버인 sessionDic에 새로운 연결이 올 때마다 새 딕션어리를 만들고 닉네임을 할당하게 해주었다.
 (..티스토리 접기 기능이 수정모드에서는 안된다.. ☹ )

 

그리고 나서 메인코스인 Program.cs 에서 다음과 같이 닉네임을 콘솔에 뿌리도록 해주었다.

    Request req = null;
    try
    {
        client.ReceiveTimeout = 5000; //최대 5초 기다려줌을 선언
        req =  new Request(client);

        Func<bool> trouble = (!client.Connected || req.Count is 0, req.err) switch
        {
            (true , _ ) => () => { WriteLine("Disconnected: by client"); return true; },
            ( _ , null) => () => { WriteLine(req.First().Value); return false; },
            ( _ ,var _) => () => { resForbadrequest(client, req.err); return true; }
        };

        if(trouble()) return;
        WriteLine("hello~ " + (req.nickname ?? "newbie")); //닉넴 출력

        if (req.url == "/") req.url = "/index.html";
        response(req);
    }
    catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
    {
        WriteLine("Disconnected: timeout");
    }
    catch (Exception ex)
    {
        WriteLine(ex);
    }
    finally
    {
        WriteLine("bye~   " + (req?.nickname ?? "anonymous")); //닉넴 출력
        client.Close();
        client.Dispose();
    }
더보기
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;

 

var ipep = new IPEndPoint(IPAddress.Any, 80);
var server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
server.Bind(ipep);
server.Listen(100);

 

#if DEBUG
if(Env.OSVersion.Platform == PlatformID.Win32NT)
{
    Process.Start("explorer", "http://localhost/test/param.html");
}
#endif
WriteLine("Port " + ipep.Port + " listening");



while (true)
{
    Socket client = server.Accept();
    Task.Run(() => newWork(client)); //자식 스레드에 인계
}




static void newWork(Socket client)
{    
    Request req = null;
    try
    {
        client.ReceiveTimeout = 5000; //최대 5초 기다려줌을 선언
        req =  new Request(client);

 

        Func<bool> trouble = (!client.Connected || req.Count is 0, req.err) switch
        {
            (true , _ ) => () => { WriteLine("Disconnected: by client"); return true; },
            ( _ , null) => () => { WriteLine(req.First().Value); return false; },
            ( _ ,var _) => () => { resForbadrequest(client, req.err); return true; }
        };

 

        if(trouble()) return;
        WriteLine("hello~ " + (req.nickname ?? "newbie"));

 

        if (req.url == "/") req.url = "/index.html";
        response(req);
    }
    catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
    {
        WriteLine("Disconnected: timeout");
    }
    catch (Exception ex)
    {
        WriteLine(ex);
    }
    finally
    {
        WriteLine("bye~   " + (req?.nickname ?? "anonymous"));
        client.Close();
        client.Dispose();
    }
}

 

static void response(Request req)
{
    string publicPath = Path.Combine(Util.sPath, "public");
    Socket client = req.client;
    Ftag ftag = Util.getFInfo(publicPath + req.url);
    FileInfo info = ftag.info;
    if (!info.Exists || !info.FullName.StartsWith(publicPath))
    {
        resForbadrequest(client, "404 Not found");
        return;
    }

 

    var sb = new StringBuilder(100);
   
    bool m = ftag.etag == req.GetD("If-None-Match");
    sb.AddL($"HTTP/1.1 {(m ? "304 Not Modified" : "200 ok")}");
    sb.AddL("Accept-Ranges: none");
    sb.AddL("Cache-Control: public, max-age=0");
    sb.AddL("Last-Modified: " + ftag.modiTime);
    sb.AddL("Etag: " + ftag.etag);
    sb.AddL("Date: " + Util.uTime);
   
    if(!m)
    {
        sb.AddL("Content-type: " + ftag.minetype);
        sb.AddL("Content-Length: " + info.Length);
    }

 

    sb.AddL("Server: test server");
    sb.AddL("Connection: close");

 

    if (req.sessionID == null)
    {
        sb.AddL("set-cookie: sessionID=" + req.newSessionID() + "; path=/; httponly");
    }
    sb.AddL();

 

    client.Send(Encoding.UTF8.GetBytes(sb.ToString()));
    if (!m) client.Send(File.ReadAllBytes(info.FullName));
}

 

static void resForbadrequest(Socket client, string msg)
{
    var sb = new StringBuilder(100);

 

    sb.AddL("HTTP/1.1 400 Bad Request");
    sb.AddL("date: " + Util.uTime);
    sb.AddL("Server: test server");
    sb.AddL("Content-type:text/plain; charset=UTF-8");
    sb.AddL("Content-Length: " + msg.Length);
    sb.AddL("Connection: close");
    sb.AddL();
    sb.AddL(msg);

 

    client.Send(Encoding.UTF8.GetBytes(sb.ToString()));
}

 

전편에 비교해서 나름 코드정리를 하였다 (구조적으로 더 손을 봐야 함)

 

그리고 최종확인.

새 브라우저로 접속하면 새 이름을 부여한다.

つづく