Tcplink: Talking to an Online Database

Intro

What this tutorial will not teach you:

  • How to set up an online database such as mysql
  • How to get data from the database and display it on an html page

Those topics are covered quite extensively across the world wide web and there are just too many different ways to do them. This tutorial specifically uses a mysql database and php to communicate with it. The main focus will be how to get and send html data to a web server with TCP.

So, you've come here for the same reason that Im writing this now: you searched or asked how to use an online database with UDK, and no matter how hard you've tried, the only answer you've gotten is either DLL bind or use tcplink. Unfortunately, that's as far as anyone has gone. So rather than keep with the going trend, I decided to take it a step further and actually explain how you might use tcplink to achieve your goals. Lets start with some general information.

Background

The first thing you have to understand is that tcp is not something special that the UDK is providing. It stands for Transmission Control Protocol and is one of the many ways to communicate with the Internet Protocol (IP). If you want to know more, hop over to Wikipedia, but the info contained there is not necessary to continue. Most web browsers use TCP to communicate with web servers and we will be doing the same. The main things you have to understand is that tcp makes a connection with the server and sends a request message to it. In most cases, we will simply send one request, get one reply, and then close our connection. Know that you can leave the connection open if you like and send multiple requests, but you would then have to figure out when one response from the server ends and the next one begins. Know too that the request we send can get data or send it, but in both cases the server will send back a response. There are a great many things to a request or response message. I will be detailing the bare minimum, but if you'd like to know more, check out the same site I used to figure this all out: TCPIPGuide

I have to give credit to UDK's Documentation as I used their example TCPLinkClient class as a starting template: TCPLink

Let's begin!

Getting Data with PHP

For my game, I wanted to have a high score system. For my first attempt, I followed the KISS mantra: no names, player accounts, etc, just the scores. So for my php, all I want to do is get the scores from the server in order from highest to smallest. I started by setting up a mysql database, inserting random scores into it, and using the following php code to get a comma separated list of the scores in html. The green comments should explain everything.


<?php

    include("mysql_connect.php"); //database connection code

    //a post will exist if a score is sent to the server
    //we send 10987 to the server, but usually the value of 
    //submit will be submit in most examples. I changed this
    //for security reasons.
    if($_POST['submit'] == "10987")
    {
        //insert sql
        $query = 'INSERT INTO highscores VALUES(' . $_POST['value'] . ')';

        //run the query
        mysql_query($query);
    }

    $query = '
    SELECT score 
    FROM highscores
    ORDER BY score DESC
    ';

    runQuery($query);

    $scorestring = ""; //will store the csv list here

    function runQuery($query)
    {

        //store the query result
        $result = mysql_query($query);

        //mysql_fetch_row will get the row and advance the pointer
        //so each time through the while loop, it fetchs a new row
        while($row = mysql_fetch_row($result))
        {

            //for each value in our row
            foreach($row as $value)
            {
                //add the value to our string with a comma
                $scorestring = $scorestring . $value . ",";
            }

        }

        //echo the string but chop off the last comma
        echo rtrim($scorestring, ",");
    }

?>

If you've noticed, this is not valid html. I exclude all doctype and header information. That is because when I send a request message to the server with tcp, it will respond with information on the file type in its header and then after the header, include everything in the document. I simply want to chop the header off from this response and get my csv (comma separated value) list. If I include the html header etc, I would have to parse all that html from the message as well, which makes things more difficult. Let's get into the unrealscript.

Spawning the TCPLink Class

So to even use our tcplink, we have to spawn it somewhere (just like any other actor). I did this inside my PlayerController class.


//this goes in below our class declaration
var TcpLinkClient mytcplink;

//this goes in our PostBeingPlay() function
mytcplink = Spawn(class'TcpLinkClient'); //spawn the class
mytcplink.PC = self; //send it a reference to the player controller

I did this in the PostBeginPlay so it spawns right when play starts. Im not sending request messages at this point. Now lets look at the TCPLinkClient itself.

TCPLinkClient Class

/*
 * An example usage of the TcpLink class
 *  
 * By Michiel 'elmuerte' Hendriks for Epic Games, Inc.
 *  
 * You are free to use this example as you see fit, as long as you 
 * properly attribute the origin. 
 */ 

class TcpLinkClient extends TcpLink;

var PlayerController PC; //reference to our player controller
var string TargetHost; //URL or P address of web server
var int TargetPort; //port you want to use for the link
var string path; //path to file you want to request
var string requesttext; //data we will send
var int score; //score the player controller will send us
var bool send; //to switch between sending and getting requests

event PostBeginPlay()
{
    super.PostBeginPlay();
}

function ResolveMe() //removes having to send a host
{
	Resolve(targethost);
}

event Resolved( IpAddr Addr )
{
    // The hostname was resolved succefully
    `Log("[TcpLinkClient] "$TargetHost$" resolved to "$ IpAddrToString(Addr));
    
    // Make sure the correct remote port is set, resolving doesn't set
    // the port value of the IpAddr structure
    Addr.Port = TargetPort;
    
	//dont comment out this log because it rungs the function bindport
    `Log("[TcpLinkClient] Bound to port: "$ BindPort() );
    if (!Open(Addr))
    {
        //`Log("[TcpLinkClient] Open failed");
    }
}

event ResolveFailed()
{
    //`Log("[TcpLinkClient] Unable to resolve "$TargetHost);
    // You could retry resolving here if you have an alternative
    // remote host.

	//send failed message to scaleform UI
	JunHud(JunPlayerController(PC).myHUD).JunMovie.CallSetHTML("Failed");
}

event Opened()
{
	// A connection was established
	//`Log("[TcpLinkClient] event opened");
	//`Log("[TcpLinkClient] Sending simple HTTP query");
    
	//The HTTP GET request
	//char(13) and char(10) are carrage returns and new lines
	if(send == false)
	{
		SendText("GET /"$path$" HTTP/1.0");
		SendText(chr(13)$chr(10));
		SendText("Host: "$TargetHost);
		SendText(chr(13)$chr(10));
		SendText("Connection: Close");
		SendText(chr(13)$chr(10)$chr(13)$chr(10));
	}
	else if(send == true && score > 0)
	{		
		requesttext = "value="$score$"&submit=10987";

		SendText("POST /"$path$" HTTP/1.0"$chr(13)$chr(10));
		SendText("Host: "$TargetHost$chr(13)$chr(10));
		SendText("User-Agent: HTTPTool/1.0"$Chr(13)$Chr(10));
		SendText("Content-Type: application/x-www-form-urlencoded"$chr(13)$chr(10));
		//we use the length of our requesttext to tell the server
		//how long our content is
		SendText("Content-Length: "$len(requesttext)$Chr(13)$Chr(10));
		SendText(chr(13)$chr(10));
		SendText(requesttext);
		SendText(chr(13)$chr(10));
		SendText("Connection: Close");
		SendText(chr(13)$chr(10)$chr(13)$chr(10));
	}
	    
	`Log("[TcpLinkClient] end HTTP query");
}

event Closed()
{
    // In this case the remote client should have automatically closed
    // the connection, because we requested it in the HTTP request.
    `Log("[TcpLinkClient] event closed");
    
    // After the connection was closed we could establish a new
    // connection using the same TcpLink instance.
}

event ReceivedText( string Text )
{	
	// receiving some text, note that the text includes line breaks
	`Log("[TcpLinkClient] ReceivedText:: "$Text);
	
    //we dont want the header info, so we split the string after two new lines
    Text = Split(Text, chr(13)$chr(10)$chr(13)$chr(10), true);
	`Log("[TcpLinkClient] SplitText:: " $Text);
	
	//First we will recieve data from the server via GET
	if (send == false)
	{
		//send data to our UI
		JunHud(JunPlayerController(PC).myHUD).JunMovie.CallSetHTML(Text);
		send = true; //next time we resolve, we will send player score
	}
}

defaultproperties
{
    TargetHost="www.alexander-fisher.com"
    TargetPort=80 //default for HTTP
	path = "jun/index.php"
	score = 0;
	send = false;
}

Hopefully the comments explained everything fairly well. We have one function and the rest are events. It's run off events because communication with a server is sort of a waiting game. You have no idea how long it will take to get data back and forth between the client and server, so you have to wait for events to fire before you can continue. For example, we wouldn't want to process data before all of it has loaded, so we wait for an event to tell us when the data is done loading. You don't see the definition for the Resolve function because it exists in a parent class we are extending from.

Understanding the SendText Messages

So when we want to get data from the server, we call mytcplink.ResolveMe() in our PlayerController.

When the Opened() event is triggered, we send a message to the server via SendText() that looks like this:

GET /jun/index.php HTTP/1.0
Host: www.alexander-fisher.com
Connection: Close

and when the event ReceivedText() is triggered, the server's response message looks like this:

HTTP/1.1 200 OK
Date: Wed, 15 Sep 2010 04:39:26 GMT
Content-Type: text/html
Connection: close
Server: Apache
Content-Length: 82

41600,41550,39750,38850,32250,22550,20000,14414,10000,2500,1000,1000,900,800,123,2

So yay, we've gotten the data we want from the server. However, all I'm interested in is the csv list. I don't want the header information. If you have noticed, the header is separated from the content by two new lines. This is the standard and we will use this knowledge along with the Split() function to parse the server response so all we have left is the csv list.

Via the ASCII table, chr(13) is the carriage return and chr(10) is a new line. $ in unrealscript is concatenation (merges strings together)

//we dont want the header info, so we split the string after two new lines
Text = Split(Text, chr(13)$chr(10)$chr(13)$chr(10), true);

So now that we have our csv list in a string, we can either save it to a more permanent variable (a string within our tcplink class) or do what I did and send it to our HUD (which would parse it further and display the leaderboard). So we understand how to create a message to get data from the server. How do we send it?

If you've used html forms at all, you would know that you can send data to the server via the POST method. We will do the same. Since our send boolean is now set to true, the next time we call mytcplink.ResolveMe() in the PlayerController, our request message will look like this (using SendText() function again):

POST /jun/index.php HTTP/1.0
Host: www.alexander-fisher.com
User-Agent: HTTPTool/1.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
 
value=1234&submit=10987
Connection: Close

That looks very similar to what the server sends us as a response. Two new lines separate our header from the content. Most forms set submit to Submit. I use 10987 (random number) for security reasons. Because submit is not set to the default Submit, it would be very hard for someone else to send a POST request to the server that would actually update the database (my php doesn't just check if submit is set, it checks if it's specifically set to 10987). Obviously, we dont want people hacking our database or submitting outrageous scores.

Last thing to note: none of this is fast. Getting data from the server and parsing strings takes a good bit of time. In other words, you wouldn't want to send and receive data during each game tick or during a performance sensitive event.

So there you have it! If you gotten this far, congratulations! That was a long read. Hopefully I've given you enough information so that you can configure tcplink to work with your own setup. If not, leave a comment and hopefully we can figure it out. Thanks for reading!