บันทึกการปรับ format data ที่ใช้รับส่งผ่าน TCP จาก BinaryFormatter มาเป็น JSON แทน

เรื่องมีอยู่ว่ามี Code เก่าอยู่ชุดนึงที่ใช้ TCPClient+TcpListener ส่ง Data ระหว่าง App กัน โดยก่อนจะส่งใช้ตัว BinaryWriter + BinaryFormatter อย่างที่หลายคนน่าจะทราบกันตัว BinaryFormatter จะถูกเอาออกใน .NET8 ถาวร เนื่องจากเรื่องของความปลอดภัย ตอนนี้ยังใช้ได้อยู่นะ แต่เจ็บแล้ว ทำทีเดียวให้จบเลยดีกว่า และไม่อยากมาแก้ไขเยอะ เพราะยังมีบางส่วนทียังเป็น .NET 4.7.2 และไป .NET6 ต่อไม่ได้อย่าง VSTO ด้วย ภาพรวมเป็นตามนี้เลยครับ

Code เดิม

- Client Snippet
TcpClient clientSocket;
clientSocket = new TcpClient();
try
{
   clientSocket.Connect(host, port);
}
catch (SocketException socketEx)
{
   throw new DSException("Connect-001", "Socket Exception", EXCEPTION_LEVEL.System, socketEx);
}
catch (ObjectDisposedException disposedException)
{
   throw new DSException("Connect-001", "Object Disposed Exception", EXCEPTION_LEVEL.System, disposedException);
}
  • ถ้าลองดู Code Snippet ในส่วนของ sendRequest / getResult จะมีการใช้ BinaryFormatter เยอะเลยครับ
public void sendRequest(ParamDTO message)
{
    try
    {
        NetworkStream ns = clientSocket.GetStream();
        ns.WriteTimeout = WriteTimeOut;
        
        BinaryWriter bw = new BinaryWriter(ns);
        MemoryStream ms = new MemoryStream();
        new BinaryFormatter().Serialize(memoryStream, obj);
        bw.Write(ms.ToArray());
        bw.Flush();
    }
    catch (System.Runtime.Serialization.SerializationException se)
    {
        throw new DSException("sendRequest-001", "Serialization Exception", EXCEPTION_LEVEL.System, se);
    }
    catch (ObjectDisposedException disposedException)
    {
        throw new DSException("sendRequest-002", "Object Disposed Exception", EXCEPTION_LEVEL.System, disposedException);
    }
}

public object getResult()
{
    try
    {
        NetworkStream ns = clientSocket.GetStream();
        ns.ReadTimeout = ReadTimeOut;
        object result =((IFormatter)new BinaryFormatter()).Deserialize((Stream)ns);
        return result;
    }
    catch (ObjectDisposedException disposedException)
    {
        throw new WmslException("", "Object Disposed Exception", EXCEPTION_LEVEL.System, disposedException);
    }
}
- Server Snippet
  • มีตัว TcpListener เอารับ Request และส่งให้ Handler จัดการต่อไป
IPAddress localAddr = IPAddress.Parse(localHost);
TcpListener serverSocket = new TcpListener(localAddr, port);

serverSocket.Start();
TcpClient clientSocket = null;
while (stillRunning)
{
    try
    {
        if (!serverSocket.Pending())
        {
            Thread.Sleep(sleepTime); // choose a number (in milliseconds) that makes sense
            continue; // skip to next iteration of loop
        }
        clientSocket = serverSocket.AcceptTcpClient();
        IPAddress clientAddr = ((IPEndPoint)clientSocket.Client.RemoteEndPoint).Address;
        
        IRTRequestHandler handler = getRequestHandler();
        handler.startClient(clientSocket);  //Call InvsRequestHandler
    }
    catch (SocketException ex)
    {
        logger.Error("Socket error");
    }
    catch (ObjectDisposedException ex)
    {
        logger.Error("Client was disposed");
    }
    catch (Exception wex)
    {
        clientSocket.GetStream().Close();
        clientSocket.Close();
        logger.Error(wex.Message);
    }
}
logger.Debug("Stop RT Server");
serverSocket.Stop();
  • ตัว Handler จัดการกับ Request ที่มาครับ
public abstract class AbstractBaseRequestHandler : IRTRequestHandler
{
    protected int readTimeout;
    protected int writeTimeout;
    protected TcpClient clientSocket;

    protected AbstractBaseRequestHandler()
    {
        this.readTimeout = 20000;
        this.writeTimeout = 5000;
    }

    public void startClient(TcpClient clientSocket)
    {
        this.clientSocket = clientSocket;
        Thread ctThread = new Thread(run);
        ctThread.Start();
    }

    protected abstract void run();
}
public class InvsRequestHandler : AbstractBaseRequestHandler, IRTRequestHandler
{
    protected override void run()
    {
        try
        {
            while (true)
            {
                NetworkStream ns = clientSocket.GetStream();
                object deserialized =((IFormatter)new BinaryFormatter()).Deserialize((Stream)ns);
                ParamDTO param = (ParamDTO)deserialized;
                //Your Business Logic         
                IList<BusinnesLogicDTO> resultls = ProcessSomeLogic(param);
                //...
                
                //Serialize Object and Send Result Back to Client
                BinaryWriter bw = new BinaryWriter(ns);                           
                MemoryStream ms = new MemoryStream();
                new BinaryFormatter().Serialize(ms, resultls);
                bw.Write(ms.ToArray());
                bw.Flush();
            }
        } 
        catch (Exception wex)
        {
            clientSocket.Close();
            logger.Error(wex.Message);
        }
    }
}

ตอนนี้อย่างน้อย Class หลักๆของ Protocol TCP อย่างตัว TcpClient Class / TcpListener และ IPAddress ยังได้ไปต่ออยู่ครับ เหลือแต่ BinaryFormatter เท่านั้น

Code ใหม่

ลองมาปรับใช้ตัว JSON แทน มันมีหลาย Case ที่ต้อง Handle เช่น

  • Null จะจัดการยังไง ?
  • Property Private ต้องมาปรับยังไง ?
  • หรือ เคส Nest Object เป็นต้นครับ

ตัว JSON เราสามารถเขียน Contract ตอน Serialize ได้ครับ เขียน Contract และทดสอบครับ จะได้เป็น Helper Class ประมาณนี้ครับ

//JSONHelper
private static readonly JsonSerializerSettings _SerializerSettings = new JsonSerializerSettings()
{
    NullValueHandling = NullValueHandling.Ignore
};

public static Byte[] serializeObject<T>(T pObj)
{
    String JSON = JsonConvert.SerializeObject(pObj, typeof(T), _SerializerSettings);
    return Encoding.Unicode.GetBytes(JSON);
}

public static T deserialize<T>(String pObj)
{
    return JsonConvert.DeserializeObject<T>(pObj, _SerializerSettings);
}
- Client Code Snippet
  • ส่วนของการ Connect ที่ใช้ (TcpClient) เหมือนเดิมครับ มีแก้ตรง sendRequest / getResult ครับ
public void sendRequest(ParamDTO message)
{
    try
    {
        NetworkStream ns = clientSocket.GetStream();
        ns.WriteTimeout = WriteTimeOut;

        Byte[] mb = serializeObject<RequestDictionaryMessage>(message);         //PING     

        ns.Write(mb, 0, mb.Length);
        ns.Flush();
    }
    catch (System.Runtime.Serialization.SerializationException se)
    {
        throw new WmslException("", "Serialization Exception", EXCEPTION_LEVEL.System, se);
    }
    catch (ObjectDisposedException disposedException)
    {
        throw new WmslException("", "Object Disposed Exception", EXCEPTION_LEVEL.System, disposedException);
    }
}

public T getResult<T>()
{
    try
    {
        NetworkStream ns = clientSocket.GetStream();
        ns.ReadTimeout = ReadTimeOut;

        StringBuilder response = new StringBuilder();
        var buffer = new byte[clientSocket.ReceiveBufferSize+10];
        while (ns.DataAvailable)
        {
            int bytes = ns.Read(buffer, 0, clientSocket.ReceiveBufferSize);
            response.Append(Encoding.Unicode.GetString(buffer, 0, bytes));
        }
        T result = deserialize<T>(response.ToString());
        return result;
    }
    catch (ObjectDisposedException disposedException)
    {
        throw new DSException("", "Object Disposed Exception", EXCEPTION_LEVEL.System, disposedException);
    }
}
- Server Code Snippet
  • ส่วนของการ Connect ที่ใช้ (TcpListener) เหมือนเดิม ปรับตรงส่วน Handler InvsRequestHandler ครับ
public class InvsRequestHandler : AbstractBaseRequestHandler, IRTRequestHandler
{
    protected override void run()
    {
        try
        {
            while (true)
            {
                NetworkStream ns = clientSocket.GetStream();
                StringBuilder response = new StringBuilder();
                var buffer = new byte[clientSocket.ReceiveBufferSize+10];
                while (ns.DataAvailable)
                {
                    int bytes = ns.Read(buffer, 0, clientSocket.ReceiveBufferSize);
                    response.Append(Encoding.Unicode.GetString(buffer, 0, bytes));
                }
                if (String.IsNullOrEmpty(response.ToString()))
                {
                    continue;
                }

                ParamDTO deserialized = JSONHelper.deserialize<ParamDTO>(response.ToString());

                //Your Business Logic         
                IList<BusinnesLogicDTO> resultls= ProcessSomeLogic(param);
                //...
                
                //Serialize Object and Send Result Back to Client
                Byte[] mb = JSONHelper.serializeObject<IList<BusinnesLogicDT>(resultls);    
                ns.Write(mb, 0, mb.Length);
                ns.Flush();
            }
        } 
        catch (Exception wex)
        {
            clientSocket.Close();
            logger.Error(wex.Message);
        }
    }
}

จบไปแล้วการการแก้ปัญหาตัว BinaryFormatter ที่แบบว่าตัวมันเองสารพัดประโยชน์จริงๆ นอกจาก TCP แล้ว จริงๆ มันใช้กับ REST ได้ด้วย แต่ Client ต้องเป็น .NET ด้วย พอมันเจอเคส Security Risk มันเจ็บพอๆหลักเลย นอกจาก Blog นี้แล้วมีอีกเคสของ Binary ที่แสบพอๆกันครับ DeeCopy/DeepClone นอกจากตัว JSON ผมมีลองตัว MessagePack และ ProtoBuf.NET อันนี้ผมมองว่าถ้า .NET Core ขึ้นไปใช้งาน ควรปรับในอนาคตนะ แต่ที่เลือกใช้ JSON เพราะมองว่าใช้ Cost น้อยที่สุด ทั้งเวลา และจุดที่ต้องแก้ไขครับ

ส่วนปัญหาที่พบหลังปรับเป็น JSON ก็มีนะครับ อาทิ เช่น TCP + JSON บน Windows7 แล้ว JSON แหว่ง

จะว่าไปยังไม่ได้เขียน Blog สรุป Step จาก .NET Framework 4.7.2 > .NET 6 เลย ปลายปี .NET 8 จะมาแล้ว

Reference


Discover more from naiwaen@DebuggingSoft

Subscribe to get the latest posts sent to your email.