Introduction
A client-server application with sockets and IO to play the Navy Battle game.
Before you start to read
This article is the second part of a two-article series. The first part covers how to draw a navy battle battlefield. The second part covers client-server connection and how to draw the coordinates in the right places. To better understand this article, you should know a little bit of socket connections (for the part 2), the namespace System.Drawing
and IO (for the part 2 too). If you don't have any idea what I'm talking about, I recommend you read the Deitel's book "C# How to Program". But if you don't have money, the documentation in the MSDN is a great help. The great deal is you to have the book and to navigate in the MSDN.
We're back again
We're back again, now to explain the client-server connection and how to draw the "X"s in the battlefield.
The server
In a client-server connection, we have to follow some steps. Below are the server steps:
- Create the listener object;
- Start the object;
- Associate it to a socket to make the transaction data;
- Send and receive data;
- And finally, close the connection when all the things are done;
In a client-server application, the methods that start the server or client must be put in threads. You must do this because the server stays listening until a connection is made, and when the client application starts, the server catches the connection request made by the client and the magic is done.
OK, now I put here the constructor of my class:
public Server(Graphics g)
{
G = g;
IPAddress IPLocalhost = IPAddress.Parse("127.0.0.1");
ServerListener = new TcpListener(IPLocalhost, 65000);
ServerListener.Start();
}
You have noticed that in my constructor I pass a Graphics
parameter called "g
". Why? The explanation is in the "Desenho
" object which is in the Running()
method.
public void Running()
{
try
{
while(true)
{
Connexion = ServerListener.AcceptSocket();
if(Connexion.Connected)
{
DataStream = new NetworkStream(Connexion);
Reader = new BinaryReader(DataStream);
Writer = new BinaryWriter(DataStream);
m_bConnect = true;
do
{
sData = Reader.ReadString();
using(Draw Desenho = new Draw())
{
Desenho.GetCoordinates(sData+
Form1.stCoordinates, G, true);
}
}while(DataStream != null &&
sData != "Close connection");
}
}
Reader.Close();
Writer.Close();
DataStream.Close();
Connexion.Close();
Application.Exit();
}
catch(SocketException SocketEx)
{
MessageBox.Show(SocketEx.Message);
}
This method has a try
/catch
block that has a socket which receives a socket object returned by the AcceptSocket()
method of my TcpListener
object. It verifies if the connection is established accessing the Connected
property and creates the datastream and the reader and the writer. What about the Graphics g
parameter? In the do while
loop, there is a statement: using
. This statement guarantees that the object is released when you have done all the necessary things with it. Notice that there is a Draw
object called Desenho
. The GetCoordinates
method of the Desenho
object is called and one of the parameters is the Graphics
object.
Caution: It's not a good idea to create a variable of type Graphics
. Like Jesse Liberty says: "Use the Graphics
object when necessary and release it." I use this guy only in my GetCoordinates
method and it's automatically released when the using
statement is finished.
The Client
Now, let's see the steps for the client:
- Create the object;
- Obtain the stream where the data will be sent and received;
- Send and receive data;
- Close the connection;
The steps for the client are not so different from the server. The client establishes a communication with the server sending a request. The client request acts like a connector, and the socket server, like a tomade. It's very simple. Now, we analyze the constructor and the RunningClient()
method.
Look at this piece of code:
public Client(Graphics G)
{
Gr = G;
}
public void RunningClient()
{
cClient = new TcpClient();
try
{
cClient.Connect("localhost", 65000);
DataStream = cClient.GetStream();
Reader = new BinaryReader(DataStream);
Writer = new BinaryWriter(DataStream);
do
{
sData = Reader.ReadString();
using(Draw Desenho = new Draw())
{
Desenho.GetCoordinates(sData +
Form1.stCoordinates, Gr, true);
}
} while(DataStream != null && sData != "Close connection");
Reader.Close();
Writer.Close();
DataStream.Close();
cClient.Close();
Application.Exit();
}
catch(SocketException ex)
{
MessageBox.Show(ex.Message);
}
}
Very probably, you've noticed that this method and the Running
method of the class Server
are much equal. And you're right. But there are a few differences. Look at this piece of code:
public void RunningClient()
{
cClient = new TcpClient();
try
{
cClient.Connect("localhost", 65000);
DataStream = cClient.GetStream();
}
Notice that the client constructor receives a Graphics
parameter, like the server constructor. Notice too that the TcpClient
object doesn't have a Start
method. As it is a client, it sends to the server a connection request with the Connect()
, while in the server there is a Start()
that listens if a client wants to establish a connection (put in a while
loop). When the connection is established, the client calls the GetStream()
method to obtain the NetworkStream
that the data will be transferred to. This information is assigned in the DataStream
object. So, the transfer will occur.
The GetCoordinates Method
This method is so important in our Navy Battle. It's used in the server as in the client. It receive three parameters: a Graphics
parameter, a string with coordinates, and a boolean. The Graphics
parameter is used only to be passed to a private
method called DrawX()
. The boolean variable is used to see if the player has inserted the coordinates to populate his battlefield or he inserted a coordinate to hit the enemy. The string variable contains the coordinates that populate the battlefield of the current user or the coordinates that the current user chooses to hit his enemy. Below is the method:
public void GetCoordinates(string sCoordinates,
Graphics G, bool bInserted)
{
int[] iLocations = new int[sCoordinates.Length];
int iLength = sCoordinates.Length;
string sReceive;
string sCompare = sCoordinates.Substring(0,2);
m_bWhatX = bInserted;
for(int i = 0; i < iLength ; i++)
{
sReceive = sCoordinates.Substring(i,1);
switch (sReceive)
{
case "A":
iLocations[i] = 90;
break;
case "a":
iLocations[i] = 90;
break;
case "B":
iLocations[i] = 140;
break;
case "b":
iLocations[i] = 140;
break;
case "C":
iLocations[i] = 190;
break;
case "c":
iLocations[i] = 190;
break;
case "D":
iLocations[i] = 240;
break;
case "d":
iLocations[i] = 240;
break;
case "E":
iLocations[i] = 290;
break;
case "e":
iLocations[i] = 290;
break;
case "1":
iLocations[i] = 40;
break;
case "2":
iLocations[i] = 90;
break;
case "3":
iLocations[i] = 140;
break;
case "4":
iLocations[i] = 190;
break;
case "5":
iLocations[i] = 240;
break;
}
}
for(int i = 0 ; i <= 8 ; i+=2)
{
if(!bInserted)
{
DrawX(iLocations[i+1], iLocations[i], G);
}
else
{
sCoordinates = sCoordinates.Remove(0,2);
while(true)
{
if(sCompare == sCoordinates.Substring(i,2))
{
DrawX(iLocations[i+1], iLocations[i], G);
MessageBox.Show("Acertou");
break;
}
else
{
DrawX(iLocations[i+1], iLocations[i], G);
break;
}
}
break;
}
}
}
Don't be scared with the size of this method. It's simple. The method gets each character of the string and puts it in a switch
statement to populate the interger array iLocations
that is used to save all the coordinates that will be used by the DrawX()
method. The numbers that iLocation
array holds, are used by the DrawX()
method to draw the "X"" in the center of some square, which is specified by the string sCoordinates
. Then the boolean bInserted
is used to say if the DrawX()
method draws a single coordinate (that is the coordinate send by the enemy) or draws the coordinates that the current user chooses to put in their battlefield.
The DrawX method
Finally, we will talk about the DrawX()
method. It is very, very simple. Look for yourself:
private void DrawX(int iX, int iY, Graphics G)
{
Pen pColor;
if(!m_bWhatX)
{
pColor = new Pen(Color.Red, 5);
}
else
{
pColor = new Pen(Color.Blue, 5);
}
Point pA = new Point(iX, iY);
Point pB = new Point(iX+30, iY+30);
Point pA1 = new Point(iX+30, iY);
Point pB1 = new Point(iX, iY+30);
G.DrawLine(pColor,pA,pB);
G.DrawLine(pColor,pA1,pB1);
}
Remember that this method draws all the coordinates send by the client populating the battlefield or draws the coordinate send by the enemy. This is done using the boolean variable m_bWhatX
that receives the value of the bInserted
parameter. I put the server's Pen
objects commented just for you to know what "X" each application draws. Do you see why this method is so simple?
Thank you again
Man, I hope that you enjoyed this article and the first part of it. I loved to write this article (it's my first time). If you've liked or if you've hated it, send me an email with your comments, doubts, corrections, or new ideas. I want to tell you something: if you want to be a good programmer, you need to do one thing: study, a lot. I said this from my own experience.
Article's History
- 07-16-2005 - The beginning of the second part of this article series.
- 07-26-2005 - I stopped in between for a while, but finally, the second part is finished. Revision of both parts done and the complete package sent to The Code Project.