עבור לתוכן

התייעצות לגבי ניהול נכון של סרבר עם הרבה קליינטים ב C#

Featured Replies

פורסם

שלום אנשים טובים.

אני בונה פרוייקט ב C# של שרת מול הרבה קליינטים, כבר התחלתי לבנות אותו, ובגלל שלא חשבתי על דברים לעומק בהתחלה התחלתי קצת להתברבר.

מה שעשיתי עד עכשיו הולך ככה:

הקמתי סרבר TCP שכל הזמן מאזין עד שקליינט מתחבר ושולח איזשהי פקודה, מבצע אותה ומחזיר תשובה בחזרה לקליינט.

כאשר הקליינט שולח פקודה, הוא מאזין בחזרה ל NETWORKSTREAM של החיבור שנוצר וממתין לתשובה מהסרבר.

כרגע כל פקודה שנשלחת מהקליינט מבוצעת ע"י אובייקט TCPCLIENT שנוצר כל פעם בעת לחיצת על כפתור או משהו דומה, מה שגורם לסרבר כל פעם ליצור NETWORKSTREAM חדש אחרי שהקליינט שולח פקודה. עד עכשיו זה היה בסדר מבחינתי, אבל עכשיו אני רוצה שהסרבר גם ייזום שליחת פקודות לקליינטים ללא שום קשר אם הקליינט שלח פקודה (מה שמאפשר לי לתפוס את ה NETWORKSTREAM שלו).

כאן בעצם מתחילה הבעיה, מכיוון שלאחר כל פקודה שהקליינט שולח והסרבר מבצע, החיבור בעצם "נמחק" כי ככה הסרבר בנוי. וגם אם אני מנסה לשמור את ה NETWORKSTREAM בצד השרת, הוא כל פעם משתנה לאחר כל פקודה שהקליינט שולח.

אני מבין שהדרך היחידה שלי שהסרבר יזום תקשורת ללקוח, היא קודם כל ע"י שינוי בקליינט, כך שלא ייצור כל פעם TCPCLIENT חדש, אלה רק אחד, ומאותו רגע שהוא מתחבר והלאה, יש צורך להמשיך עם אותו אובייקט (עם אותו NETWORKSTREAM) בשביל לשלוח פקודות לשרת.

הדילמה העיקרית שלי היא בצד השרת. הוא אמור לקבל פקודות ממאות משתמשים, עד עכשיו היה ת'רד שרץ ברקע ומאזין לפקודות שמגיעות מכל IP.

איך אני יכול לגרום לו לשמור על כל NETWORKSTREAM של כל משתמש שמתחבר אליו? האם להגדיר מן מערך שכזה שבו אני שומר את הפרטים של כל יוזר וגם את ה NETWORKSTREAM שנוצר בינהם? ואם כן, עבור כל יוזר כזה אני צריך להריץ ת'רד שמאזין לנתונים חדשים שמגיעים מאותו STREAM?? הרי זה מסתכם בהמון המון ת'רדים שרצים במקבלים ומאזינים כמו חמורים בלולאות. האם זה פתרון יעיל?

אגב, אני לא מעוניין בשמירת IP של כל יוזר, ושכל יוזר יאזין לפקודות בפורט שונה, כי אז אני אצטרך לפתוח פורטים לכל קליינט. זו הסיבה שאני רוצה לתפוס את ה NETWORKSTREAM כי ככה המידע עובר לקליינט ללא בעיה.

אשמח לעצות, ממש נתקעתי פה.

(וכן, אני יודע שזה לא מעשה חכם לכתוב סרבר ב C#, אבל זו השפה הכי ידידותית שאני מכיר. ממש אין לי כוח ל C++)

אגב אם זה מעניין מישהו, הפרויקט הוא משחק פוקר ברשת. פרוייקט גמר הנדסאי תוכנה.

אם הקוד עוזר למישהו להבין קצת יותר טוב איך השרת עובד כרגע אז הנה

הקליינט פחות קריטי כי זה לא בעיה לסדר:

ההאזנה לפורט:

private void ListenForClients()
{

while (true)
{
//blocks until a client has connected to the server
TcpClient client = this.tcpListener.AcceptTcpClient();
client.ReceiveTimeout = 10000;
client.SendTimeout = 10000;


//create a thread to handle communication
//with connected client
Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClientComm));
//clientThread.SetApartmentState(ApartmentState.STA);
clientThread.Start(client);
}
}

הטיפול במידע שהגיע ברגע שקליינט שלח פקודה:

private void HandleClientComm(object client)
{
const int BUFF_MAX = 1000;//1 KB
TcpClient tcpClient = (TcpClient)client;
tcpClient.ReceiveBufferSize = BUFF_MAX;

string clientIPAddress = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address.ToString();

NetworkStream clientStream = tcpClient.GetStream();
byte[] message = new byte[BUFF_MAX];
int bytesRead = 0; ;
byte HeaderLength;
byte command;

while (true)
{
bytesRead = 0;
try //blocks until a client sends a message
{
//Parse package
HeaderLength = (byte)clientStream.ReadByte();
command = (byte)clientStream.ReadByte();
clientStream.Read(message, 0, BUFF_MAX);
ExcecuteCommand(tcpClient, command, message);
}
catch //a socket error has occured
{
break;
}
if (bytesRead == 0) //the client has disconnected from the server
break;
}
}

פורסם

קודם כל, לכתוב סרבר ב-C# יכול להיות דווקא כן חכם במקרים מסויימים, היות ותכנות ב-C# מייצר קוד שהוא קל יותר לתחזוקה ואמין יותר. אם במקרה שלך אתה מטפל רק במאות משתמשים בו"ז אז אולי אפילו לא תרגיש בהבדלי הביצועים בין C ל-C#.

בנוגע ל-Thread-ים: לפתוח Thread חדש עבור כל משתמש זה רעיון שהוא פשוט יחסית, אבל הוא לא טוב כשמדובר על מאות משתמשים. Thread-ים הם מצרכים יקרים של מערכת ההפעלה, מה שאומר ששימוש מוגזם בהם יגרום לבעיות בין אם תכתוב ב-C או ב-C#. המאמר הזה:

http://www.codeproject.com/KB/IP/single_threaded_nhttpd.aspx

מסביר בצורה די יפה איך לעבוד עם כמה Socket-ים במקביל ב-Thread אחד.

לגבי חיבור יזום ללקוח - כדי לאפשר לסרבר "להודיע" משהו ללקוח האפשרויות שלך הן:

1. במידה ואתה נמצא בסביבת רשת לוקאלית אתה יכול להשתמש ב-UDP. הקליינט יכול להאזין בעצמו על פורט UDP ולהודיע לשרת מה מספרו. השרת יזכור מה הכתובת של הלקוח וייזום שליחת הודעה לסוקט ה-UDP שלו כשיש לו משהו לשלוח. ככה אתה לא צריך להחזיק חיבור TCP פתוח. הבעיה היא שבאינטרנט, כאשר בדרך כלל אתה מחובר דרך NAT שלא מאפשר כניסת חיבורים נכנסים, זה לא יעבוד.

2. להשאיר את חיבור ה-TCP פתוח כל עוד הלקוח רוצה לקבל הודעות מהשרת.

בכל אחד מהפתרונות אתה כן תצטרך להחזיק בזיכרון מחלקה שתשמור את המצב של כל אחד מהלקוחות.

בהצלחה

פורסם
  • מחבר

קודם כל תודה על ההסבר המפורט!

עברתי על המאמר שצרפת. אמנם הוא נוגע בנושא של שרת HTTP, אבל אחרי קריאה מעמיקה יותר גיליתי את קסם ה NON BLOCKING SOCKET והפונקציה ()Socket.Select, אני מניח שזו הדרך הכי טובה ליישם את השרת שלי.

אגב לא הבנתי בשיט את כל הנושא של ה IMultiplexed... אבל אם הבנתי נכון, זה לא הכרחי לצרכים שלי, או שאני טועה? מספיק לי רק הת'רד שמריץ את ה NON BLOCKING SOCKET

UDP לא פותר לי את הבעיה, מאותה סיבה שאני רוצה לאפשר גם תקשורת מאחורי NAT, אני מניח שהצעת UDP במידה ואין משתמשים מאחורי NAT מהסיבה שהפרוטוקול יותר מהיר מ TCP, וב LAN הסיכוי לשגיאות/אי קבלת חבילה קטן בהרבה מתשדורת החוצה...

המון תודה לא הייתי מוצא את הקסם הזה בלעדיך לא חשבתי אפילו שיש משהו בכיוון הזה!!

אני מבין שהרעיון הוא, לדחוף כל SOCKET שנוצר ל ARRAYLIST, ואז לקרוא ל ()SELECT, שבעצם מנפה לי מהרשימה את כל הסוקטים שלא התקבלו בהם נתונים חדשים, ואני מגדיר לה מן TIMEOUT ב MICROSECONDS שתבדוק אם לכל סוקט הגיעו נתונים?

האם הקליינט אמור לשלוח את הנתונים במסגרת הזמן ש ()SELECT רצה? או שהנתונים יגיעו לסוקטים בכל מקרה כל עוד אני שומר על ההפניה שלהם באיזשהו מקום?

המון תודה בינתיים זה נתן לי אחלה כיוון.

פורסם

אכן, השימוש בSELECT הוא שימוש נפוץ מאוד ומתאים למקרה שלך.

אני ממליץ לך לקרוא גם על תכנות אסנכרוני. זה פתרון שונה מהשימוש ב- select, ולעיתים הוא פשוט יותר לכתיבה (תלוי במבנה הקוד שלך ובדברים שאתה מעוניין לבצע בסופו של דבר).

פורסם
  • מחבר

תודה.

שאלה קטנה, האם SELECT מוחקת את כל ה SOCKETS הריקים שלא התקבל בהם מידע??אם כן, איך אני שומר על SOCKET פתוח בצד השרת מול הלקוח? אני מקווה שהבנתי את זה לא נכון...

פורסם

מטרת הפונקציה select היא להודיע לך באילו סוקטים אתה יכול להשתמש לקריאה/כתיבה. היא לא סוגרת לך סוקטים או מבצעת שליחה או קבלה בעצמה.

פורסם
  • מחבר

אוקיי שוב תודה לשניכם אני אכנס לזה ברגע שיתפנה לי זמן.

פורסם

יצא לי לכתוב בדיוק מאמר וספרייה שתבצע את זה בשבילך (כולל קוד המקור),

אם תירצה תוכל להשתמש בספרייה עצמה או להבין את הרעיון הכללי:

http://www.codeproject.com/KB/IP/NetComm.aspx

מקווה שהמאמר יעזור, בעזרת הספרייה הזו יצרתי תיקשורת בין 5 מחשבים (2 ששולטים על 2 רובוטים מרחוק

ושרת)

בהצלחה...!

פורסם
  • מחבר

תודה רבה, אני עיינתי בזה בקצרה, זה בערך הרעיון שאני רציתי לממש.

רק צריך לשלב את זה עם ה NON BLOCKING ACCEPT.

פעם ראשונה שאני קורא קוד ב VB... אלוהים ישמור...אם אני מבין נכון, הסרבר עובד ככה:

מאזין לפורט 2020 (משהו כזה)

ברגע שנוצר חיבור עם קליינט, הוא בודק אם ה ID שלו כבר קיים, במידה וכן הוא משרשר לו מספר לסוף (האופרטור & משרשר מחרוזות ב VB?)

לאחר שנקבע ה ID, אתה מריץ ת'רד עבור כל קליינט ושם מאזין לסוקט שלו, ודחוף אותו לרשימה של קליינטים מחוברים.

אם כן זה מה שאני חשבתי לעשות, אבל אחרי עצות פה הבנתי שלמקרה שלי זה לא יהיה יעיל, כי הסרבר שלי לא אמור לעבוד מול מספר קטן של קליינטים, דבר שייצור לי מאות ת'רדים...

האו שבאמת נכשלתי בפעם הראשונה לקרוא קוד ב VB...?

פורסם

מצטער על התגובה האיטית (חזרתי מהצבא והיה לי המון דברים לעשות).

VB זה אחד מהדברים לדעתי ;)

ואכן צדקת, הוא משרשר לו מספר לסוף. וכפי שציינת לכול קליינט חדש אני מוסיף Thread חדש.

למה שייצור מאות ת'רדים עם מספר קטן של משתמשים? היחס בין מספר הת'רדים למשתמשים הוא יחס ליניארי.

בנוסף, בכדי שהמשתמשים יוכלו להעביר הודעות האחד מן השני יצרתי פונקציה בשרת שקוראת את המידע שנשלח מכול משתמשים

ובודקת למי הוא מיועד ומעבירה אותו בהתאם.

התקשורת הזו עבדה על פרוטוקול TCP (כפי שאתה כבר יודע) והייתה צריכה להיות מהירה ביותר (בגלל שזה קרב רובוטים

שאנו צריכים להעביר פקודות של תזוזה במהירות). התקשורת עבדה בצורה חלקה ולעיתים רחוקות ביותר היו בעיות.

ייעלתי את התקשורת באמצעות כמה שינויים בהגדרות כפי שכבר ציינתי במאמר.

בהצלחה! ;)

פורסם
  • מחבר
למה שייצור מאות ת'רדים עם מספר קטן של משתמשים?

כי זה משחק פוקר ONLINE שאני רוצה שיהיה מסוגל גם להריץ מאות שחקנים. (לא שזה יקרה אבל אני רוצה שזה יהיה בנוי ככה).

UNSIGNEDINTEGER, בדקתי את הפונקציה SELECT, ואכן היא מוחקת מהרשימה את כל הסוקטים שאין בהם נתונים חדשים....

האם אתה חושב שכדאי לעשות 2 רשימות לסוקטים וכל פעם להעתיק את הרשימה לפני שאני קורא ל SELECT? (בוא נגיד שהרשימה תכיל 500 סוקטים), זה הולך לרוץ בלולאה אין סופית כל הקטע הזה, הנה דוגמא של הקוד, משהו שמצאתי באינטרנט שאני מנסה להלביש


_Server.Bind(Ipep);
_Server.Listen(100); //MAX pending connections
Console.WriteLine("Listening on port 3000...");
while (true)
{
if (_Server.Poll(0, SelectMode.SelectRead)) //check if new socket connected
{
Socket client1 = _Server.Accept();
int i = sockList.Add(client1); //save new socket

Console.WriteLine("Monitoring {0} sockets...", sockList.Count);
}
copyList = new ArrayList(sockList); //copy users socket list before calling select()
if (copyList.Count>0)
{
Socket.Select(copyList, null, null, 10); //filter sockets which has new incomming data
foreach (Socket client in copyList) //DATA HANDLING
{
//read each socket and do something with data...
}
}
}

ארכיון

דיון זה הועבר לארכיון ולא ניתן להוסיף בו תגובות חדשות.

דיונים חדשים