Problem med ordning på meddelande mellan server/klient
Halloj!
Jag håller på med ett projekt i nätverksprogrammering och jag ville testa att göra en liten "rough" version av spelet skitgubbe som man ska kunna spela i konsolen. Problemet är att det blir något knas när klienten, vars tur det blir efter att första spelaren gjort sitt drag, inte får rätt information. I början av varje "turn" broadcastas "public_state" out till alla klienter, som innehåller information om vems tur det är, hur många spelare som är anslutna och så vidare. Sedan skickas en instruktion ut till den spelare vars tur det är. Det kan vara något i stil med "play_card" eller "take_stack" beroende på vilka möjligheter servern har bestämt att spelaren har.
Ett exempel på problemet är följande sekvens:
Som ni ser får klienten som just gjorde sitt drag rätt "public state" (som broadcastas till alla). Problemet är att klienten som ska få sin instruktion om vad den ska göra inte får något public state, utan endast får sin instruktion. Jag förstår inte varför detta händer, för i min klientkod har jag två separata recv-calls, ett för första meddelandet och ett för andra. Det konstiga är dessutom att det ibland inte händer, utan funkar några rundor innan det händer igen, men det verkar vara ovanligt. Jag länkar mitt github repository till projektet nedan så får någon snäll själ gärna testa att köra koden på sin burk.
https://github.com/Alice1576/skitgubbe
Det enda som behövs är modulerna "Cards.py", "client.py" och "server.py". Den relevanta logiken på serversidan ligger under "game_loop()" och den relevanta logiken på klientsidan ligger under "handle_instructions()".
PS: korten anges med deras position + 1, så t.ex. spelades kortet 12 - CardSuites.clubs genom att skriva "1" i steg 1.
PS2: jag vet att koden är rätt sunkig; detta är mitt fjärde projekt någonsin och mitt andra projekt någonsin där jag jobbar med objektsorienterad programmering och nätverksprogrammering, så jag är rätt kass på båda.
Jag har inte tittat på koden än, men jag kan tänka mig att det inte är garanterat i det aktuella fallet att alla meddelanden i en broadcast kommer fram före ett meddelande till en specifik adress, fast det senare sänds senare.
För UDP vet man t.ex. inte om de kommer fram i ordning, eller om de kommer fram alls. Med TCP har man en kanal mellan två noder och vet i vilken ordning saker kommer, och huruvida kanalen har brutits.
På ett lokalt nätverk kan man kanske veta mer.
Jag har inte tittat på koden än, men jag kan tänka mig att det inte är garanterat i det aktuella fallet att alla meddelanden i en broadcast kommer fram före ett meddelande till en specifik adress, fast det senare sänds senare.
Hmm, jag misstänkte att det var något sådant som var fel...
Har du några generella tips på hur man bygger sitt system så att det inte kan hända? Jag funderade på att skicka både den privata instruktionen och broadcasten i ett meddelande, men på ett sätt så att endast rätt klient kan komma åt instruktionen. Men ur ett "säkerhetsperspektiv" verkar det dumt att göra så; det verkar enkelt att fuska då (inte för att någon skulle fuska i ett projekt jag använder för att lära mig, men du förstår...).
En konstig grej för övrigt är att om jag har tre print statements - i exakt ordningen nedan - så får jag följande output:
Så uppenbarligen tas mitt public_state emot av klienten, och det sker först. Men av någon anledning skrivs variabeln public_state över. Fattar inte riktigt varför det blir så. Här är klientsidan för att få detta felmeddelande:
def handle_instructions(client_socket):
while True:
try:
public_state = pickle.loads(client_socket.recv(2048))
print(public_state)
print(public_state)
print(f"Players: {public_state['player count']}")
print("---------")
print(public_state["turn"])
print("---------")
print(f"Stack: {public_state['stack']}")
print("---------")
print(public_state)
try:
instruction = pickle.loads(client_socket.recv(2048))
if instruction["action"] == "play_card":
print(f"Your hand: {instruction['hand']}")
card_index = int(input(f"Please input a card you would like to play: "))
response = {"action": "play_card", "index": card_index}
serialized_response = pickle.dumps(response)
client_socket.send(serialized_response)
elif instruction["action"] == "take_stack":
input("You cannot play any card. Please press any button to take the entire stack")
response = {"action": "take_stack"}
serialized_response = pickle.dumps(response)
client_socket.send(serialized_response)
except Exception as e:
continue
except Exception as e:
print(f"Error {e}")
Jag ser inte hur utskriften där hör ihop med koden.
Det går nog bra att skicka allting till alla, och i så fall ha en krypteringsnyckel för varje klient så att bara den kan dekryptera det som är avsett för den.
Om nätverksprogrammering finns hela kurser, och om kryptering dessutom hela kurser. Jag är ingen expert. Ett sätt att klara sig om man använder UDP är att ha ett löpnummer för varje klient man skickar till, och då kan klienten avgöra om nånting har tappats bort eller kommer i fel ordning. Eller ordningsnummer för broadcastarna: "här får du ett meddelande, du ska ha fått broadcast 47 redan". "Va, jag har inte fått broadcast 47".
Utskriften kommer från de tre print-statementsen i den yttersta "try-except"-delen. Precis under while True: ... Jag har kanske bara satt ihop blocken på ett dumt sätt.
Alla meddelanden skickas som serialiserade dictionaries. Så det är dictet som motsvarar "public_state" som broadcastas från servern jag ville printa med mina tre print-statements print(public_state). Som du ser modifierar jag inte mitt public_state någonstans, men trots detta blir utskrifterna inte samma alla tre gånger (första utskriften blir rätt, resterande två blir ett helt annat meddelande). Så min variabel public_state skrivs över på något sätt?
Men ska inte "--------" och lite annat skrivas ut också?
Har du flera processer eller trådar som kör samma kod, så att du ser utskrift från flera olika?
Men ska inte "--------" och lite annat skrivas ut också?
Jo precis, men det sker bara hos den andra klienten, av någon skum anledning.
Jag kör två klienter parallellt, och den klienten som just gjorde sitt drag får rätt prints:
Min Python tycker inte om list[Card] i init-funktionen. Har jag för gammal Python?
Vet ej. Hur importade du Cards? Testa att skriva from Cards import..., där "..." är alla olika klasser i Cards, om det inte det var så du skrev.
Eller körde du koden bara rakt av github? I så fall kanske det är versionen som spökar.
Jag klonade github-repot och gjorde sen
python server.py
Vilken pythonversion kör du?
Jag ska inte bryta mig in i er diskussion men jag har några kommentarer.
Nätverksdelen är trivial och den fungerar säkert. Du använder ordet broadcast men det är inte en "riktig" UDP-broadcast utan du loopar dina klienter och skickar TCP-meddelanden så det fungerar och det är ett vanligt sätt att hantera det. Det har fördelen att du VET att meddelanden kommer fram och i den ordning de sänds ut. Det gör du däremot inte om du använder UDP som Laguna säger.
Det är en bra vana att ha en recv i din loop och anropa "handlers" baserat på innehållet i meddelandet. Det är möjligt att du kan förvänta dig ett statemeddelande innan varje kommando men det är bättre att inte förutsätta det så blir felhanteringen enklare. Annars är nog klientkoden ok.
Serverkoden är rätt komplex och jag föreslår att du förenklar den till det minimala och sen lägger till små stycken kod och provkör innan du lägger till nästa. Jag har inte satt mig in i hela koden men jag ser några små saker som du kan förenkla. Du har en struktur för client som innehåller din socket och en struktur för spelare som innehåller tillståndet. Jag kan inte se att du någon gång vill skilja på dem om du inte hanterar nätverksfel så att samma klient kopplas ner och kopplar upp sig igen och det gör du inte som det ser ut.
För att förenkla mer så skulle jag kapsla in mycket att koden när du hoppar runt i listorna i små funktioner med meningsfulla namn. Det gör det enklare för utomstående att förstå hur du tänker om man kan läsa som text. Alternativt kommentera rikligt. Vilket du använder är en smaksak.
Så mitt förslag.
Gör om klienten så att den har en recv i loopen och hantera allt som kommer in med en villkorssats av valfritt slag. Din if-then-else duger bra till den. Där har du dina alternativ public_state, play_card och take_stack OCH en felhantering om det kommer in saker du inte känner igen.
Gör om servern så att den bara initierar och låter klienterna koppla upp. När det är gjort och testat så lägger du till att skicka ut public_state och verifierar att klienterna tar emot det.
När det är verifierat så kan du börja med din spellogik och då föreslår jag att du förenklar ditt spel, inte för att skitgubbe är komplicerat men det är tillräckligt komplext för att du ska ha svårt att se var saker går fel. Börja med ett enkelt "välj ett kort" och se till att det fungerar.. osv.
Sen föreslår jag att du kör servern i en miljö där du kan debugga den. Alternativet med utskrifter fungerar dåligt om det är många fel och komplex kod. Då blir utskriftslogiken i sig krånglig.
Testa det och återkom om det inte löser ditt problem så får jag försöka sätta mig in i din logik om inte någon annan hinner lösa det åt dig.
Jag hittade äntligen felet och har justerat koden därefter, och nu funkar det som det ska! Det var ett riktigt korkat fel också så jag fattar inte hur jag kunde missa det så länge.
Jag grönmarkerar tråden nu! :D