צד השרת
תוכנת השרת שבה נעשה שימוש במדריך זה מאזינה ללקוח בפורט 2021. ברגע שלקוח התחבר היא מתחילה להאזין להודעות שלו, וברגע שהוא מתנתק היא חוזרת להתחלה ומאזינה ללקוח הבא שיתחבר אחריו. כרגע הצ'אט שלנו תומך רק בלקוח אחד מחובר.
תצלום השרת
אנו לא נקרא את צד השרת והלקוח על כלל שורותיו, אלא נעבור על החלקים החשובים בלבד. צד השרת מכיל את המשתנים הבאים:
TcpListener tcpListener; //Allows us to open a socket to listen any incoming message
bool listening = false; //True if the server is currently listening
justify;line-height:normal;text-autospace:none;direction:ltr;unicode-bidi:embed">TcpClient tcpClient; //The TCP client that will connect
tcpListener – המשתנה החשוב ביותר: באמצעותו אנו יכלים להאזין ללקוחות אשר יתחברו. הוא מאפשר לנו לפתוח את הסוקט ולהאזין.
listening – משתנה בוליאני. ערכו אמת אם אנו כרגע מאזינים ללקוחות.
tcpClient – כאשר משתמש מתחבר אובייקט ה-tcpListener מחזיר לנו אובייקט TcpClient אשר מאפשר לנו להתייחס ללקוח שהתחבר. באמצעות אובייקט זה אנו יכלים להאזין להודעות שהלקוח שולח וגם לשלוח לו הודעות בחזרה.
ראשית נעבור על חלק הקוד הבא:
/* Because we are using Multi-threading we need to be
* able to update the output textbox (chat area). We are using
* the InvokeRequired method – this prevents the cross thread access exception.
*/
private delegate void AddToOutput_Delegate(
left;line-height:normal;text-autospace:none;
direction:ltr;unicode-bidi:embed">private void AddToOutput(string str)
direction:ltr;unicode-bidi:embed">private void AddToOutput(string str)
{
8pt;color:blue">if (InvokeRequired)Invoke(new AddToOutput_Delegate(AddToOutput), str);
color:blue">return;
8pt;">}
left;text-indent:36.0pt;line-height:normal;text-autospace:none;direction:ltr;unicode-bidi:embed"> (!this.IsDisposed) //If our form still accessable (not closed)
Output_TextBox.AppendText(DateTime.Now.ToString() + ": " + str + "n");
}
בדומה לכל צ'אט, הצ'אט שלנו מכיל תיבת טקסט עיקרית שבה מופיעה כל השיחה, ולתיבה זו קראנו "Output_TextBox". באמצעות המתודה AddToOutput נעדכן את תיבה זו. אבל למה מתודה זו נראת מורכבת יותר מן הרגיל? הסיבה לכך היא מספר ההליכים שניגשים אליה. ההליך העיקרי אשר מאזין להודעות שהלקוח שולח הינו הליך נפרד מן ההליך הראשי, ברגע שהוא קורא לפונקציה זו הוא יגיע לתנאי הראשון. התנאי הראשון בודק אם ההליך שקרא לפונקציה הוא ההליך הראשי, והתנאי יחזיר אמת ברגע שההליך אינו ההליך הראשי. לכן כאשר הליך קבלת ההודעות יקרא לפונקציה זו הוא יספיק לבצע רק את שתי הפקודות הבאות:
Invoke(new AddToOutput_Delegate(AddToOutput), str);
return;
הפקודה הראשונה אומרת להליך הראשי לבצע את אותה הפונקציה עם אותו הפרמטר, כך לא תתרחש שום שגיאה. הפקודה השנייה יוצאת באופן מיידי מן הפונקציה וכך ההליך הנפרד לא מבצע שום קוד הקשור בממשק המשתמש, ומונע משגיאות להתקיים. המתודה MainForm_Load מאפשרת לנו לפתוח את השרת ולהתחיל להאזין ללקוחות בפורט 2021 ברגע שהחלון שלנו עולה:
/* Starts listening on a specific socket.
* Multi-threading is used to keep the UI responsive.
*/
private void MainForm_Load(object sender, EventArgs e)
{
8.0pt;">AddToOutput("Server loaded.");text-autospace:none;direction:ltr;unicode-bidi:embed">true; //We are starting to listen 8.0pt;color:blue">new Thread(new ThreadStart(Listen_Thread)).Start(); //Starts listening on a seperate thread 8.0pt;">AddToOutput("Started listening…");
}
ראשית אנו יוצרים אובייקט TcpListener שמאזין בפורט 2021 ומתחילים את ההאזנה. המשתנה listening הופך לאמת (השרת התחיל להאזין) והליך חדש נפתח להאזנה.
new Thread(new ThreadStart(Listen_Thread)).Start(); //Starts listening on a seperate thread
כפי שניתן לראות, שורה זו יוצרת הליך חדש. ההליך מתבצע במתודה Listen_Thread, המתודה מכילה את הקוד הבא:
/* This is where all of the background work is done, allows the UI stay
* responsive and not get stuck in a specific line or a loop.
* With network enabled application multi-threading is the basic and most
* important part of the application.
*/
private void Listen_Thread()
{
8.0pt;color:blue">while (listening){
tcpClient = tcpListener.AcceptTcpClient();
left;line-height:normal;text-autospace:none;direction:ltr;unicode-bidi:embed"> AddToOutput("Client connected");
try //Listen to the connected client, if an error occurs, stop listening
{
while (tcpClient.Connected)
{
NetworkStream netStream = tcpClient.GetStream();
left;line-height:normal;text-autospace:none;direction:ltr;unicode-bidi:embed"> byte[] receivedBytes = new byte[tcpClient.ReceiveBufferSize]; left;line-height:normal;text-autospace:none;
direction:ltr;unicode-bidi:embed"> if (netStream.Read(receivedBytes, 0, receivedBytes.Length) != 0)
{
//Starts analyzing the data received
left;line-height:normal;text-autospace:none;direction:ltr;unicode-bidi:embed"> int i = 0; i <= receivedBytes.Length; i++)
{
if (i + ChatMessage.MinimumBytes >= receivedBytes.Length) //Message is not long enough to be read
left;line-height:normal;text-autospace:none;
direction:ltr;unicode-bidi:embed">
direction:ltr;unicode-bidi:embed">
ChatMessage cMessage = ChatMessage.FromBytes(receivedBytes, i); //Converts the byte array into a ChatMessage object again
left;line-height:normal;text-autospace:none;direction:ltr;unicode-bidi:embed"> if (cMessage.IsMessageValid)
{
AddToOutput(cMessage.NickName + ": " + cMessage.Message); //Updates the UI with the message received
left;line-height:normal;text-autospace:none;direction:ltr;unicode-bidi:embed"> //Increase the offset, skip the message bytes we just analyzed
i += cMessage.BytesCount – 1;
}
left;line-height:normal;text-autospace:none;direction:ltr;unicode-bidi:embed"> } left;line-height:normal;text-autospace:none;
direction:ltr;unicode-bidi:embed"> }
else //Client got disconnected
break;
left;line-height:normal;text-autospace:none;direction:ltr;unicode-bidi:embed"> }
}
catch (Exception ex)
{
}
left;line-height:normal;text-autospace:none;direction:ltr;unicode-bidi:embed"> tcpClient.Close();
tcpClient = null; //Empties the tcpClient object – we no longer need it
AddToOutput("Client disconnected"); //Client go disconnected, listen to other client
}
catch (Exception ex)
{
//An error had occured, probably TcpListener stopped listening using the Stop() method
listening = false;
}
left;text-indent:36.0pt;line-height:normal;text-autospace:none;direction:ltr;unicode-bidi:embed"> left;line-height:normal;text-autospace:none;
direction:ltr;unicode-bidi:embed">}
הליך זה הוא ההליך החשוב ביותר בצד השרת. נתאר את מה שההליך מבצע בנקודות:
- נאזין ללקוח שיתחבר, ההליך שלנו יתקע בשורה הבאה עד שהלקוח יתחבר:
- לאחר שהלקוח התחבר ניצור לולאה שתאזין ללקוח עד שהקשר יתנתק:
- בשלב זה ננתח את כל המידע שהתקבל מן הלקוח. באמצעות לולאת For נעבור על כך מערך הבייטים של ההודעה שהתקבלה:
- ברגע שהמשתמש התנתק (כשבשלב 2 קיבלנו 0 בייטים מהפונקציה netStream.Read) נסגור את השיחה שלנו באמצעות המתודה tcpClient.Close ונרוקן את האובייקט tcpClient. נעדכן את ממשק המשתמש בהודעה "Client Disconnected" ונחזור לשלב 1 כל עוד אנחנו מאזינים.
tcpClient = tcpListener.AcceptTcpClient();
ברגע שלקוח יתחבר נעדכן את הממשק ("Client Connected") וכך ניידע את המשתמש שלקוח יתחבר. ערכו של tcpClient יהיה הלקוח המרוחק שיתחבר.
while (tcpClient.Connected) { …
בתוך לולאה זו נגיד להליך לבדוק אם הגיעה הודעה חדשה. ניצור מערך של בייטים אשר יכיל את המידע שה-tcpClient שלנו רוצה לשלוח (או בצורה יותר ברורה: המידע שהשרת צריך לקבל מהלקוח). נשתמש בפונקציה netStream.Read בכדי לקרוא את המידע שהתקבל מן הלקוח. פונקציה זו תחזיר לנו את מספר הבייטים שהתקבלו מן הלקוח, ואם מספר זה שווה ל-0 אנו יודעים שהלקוח התנתק, לכן נצא מתוך הלולאה ונעבור לשלב 4. אם המידע שהתקבל גדול מ-0 נעבור לשלב שלוש.
for (int i = 0; i <= receivedBytes.Length; i++)
{
8.0pt;color:blue">if (i + ChatMessage.MinimumBytes >= receivedBytes.Length) //Message is not long enough to be readtext-autospace:none;direction:ltr;unicode-bidi:embed"> cMessage = ChatMessage.FromBytes(receivedBytes, i); //Converts the byte array into a ChatMessage object again left;text-indent:36.0pt;line-height:normal;
text-autospace:none;direction:ltr;unicode-bidi:embed"> (cMessage.IsMessageValid) 8.0pt;">{
AddToOutput(cMessage.NickName + ": " + cMessage.Message); //Updates the UI with the message received
margin-left:36.0pt;margin-bottom:.0001pt;text-align:left;text-indent:36.0pt;line-height:normal;text-autospace:none;direction:
ltr;unicode-bidi:embed"> margin-left:36.0pt;margin-bottom:.0001pt;text-align:left;text-indent:36.0pt;
line-height:normal;text-autospace:none;direction:
ltr;unicode-bidi:embed">i += cMessage.BytesCount – 1; 8.0pt;">} left;line-height:normal;text-autospace:none;
direction:ltr;unicode-bidi:embed">}
נשתמש בפונקציות שיצרנו במחלקת ה-ChatMessage בכדי לפענח את המידע שהתקבל. ראשית נבדוק אם המערך שלנו מכיל מספיק בייטים מהאינדקס (i) שציינו, כך שלא נצא מתוך תחום המערך ותתקיים שגיאה. כמות הבייטים המינימלית שווה ל-MinimumBytes (קבוע שארכו הוא 8), כך שההודעה חייבת להכיל את המספר המינימלי הזה. לאחר שהבדיקה עברה בהצלחה ויש לנו מספיק בייטים לנתח, נשלוף את אובייקט ה-ChatMessage מתוך מערך הבייטים באמצעות האינדקס הנוכחי שלנו והפונקציה ChatMessage.FromBytes. לאחר מכן נבדוק אם ההודעה תקפה באמצעות הפונקציה IsMessageValid, במידה וכן קיבלנו הודעה תקפה (שמכילה תוכן) נוסיף את ההודעה לממשק המשתמש (לתיבת הצ'אט) וכך נעדכן את המשתמש. כעת נפסח על כל הבייטים שכרגע ניתחנו על ידי העלאת משתנה האינדקס (i) במספר הבייטים שכבר ניתחנו.
עד כה דיברנו רק על קבלת מידע מהלקוח. אם כן, כיצד אנו שולחים מידע? פשוט מאוד:
private void Send_Button_Click(object sender, EventArgs e)
{
ChatMessage cMessage = new ChatMessage("Server", Input_TextBox.Text);
8pt;color:blue">if (tcpClient != null)tcpClient.GetStream().Write(cMessage.GetBytes(), 0, cMessage.BytesCount); //Turns the ChatMessage object into a byte array and sends it out to the remote client
left;text-indent:36.0pt;line-height:normal;text-autospace:none;direction:ltr;unicode-bidi:embed">": " + cMessage.Message); //Updating the UI left;text-indent:36.0pt;line-height:normal;
text-autospace:none;direction:ltr;unicode-bidi:embed"> left;text-indent:36.0pt;line-height:normal;
text-autospace:none;direction:ltr;unicode-bidi:embed"> left;text-indent:36.0pt;line-height:normal;
text-autospace:none;direction:ltr;unicode-bidi:embed">false;
}
- יוצרים אובייקט ChatMessage חדש אשר מכיל את הכינוי של השולח (במקרה שלנו השם הוא Server"") ואת תוכן ההודעה (אשר אנו לוקחים מתוך תיבת הטקסט).
- אם הלקוח מחובר (ערכו של האובייקט אינו null), נשיג את ה-NetworkStream שלו ונכתוב אליו את מערך הבייטים של אובייקט ה-ChatMessage. נעשה זאת באמצעות המתודה tcpClient.GetStream().Write. הפרמטר הראשון יהיה מערך הבייטים שנרצה לשלוח, המיקום שבו ההודעה מתחילה (תמיד יהיה שווה ל-0) וכמות הבייטים שנשלח.
- נעדכן את תיבת הצ'אט עם ההודעה שנשלחה.
כאשר אנו סוגרים את התוכנה עלינו גם לסגור את החיבור לשרת וכל תקשורת אחרת שפנויה. אנו נעשה זאת באופן הבא:
/* Stops listening for any clients and closes the form
* Always close any open sockets when application is about to close.
*/
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
listening = false; //Allows the listen thread end
if (tcpClient != null) tcpClient.Close(); //Closes the client socket
tcpListener.Stop(); //Closes the socket, stops listening
}
כעת סגרנו כל תקשורת קיימת והתוכנה תסגר.
הקישור לזיכרון RAM לא נכון
זה לא נכון לקשר מכאן לזכרון RAM כי זה לא הנושא.
מדובר כאן על זכרון של תהליכים, לכל תהליך מוקצה אזור זכרון ואת אותו זכרון הוא לא יכול לחלוק עם תהליכים אחרים.
אין שום טעם לקשר את זה לזכרון RAM למרות שהזכרון הזה נמצא שם.
חוץ מזה מדריך מעולה!!
אתם אתר נפלא שתמיד מחכים אותי.
כתבה מעולה…
תודה רבה קודם כל, אפשר ללמוד המון.
אפשר לשפר בכך שתפרט מעט יותר על כל מיני מושגים שמבחינתך הם בסיסיים אך מבחינת אנשים שאתה בא ללמד הם ממש לא.
בכל אופן ישר כח ותמשיך כך.מצוין!
למה בדוד-מת?
הוא מת כבר לפני שנים.
לא רלוונטי
ב-.NET יש תשתית WCF שהיא תשתית ה-communication של ה-Framework.
לעבוד ישירות מול המחלקות המתוארות זה חסר טעם ולא נכון מבחינת ארכיטקטורה.
התשתית שהיא עשירה לאין ערוך ממה שתואר כאן, מאפשרת העברת נתונים באופן נוח ו"שקוף" ללקוחות המאזינים ולקבוע את צורת התקשורת (Named Pipes, וכיו"ב) בקונפיגרציה.
היא כמובן גם מאפשרת להתמודד עם תרחישים פשוטים ועד תרחישים מסובכים הרבה יותר בהם לדוגמה הנתונים נאספים ממספר מערכות בתהליך טרנזאקטיבי או תרחישים אחרים.
תודה רבה
מדריך מעולה ,נפל עלי כיאלו הזמנתי אותו אישית
בינוני.
מצטער שזה יוצא כ"כ תקיף, אבל הקוד שנכתב הוא ברמה בינונית מאוד.
כל העטיפה, השימוש בThreadים בצורה לא נכונה, עבודה מול UI בצורה לא נכונה, חוסר עבודה בקונבנציות…
ד"א, לבחור שכתב שכדאי לכתוב את זה בWCF – אתה טועה. WCF מתאים לשירותים (SOA במיוחד) ולא לתקשורת כזו. זה סתם overhead לא הכרחי.
סוגרים על פרוטוקול ושולחים בraw sockets כמו שהבחור עשה… אלא אם יש לך שרת מרכזי שמולו אתה עובד (כמו שlive messenger עובד) ואז הארכיטקטורה שלך צריכה להשתנות ואולי WCF יותר יתאים.
בכל מקרה- בחור, רצון יש אבל ידע לוקה בחסר.
6 -קצת צניעות לא תזיק וגם לא ידע
WCF לא קשורה בהכרח ל- SOA למרות שהיא דרך אופרטיבית אידיאלית להשיג SOA.
WCF זו תשתית התקשורת של ה-FRAMEWORK והיא מטפלת בהעברת נתונים באשר היא.
מבחינת ארכיטקטורה הרצון שלך הוא להגדיר מה אתה רוצה להעביר בצורה פשוטה בקוד (גישת AOP שבה עושה שימוש WCF)ולקבוע בקונפיגרציה האם אתה רוצה העברה פשוטה כמו NAMED PIPES או TCP או משהו כמו HTTP או כל פרוטוקול אחר בהתאם לצורך ול-HOST.
אתה יכול לעבור מצורת העברה אחת לאחרת ללא שינוי קוד.
לעיתים בכלל לא מדובר על העברת נתונים בין מכונות אלא על העברה פשוטה בין אפליקציות. זה בכלל לא משנה.
תכנות נכון הוא להבטיח את המחר, את היכולת לשנות ולגדול בלי שכתובים מהותיים.
במקרה שלך, אתה מציע לכתוב קוד "בסיסי" (שהוא אגב הרבה יותר מורכב מ-WCF) ומחר כשהצורך משתנה או גדל להחליף את כולו.
אני מצטער, אבל גם לך יש קצת שעורי בית להשלים.
מיושן – יש היום WCF
בשביל ליצור תקשורות ב
.NET
לא משתשמים בדרך הפרמטיבית הזאת עם על הכבוד , יש טכנולוגיה שנקראת
WCF ( שמחליפה את הדרך שאתה מציג)
שאתה כותב קוד קצר ומאחורי הקלעים עולם ומלאו(שמכיל את הדרך הזאת) כולל אבטחה ( RSA גם). ניתן לבחור בשלל אפשריות מתוך ה
WIZARD
אבטחה – סוג תקשורות TCP UDP WSHTTPS ועוד הרבה .
מטרת המדריך
שלום, מטרת המדריך אינה להציג למשתמשים את הדרך הפשוטה והקלה ביותר ליצירת תקשורת בסביבה זו. המטרה היא לא להעביר את המידע במספר השורות הקצר ביותר או בדרך הקלה ביותר. מטרת המדריך היא ללמד את המשתמשים ליצור תקשורת ברמה נמוכה יותר תוך כדי שהם לומדים כיצד חבילות המידע נשלחות ומה קורה בעצם ברקע. במדריך המשתמשים לומדים כיצד להשתמש ב-Multi-Threading ובקבצי DLL. אומנם הדרך שבה המידע הועבר בין ההליכים אינה הדרך הטובה ביותר ולא הכי נכונה אך היא המובנת ביותר. כמובן ניתן היה למשל להשתמש במחלקת ה-SynchronizationContext בכדי להעביר מידע בין ההליכים בצורה נכונה יותר.
למי שמגיב נגד, ולכותב המדריך
אני לא ראיתי אף מדריך שמסביר זאת
מי שחושב שהמדריך לא טוב, שיכתוב מדריך בבקשה.
אני כרגע לומד את זה ואני אומר תודה רבה למי שכתב את המדריך.
אני מקווה שיעזור לי.