יום שישי, 4 במאי 2018

ניהול תקשורת בין משתמשים ללא "צד שרת"

תקשורת ללא "צד-שרת"



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



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



לצורך כך ניצור בבסיס הנתונים טבלה בשם "PlayersWaitingForGame":

 



ה-ID יהיה כמובן PK ו-Identity.
ה-PlayerID מקושר בקשר גומלין לטבלת Players.
ה-SecondPlayerID מקושר גם הוא בקשר גומלין לטבלת Players.





הרעיון:
שני אנשים שמחפשים יריב צריכים למצוא אחד את השני.

1. שחקן_א (PlayerID=34):
בודק ומגלה שבטבלת PlayersWaitingForGame אין אף שחקן שמחכה שישחקו איתו.
(או שהטבלה ריקה או שהיא לא ריקה אבל כל הרשומות בה הן InConnectionAttempt=1, על כך נסביר בהמשך).
מאחר ואין לו יריב זמין הוא מוסיף את עצמו לטבלה, וממתין (בודק כל פרק זמן קצר) שמישהו יענה לבקשה שלו.

כך נראית כעת הטבלה:


2. שחקן_ב (PlayerID=71):
בודק ומגלה שבטבלת PlayersWaitingForGame יש שחקן בעל PlayerID=34 שמחכה שישחקו איתו.
הוא מוסיף את עצמו בתור יריב פוטנציאלי בעמודת SecondPlayerID ומסמן שהמצב כרגע הוא "InConnectionAttempt" כדי שאף אחד לא יפריע לשניהם.
השחקן ממתין שהיריב יבחין בנעשה.

כך נראית כעת הטבלה:


3. שחקן_א (PlayerID=34):
בודק ומגלה שבטבלת PlayersWaitingForGame שחקן PlayerID=71 נענה לבקשה שלו .
מאחר ושני הצדדים למשחק יודעים כעת את ה-ID של היריב שלהם, הוא מוחק את הרשומה מהטבלה.

כך נראית כעת הטבלה:


4. שחקן_ב (PlayerID=71):
בודק ומגלה שבטבלת PlayersWaitingForGame הרשומה שפתח שחקן_א נמחקה.
הנה מה טוב ומה נעים הוא מתחיל לשחק עם יריבו!





וכך שני שחקנים מצאו יריב למשחק: הם לחצו על "מצא לי שחקן יריב", השיגו ID של שחקן יריב ואיתו הם יכולים להתחיל לשחק.




















הקוד שדואג לארבעת השלבים:

חשוב להבין שאנחנו כותבים קוד שלא יודע מראש האם אנחנו "שחקן_א" או "שחקן_ב", ולכן עלינו לגלות זאת.




עבור שלבים 1 ו2: יצירת פרוצדורה.

הפרוצדורה מקבלת את ה-PlayerID שלנו.
אם אנחנו "שחקן_א" (משום שבטבלת PlayersWaitingForGame אין אף שחקן שמחכה שישחקו איתו. או שהטבלה ריקה או שהיא לא ריקה אבל כל הרשומות בה הן InConnectionAttempt=1):
היא מוסיפה את ה-PlayerID שלנו בתור רשומה חדשה לטבלה.
במקרה כזה היא מחזירה 1-.

אם אנחנו "שחקן_ב" (משום שבטבלת PlayersWaitingForGame יש שחקן ממתין):
היא מוסיפה את ה-PlayerID בתור יריב פוטנציאלי בעמודת SecondPlayerID ומסמנת שהמצב כרגע הוא InConnectionAttempt=1 כדי שאף אחד לא יפריע לשניהם.
במקרה כזה היא מחזירה את ה-PlayerID של השחקן היריב.



CREATE procedure [dbo].[CheckConnection]
@AskerPlayerID int --  parameter from java

AS

declare @MaxWaitID int -- The ID of the player waiting the longest
declare @ZeroCount int
declare @Count int
declare @tmp int

-- Check if @AskerPlayerID is already in the table because of a stupid programmer:

set @Count = (select count(*) from PlayersWaitingForGame
                  where [PlayerID] = @AskerPlayerID)
if (@Count > 0)
begin
select -2 as ReturnID  -- a stupid programmer
RETURN
end

set @Count = 0
set @Count = (select count(*) from PlayersWaitingForGame
                  where [SecondPlayerID] = @AskerPlayerID)
if (@Count > 0)
begin
select -3 as ReturnID  -- a stupid programmer
RETURN
end






-- Check whether there are available players waiting
set @ZeroCount = (select count(*) from PlayersWaitingForGame
                  where InConnectionAttempt = 0)


-- If no players are available
-- insert the asker to the table with connection attempt 'Zero'
if (@ZeroCount = 0)
begin
select -1 as ReturnID  -- there is no one to connect
insert into PlayersWaitingForGame
values(@AskerPlayerID, 0, null)
RETURN
    end




-- If there are players available
else
begin
set  @MaxWaitID = (select min(ID) 
   from PlayersWaitingForGame
   where InConnectionAttempt = 0)

-- select  @MaxWaitID as ReturnID 
select PlayerID as ReturnID
from PlayersWaitingForGame
where ID = @MaxWaitID


update PlayersWaitingForGame 
set InConnectionAttempt = 1
,SecondPlayerID = @AskerPlayerID
where ID = @MaxWaitID
end

GO









כדי לקרוא לפרוצדורה מהגאווה, נשתמש בפונקציה "searchAnotherUserWaitingForGame":

    // פונקציה שמחפשת בבסיס הנתונים שחקן יריב
    // אם היא מצאה היא מחזירה את האיידי שלו
    // אם היא לא מצאה היא שמה בבסיס הנתונים את האיידי שהיא קיבלה ששייך לשחקן שלנו ומחזירה  מינוס אחד
    // אם יש שגיאה היא מחזירה את השגיאה על ידיהפרמטר שהיא קיבלה ומחזירה  מינוס שתיים
    public int searchAnotherUserWaitingForGame(CustomError CE, int MyPlayerID)
    {
        Connection con = null;
        Statement st = null;
        ResultSet rs = null;
        String selectString;
        int pID = -2;

        
        
        // בדיקה שהרשימה שקיבלנו מאותחלת
        if(CE == null){
            CE.CustomMessage = "Error in 'searchUserWaitingForGame': CE = null";
            CE.isThereError = true;
            return -2;
        }
        
        
        // הפרוצדורה הזו מחזירה את האיידי של מי שיכול לשחק נגדנו
        // אם אין אף אחד היא מוסיפה את הבקשה שלנו אל הטבלה כבקשה חדשה שמישהו צריך למצוא
        selectString
                = "EXEC [dbo].[CheckConnection] " + MyPlayerID + " ";
        
        
        
        try
        {
           con = getConnection();
           st = con.createStatement();
           rs = st.executeQuery(selectString);
           if(rs.next())
           {
               pID = rs.getInt("ReturnID");
           }
           else{
               CE.CustomMessage = "Error in 'searchUserWaitingForGame': rs.next() = false";
               CE.isThereError = true;
               return -2;
           }
        }
        
        catch(Exception e)
        {
            CE.CustomMessage = "Error in 'searchUserWaitingForGame'";
            CE.SystemMessage = e.getMessage();
            CE.isThereError = true;
        }
        
        finally { // צריך לעשות סגירה של החיבור גם אם נפלנו באקספטשן
            closeStatementAndConnection(con, st);
        }
        return pID;
    }
    








אם קיבלנו 1- הרי שאנחנו "שחקן_א" ועלינו לבצע את שלב 3:

עלינו לבדוק שוב ושוב את הטבלה עד שמישהו יענה לבקשה שלנו.

לשם כך נקרא שוב ושוב לפונקציה "seeIfSomeoneFoundMe":
(בדיליי קצר בין כל קריאה כדי לא להעמיס על בסיס הנתונים)

    // הפונקציה בודקת האם כבר מישהו מצא את השחקן בטבלה ורוצה לשחק איתו
    // אם כן הפונקציה תחזיר את האיידי שלו
    // אם לא הפונקציה תחזיר  מינוס אחד
    // אם הייתה שגיאה הפונקציה תחזיר מינוס שתיים
    
    // בנוסף הפונקציה מוחקת את השחקן מהטבלה באם התגלה שמישהו מצא אותו
    public int seeIfSomeoneFoundMe(CustomError CE, int MyPlayerID){
        Connection con = null;
        Statement st = null;
        ResultSet rs = null;
        String selectString;
        int pID = -2;
        
        
        
        
        if(CE == null){
            CE.CustomMessage = "Error in 'SeeIfSomeoneFoundMe': CE = null";
            CE.isThereError = true;
            return -2;
        }
        
        
        
        selectString 
                = " SELECT [SecondPlayerID] "
                + " FROM [dbo].[" + TableName + "] "
                + " WHERE [InConnectionAttempt] = 1 AND [PlayerID] = " + MyPlayerID + " ";
        
        
        
        try
        {
           con = getConnection();
           st = con.createStatement();
           rs = st.executeQuery(selectString);
           if(rs.next()) // אם יש אדם שמצא אותנו
           {
               pID = rs.getInt("SecondPlayerID");
           }
           else{
               pID = -1;
           }
        
           
           
        
            //// מכאן הפונקציה מוחקת אותי מהטבלה מאחר והתגלה שמצאו אותי
            
            if(pID > -1){ // אם שחקן כלשהו מצא אותי
                selectString 
                        = " DELETE FROM [dbo].[" + TableName + "] "
                        + " WHERE [PlayerID] = " +  MyPlayerID + " ";
                
                int affectedRows = st.executeUpdate(selectString);
                
                
                if(affectedRows < 1){
                    CE.CustomMessage = "Error in 'SeeIfSomeoneFoundMe': affectedRows < 1";
                    CE.isThereError = true;
                }
            }
        }

        
        catch(Exception e)
        {
            CE.CustomMessage = "Error in 'SeeIfSomeoneFoundMe'";
            CE.SystemMessage = e.getMessage();
            CE.isThereError = true;
        }
        
        
        
        
        finally { // צריך לעשות סגירה של החיבור גם אם נפלנו באקספטשן
            closeStatementAndConnection(con, st);
        }
        return pID;
        
    }
    








אם קיבלנו מספר חיובי הרי שאנחנו "שחקן_ב" ועלינו לבצע את שלב 4:

עלינו לבדוק שוב ושוב את הטבלה עד שהיריב יבחין בנו וימחק את הרשומה.

לשם כך נקרא שוב ושוב (בדיליי) לפונקציה "seeIfOpponentNotice":
    // בדיקה האם היריב הבחין שמצאתי אותו
    // אם הוא הבחין הוא היה אמור למחוק את השורה שלו
    // לכן פשוט נבדוק האם הוא מחק את השורה מהטבלה
    public boolean seeIfOpponentNotice(CustomError CE, int MyPlayerID){
        Connection con = null;
        Statement st = null;
        ResultSet rs = null;
        String selectString;
        int pID = -2;
        
        
        // בדיקה שהרשימה שקיבלנו מאותחלת
        if(CE == null){
            CE.CustomMessage = "Error in 'SeeIfOpponentNotice': CE = null";
            CE.isThereError = true;
            return false;
        }
        
        
        selectString 
                = " SELECT [SecondPlayerID] "
                + " FROM [dbo].[" + TableName + "] "
                + " WHERE [SecondPlayerID] = " + MyPlayerID +" ";
        
        try
        {
           con = getConnection();
           st = con.createStatement();
           rs = st.executeQuery(selectString);
           if(rs.next()) // אם יש רשומה שהוחזרה הרי שהיריב לא מצא אותנו ולא מחק עדיין את הרשומה
           {
               return false;
           }
           else{ // אם היריב הבחין בנו ומחק את השורה מהטבלה כמו שהוא אמור
               return true;
           }
        }
        
        catch(Exception e)
        {
            CE.CustomMessage = "Error in 'SeeIfSomeoneFoundMe'";
            CE.SystemMessage = e.getMessage();
            CE.isThereError = true;
        }
        
        finally {
            closeStatementAndConnection(con, st);
        }
        return false; // בעיקרון לעולם לא אמורים להגיע לשורה הזאת
    }







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





יצירת דיליי בג'אווה:
                                                                                                                              try {
                                                                                                                                  TimeUnit.SECONDS.sleep(1);
                                                                                                                              } catch (Exception e) { }














עוד חודש...

אין תגובות:

הוסף רשומת תגובה