เรื่องมีอยู่ว่ามี 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
- Deserialization risks in use of BinaryFormatter and related types | Microsoft Learn
- C# - Deserialize JSON as a stream | MAKOLYTE
- How to read serialized and deserialized json object from TCPClient using WPF in C#? - Stack Overflow
Discover more from naiwaen@DebuggingSoft
Subscribe to get the latest posts sent to your email.