INTRODUCTION |
The exchange of data between computer systems plays an extremely important role in our interconnected world. Therefore we often speak of the combined computer- and communication technologies that should be mastered. In this chapter you learn how to handle the data exchange between two computer systems using the TCP/IP protocol which is used in all Internet connections, for example the Web and all streaming services (data clouds, voice, music, video transmission). |
TCPCOM: AN EVENT-DRIVEN SOCKET LIBRARY |
The socket programming is based on the client-server model, which has already been described in Section 6.2. The server and the client programs are not completely symmetrical. In particular, first the server program must be started before the client program can connect to the server. In order to identify the server on the Internet, its IP address is used. In addition, the server has 65536 communication channels (IP ports), that is selected with a number in the range 0..64535. When the server starts, it creates a server socket (like a electrical plug) that uses a particular port and goes in a wait state. We say that the server is listening for an incoming client, so the server is in the LISTENING state. The client creates a client socket (the plug counterpart) and tries to establish a communication link to the server using its IP address and port number. The library tcpcom simplifies the socket programming essentially, since it describes the current state of the server and client with state variables. The change of the variable is considered to be caused by an event. This programming model corresponds to the natural feeling of many people to describe the communication between two partners by a sequence of events. As usual in a event-driven model, a callback function, here called stateChanged(state, msg) is invoked by the system, when an event is fired. The Python module is integrated into TigerJython, but can also be downloaded from here to be studied or used outside TigerJython. The server is started by creating a TCPServer object specifying the ports and the callback function onStateChanged() and embarks in the LISTENING state. from tcpcom import TCPServer server = TCPServer(port, stateChanged = onStateChanged) The callback onStateChanged (state, msg) has two string parameters state and msg that describe the status change of the server:
The client starts with the creation of a TCPClient object specifying the IP address of the server, the port and the callback function onStateChanged (). By invoking connect() it starts a connection trial. from tcpcom import TCPClient client = TCPClient(host, port, stateChanged = onStateChanged) client.connect() Again, the callback onStateChanged (state, msg) has two string parameters state and msg, describing the state change of the client:
The call to connect() is blocking, which means that the function returns True once the connection has succeeded, or False after a certain timeout period (approximately 10 seconds), if the connection fails. The information about the success or failure of the connection can also be detected via the callback. You can try out the client-server programs on the same PC by starting two TigerJython windows. In this case you choose the host address localhost. Using two different computers for the client and the server is more close to reality. They must be connected with a network cable or via wireless LAN and the link must be open for TCP/IP communication with the selected port. If the connection fails with your normal hotspot (WLAN access point), this is mostly due to firewall restrictions. In this case you can use your own router or start a mobile hot spot app on your smartphone. Access of the mobile phone to the Internet is not necessary. As your first socket programming duty you create a server that provides a time service. When a client logs on, it sends the current time (with date) back to the client. There are numerous such time server on the Internet and you can be proud that you are already able to code a professional server application. In order to turn off the time server, you use a well-known trick: The main program "hangs" in a TigerJython modal message dialog opened by the blocking function msgDlg(). When the function returns by pressing the OK or clicking the close button, the server is stopped by calling terminate(). from tcpcom import TCPServer import datetime def onStateChanged(state, msg): print(state, msg) if state == TCPServer.CONNECTED: server.sendMessage(str(datetime.datetime.now())) server.disconnect() port = 5000 server = TCPServer(port, stateChanged = onStateChanged) msgDlg("Time Server running. OK to stop") server.terminate() The client first asks for the IP address and tries to establish the link to the server by calling connect(). The time information received back from the server is written in a dialog window. from tcpcom import TCPClient def onStateChanged(state, msg): print(state, msg) if state == TCPClient.MESSAGE: msgDlg("Server reports local date/time: " + msg) if state == TCPClient.CONNECTION_FAILED: msgDlg("Server " + host + " not available") host = inputString("Time Server IP Address?") port = 5000 client = TCPClient(host, port, stateChanged = onStateChanged) client.connect()
|
MEMO |
The server and the client implement the event model with a callback function onStateChanged(state, msg). The two parameters provide important information about the event. Make sure that you finish the server with terminate(), in order to release the IP port. |
ECHO-SERVER |
The next training session is famous because it shows the archetype of a client-server communication. The server makes nothing else than sending back the non-modified message received from the client. For this reason it is called an echo server. The system can be easily extended so that the server analyzes the message received from the client and returns a tailored response. This is exactly the concept of all WEB servers, because they reply to a HTTP request issued by the client browser. You first code the echo server and start it immediately. As you see, the code differs only slightly from the time server. For illustration and debugging purposes you display the state and msg parameter values in the console window. from tcpcom import TCPServer def onStateChanged(state, msg): print(state, msg) if state == TCPServer.MESSAGE: server.sendMessage(msg) port = 5000 server = TCPServer(port, stateChanged = onStateChanged) msgDlg("Echo Server running. OK to stop") server.terminate() For the client you invest a bit more effort by using the EntryDialog class to display a non-modal dialog to show status information. Also in other contexts the EntryDialog is very convenient, since you can easily add editable and non-editable text fields and other GUI elements such as various types of buttons and sliders. from tcpcom import TCPClient from entrydialog import * import time def onStateChanged(state, msg): print(state, msg) if state == TCPClient.MESSAGE: status.setValue("Reply: " + msg) if state == TCPClient.DISCONNECTED: status.setValue("Server died") def showStatusDialog(): global dlg, btn, status status = StringEntry("Status: ") status.setEditable(False) pane1 = EntryPane(status) btn = ButtonEntry("Finish") pane2 = EntryPane(btn) dlg = EntryDialog(pane1, pane2) dlg.setTitle("Client Information") dlg.show() host = "localhost" port = 5000 showStatusDialog() client = TCPClient(host, port, stateChanged = onStateChanged) status.setValue("Trying to connect to " + host + ":" + str(port) + "...") time.sleep(2) rc = client.connect() if rc: time.sleep(2) n = 0 while not dlg.isDisposed(): if client.isConnected(): status.setValue("Sending: " + str(n)) time.sleep(0.5) client.sendMessage(str(n), 5) # block for max 5 s n += 1 if btn.isTouched(): dlg.dispose() time.sleep(0.5) client.disconnect() else: status.setValue("Connection failed.") |
MEMO |
In the client program you use the function sendMessage(msg, timeout) with an additional timeout parameter. The call is blocking for a maximum time interval (in seconds) until the server sends back a response. sendMessage() returns either the server's response or None, if no response is received within the given timeout. It is important to know the difference between modal and non-modal (modeless) dialog boxes. While the modal window blocks the program until the dialog is closed, with a modeless dialog the program continues, so that at any time status information can be displayed and user input read by the running program. |
TWO PERSONS ONLINE GAME WITH TURTLE GRAPHICS |
"Sinking Boats" is a popular game between two people, in which the memory plays an important role. The playing boards of each player is a one or two dimensional arrangement of grid cells where vessels are placed. They occupy one or multiple cells and have different values depending on the type of the ship. By turns, each player denotes a cell where, so to speak, a bomb is dropped. If the cell contains a portion of a ship, it will be sunk, so removed from the board and its value credited to the aggressor. If one of the players has no more ships, the game ends and the player with the greater credit wins. In the simplest version, the ships are displayed as colored square cells in a one-dimensional array. All ships are of equal rank. The winner is the player who has first eliminated all enemy ships. As for the game logic, the client and server programs are largely identical. To show the board, it is sufficient to use elementary drawing operations from turtle graphics that you know for a long time. You select a cell numbering from 1 to 10 and you have to produce 4 different random numbers out of 1..10 for the random selection of the ships. To do so, is is elegant to use the library function random.sample(). To launch a bomb, you send the full Python command to the partner who executes it with exec(). (Such a dynamic code execution is only possible in a few programming languages.) To find out whether there has been a hit, you test the background color with getPixelColorStr(). In the game, the two players are equal, but their programs vary slightly, depending on who is server and and who is client. In addition, the server must start first. from gturtle import * from tcpcom import TCPServer from random import sample def initGame(): clear("white") for x in range(-250, 250, 50): setPos(x, 0) setFillColor("gray") startPath() repeat 4: forward(50) right(90) fillPath() def createShips(): setFillColor("red") li = sample(range(1, 10), 4) # 4 unique random numbers 1..10 for i in li: fill(-275 + i * 50, 25) def onMouseHit(x, y): global isMyTurn setPos(x, y) if getPixelColorStr() == "white" or isOver or not isMyTurn: return server.sendMessage("setPos(" + str(x) + "," + str(y) + ")") isMyTurn = False def onCloseClicked(): server.terminate() dispose() def onStateChanged(state, msg): global isMyTurn, myHits, partnerHits if state == TCPServer.LISTENING: setStatusText("Waiting for game partner...") initGame() if state == TCPServer.CONNECTED: setStatusText("Partner entered my game room") createShips() if state == TCPServer.MESSAGE: if msg == "hit": myHits += 1 setStatusText("Hit! Partner's remaining fleet size " + str(4 - myHits)) if myHits == 4: setStatusText("Game over, You won!") isOver = True elif msg == "miss": setStatusText("Miss! Partner's remaining fleet size " + str(4 - myHits)) else: exec(msg) if getPixelColorStr() != "gray": server.sendMessage("hit") setFillColor("gray") fill() partnerHits += 1 if partnerHits == 4: setStatusText("Game over, Play partner won!") isOver = True return else: server.sendMessage("miss") setStatusText("Make your move") isMyTurn = True makeTurtle(mouseHit = onMouseHit, closeClicked = onCloseClicked) addStatusBar(30) hideTurtle() port = 5000 server = TCPServer(port, stateChanged = onStateChanged) isOver = False isMyTurn = False myHits = 0 partnerHits = 0 The client program is almost the same: from gturtle import * from random import sample from tcpcom import TCPClient def initGame(): for x in range(-250, 250, 50): setPos(x, 0) setFillColor("gray") startPath() repeat 4: forward(50) right(90) fillPath() def createShips(): setFillColor("green") li = sample(range(1, 10), 4) # 4 unique random numbers 1..10 for i in li: fill(-275 + i * 50, 25) def onMouseHit(x, y): global isMyTurn setPos(x, y) if getPixelColorStr() == "white" or isOver or not isMyTurn: return client.sendMessage("setPos(" + str(x) + "," + str(y) + ")") isMyTurn = False def onCloseClicked(): client.disconnect() dispose() def onStateChanged(state, msg): global isMyTurn, myHits, partnerHits if state == TCPClient.DISCONNECTED: setStatusText("Partner disappeared") initGame() elif state == TCPClient.MESSAGE: if msg == "hit": myHits += 1 setStatusText("Hit! Partner's remaining fleet size " + str(4 - myHits)) if myHits == 4: setStatusText("Game over, You won!") isOver = True elif msg == "miss": setStatusText("Miss! Partner's remaining fleet size " + str(4 - myHits)) else: exec(msg) if getPixelColorStr() != "gray": client.sendMessage("hit") setFillColor("gray") fill() partnerHits += 1 if partnerHits == 4: setStatusText("Game over, Play partner won") isOver = True return else: client.sendMessage("miss") setStatusText("Make your move") isMyTurn = True makeTurtle(mouseHit = onMouseHit, closeClicked = onCloseClicked) addStatusBar(30) hideTurtle() initGame() host = "localhost" port = 5000 client = TCPClient(host, port, stateChanged = onStateChanged) setStatusText("Client connecting...") isOver = False myHits = 0 partnerHits = 0 if client.connect(): setStatusText("Connected. Make your first move!") createShips() isMyTurn = True else: setStatusText("Server game room closed")
|
MEMO |
In two players games, the players can have the same or different game situations. In the second case, which includes most card games, the game must necessarily be played with two computers, because the game situation must be kept secret. Instead of always let the client start the game, the player to move first could be selected randomly or by negotiating. Both programs are completed by clicking on the close button in the title bar. But then some additional "cleanup" is required like stopping the server or breaking the communication link. To do so, register the callback onCloseClicked() and the default closing process is disabled. It is now up to you, to perform the adequate operation in the callback and closing the turtle window by calling dispose(). Disregarding so, the Task Manager is the needed to get you out of a jam. |
TWO PERSONS ONLINE GAME WITH GAMEGRID |
For more complex games, it is of great advantage to refer to a game library that simplifies the code considerably. You already learned in Chapter 7 how to use GameGrid, a full-featured game engine integrated into TigerJython. By combining GameGrid with tcpcom, you can create sophisticated multi-person online games where the game partners are located anywhere in the world. For illustration you expand the "Sinking Boats" game in 2 dimensions. The game logic remains unchanged, however you transfer now x and y coordinates of the selected cell. On the receiving side, the partner can find out if one of his ships is hit and report back a "hit" or "miss" message. In game programming it is important to make a special effort that games proceed "reasonably", even if the two players behave somewhat unreasonably. So the firing of bombs must be disabled if it is not the player's move. When one of the programs terminates unexpectedly, the partner should be informed. Although these additional checks blow up the code, this is the hallmark of careful programming Since a large part of the code for the server and client is the same, and code duplication is one of the greatest sins of programming, the common code is exported into a module shiplib.py that can imported by both applications. Different behavior is taken into account by additional parameters like node that refers to a TCPServer or TCPClient object. Library: # shiplib.py from gamegrid import * isOver = False isMyMove = False dropLoc = None myHits = 0 partnerHits = 0 nbShips = 2 class Ship(Actor): def __init__(self): Actor.__init__(self, "sprites/boat.gif") def handleMousePress(node, loc): global isMyMove, dropLoc dropLoc = loc if not isMyMove or isOver: return node.sendMessage("" + str(dropLoc.x) + str(dropLoc.y)) # send location setStatusText("Bomb fired. Wait for result...") isMyMove = False def handleMessage(node, state, msg): global isMyMove, myHits, partnerHits, first, isOver if msg == "hit": myHits += 1 setStatusText("Hit! Partner's fleet size " + str(nbShips - myHits) + ". Wait for partner's move!") addActor(Actor("sprites/checkgreen.gif"), dropLoc) if myHits == nbShips: setStatusText("Game over, You won!") isOver = True elif msg == "miss": setStatusText("Miss! Partner's fleet size " + str(nbShips - myHits) + ". Wait for partner's move!") addActor(Actor("sprites/checkred.gif"), dropLoc) else: x = int(msg[0]) y = int(msg[1]) loc = Location(x, y) bomb = Actor("sprites/explosion.gif") addActor(bomb, loc) delay(2000) bomb.removeSelf() refresh() actor = getOneActorAt(loc, Ship) if actor != None: actor.removeSelf() refresh() node.sendMessage("hit") partnerHits += 1 if partnerHits == nbShips: setStatusText("Game over! Partner won") isOver = True return else: node.sendMessage("miss") isMyMove = True setStatusText("You fire!") By using the external module shiplib.py the code of the server and the client becomes much simpler and clearer. Server: from gamegrid import * from tcpcom import TCPServer import shiplib def onMousePressed(e): loc = toLocationInGrid(e.getX(), e.getY()) shiplib.handleMousePress(server, loc) def onStateChanged(state, msg): global first if state == TCPServer.PORT_IN_USE: setStatusText("TCP port occupied. Restart IDE.") elif state == TCPServer.LISTENING: setStatusText("Waiting for a partner to play") if first: first = False else: removeAllActors() for i in range(shiplib.nbShips): addActor(shiplib.Ship(), getRandomEmptyLocation()) elif state == TCPServer.CONNECTED: setStatusText("Client connected. Wait for partner's move!") elif state == TCPServer.MESSAGE: shiplib.handleMessage(server, state, msg) def onNotifyExit(): server.terminate() dispose() makeGameGrid(6, 6, 50, Color.red, False, mousePressed = onMousePressed, notifyExit = onNotifyExit) addStatusBar(30) for i in range(shiplib.nbShips): addActor(shiplib.Ship(), getRandomEmptyLocation()) show() port = 5000 first = True server = TCPServer(port, stateChanged = onStateChanged) shiplib.node = server Client: from gamegrid import * from tcpcom import TCPClient import shiplib def onMousePressed(e): loc = toLocationInGrid(e.getX(), e.getY()) shiplib.handleMousePress(client, loc) def onStateChanged(state, msg): if state == TCPClient.CONNECTED: setStatusText("Connection established. You fire!") shiplib.isMyMove = True elif state == TCPClient.CONNECTION_FAILED: setStatusText("Connection failed") elif state == TCPClient.DISCONNECTED: setStatusText("Server died") shiplib.isMyMove = False elif state == TCPClient.MESSAGE: shiplib.handleMessage(client, state, msg) def onNotifyExit(): client.disconnect() dispose() makeGameGrid(6, 6, 50, Color.red, False, mousePressed = onMousePressed, notifyExit = onNotifyExit) addStatusBar(30) for i in range(shiplib.nbShips): addActor(shiplib.Ship(), getRandomEmptyLocation()) show() host = "localhost" port = 5000 client = TCPClient(host, port, stateChanged = onStateChanged) client.connect()
|
MEMO |
The end of the game is a special situation that must be programmed carefully. Since it is an "emergency state", you are using a global flag isOver that is set to True when the game is over. It also raises the question of whether the game can be played again without restarting the server or client program. In this implementation, the client needs to terminate his program and the server then goes back into the LISTENING state, waiting for a new client to play again. The programs could be improved in many ways. As general rule game programming is very attractive because there are no limits for the imagination and ingenuity of the developer. Moreover, after all your effort, relaxing and playing the game is also lot of fun. Until now the two playmates have to start their programs according to the rule "First, the server and then the client". To circumvent this restriction the following trick could be applied: A program always starts first as a client and tries to create a connection to the server. If this fails, it starts a server [more... Such a system is called "Peer-to-Peer Communication"] The two programs are identical in this case]. |
COMMUNICATION WITH HANDSHAKE |
Even in every days life, communication between two partners need some kind of synchronization. In particular, data may be sent only if the recipient is actually ready for the reception and further processing. Failure to observe this rule may result in data loss or even block the programs. Different computing power of the two nodes and a variing transmission time must be considered too. One known method to get these timing problems under control, is to provide feedback from the receiver to the transmitter, which can be compared with an amicable handshake. The process is basically simple: data is transmitted in blocks and the receiver acknowledges the correct reception and and readiness for the next block with a feedback. Only when it is received, the next block is sent. The feedback may also cause the transmitter to send the same block again [more... Different temporal behavior can be compensated by implementing buffers (Queues, FIFO)].To demonstrate the handshake principle, your the turtle program of the server draws relatively slowly lines dictated by the client's mouse clicks. The client's turtle moves much faster. Therefore the client must wait from click to click until the server reports back that he has completed its drawing operation and is ready to dispatch the next command. Server: from gturtle import * from tcpcom import TCPServer def onCloseClicked(): server.terminate() dispose() def onStateChanged(state, msg): if state == TCPServer.MESSAGE: li = msg.split(",") x = float(li[0]) y = float(li[1]) moveTo(x, y) dot(10) server.sendMessage("ok") makeTurtle(mouseHit = onMouseHit, closeClicked = onCloseClicked) port = 5000 server = TCPServer(port, stateChanged = onStateChanged) Client: from gturtle import * from tcpcom import TCPClient def onMouseHit(x, y): global isReady if not isReady: return isReady = False client.sendMessage(str(x) + "," + str(y)) moveTo(x, y) dot(10) def onCloseClicked(): client.disconnect() dispose() def onStateChanged(state, msg): global isReady if state == TCPClient.MESSAGE: isReady = True makeTurtle(mouseHit = onMouseHit, closeClicked = onCloseClicked) speed(-1) host = "localhost" port = 5000 isReady = True client = TCPClient(host, port, stateChanged = onStateChanged) client.connect()
|
MEMO |
To bring the actions of the transmitter and receiver in an orderly temporal sequence, the transmitter waits to send the next data until he receives a feedback that the receiver is ready for further processing. |
EXERCISES |
|
|