diff --git a/Tutorials/EchoClient/App.config b/Tutorials/EchoClient/App.config
new file mode 100644
index 0000000..d740e88
--- /dev/null
+++ b/Tutorials/EchoClient/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/EchoClient/ClientSimpleTcp.cs b/Tutorials/EchoClient/ClientSimpleTcp.cs
new file mode 100644
index 0000000..62fa8fe
--- /dev/null
+++ b/Tutorials/EchoClient/ClientSimpleTcp.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Net.Sockets;
+using System.Net;
+
+namespace csharp_test_client
+{
+ public class ClientSimpleTcp
+ {
+ public Socket Sock = null;
+ public string LatestErrorMsg;
+
+
+ //소켓연결
+ public bool Connect(string ip, int port)
+ {
+ try
+ {
+ IPAddress serverIP = IPAddress.Parse(ip);
+ int serverPort = port;
+
+ Sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+ Sock.Connect(new IPEndPoint(serverIP, serverPort));
+
+ if (Sock == null || Sock.Connected == false)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LatestErrorMsg = ex.Message;
+ return false;
+ }
+ }
+
+ public Tuple Receive()
+ {
+
+ try
+ {
+ byte[] ReadBuffer = new byte[2048];
+ var nRecv = Sock.Receive(ReadBuffer, 0, ReadBuffer.Length, SocketFlags.None);
+
+ if (nRecv == 0)
+ {
+ return null;
+ }
+
+ return Tuple.Create(nRecv,ReadBuffer);
+ }
+ catch (SocketException se)
+ {
+ LatestErrorMsg = se.Message;
+ }
+
+ return null;
+ }
+
+ //스트림에 쓰기
+ public void Send(byte[] sendData)
+ {
+ try
+ {
+ if (Sock != null && Sock.Connected) //연결상태 유무 확인
+ {
+ Sock.Send(sendData, 0, sendData.Length, SocketFlags.None);
+ }
+ else
+ {
+ LatestErrorMsg = "먼저 채팅서버에 접속하세요!";
+ }
+ }
+ catch (SocketException se)
+ {
+ LatestErrorMsg = se.Message;
+ }
+ }
+
+ //소켓과 스트림 닫기
+ public void Close()
+ {
+ if (Sock != null && Sock.Connected)
+ {
+ //Sock.Shutdown(SocketShutdown.Both);
+ Sock.Close();
+ }
+ }
+
+ public bool IsConnected() { return (Sock != null && Sock.Connected) ? true : false; }
+ }
+}
diff --git a/Tutorials/EchoClient/DevLog.cs b/Tutorials/EchoClient/DevLog.cs
new file mode 100644
index 0000000..4d45fbe
--- /dev/null
+++ b/Tutorials/EchoClient/DevLog.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace csharp_test_client
+{
+ public class DevLog
+ {
+ static System.Collections.Concurrent.ConcurrentQueue logMsgQueue = new System.Collections.Concurrent.ConcurrentQueue();
+
+ static Int64 출력가능_로그레벨 = (Int64)LOG_LEVEL.TRACE;
+
+
+
+ static public void Init(LOG_LEVEL logLevel)
+ {
+ ChangeLogLevel(logLevel);
+ }
+
+ static public void ChangeLogLevel(LOG_LEVEL logLevel)
+ {
+ Interlocked.Exchange(ref 출력가능_로그레벨, (int)logLevel);
+ }
+
+ public static LOG_LEVEL CurrentLogLevel()
+ {
+ var curLogLevel = (LOG_LEVEL)Interlocked.Read(ref 출력가능_로그레벨);
+ return curLogLevel;
+ }
+
+ static public void Write(string msg, LOG_LEVEL logLevel = LOG_LEVEL.TRACE,
+ [CallerFilePath] string fileName = "",
+ [CallerMemberName] string methodName = "",
+ [CallerLineNumber] int lineNumber = 0)
+ {
+ if (CurrentLogLevel() <= logLevel)
+ {
+ logMsgQueue.Enqueue(string.Format("{0}:{1}| {2}", DateTime.Now, methodName, msg));
+ }
+ }
+
+ static public bool GetLog(out string msg)
+ {
+ if (logMsgQueue.TryDequeue(out msg))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ }
+
+
+ public enum LOG_LEVEL
+ {
+ TRACE,
+ DEBUG,
+ INFO,
+ WARN,
+ ERROR,
+ DISABLE
+ }
+}
diff --git a/Tutorials/EchoClient/Packet.cs b/Tutorials/EchoClient/Packet.cs
new file mode 100644
index 0000000..72f42dc
--- /dev/null
+++ b/Tutorials/EchoClient/Packet.cs
@@ -0,0 +1,260 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace csharp_test_client
+{
+ struct PacketData
+ {
+ public Int16 DataSize;
+ public Int16 PacketID;
+ public SByte Type;
+ public byte[] BodyData;
+ }
+
+ public class PacketDump
+ {
+ public static string Bytes(byte[] byteArr)
+ {
+ StringBuilder sb = new StringBuilder("[");
+ for (int i = 0; i < byteArr.Length; ++i)
+ {
+ sb.Append(byteArr[i] + " ");
+ }
+ sb.Append("]");
+ return sb.ToString();
+ }
+ }
+
+
+ public class ErrorNtfPacket
+ {
+ public ERROR_CODE Error;
+
+ public bool FromBytes(byte[] bodyData)
+ {
+ Error = (ERROR_CODE)BitConverter.ToInt16(bodyData, 0);
+ return true;
+ }
+ }
+
+
+ public class LoginReqPacket
+ {
+ byte[] UserID = new byte[PacketDef.MAX_USER_ID_BYTE_LENGTH];
+ byte[] UserPW = new byte[PacketDef.MAX_USER_PW_BYTE_LENGTH];
+
+ public void SetValue(string userID, string userPW)
+ {
+ Encoding.UTF8.GetBytes(userID).CopyTo(UserID, 0);
+ Encoding.UTF8.GetBytes(userPW).CopyTo(UserPW, 0);
+ }
+
+ public byte[] ToBytes()
+ {
+ List dataSource = new List();
+ dataSource.AddRange(UserID);
+ dataSource.AddRange(UserPW);
+ return dataSource.ToArray();
+ }
+ }
+
+ public class LoginResPacket
+ {
+ public Int16 Result;
+
+ public bool FromBytes(byte[] bodyData)
+ {
+ Result = BitConverter.ToInt16(bodyData, 0);
+ return true;
+ }
+ }
+
+
+ public class RoomEnterReqPacket
+ {
+ int RoomNumber;
+ public void SetValue(int roomNumber)
+ {
+ RoomNumber = roomNumber;
+ }
+
+ public byte[] ToBytes()
+ {
+ List dataSource = new List();
+ dataSource.AddRange(BitConverter.GetBytes(RoomNumber));
+ return dataSource.ToArray();
+ }
+ }
+
+ public class RoomEnterResPacket
+ {
+ public Int16 Result;
+ public Int64 RoomUserUniqueId;
+
+ public bool FromBytes(byte[] bodyData)
+ {
+ Result = BitConverter.ToInt16(bodyData, 0);
+ RoomUserUniqueId = BitConverter.ToInt64(bodyData, 2);
+ return true;
+ }
+ }
+
+ public class RoomUserListNtfPacket
+ {
+ public int UserCount = 0;
+ public List UserUniqueIdList = new List();
+ public List UserIDList = new List();
+
+ public bool FromBytes(byte[] bodyData)
+ {
+ var readPos = 0;
+ var userCount = (SByte)bodyData[readPos];
+ ++readPos;
+
+ for (int i = 0; i < userCount; ++i)
+ {
+ var uniqeudId = BitConverter.ToInt64(bodyData, readPos);
+ readPos += 8;
+
+ var idlen = (SByte)bodyData[readPos];
+ ++readPos;
+
+ var id = Encoding.UTF8.GetString(bodyData, readPos, idlen);
+ readPos += idlen;
+
+ UserUniqueIdList.Add(uniqeudId);
+ UserIDList.Add(id);
+ }
+
+ UserCount = userCount;
+ return true;
+ }
+ }
+
+ public class RoomNewUserNtfPacket
+ {
+ public Int64 UserUniqueId;
+ public string UserID;
+
+ public bool FromBytes(byte[] bodyData)
+ {
+ var readPos = 0;
+
+ UserUniqueId = BitConverter.ToInt64(bodyData, readPos);
+ readPos += 8;
+
+ var idlen = (SByte)bodyData[readPos];
+ ++readPos;
+
+ UserID = Encoding.UTF8.GetString(bodyData, readPos, idlen);
+ readPos += idlen;
+
+ return true;
+ }
+ }
+
+
+ public class RoomChatReqPacket
+ {
+ Int16 MsgLen;
+ byte[] Msg;//= new byte[PacketDef.MAX_USER_ID_BYTE_LENGTH];
+
+ public void SetValue(string message)
+ {
+ Msg = Encoding.UTF8.GetBytes(message);
+ MsgLen = (Int16)Msg.Length;
+ }
+
+ public byte[] ToBytes()
+ {
+ List dataSource = new List();
+ dataSource.AddRange(BitConverter.GetBytes(MsgLen));
+ dataSource.AddRange(Msg);
+ return dataSource.ToArray();
+ }
+ }
+
+ public class RoomChatResPacket
+ {
+ public Int16 Result;
+
+ public bool FromBytes(byte[] bodyData)
+ {
+ Result = BitConverter.ToInt16(bodyData, 0);
+ return true;
+ }
+ }
+
+ public class RoomChatNtfPacket
+ {
+ public Int64 UserUniqueId;
+ public string Message;
+
+ public bool FromBytes(byte[] bodyData)
+ {
+ UserUniqueId = BitConverter.ToInt64(bodyData, 0);
+
+ var msgLen = BitConverter.ToInt16(bodyData, 8);
+ byte[] messageTemp = new byte[msgLen];
+ Buffer.BlockCopy(bodyData, 8 + 2, messageTemp, 0, msgLen);
+ Message = Encoding.UTF8.GetString(messageTemp);
+ return true;
+ }
+ }
+
+
+ public class RoomLeaveResPacket
+ {
+ public Int16 Result;
+
+ public bool FromBytes(byte[] bodyData)
+ {
+ Result = BitConverter.ToInt16(bodyData, 0);
+ return true;
+ }
+ }
+
+ public class RoomLeaveUserNtfPacket
+ {
+ public Int64 UserUniqueId;
+
+ public bool FromBytes(byte[] bodyData)
+ {
+ UserUniqueId = BitConverter.ToInt64(bodyData, 0);
+ return true;
+ }
+ }
+
+
+
+ public class RoomRelayNtfPacket
+ {
+ public Int64 UserUniqueId;
+ public byte[] RelayData;
+
+ public bool FromBytes(byte[] bodyData)
+ {
+ UserUniqueId = BitConverter.ToInt64(bodyData, 0);
+
+ var relayDataLen = bodyData.Length - 8;
+ RelayData = new byte[relayDataLen];
+ Buffer.BlockCopy(bodyData, 8, RelayData, 0, relayDataLen);
+ return true;
+ }
+ }
+
+
+ public class PingRequest
+ {
+ public Int16 PingNum;
+
+ public byte[] ToBytes()
+ {
+ return BitConverter.GetBytes(PingNum);
+ }
+
+ }
+}
diff --git a/Tutorials/EchoClient/PacketBufferManager.cs b/Tutorials/EchoClient/PacketBufferManager.cs
new file mode 100644
index 0000000..590be3f
--- /dev/null
+++ b/Tutorials/EchoClient/PacketBufferManager.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace csharp_test_client
+{
+ class PacketBufferManager
+ {
+ int BufferSize = 0;
+ int ReadPos = 0;
+ int WritePos = 0;
+
+ int HeaderSize = 0;
+ int MaxPacketSize = 0;
+ byte[] PacketData;
+ byte[] PacketDataTemp;
+
+ public bool Init(int size, int headerSize, int maxPacketSize)
+ {
+ if (size < (maxPacketSize * 2) || size < 1 || headerSize < 1 || maxPacketSize < 1)
+ {
+ return false;
+ }
+
+ BufferSize = size;
+ PacketData = new byte[size];
+ PacketDataTemp = new byte[size];
+ HeaderSize = headerSize;
+ MaxPacketSize = maxPacketSize;
+
+ return true;
+ }
+
+ public bool Write(byte[] data, int pos, int size)
+ {
+ if (data == null || (data.Length < (pos + size)))
+ {
+ return false;
+ }
+
+ var remainBufferSize = BufferSize - WritePos;
+
+ if (remainBufferSize < size)
+ {
+ return false;
+ }
+
+ Buffer.BlockCopy(data, pos, PacketData, WritePos, size);
+ WritePos += size;
+
+ if (NextFree() == false)
+ {
+ BufferRelocate();
+ }
+ return true;
+ }
+
+ public ArraySegment Read()
+ {
+ var enableReadSize = WritePos - ReadPos;
+
+ if (enableReadSize < HeaderSize)
+ {
+ return new ArraySegment();
+ }
+
+ var packetDataSize = BitConverter.ToInt16(PacketData, ReadPos);
+ if (enableReadSize < packetDataSize)
+ {
+ return new ArraySegment();
+ }
+
+ var completePacketData = new ArraySegment(PacketData, ReadPos, packetDataSize);
+ ReadPos += packetDataSize;
+ return completePacketData;
+ }
+
+ bool NextFree()
+ {
+ var enableWriteSize = BufferSize - WritePos;
+
+ if (enableWriteSize < MaxPacketSize)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ void BufferRelocate()
+ {
+ var enableReadSize = WritePos - ReadPos;
+
+ Buffer.BlockCopy(PacketData, ReadPos, PacketDataTemp, 0, enableReadSize);
+ Buffer.BlockCopy(PacketDataTemp, 0, PacketData, 0, enableReadSize);
+
+ ReadPos = 0;
+ WritePos = enableReadSize;
+ }
+ }
+}
diff --git a/Tutorials/EchoClient/PacketDefine.cs b/Tutorials/EchoClient/PacketDefine.cs
new file mode 100644
index 0000000..396078c
--- /dev/null
+++ b/Tutorials/EchoClient/PacketDefine.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace csharp_test_client
+{
+ class PacketDef
+ {
+ public const Int16 PACKET_HEADER_SIZE = 5;
+ public const int MAX_USER_ID_BYTE_LENGTH = 16;
+ public const int MAX_USER_PW_BYTE_LENGTH = 16;
+ }
+
+ public enum PACKET_ID : ushort
+ {
+ PACKET_ID_ECHO = 101,
+
+ // Ping(Heart-beat)
+ PACKET_ID_PING_REQ = 201,
+ PACKET_ID_PING_RES = 202,
+
+ PACKET_ID_ERROR_NTF = 203,
+
+
+ // 로그인
+ PACKET_ID_LOGIN_REQ = 701,
+ PACKET_ID_LOGIN_RES = 702,
+
+
+ PACKET_ID_ROOM_ENTER_REQ = 721,
+ PACKET_ID_ROOM_ENTER_RES = 722,
+ PACKET_ID_ROOM_USER_LIST_NTF = 723,
+ PACKET_ID_ROOM_NEW_USER_NTF = 724,
+
+ PACKET_ID_ROOM_LEAVE_REQ = 726,
+ PACKET_ID_ROOM_LEAVE_RES = 727,
+ PACKET_ID_ROOM_LEAVE_USER_NTF = 728,
+
+ PACKET_ID_ROOM_CHAT_REQ = 731,
+ PACKET_ID_ROOM_CHAT_RES = 732,
+ PACKET_ID_ROOM_CHAT_NOTIFY = 733,
+
+ PACKET_ID_ROOM_RELAY_REQ = 741,
+ PACKET_ID_ROOM_RELAY_NTF = 742,
+ }
+
+
+ public enum ERROR_CODE : Int16
+ {
+ ERROR_NONE = 0,
+
+
+
+ ERROR_CODE_USER_MGR_INVALID_USER_UNIQUEID = 112,
+
+ ERROR_CODE_PUBLIC_CHANNEL_IN_USER = 114,
+
+ ERROR_CODE_PUBLIC_CHANNEL_INVALIDE_NUMBER = 115,
+ }
+}
diff --git a/Tutorials/EchoClient/PacketProcessForm.cs b/Tutorials/EchoClient/PacketProcessForm.cs
new file mode 100644
index 0000000..32bea27
--- /dev/null
+++ b/Tutorials/EchoClient/PacketProcessForm.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace csharp_test_client
+{
+ public partial class mainForm
+ {
+ Dictionary> PacketFuncDic = new Dictionary>();
+
+ void SetPacketHandler()
+ {
+ PacketFuncDic.Add(PACKET_ID.PACKET_ID_ECHO, PacketProcess_Echo);
+ PacketFuncDic.Add(PACKET_ID.PACKET_ID_ERROR_NTF, PacketProcess_ErrorNotify);
+ PacketFuncDic.Add(PACKET_ID.PACKET_ID_LOGIN_RES, PacketProcess_LoginResponse);
+ PacketFuncDic.Add(PACKET_ID.PACKET_ID_ROOM_ENTER_RES, PacketProcess_RoomEnterResponse);
+ PacketFuncDic.Add(PACKET_ID.PACKET_ID_ROOM_USER_LIST_NTF, PacketProcess_RoomUserListNotify);
+ PacketFuncDic.Add(PACKET_ID.PACKET_ID_ROOM_NEW_USER_NTF, PacketProcess_RoomNewUserNotify);
+ PacketFuncDic.Add(PACKET_ID.PACKET_ID_ROOM_LEAVE_RES, PacketProcess_RoomLeaveResponse);
+ PacketFuncDic.Add(PACKET_ID.PACKET_ID_ROOM_LEAVE_USER_NTF, PacketProcess_RoomLeaveUserNotify);
+ PacketFuncDic.Add(PACKET_ID.PACKET_ID_ROOM_CHAT_RES, PacketProcess_RoomChatResponse);
+ PacketFuncDic.Add(PACKET_ID.PACKET_ID_ROOM_CHAT_NOTIFY, PacketProcess_RoomChatNotify);
+ PacketFuncDic.Add(PACKET_ID.PACKET_ID_ROOM_RELAY_NTF, PacketProcess_RoomRelayNotify);
+ }
+
+ void PacketProcess(PacketData packet)
+ {
+ var packetType = (PACKET_ID)packet.PacketID;
+ //DevLog.Write("Packet Error: PacketID:{packet.PacketID.ToString()}, Error: {(ERROR_CODE)packet.Result}");
+ //DevLog.Write("RawPacket: " + packet.PacketID.ToString() + ", " + PacketDump.Bytes(packet.BodyData));
+
+ if (PacketFuncDic.ContainsKey(packetType))
+ {
+ PacketFuncDic[packetType](packet.BodyData);
+ }
+ else
+ {
+ DevLog.Write("Unknown Packet Id: " + packet.PacketID.ToString());
+ }
+ }
+
+ void PacketProcess_Echo(byte[] bodyData)
+ {
+ DevLog.Write($"Echo 받음: {bodyData.Length}");
+ }
+
+ void PacketProcess_ErrorNotify(byte[] bodyData)
+ {
+ var notifyPkt = new ErrorNtfPacket();
+ notifyPkt.FromBytes(bodyData);
+
+ DevLog.Write($"에러 통보 받음: {notifyPkt.Error}");
+ }
+
+
+ void PacketProcess_LoginResponse(byte[] bodyData)
+ {
+ var responsePkt = new LoginResPacket();
+ responsePkt.FromBytes(bodyData);
+
+ DevLog.Write($"로그인 결과: {(ERROR_CODE)responsePkt.Result}");
+ }
+
+
+ void PacketProcess_RoomEnterResponse(byte[] bodyData)
+ {
+ var responsePkt = new RoomEnterResPacket();
+ responsePkt.FromBytes(bodyData);
+
+ DevLog.Write($"방 입장 결과: {(ERROR_CODE)responsePkt.Result}");
+ }
+
+ void PacketProcess_RoomUserListNotify(byte[] bodyData)
+ {
+ var notifyPkt = new RoomUserListNtfPacket();
+ notifyPkt.FromBytes(bodyData);
+
+ for (int i = 0; i < notifyPkt.UserCount; ++i)
+ {
+ AddRoomUserList(notifyPkt.UserUniqueIdList[i], notifyPkt.UserIDList[i]);
+ }
+
+ DevLog.Write($"방의 기존 유저 리스트 받음");
+ }
+
+ void PacketProcess_RoomNewUserNotify(byte[] bodyData)
+ {
+ var notifyPkt = new RoomNewUserNtfPacket();
+ notifyPkt.FromBytes(bodyData);
+
+ AddRoomUserList(notifyPkt.UserUniqueId, notifyPkt.UserID);
+
+ DevLog.Write($"방에 새로 들어온 유저 받음");
+ }
+
+
+ void PacketProcess_RoomLeaveResponse(byte[] bodyData)
+ {
+ var responsePkt = new RoomLeaveResPacket();
+ responsePkt.FromBytes(bodyData);
+
+ DevLog.Write($"방 나가기 결과: {(ERROR_CODE)responsePkt.Result}");
+ }
+
+ void PacketProcess_RoomLeaveUserNotify(byte[] bodyData)
+ {
+ var notifyPkt = new RoomLeaveUserNtfPacket();
+ notifyPkt.FromBytes(bodyData);
+
+ RemoveRoomUserList(notifyPkt.UserUniqueId);
+
+ DevLog.Write($"방에서 나간 유저 받음");
+ }
+
+
+ void PacketProcess_RoomChatResponse(byte[] bodyData)
+ {
+ var responsePkt = new RoomChatResPacket();
+ responsePkt.FromBytes(bodyData);
+
+ var errorCode = (ERROR_CODE)responsePkt.Result;
+ var msg = $"방 채팅 요청 결과: {(ERROR_CODE)responsePkt.Result}";
+ if (errorCode == ERROR_CODE.ERROR_NONE)
+ {
+ DevLog.Write(msg, LOG_LEVEL.ERROR);
+ }
+ else
+ {
+ AddRoomChatMessageList(0, msg);
+ }
+ }
+
+
+ void PacketProcess_RoomChatNotify(byte[] bodyData)
+ {
+ var responsePkt = new RoomChatNtfPacket();
+ responsePkt.FromBytes(bodyData);
+
+ AddRoomChatMessageList(responsePkt.UserUniqueId, responsePkt.Message);
+ }
+
+ void AddRoomChatMessageList(Int64 userUniqueId, string msgssage)
+ {
+ var msg = $"{userUniqueId}: {msgssage}";
+
+ if (listBoxRoomChatMsg.Items.Count > 512)
+ {
+ listBoxRoomChatMsg.Items.Clear();
+ }
+
+ listBoxRoomChatMsg.Items.Add(msg);
+ listBoxRoomChatMsg.SelectedIndex = listBoxRoomChatMsg.Items.Count - 1;
+ }
+
+
+ void PacketProcess_RoomRelayNotify(byte[] bodyData)
+ {
+ var notifyPkt = new RoomRelayNtfPacket();
+ notifyPkt.FromBytes(bodyData);
+
+ var stringData = Encoding.UTF8.GetString(notifyPkt.RelayData);
+ DevLog.Write($"방에서 릴레이 받음. {notifyPkt.UserUniqueId} - {stringData}");
+ }
+ }
+}
diff --git a/Tutorials/EchoClient/Program.cs b/Tutorials/EchoClient/Program.cs
new file mode 100644
index 0000000..7b9e1ff
--- /dev/null
+++ b/Tutorials/EchoClient/Program.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Versioning;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace csharp_test_client
+{
+ [SupportedOSPlatform("windows10.0.177630")]
+ static class Program
+ {
+ ///
+ /// 해당 응용 프로그램의 주 진입점입니다.
+ ///
+ [STAThread]
+ static void Main()
+ {
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
+ Application.Run(new mainForm());
+ }
+ }
+}
diff --git a/Tutorials/EchoClient/Properties/AssemblyInfo.cs b/Tutorials/EchoClient/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..d3e425d
--- /dev/null
+++ b/Tutorials/EchoClient/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// 어셈블리에 대한 일반 정보는 다음 특성 집합을 통해
+// 제어됩니다. 어셈블리와 관련된 정보를 수정하려면
+// 이러한 특성 값을 변경하세요.
+[assembly: AssemblyTitle("csharp_test_client")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("csharp_test_client")]
+[assembly: AssemblyCopyright("Copyright © 2016")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에
+// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면
+// 해당 형식에 대해 ComVisible 특성을 true로 설정하세요.
+[assembly: ComVisible(false)]
+
+// 이 프로젝트가 COM에 노출되는 경우 다음 GUID는 typelib의 ID를 나타냅니다.
+[assembly: Guid("9e4b5e72-4e76-4e22-90b0-e53275a99018")]
+
+// 어셈블리의 버전 정보는 다음 네 가지 값으로 구성됩니다.
+//
+// 주 버전
+// 부 버전
+// 빌드 번호
+// 수정 버전
+//
+// 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호가 자동으로
+// 지정되도록 할 수 있습니다.
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Tutorials/EchoClient/Properties/Resources.Designer.cs b/Tutorials/EchoClient/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..fbcd69d
--- /dev/null
+++ b/Tutorials/EchoClient/Properties/Resources.Designer.cs
@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+//
+// 이 코드는 도구를 사용하여 생성되었습니다.
+// 런타임 버전:4.0.30319.42000
+//
+// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
+// 이러한 변경 내용이 손실됩니다.
+//
+//------------------------------------------------------------------------------
+
+namespace csharp_test_client.Properties
+{
+
+
+ ///
+ /// 지역화된 문자열 등을 찾기 위한 강력한 형식의 리소스 클래스입니다.
+ ///
+ // 이 클래스는 ResGen 또는 Visual Studio와 같은 도구를 통해 StronglyTypedResourceBuilder
+ // 클래스에서 자동으로 생성되었습니다.
+ // 멤버를 추가하거나 제거하려면 .ResX 파일을 편집한 다음 /str 옵션을 사용하여
+ // ResGen을 다시 실행하거나 VS 프로젝트를 다시 빌드하십시오.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources
+ {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources()
+ {
+ }
+
+ ///
+ /// 이 클래스에서 사용하는 캐시된 ResourceManager 인스턴스를 반환합니다.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager
+ {
+ get
+ {
+ if ((resourceMan == null))
+ {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("csharp_test_client.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// 이 강력한 형식의 리소스 클래스를 사용하여 모든 리소스 조회에 대해
+ /// 현재 스레드의 CurrentUICulture 속성을 재정의합니다.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture
+ {
+ get
+ {
+ return resourceCulture;
+ }
+ set
+ {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/Tutorials/EchoClient/Properties/Resources.resx b/Tutorials/EchoClient/Properties/Resources.resx
new file mode 100644
index 0000000..ffecec8
--- /dev/null
+++ b/Tutorials/EchoClient/Properties/Resources.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Tutorials/EchoClient/Properties/Settings.Designer.cs b/Tutorials/EchoClient/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..aca6dcd
--- /dev/null
+++ b/Tutorials/EchoClient/Properties/Settings.Designer.cs
@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace csharp_test_client.Properties
+{
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
+ {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default
+ {
+ get
+ {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/Tutorials/EchoClient/Properties/Settings.settings b/Tutorials/EchoClient/Properties/Settings.settings
new file mode 100644
index 0000000..abf36c5
--- /dev/null
+++ b/Tutorials/EchoClient/Properties/Settings.settings
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/Tutorials/EchoClient/csharp_test_client.csproj b/Tutorials/EchoClient/csharp_test_client.csproj
new file mode 100644
index 0000000..ba2a444
--- /dev/null
+++ b/Tutorials/EchoClient/csharp_test_client.csproj
@@ -0,0 +1,20 @@
+
+
+ net8.0-windows10.0.17763.0
+ WinExe
+ false
+ true
+ true
+
+
+ ..\bin\
+
+
+
+
+
+ all
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/EchoClient/csharp_test_client.sln b/Tutorials/EchoClient/csharp_test_client.sln
new file mode 100644
index 0000000..8a34e3c
--- /dev/null
+++ b/Tutorials/EchoClient/csharp_test_client.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.25420.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp_test_client", "csharp_test_client.csproj", "{9E4B5E72-4E76-4E22-90B0-E53275A99018}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {9E4B5E72-4E76-4E22-90B0-E53275A99018}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9E4B5E72-4E76-4E22-90B0-E53275A99018}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9E4B5E72-4E76-4E22-90B0-E53275A99018}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9E4B5E72-4E76-4E22-90B0-E53275A99018}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/Tutorials/EchoClient/mainForm.Designer.cs b/Tutorials/EchoClient/mainForm.Designer.cs
new file mode 100644
index 0000000..48e4419
--- /dev/null
+++ b/Tutorials/EchoClient/mainForm.Designer.cs
@@ -0,0 +1,454 @@
+namespace csharp_test_client
+{
+ partial class mainForm
+ {
+ ///
+ /// 필수 디자이너 변수입니다.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// 사용 중인 모든 리소스를 정리합니다.
+ ///
+ /// 관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form 디자이너에서 생성한 코드
+
+ ///
+ /// 디자이너 지원에 필요한 메서드입니다.
+ /// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
+ ///
+ private void InitializeComponent()
+ {
+ btnDisconnect = new System.Windows.Forms.Button();
+ btnConnect = new System.Windows.Forms.Button();
+ groupBox5 = new System.Windows.Forms.GroupBox();
+ textBoxPort = new System.Windows.Forms.TextBox();
+ label10 = new System.Windows.Forms.Label();
+ checkBoxLocalHostIP = new System.Windows.Forms.CheckBox();
+ textBoxIP = new System.Windows.Forms.TextBox();
+ label9 = new System.Windows.Forms.Label();
+ button1 = new System.Windows.Forms.Button();
+ textSendText = new System.Windows.Forms.TextBox();
+ labelStatus = new System.Windows.Forms.Label();
+ listBoxLog = new System.Windows.Forms.ListBox();
+ label1 = new System.Windows.Forms.Label();
+ textBoxUserID = new System.Windows.Forms.TextBox();
+ textBoxUserPW = new System.Windows.Forms.TextBox();
+ label2 = new System.Windows.Forms.Label();
+ button2 = new System.Windows.Forms.Button();
+ Room = new System.Windows.Forms.GroupBox();
+ textBoxRelay = new System.Windows.Forms.TextBox();
+ btnRoomRelay = new System.Windows.Forms.Button();
+ btnRoomChat = new System.Windows.Forms.Button();
+ textBoxRoomSendMsg = new System.Windows.Forms.TextBox();
+ listBoxRoomChatMsg = new System.Windows.Forms.ListBox();
+ label4 = new System.Windows.Forms.Label();
+ listBoxRoomUserList = new System.Windows.Forms.ListBox();
+ btn_RoomLeave = new System.Windows.Forms.Button();
+ btn_RoomEnter = new System.Windows.Forms.Button();
+ textBoxRoomNumber = new System.Windows.Forms.TextBox();
+ label3 = new System.Windows.Forms.Label();
+ groupBox5.SuspendLayout();
+ Room.SuspendLayout();
+ SuspendLayout();
+ //
+ // btnDisconnect
+ //
+ btnDisconnect.Font = new System.Drawing.Font("맑은 고딕", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 129);
+ btnDisconnect.Location = new System.Drawing.Point(421, 55);
+ btnDisconnect.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ btnDisconnect.Name = "btnDisconnect";
+ btnDisconnect.Size = new System.Drawing.Size(88, 32);
+ btnDisconnect.TabIndex = 29;
+ btnDisconnect.Text = "접속 끊기";
+ btnDisconnect.UseVisualStyleBackColor = true;
+ btnDisconnect.Click += btnDisconnect_Click;
+ //
+ // btnConnect
+ //
+ btnConnect.Font = new System.Drawing.Font("맑은 고딕", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 129);
+ btnConnect.Location = new System.Drawing.Point(420, 20);
+ btnConnect.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ btnConnect.Name = "btnConnect";
+ btnConnect.Size = new System.Drawing.Size(88, 32);
+ btnConnect.TabIndex = 28;
+ btnConnect.Text = "접속하기";
+ btnConnect.UseVisualStyleBackColor = true;
+ btnConnect.Click += btnConnect_Click;
+ //
+ // groupBox5
+ //
+ groupBox5.Controls.Add(textBoxPort);
+ groupBox5.Controls.Add(label10);
+ groupBox5.Controls.Add(checkBoxLocalHostIP);
+ groupBox5.Controls.Add(textBoxIP);
+ groupBox5.Controls.Add(label9);
+ groupBox5.Location = new System.Drawing.Point(12, 15);
+ groupBox5.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ groupBox5.Name = "groupBox5";
+ groupBox5.Padding = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ groupBox5.Size = new System.Drawing.Size(403, 65);
+ groupBox5.TabIndex = 27;
+ groupBox5.TabStop = false;
+ groupBox5.Text = "Socket 더미 클라이언트 설정";
+ //
+ // textBoxPort
+ //
+ textBoxPort.Location = new System.Drawing.Point(225, 25);
+ textBoxPort.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ textBoxPort.MaxLength = 6;
+ textBoxPort.Name = "textBoxPort";
+ textBoxPort.Size = new System.Drawing.Size(51, 23);
+ textBoxPort.TabIndex = 18;
+ textBoxPort.Text = "32452";
+ textBoxPort.WordWrap = false;
+ //
+ // label10
+ //
+ label10.AutoSize = true;
+ label10.Location = new System.Drawing.Point(163, 30);
+ label10.Name = "label10";
+ label10.Size = new System.Drawing.Size(62, 15);
+ label10.TabIndex = 17;
+ label10.Text = "포트 번호:";
+ //
+ // checkBoxLocalHostIP
+ //
+ checkBoxLocalHostIP.AutoSize = true;
+ checkBoxLocalHostIP.Checked = true;
+ checkBoxLocalHostIP.CheckState = System.Windows.Forms.CheckState.Checked;
+ checkBoxLocalHostIP.Location = new System.Drawing.Point(285, 30);
+ checkBoxLocalHostIP.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ checkBoxLocalHostIP.Name = "checkBoxLocalHostIP";
+ checkBoxLocalHostIP.Size = new System.Drawing.Size(102, 19);
+ checkBoxLocalHostIP.TabIndex = 15;
+ checkBoxLocalHostIP.Text = "localhost 사용";
+ checkBoxLocalHostIP.UseVisualStyleBackColor = true;
+ //
+ // textBoxIP
+ //
+ textBoxIP.Location = new System.Drawing.Point(68, 24);
+ textBoxIP.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ textBoxIP.MaxLength = 6;
+ textBoxIP.Name = "textBoxIP";
+ textBoxIP.Size = new System.Drawing.Size(87, 23);
+ textBoxIP.TabIndex = 11;
+ textBoxIP.Text = "0.0.0.0";
+ textBoxIP.WordWrap = false;
+ //
+ // label9
+ //
+ label9.AutoSize = true;
+ label9.Location = new System.Drawing.Point(6, 29);
+ label9.Name = "label9";
+ label9.Size = new System.Drawing.Size(62, 15);
+ label9.TabIndex = 10;
+ label9.Text = "서버 주소:";
+ //
+ // button1
+ //
+ button1.Font = new System.Drawing.Font("맑은 고딕", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 129);
+ button1.Location = new System.Drawing.Point(319, 88);
+ button1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ button1.Name = "button1";
+ button1.Size = new System.Drawing.Size(100, 32);
+ button1.TabIndex = 39;
+ button1.Text = "echo 보내기";
+ button1.UseVisualStyleBackColor = true;
+ button1.Click += button1_Click;
+ //
+ // textSendText
+ //
+ textSendText.Location = new System.Drawing.Point(12, 92);
+ textSendText.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ textSendText.MaxLength = 32;
+ textSendText.Name = "textSendText";
+ textSendText.Size = new System.Drawing.Size(301, 23);
+ textSendText.TabIndex = 38;
+ textSendText.Text = "test1";
+ textSendText.WordWrap = false;
+ //
+ // labelStatus
+ //
+ labelStatus.AutoSize = true;
+ labelStatus.Location = new System.Drawing.Point(10, 719);
+ labelStatus.Name = "labelStatus";
+ labelStatus.Size = new System.Drawing.Size(112, 15);
+ labelStatus.TabIndex = 40;
+ labelStatus.Text = "서버 접속 상태: ???";
+ //
+ // listBoxLog
+ //
+ listBoxLog.FormattingEnabled = true;
+ listBoxLog.HorizontalScrollbar = true;
+ listBoxLog.ItemHeight = 15;
+ listBoxLog.Location = new System.Drawing.Point(10, 532);
+ listBoxLog.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ listBoxLog.Name = "listBoxLog";
+ listBoxLog.Size = new System.Drawing.Size(490, 169);
+ listBoxLog.TabIndex = 41;
+ //
+ // label1
+ //
+ label1.AutoSize = true;
+ label1.Location = new System.Drawing.Point(10, 153);
+ label1.Name = "label1";
+ label1.Size = new System.Drawing.Size(45, 15);
+ label1.TabIndex = 42;
+ label1.Text = "UserID:";
+ //
+ // textBoxUserID
+ //
+ textBoxUserID.Location = new System.Drawing.Point(62, 149);
+ textBoxUserID.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ textBoxUserID.MaxLength = 6;
+ textBoxUserID.Name = "textBoxUserID";
+ textBoxUserID.Size = new System.Drawing.Size(87, 23);
+ textBoxUserID.TabIndex = 43;
+ textBoxUserID.Text = "jacking75";
+ textBoxUserID.WordWrap = false;
+ //
+ // textBoxUserPW
+ //
+ textBoxUserPW.Location = new System.Drawing.Point(220, 150);
+ textBoxUserPW.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ textBoxUserPW.MaxLength = 6;
+ textBoxUserPW.Name = "textBoxUserPW";
+ textBoxUserPW.Size = new System.Drawing.Size(87, 23);
+ textBoxUserPW.TabIndex = 45;
+ textBoxUserPW.Text = "jacking75";
+ textBoxUserPW.WordWrap = false;
+ //
+ // label2
+ //
+ label2.AutoSize = true;
+ label2.Location = new System.Drawing.Point(158, 154);
+ label2.Name = "label2";
+ label2.Size = new System.Drawing.Size(53, 15);
+ label2.TabIndex = 44;
+ label2.Text = "PassWD:";
+ //
+ // button2
+ //
+ button2.Font = new System.Drawing.Font("맑은 고딕", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 129);
+ button2.Location = new System.Drawing.Point(316, 144);
+ button2.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ button2.Name = "button2";
+ button2.Size = new System.Drawing.Size(100, 32);
+ button2.TabIndex = 46;
+ button2.Text = "Login";
+ button2.UseVisualStyleBackColor = true;
+ button2.Click += button2_Click;
+ //
+ // Room
+ //
+ Room.Controls.Add(textBoxRelay);
+ Room.Controls.Add(btnRoomRelay);
+ Room.Controls.Add(btnRoomChat);
+ Room.Controls.Add(textBoxRoomSendMsg);
+ Room.Controls.Add(listBoxRoomChatMsg);
+ Room.Controls.Add(label4);
+ Room.Controls.Add(listBoxRoomUserList);
+ Room.Controls.Add(btn_RoomLeave);
+ Room.Controls.Add(btn_RoomEnter);
+ Room.Controls.Add(textBoxRoomNumber);
+ Room.Controls.Add(label3);
+ Room.Location = new System.Drawing.Point(13, 213);
+ Room.Name = "Room";
+ Room.Size = new System.Drawing.Size(495, 312);
+ Room.TabIndex = 47;
+ Room.TabStop = false;
+ Room.Text = "Room";
+ //
+ // textBoxRelay
+ //
+ textBoxRelay.Location = new System.Drawing.Point(306, 30);
+ textBoxRelay.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ textBoxRelay.MaxLength = 6;
+ textBoxRelay.Name = "textBoxRelay";
+ textBoxRelay.Size = new System.Drawing.Size(109, 23);
+ textBoxRelay.TabIndex = 55;
+ textBoxRelay.Text = "test";
+ textBoxRelay.WordWrap = false;
+ //
+ // btnRoomRelay
+ //
+ btnRoomRelay.Font = new System.Drawing.Font("맑은 고딕", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 129);
+ btnRoomRelay.Location = new System.Drawing.Point(420, 25);
+ btnRoomRelay.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ btnRoomRelay.Name = "btnRoomRelay";
+ btnRoomRelay.Size = new System.Drawing.Size(66, 32);
+ btnRoomRelay.TabIndex = 54;
+ btnRoomRelay.Text = "Relay";
+ btnRoomRelay.UseVisualStyleBackColor = true;
+ btnRoomRelay.Click += btnRoomRelay_Click;
+ //
+ // btnRoomChat
+ //
+ btnRoomChat.Font = new System.Drawing.Font("맑은 고딕", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 129);
+ btnRoomChat.Location = new System.Drawing.Point(437, 265);
+ btnRoomChat.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ btnRoomChat.Name = "btnRoomChat";
+ btnRoomChat.Size = new System.Drawing.Size(50, 32);
+ btnRoomChat.TabIndex = 53;
+ btnRoomChat.Text = "chat";
+ btnRoomChat.UseVisualStyleBackColor = true;
+ btnRoomChat.Click += btnRoomChat_Click;
+ //
+ // textBoxRoomSendMsg
+ //
+ textBoxRoomSendMsg.Location = new System.Drawing.Point(13, 269);
+ textBoxRoomSendMsg.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ textBoxRoomSendMsg.MaxLength = 32;
+ textBoxRoomSendMsg.Name = "textBoxRoomSendMsg";
+ textBoxRoomSendMsg.Size = new System.Drawing.Size(419, 23);
+ textBoxRoomSendMsg.TabIndex = 52;
+ textBoxRoomSendMsg.Text = "test1";
+ textBoxRoomSendMsg.WordWrap = false;
+ //
+ // listBoxRoomChatMsg
+ //
+ listBoxRoomChatMsg.FormattingEnabled = true;
+ listBoxRoomChatMsg.ItemHeight = 15;
+ listBoxRoomChatMsg.Location = new System.Drawing.Point(144, 81);
+ listBoxRoomChatMsg.Name = "listBoxRoomChatMsg";
+ listBoxRoomChatMsg.Size = new System.Drawing.Size(343, 169);
+ listBoxRoomChatMsg.TabIndex = 51;
+ //
+ // label4
+ //
+ label4.AutoSize = true;
+ label4.Location = new System.Drawing.Point(10, 64);
+ label4.Name = "label4";
+ label4.Size = new System.Drawing.Size(55, 15);
+ label4.TabIndex = 50;
+ label4.Text = "User List:";
+ //
+ // listBoxRoomUserList
+ //
+ listBoxRoomUserList.FormattingEnabled = true;
+ listBoxRoomUserList.ItemHeight = 15;
+ listBoxRoomUserList.Location = new System.Drawing.Point(13, 82);
+ listBoxRoomUserList.Name = "listBoxRoomUserList";
+ listBoxRoomUserList.Size = new System.Drawing.Size(123, 169);
+ listBoxRoomUserList.TabIndex = 49;
+ //
+ // btn_RoomLeave
+ //
+ btn_RoomLeave.Font = new System.Drawing.Font("맑은 고딕", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 129);
+ btn_RoomLeave.Location = new System.Drawing.Point(216, 24);
+ btn_RoomLeave.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ btn_RoomLeave.Name = "btn_RoomLeave";
+ btn_RoomLeave.Size = new System.Drawing.Size(66, 32);
+ btn_RoomLeave.TabIndex = 48;
+ btn_RoomLeave.Text = "Leave";
+ btn_RoomLeave.UseVisualStyleBackColor = true;
+ btn_RoomLeave.Click += btn_RoomLeave_Click;
+ //
+ // btn_RoomEnter
+ //
+ btn_RoomEnter.Font = new System.Drawing.Font("맑은 고딕", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 129);
+ btn_RoomEnter.Location = new System.Drawing.Point(144, 23);
+ btn_RoomEnter.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ btn_RoomEnter.Name = "btn_RoomEnter";
+ btn_RoomEnter.Size = new System.Drawing.Size(66, 32);
+ btn_RoomEnter.TabIndex = 47;
+ btn_RoomEnter.Text = "Enter";
+ btn_RoomEnter.UseVisualStyleBackColor = true;
+ btn_RoomEnter.Click += btn_RoomEnter_Click;
+ //
+ // textBoxRoomNumber
+ //
+ textBoxRoomNumber.Location = new System.Drawing.Point(98, 25);
+ textBoxRoomNumber.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ textBoxRoomNumber.MaxLength = 6;
+ textBoxRoomNumber.Name = "textBoxRoomNumber";
+ textBoxRoomNumber.Size = new System.Drawing.Size(38, 23);
+ textBoxRoomNumber.TabIndex = 44;
+ textBoxRoomNumber.Text = "0";
+ textBoxRoomNumber.WordWrap = false;
+ //
+ // label3
+ //
+ label3.AutoSize = true;
+ label3.Location = new System.Drawing.Point(5, 31);
+ label3.Name = "label3";
+ label3.Size = new System.Drawing.Size(90, 15);
+ label3.TabIndex = 43;
+ label3.Text = "Room Number:";
+ //
+ // mainForm
+ //
+ AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
+ AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ ClientSize = new System.Drawing.Size(524, 751);
+ Controls.Add(Room);
+ Controls.Add(button2);
+ Controls.Add(textBoxUserPW);
+ Controls.Add(label2);
+ Controls.Add(textBoxUserID);
+ Controls.Add(label1);
+ Controls.Add(labelStatus);
+ Controls.Add(listBoxLog);
+ Controls.Add(button1);
+ Controls.Add(textSendText);
+ Controls.Add(btnDisconnect);
+ Controls.Add(btnConnect);
+ Controls.Add(groupBox5);
+ FormBorderStyle = System.Windows.Forms.FormBorderStyle.Fixed3D;
+ Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+ Name = "mainForm";
+ Text = "네트워크 테스트 클라이언트";
+ FormClosing += mainForm_FormClosing;
+ Load += mainForm_Load;
+ groupBox5.ResumeLayout(false);
+ groupBox5.PerformLayout();
+ Room.ResumeLayout(false);
+ Room.PerformLayout();
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private System.Windows.Forms.Button btnDisconnect;
+ private System.Windows.Forms.Button btnConnect;
+ private System.Windows.Forms.GroupBox groupBox5;
+ private System.Windows.Forms.TextBox textBoxPort;
+ private System.Windows.Forms.Label label10;
+ private System.Windows.Forms.CheckBox checkBoxLocalHostIP;
+ private System.Windows.Forms.TextBox textBoxIP;
+ private System.Windows.Forms.Label label9;
+ private System.Windows.Forms.Button button1;
+ private System.Windows.Forms.TextBox textSendText;
+ private System.Windows.Forms.Label labelStatus;
+ private System.Windows.Forms.ListBox listBoxLog;
+ private System.Windows.Forms.Label label1;
+ private System.Windows.Forms.TextBox textBoxUserID;
+ private System.Windows.Forms.TextBox textBoxUserPW;
+ private System.Windows.Forms.Label label2;
+ private System.Windows.Forms.Button button2;
+ private System.Windows.Forms.GroupBox Room;
+ private System.Windows.Forms.Button btn_RoomLeave;
+ private System.Windows.Forms.Button btn_RoomEnter;
+ private System.Windows.Forms.TextBox textBoxRoomNumber;
+ private System.Windows.Forms.Label label3;
+ private System.Windows.Forms.Button btnRoomChat;
+ private System.Windows.Forms.TextBox textBoxRoomSendMsg;
+ private System.Windows.Forms.ListBox listBoxRoomChatMsg;
+ private System.Windows.Forms.Label label4;
+ private System.Windows.Forms.ListBox listBoxRoomUserList;
+ private System.Windows.Forms.Button btnRoomRelay;
+ private System.Windows.Forms.TextBox textBoxRelay;
+ }
+}
+
diff --git a/Tutorials/EchoClient/mainForm.cs b/Tutorials/EchoClient/mainForm.cs
new file mode 100644
index 0000000..ca58439
--- /dev/null
+++ b/Tutorials/EchoClient/mainForm.cs
@@ -0,0 +1,376 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Linq;
+using System.Runtime.Versioning;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace csharp_test_client
+{
+ [SupportedOSPlatform("windows10.0.177630")]
+ public partial class mainForm : Form
+ {
+ ClientSimpleTcp Network = new ClientSimpleTcp();
+
+ bool IsNetworkThreadRunning = false;
+ bool IsBackGroundProcessRunning = false;
+
+ System.Threading.Thread NetworkReadThread = null;
+ System.Threading.Thread NetworkSendThread = null;
+
+ PacketBufferManager PacketBuffer = new PacketBufferManager();
+ Queue RecvPacketQueue = new Queue();
+ Queue SendPacketQueue = new Queue();
+
+ System.Windows.Forms.Timer dispatcherUITimer = new();
+
+
+ public mainForm()
+ {
+ InitializeComponent();
+ }
+
+ private void mainForm_Load(object sender, EventArgs e)
+ {
+ PacketBuffer.Init((8096 * 10), PacketDef.PACKET_HEADER_SIZE, 1024);
+
+ IsNetworkThreadRunning = true;
+ NetworkReadThread = new System.Threading.Thread(this.NetworkReadProcess);
+ NetworkReadThread.Start();
+ NetworkSendThread = new System.Threading.Thread(this.NetworkSendProcess);
+ NetworkSendThread.Start();
+
+ IsBackGroundProcessRunning = true;
+ /*dispatcherUITimer = new System.Windows.Threading.DispatcherTimer();
+ dispatcherUITimer.Tick += new EventHandler(BackGroundProcess);
+ dispatcherUITimer.Interval = new TimeSpan(0, 0, 0, 0, 100);
+ dispatcherUITimer.Start();*/
+ dispatcherUITimer.Tick += new EventHandler(BackGroundProcess);
+ dispatcherUITimer.Interval = 100;
+ dispatcherUITimer.Start();
+
+ btnDisconnect.Enabled = false;
+
+ SetPacketHandler();
+ DevLog.Write("프로그램 시작 !!!", LOG_LEVEL.INFO);
+ }
+
+ private void mainForm_FormClosing(object sender, FormClosingEventArgs e)
+ {
+ IsNetworkThreadRunning = false;
+ IsBackGroundProcessRunning = false;
+
+ Network.Close();
+ }
+
+ private void btnConnect_Click(object sender, EventArgs e)
+ {
+ string address = textBoxIP.Text;
+
+ if (checkBoxLocalHostIP.Checked)
+ {
+ address = "127.0.0.1";
+ }
+
+ int port = Convert.ToInt32(textBoxPort.Text);
+
+ if (Network.Connect(address, port))
+ {
+ labelStatus.Text = string.Format("{0}. 서버에 접속 중", DateTime.Now);
+ btnConnect.Enabled = false;
+ btnDisconnect.Enabled = true;
+
+ DevLog.Write($"서버에 접속 중", LOG_LEVEL.INFO);
+ }
+ else
+ {
+ labelStatus.Text = string.Format("{0}. 서버에 접속 실패", DateTime.Now);
+ }
+ }
+
+ private void btnDisconnect_Click(object sender, EventArgs e)
+ {
+ SetDisconnectd();
+ Network.Close();
+ }
+
+ private void button1_Click(object sender, EventArgs e)
+ {
+ if (string.IsNullOrEmpty(textSendText.Text))
+ {
+ MessageBox.Show("보낼 텍스트를 입력하세요");
+ return;
+ }
+
+ var body = Encoding.UTF8.GetBytes(textSendText.Text);
+
+ PostSendPacket(PACKET_ID.PACKET_ID_ECHO, body);
+
+ DevLog.Write($"Echo 요청: {textSendText.Text}, {body.Length}");
+ }
+
+
+
+ void NetworkReadProcess()
+ {
+ const Int16 PacketHeaderSize = PacketDef.PACKET_HEADER_SIZE;
+
+ while (IsNetworkThreadRunning)
+ {
+ if (Network.IsConnected() == false)
+ {
+ System.Threading.Thread.Sleep(1);
+ continue;
+ }
+
+ var recvData = Network.Receive();
+
+ if (recvData != null)
+ {
+ PacketBuffer.Write(recvData.Item2, 0, recvData.Item1);
+
+ while (true)
+ {
+ var data = PacketBuffer.Read();
+ if (data.Count < 1)
+ {
+ break;
+ }
+
+ var packet = new PacketData();
+ packet.DataSize = (short)(data.Count - PacketHeaderSize);
+ packet.PacketID = BitConverter.ToInt16(data.Array, data.Offset + 2);
+ packet.Type = (SByte)data.Array[(data.Offset + 4)];
+ packet.BodyData = new byte[packet.DataSize];
+ Buffer.BlockCopy(data.Array, (data.Offset + PacketHeaderSize), packet.BodyData, 0, (data.Count - PacketHeaderSize));
+ lock (((System.Collections.ICollection)RecvPacketQueue).SyncRoot)
+ {
+ RecvPacketQueue.Enqueue(packet);
+ }
+ }
+ //DevLog.Write($"받은 데이터: {recvData.Item2}", LOG_LEVEL.INFO);
+ }
+ else
+ {
+ Network.Close();
+ SetDisconnectd();
+ DevLog.Write("서버와 접속 종료 !!!", LOG_LEVEL.INFO);
+ }
+ }
+ }
+
+ void NetworkSendProcess()
+ {
+ while (IsNetworkThreadRunning)
+ {
+ System.Threading.Thread.Sleep(1);
+
+ if (Network.IsConnected() == false)
+ {
+ continue;
+ }
+
+ lock (((System.Collections.ICollection)SendPacketQueue).SyncRoot)
+ {
+ if (SendPacketQueue.Count > 0)
+ {
+ var packet = SendPacketQueue.Dequeue();
+ Network.Send(packet);
+ }
+ }
+ }
+ }
+
+
+ void BackGroundProcess(object sender, EventArgs e)
+ {
+ ProcessLog();
+
+ try
+ {
+ var packet = new PacketData();
+
+ lock (((System.Collections.ICollection)RecvPacketQueue).SyncRoot)
+ {
+ if (RecvPacketQueue.Count() > 0)
+ {
+ packet = RecvPacketQueue.Dequeue();
+ }
+ }
+
+ if (packet.PacketID != 0)
+ {
+ PacketProcess(packet);
+ }
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(string.Format("ReadPacketQueueProcess. error:{0}", ex.Message));
+ }
+ }
+
+ private void ProcessLog()
+ {
+ // 너무 이 작업만 할 수 없으므로 일정 작업 이상을 하면 일단 패스한다.
+ int logWorkCount = 0;
+
+ while (IsBackGroundProcessRunning)
+ {
+ System.Threading.Thread.Sleep(1);
+
+ string msg;
+
+ if (DevLog.GetLog(out msg))
+ {
+ ++logWorkCount;
+
+ if (listBoxLog.Items.Count > 512)
+ {
+ listBoxLog.Items.Clear();
+ }
+
+ listBoxLog.Items.Add(msg);
+ listBoxLog.SelectedIndex = listBoxLog.Items.Count - 1;
+ }
+ else
+ {
+ break;
+ }
+
+ if (logWorkCount > 8)
+ {
+ break;
+ }
+ }
+ }
+
+
+ public void SetDisconnectd()
+ {
+ if (btnConnect.Enabled == false)
+ {
+ btnConnect.Enabled = true;
+ btnDisconnect.Enabled = false;
+ }
+
+ SendPacketQueue.Clear();
+
+ listBoxRoomChatMsg.Items.Clear();
+ listBoxRoomUserList.Items.Clear();
+
+ labelStatus.Text = "서버 접속이 끊어짐";
+ }
+
+ public void PostSendPacket(PACKET_ID packetID, byte[] bodyData)
+ {
+ if (Network.IsConnected() == false)
+ {
+ DevLog.Write("서버 연결이 되어 있지 않습니다", LOG_LEVEL.ERROR);
+ return;
+ }
+
+ Int16 bodyDataSize = 0;
+ if (bodyData != null)
+ {
+ bodyDataSize = (Int16)bodyData.Length;
+ }
+ var packetSize = bodyDataSize + PacketDef.PACKET_HEADER_SIZE;
+
+ List dataSource = new List();
+ dataSource.AddRange(BitConverter.GetBytes((Int16)packetSize));
+ dataSource.AddRange(BitConverter.GetBytes((Int16)packetID));
+ dataSource.AddRange(new byte[] { (byte)0 });
+
+ if (bodyData != null)
+ {
+ dataSource.AddRange(bodyData);
+ }
+
+ SendPacketQueue.Enqueue(dataSource.ToArray());
+ }
+
+ void AddRoomUserList(Int64 userUniqueId, string userID)
+ {
+ var msg = $"{userUniqueId}: {userID}";
+ listBoxRoomUserList.Items.Add(msg);
+ }
+
+ void RemoveRoomUserList(Int64 userUniqueId)
+ {
+ object removeItem = null;
+
+ foreach( var user in listBoxRoomUserList.Items)
+ {
+ var items = user.ToString().Split(":");
+ if( items[0].ToInt64() == userUniqueId)
+ {
+ removeItem = user;
+ return;
+ }
+ }
+
+ if (removeItem != null)
+ {
+ listBoxRoomUserList.Items.Remove(removeItem);
+ }
+ }
+
+
+ // 로그인 요청
+ private void button2_Click(object sender, EventArgs e)
+ {
+ var loginReq = new LoginReqPacket();
+ loginReq.SetValue(textBoxUserID.Text, textBoxUserPW.Text);
+
+ PostSendPacket(PACKET_ID.PACKET_ID_LOGIN_REQ, loginReq.ToBytes());
+ DevLog.Write($"로그인 요청: {textBoxUserID.Text}, {textBoxUserPW.Text}");
+ }
+
+ private void btn_RoomEnter_Click(object sender, EventArgs e)
+ {
+ var requestPkt = new RoomEnterReqPacket();
+ requestPkt.SetValue(textBoxRoomNumber.Text.ToInt32());
+
+ PostSendPacket(PACKET_ID.PACKET_ID_ROOM_ENTER_REQ, requestPkt.ToBytes());
+ DevLog.Write($"방 입장 요청: {textBoxRoomNumber.Text} 번");
+ }
+
+ private void btn_RoomLeave_Click(object sender, EventArgs e)
+ {
+ PostSendPacket(PACKET_ID.PACKET_ID_ROOM_LEAVE_REQ, null);
+ DevLog.Write($"방 입장 요청: {textBoxRoomNumber.Text} 번");
+ }
+
+ private void btnRoomChat_Click(object sender, EventArgs e)
+ {
+ if(textBoxRoomSendMsg.Text.IsEmpty())
+ {
+ MessageBox.Show("채팅 메시지를 입력하세요");
+ return;
+ }
+
+ var requestPkt = new RoomChatReqPacket();
+ requestPkt.SetValue(textBoxRoomSendMsg.Text);
+
+ PostSendPacket(PACKET_ID.PACKET_ID_ROOM_CHAT_REQ, requestPkt.ToBytes());
+ DevLog.Write($"방 채팅 요청");
+ }
+
+ private void btnRoomRelay_Click(object sender, EventArgs e)
+ {
+ if( textBoxRelay.Text.IsEmpty())
+ {
+ MessageBox.Show("릴레이 할 데이터가 없습니다");
+ return;
+ }
+
+ var bodyData = Encoding.UTF8.GetBytes(textBoxRelay.Text);
+ PostSendPacket(PACKET_ID.PACKET_ID_ROOM_RELAY_REQ, bodyData);
+ DevLog.Write($"방 릴레이 요청");
+ }
+ }
+}
diff --git a/Tutorials/EchoClient/mainForm.resx b/Tutorials/EchoClient/mainForm.resx
new file mode 100644
index 0000000..b92c163
--- /dev/null
+++ b/Tutorials/EchoClient/mainForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Tutorials/PvPGameServer_Client/Packet.cs b/Tutorials/PvPGameServer_Client/Packet.cs
index 67ecc7c..1645f73 100644
--- a/Tutorials/PvPGameServer_Client/Packet.cs
+++ b/Tutorials/PvPGameServer_Client/Packet.cs
@@ -7,15 +7,7 @@
using System.Threading.Tasks;
namespace csharp_test_client
-{
- struct PacketData
- {
- public Int16 DataSize;
- public Int16 PacketID;
- public SByte Type;
- public byte[] BodyData;
- }
-
+{
public struct MsgPackPacketHeadInfo
{
const int PacketHeaderMsgPackStartPos = 3;
diff --git a/Tutorials/PvPGameServer_Client/Program.cs b/Tutorials/PvPGameServer_Client/Program.cs
index a98e647..7b9e1ff 100644
--- a/Tutorials/PvPGameServer_Client/Program.cs
+++ b/Tutorials/PvPGameServer_Client/Program.cs
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace csharp_test_client
{
+ [SupportedOSPlatform("windows10.0.177630")]
static class Program
{
///
diff --git a/Tutorials/PvPGameServer_Client/csharp_test_client.csproj b/Tutorials/PvPGameServer_Client/csharp_test_client.csproj
index 7a62f55..88ff92e 100644
--- a/Tutorials/PvPGameServer_Client/csharp_test_client.csproj
+++ b/Tutorials/PvPGameServer_Client/csharp_test_client.csproj
@@ -1,6 +1,6 @@
- net8.0-windows
+ net8.0-windows10.0.17763.0
WinExe
false
true
diff --git a/Tutorials/PvPGameServer_Client/mainForm.cs b/Tutorials/PvPGameServer_Client/mainForm.cs
index 7ecdd32..d67e252 100644
--- a/Tutorials/PvPGameServer_Client/mainForm.cs
+++ b/Tutorials/PvPGameServer_Client/mainForm.cs
@@ -5,12 +5,15 @@
using System.Data;
using System.Drawing;
using System.Linq;
+using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;
+using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;
namespace csharp_test_client
{
+ [SupportedOSPlatform("windows10.0.177630")]
public partial class mainForm : Form
{
ClientSimpleTcp Network = new ClientSimpleTcp();
@@ -94,6 +97,7 @@ private void btnDisconnect_Click(object sender, EventArgs e)
Network.Close();
}
+ // 에코
private void button1_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(textSendText.Text))
@@ -103,8 +107,10 @@ private void button1_Click(object sender, EventArgs e)
}
var body = Encoding.UTF8.GetBytes(textSendText.Text);
-
- PostSendPacket(PACKET_ID.PACKET_ID_ECHO, body);
+ var packetData = new byte[body.Length + MsgPackPacketHeadInfo.HeadSize];
+ Buffer.BlockCopy(body, 0, packetData, MsgPackPacketHeadInfo.HeadSize, body.Length);
+
+ PostSendPacket(PACKET_ID.PACKET_ID_ECHO, packetData);
DevLog.Write($"Echo 요청: {textSendText.Text}, {body.Length}");
}