Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro Visual C++-CLI And The .NET 2.0 Platform (2006) [eng]-1

.pdf
Скачиваний:
70
Добавлен:
16.08.2013
Размер:
24.18 Mб
Скачать

698 C H A P T E R 1 7 N E T W O R K P R O G R A M M I N G

One possible problem is that you exceed the maximum pending connection queue size that the machine supports. To stop this from happening, you must make sure that the value you pass is less than or equal to SocketOptionName::MaxConnections. Here is the code to set the maximum pending connection queue size:

socket->Listen((int)SocketOptionName::MaxConnections);

Caution Even though SocketOptionName::MaxConnections appears to be a value that you would get or set using the GetSocketOption() or SetSocketOption() method, you actually just use it like a constant. I cover socket options later in the chapter.

Accept the Connection

The accepting of a connection is not any more difficult than any of the preceding steps; it’s just one line of code:

Socket^ client = socket->Accept();

As you can see, you don’t have much in the way of options. But believe it or not, how this command is processed is crucial in determining whether the server processes one or multiple clients. The reason is that the Accept() method blocks. That is to say, it waits until it gets a connection from a client. What this means to the program is that, without more than one thread of execution, the program will stop cold on this method, waiting for a connection.

So how do you get around this? There are multiple ways people have implemented their code to address this. I will show you the easiest method here (at least I think it’s the easiest).

Place the Accepted Connection on Its Own Thread

Here is the simplest approach: Put the Accept() method in an infinite where loop and then create threads for each accepted client:

while(true)

{

Console::WriteLine("Waiting for client connection."); Socket^ client = tcpListener->Accept();

Thread ^thr = gcnew Thread(

gcnew ParameterizedThreadStart(server, &TcpServer::ProcessThread)); thr->Start(client);

}

With the addition of the ParameterizedThreadStart delegate in version 2.0 of the .NET Framework, things have gotten so easy. Just create a thread and pass on the newly accepted client socket. (Prior to version 2.0 you had to figure out some method of passing the client socket to the thread.)

You might want to review Chapter 16 if the above code looks strange to you, as I covered threads and ParameterizedThreadStart in quite a bit of detail in that chapter.

Now that there is an accepted client-server socket all set and ready, this is where things can get more complicated because now developers actually get a chance to do their own thing.

Send a Message

There are two ways of sending a message: either synchronously or asynchronously. I’ll cover asynchronous in detail later in the chapter, but here is the basic difference: Synchronous sending blocks

C H A P T E R 1 7 N E T W O R K P R O G R A M M I N G

699

until the message is sent, whereas asynchronous sending does not block and continues execution of the code without stopping for the send to complete.

To send a message synchronously, you use one of the following overloaded Send() methods:

Socket.Send (array<unsigned char>^)

Socket.Send (array<unsigned char>^, SocketFlags)

Socket.Send (array<unsigned char>^, int length, SocketFlags)

Socket.Send (array<unsigned char>^, int start, int length, SocketFlags)

As you can see, each just expands upon the parameters from the other. The first parameter is the unsigned byte array of the message being sent. The first added parameter is SocketFlags (for a server it will most likely be None). Next is the length of the message being sent, and finally comes the start point within the unsigned char array (use this if you want to start sending from someplace other than the actual start of the message array).

With version 2.0 of the .NET Framework, two additional Send() methods were added, both allowing for the sending of unsigned char data within Generic ILists:

Socket.Send (Generic IList)

Socket.Send (Generic IList, SocketFlags)

When sending a message from a server, I usually use

array<unsigned char>^ message = Encoding::ASCII->GetBytes("Successful connection");

client->Send(message);

when the message buffer length matches the length of the data being sent (as shown here), or I use

client->Send(message, messagelength, SocketFlags::None);

when the message buffer length does not match the length of the data being sent—for example, when a generic length buffer is populated by a variable-length message.

Receive a Message

Just as when you’re sending a message, you have two ways of receiving a message: synchronous or asynchronous. I’ll cover asynchronous receive in detail later in the chapter, but the basic difference is as follows: Synchronous receiving blocks until the message is received, whereas asynchronous receiving sets up an event that waits for the message to be received and then continues on without stopping. Then when the message is finally received, the previously set up event is triggered.

The Receive() method overloads are exactly the same as the sends:

int Socket.Receive (array<unsigned char>^)

int Socket.Receive (array<unsigned

char>^, SocketFlags)

int Socket.Receive (array<unsigned

char>^, int length, SocketFlags)

int Socket.Receive (array<unsigned char>^, int start, int length, SocketFlags)

The first parameter is the received unsigned byte array of the message. The next parameter is SocketFlags—for a server most likely None or Peek (Peek allows you to look into the buffer without actually taking it out). Next is the length of the message to extract from the receive stream, and finally comes the start point within the receiving unsigned char array (use this if you want to place the incoming message someplace other than the actual start of the message array).

700 C H A P T E R 1 7 N E T W O R K P R O G R A M M I N G

With version 2.0 of the .NET Framework, two additional Receive() methods were added, both allowing for the receiving of unsigned char data within Generic ILists:

Socket.Receive (Generic IList)

Socket.Receive (Generic IList, SocketFlags)

All receive methods return the number of unsigned char received or zero [0] if the connection was closed by the client. I use the zero [0] return value to my advantage as I use it to break out of my data input loops for each instance of a socket connection.

In the following simple example, since the number of unsigned chars being received is unknown (and also irrelevant), I use the following code to receive data:

if ((rcv = client->Receive(message)) == 0) break;

Normally, with more advanced servers you place the length of the following received message, formatted as an int, in the unsigned char array buf:

if (client->Receive(buf, 4, SocketFlags::Peek) > 0)

{

int length = BitConverter::ToInt32(buf, 0); buf = gcnew array<Byte>(length);

Then to actually receive the message you use a while loop:

int total = 0; int recv;

int dataLeft = length;

while (total < length) // TCP has an unprotected Message boundary

{

if ((recv = client->Receive(buf, total, dataLeft, SocketFlags::None)) == 0)

{

client->Close(); break;

}

total += recv; dataLeft -= recv;

}

Why is all of this code needed? Remember earlier I mentioned a gotcha? TCP simply sends a stream of data. There is a guarantee that the data will get to its destination and in order, but there is no guarantee that it will all get there at the same time. It is perfectly possible that half the sent message will get to the receiver process at the time the Receive() method is called. With the previous code, the Receive() method will read the rest of the message when it finally arrives. Likewise, it is possible that two messages will be received at one time. Thus, this process will allow the two messages to be split and processed separately (assuming that in your sent message you prefix the sent data with the number of bytes of data sent).

Example TCP Server

Okay, now that we have reviewed all the pieces, let’s see a complete TCP server example. Listing 17-1 is the de facto “Hello World” of network software development: the echo server. It takes in a stream of data from a client (which we will cover next), dumps it to the server console, and then sends the same message back to the client. Unlike most introductory versions of the echo, which show a server that can handle only one client at a time, I skipped ahead and have shown how to write the server so that it can process any number of concurrent (at the same time) clients.

C H A P T E R 1 7 N E T W O R K P R O G R A M M I N G

701

Listing 17-1. A TCP Server That Accepts Multiple Concurrent Clients

using namespace System; using namespace System::Net;

using namespace System::Net::Sockets; using namespace System::Threading; using namespace System::Text;

ref class TcpServer

{

public:

void ProcessThread(Object ^clientObj);

};

void TcpServer::ProcessThread(Object ^clientObj)

{

Socket^ client = (Socket^)clientObj;

IPEndPoint^ clientEP = (IPEndPoint^)client->RemoteEndPoint;

Console::WriteLine("Connected on IP: {0} Port: {1}", clientEP->Address, clientEP->Port);

array<unsigned char>^ msg = Encoding::ASCII->GetBytes( String::Format("Successful connection to the server on port {0}",

clientEP->Port));

client->Send(msg);

int rcv; while (true)

{

msg = gcnew array<unsigned char>(1024);

if ((rcv = client->Receive(msg)) == 0) break;

Console::WriteLine("Port[{0}] {1}",

clientEP->Port, Encoding::ASCII->GetString(msg, 0, rcv));

client->Send(msg, rcv, SocketFlags::None);

}

client->Close();

Console::WriteLine("Connection to IP: {0} Port {1} closed.", clientEP->Address, clientEP->Port);

}

void main()

{

TcpServer^ server = gcnew TcpServer();

Socket^ tcpListener = gcnew Socket(AddressFamily::InterNetwork, SocketType::Stream, ProtocolType::Tcp);

702 C H A P T E R 1 7 N E T W O R K P R O G R A M M I N G

IPEndPoint^ iped = gcnew IPEndPoint(IPAddress::Any, 12345); tcpListener->Bind(iped);

tcpListener->Listen((int)SocketOptionName::MaxConnections);

while(true)

{

Console::WriteLine("Waiting for client connection."); Socket^ client = tcpListener->Accept();

Thread ^thr = gcnew Thread(

gcnew ParameterizedThreadStart(server, &TcpServer::ProcessThread)); thr->Start(client);

}

}

I’ve already covered every bit of this code, but I would like to point out that this code has no way of exiting unless you kill the console (or press Ctrl-C). I did this so as so as not to add add any additional complexity to the network code in the example. There are many solutions to this problem, most involving event handling of keystrokes received on the server machine, but for this example, killing the window just suited it fine. When you run TcpServer.exe, you should get something like Figure 17-1.

Figure 17-1. The TCP server in action

The TCP Client

A TCP client is simpler than a TCP server, at least when it comes to establishing a connection. The code for processing a message, on the other hand, is just as simple or complex as that of the server, since they are mirror images of each other. In other words, when the server sends a message, the client receives it, and vice versa.

Only two tasks need to be performed by the client to establish a connection to a client:

1.Create a socket.

2.Connect to a server IPEndPoint.

The process of creating a TCP client socket is the same as that for a TCP server socket:

Socket^ socket = gcnew Socket(AddressFamily::InterNetwork,

SocketType::Stream,

ProtocolType::Tcp);

Also just like a TCP server, this constructor creates a socket to a version 4 IP address that supports reliable, two-way, connection-based byte streams without duplication of data and without preservation of boundaries using the TCP protocol.

Since there is nothing new here, let’s move on.

C H A P T E R 1 7 N E T W O R K P R O G R A M M I N G

703

Connect to a Server IPEndPoint

Connecting to a TCP server’s IPEndPoint starts with the creation of an IPEndPoint that points to the server. Just as you do with the server, you will probably create the IPEndPoint using

IPEndPoint^ iped = gcnew IPEndPoint(IPAddress::Parse("127.0.0.1"), port);

But there is nothing stopping you from using any of the myriad of other ways available to you. Look carefully at the code. It looks the same as that for the server, but there is a difference. Instead

of the IP address pointing to the local machine where the socket resides, it points to the IP address of the remote machine where you want the connection to be made.

Once you have an IPEndPoint that points to the server, all it takes to make a connection to the server is this:

try

{

server->Connect(iped);

}

catch (SocketException^ se)

{

Console::WriteLine("Connection to server failed with error: {0}", se->Message);

return;

}

Notice that I made the call to the Connect() method within a try/catch block. The reason is that if the connection attempt fails, then a SocketException is thrown. In the previous example I immediately give up, but in your code more than likely you will capture the exception, note it somehow, and then try again.

Example TCP Client

I’m going to move on to the TCP client example as there is no new code to explore when it comes to sending and receiving messages.

Listing 17-2 is just a simple program that connects to a TCP server, receives a connection message from the server, and then proceeds to send messages (which you type in from the console) to the server. After the message is sent, the program waits for the server to send (echo) it back.

Listing 17-2. A TCP Client

using namespace System; using namespace System::Net;

using namespace System::Net::Sockets; using namespace System::Threading; using namespace System::Text;

void main()

{

Socket^ server = gcnew Socket(AddressFamily::InterNetwork, SocketType::Stream, ProtocolType::Tcp);

try

{

IPEndPoint^ iped =

gcnew IPEndPoint(IPAddress::Parse("127.0.0.1"), 12345); server->Connect(iped);

}

704 C H A P T E R 1 7 N E T W O R K P R O G R A M M I N G

catch (SocketException^ se)

{

Console::WriteLine("Connection Failed with error: {0}", se->Message); return;

}

array<unsigned char>^ msg = gcnew array<unsigned char>(1024); int rcv = server->Receive(msg);

Console::WriteLine(Encoding::ASCII->GetString(msg, 0, rcv));

while (true)

{

Console::Write("Message ('q' to quit): "); String^ input = Console::ReadLine();

if (input->ToLower()->Equals("q")) break;

msg = Encoding::ASCII->GetBytes(input); server->Send(msg, msg->Length, SocketFlags::None);

msg = gcnew array<unsigned char>(1024); rcv = server->Receive(msg);

Console::WriteLine(Encoding::ASCII->GetString(msg, 0, rcv));

}

Console::WriteLine("Ended connection with server."); server->Shutdown(SocketShutdown::Both); server->Close();

}

Notice this time that unlike the server, the client does have a way of exiting cleanly.

Closing the Connection

Without a close process, a clean break between the server and the client is not possible, as once a connection is made the only clean way of closing the connection is by the client (as in this case) or the server executing a Close() method on the Socket.

What happens if you don’t call the Close() method and just exit the client? The answer is that the next time the server tries to do a read it throws a SocketException. Okay, you could just capture the exception, but that is not the cleanest way of shutting down the connection.

It is the Close() method that causes the Receive() method to receive a zero byte stream (along with some complicated hidden connection cleanup stuff that we don’t have to worry about).

This leaves the unexplained Shutdown() method. This method is designed to make the shutdown process cleaner as it disables the sender, receiver, or both sockets. Thus, it stops extraneous messages from being sent during the disconnection process.

Disconnecting from a Socket

What happens if you want to change the server being connected to partway through the process? You could close the connection and create a new one from scratch, or you can disconnect from the current socket using the Disconnect() method and then reconnect it to a new server.

C H A P T E R 1 7 N E T W O R K P R O G R A M M I N G

705

The Disconnect() method takes one parameter, a Boolean value that when set to true allows the socket to be reused. When the parameter is set to false the Disconnect() method acts like a Close() method. Here is a snippet of code showing the Disconnect() method in action:

client->Shutdown(SocketShutdown::Both); client->Disconnect(true);

if (client->Connected)

{

Console::WriteLine("Not good I'm still connnected!");

}

else

{

try

{

IPEndPoint^ iped =

gcnew IPEndPoint(IPAddress::Parse("127.0.0.1"), 12345); server->Connect(iped);

}

catch (SocketException^ se)

{

Console::WriteLine("Connection Failed with error: {0}", se->Message); return;

}

}

In this code I also show the Connected property that, as you can see, indicates whether a socket is currently connected.

Okay, now that you are dangerous when it comes to TCP, let’s move on and take a look at connectionless sockets and UDP (User Datagram Protocol), its most common method of being implemented.

When you run TcpClient.exe, you should get something like Figure 17-2.

Figure 17-2. The TCP client in action

Connectionless Sockets

Developing connectionless sockets code is still primarily based on the client-server architecture. However, a client-server architecture need not be as strictly enforced as it is with TCP, as once a socket is open it can send to and receive from many different sockets during the course of the socket’s lifetime.

Developing connectionless sockets code using UDP takes a different mind-set than developing connected socket code with TCP. There are primarily four reasons why this is so:

706C H A P T E R 1 7 N E T W O R K P R O G R A M M I N G

Data is sent in self-contained packages instead of a stream of data.

Network messages are not guaranteed to arrive in the same order as they were sent.

There is no guarantee that duplicated messages won’t arrive.

Network messages are not guaranteed to arrive at the destination.

So how does this change your mind-set? First off, since data comes in packages you don’t have to worry about the boundaries of the message being sent. In other words, when you read a UDP package, you know you have all the data that was sent for that particular package. That is a major plus in my book, as much of the code to implement TCP involves extracting data out of a stream.

But I guess there has to be a little bad with the good, because you are not guaranteed that messages will arrive in the order sent or, even worse, that messages will arrive at all. Many approaches have been developed to address these issues. Most of them involve a sequence number and either a positive or negative acknowledgment.

Personally, I have a simple approach to UDP coding. I read packages, and if the sequence number is greater than the last, I keep it and throw away the rest. How can I do this? I use UDP in only one scenario: computer games where messages come in fast and furious and if you miss one it doesn’t really matter since the next will fill you in on what you missed. For every other scenario, I use TCP. I have developed a simple implementation based on positive acknowledgments that allows a client to re-send specific messages if they did not arrive at the destination (but this is well out of the scope of this book).

What does this all mean to you? Use TCP unless order and missed packages are not significant (or you are really good at coding in UDP, in which case you are probably not reading this book).

UDP Server

There really isn’t much difference between the server and the client except that the server is waiting for packages from someplace, and a client is initiating the conversation and expecting some type of action from the server (though not necessarily a response package).

Only two tasks need to be performed by the server to create a location for a client to connect to:

1.Create a socket.

2.Bind the socket to an IPEndPoint.

The code for both of these is very similar to that for TCP.

Create a Socket

Just like with TCP, before you can do any UDP communication you need to create a socket through which the messages will flow. For a UDP connection there is only one constructor that you have to worry about:

Socket^ socket = gcnew Socket(AddressFamily::InterNetwork,

SocketType::Dgram,

ProtocolType::Udp);

This constructor creates a socket to a version 4 IP address that supports connectionless, unreliable messages (messages might be lost or duplicated, or arrive out of order) of a fixed (usually small) maximum length using the UDP protocol.

C H A P T E R 1 7 N E T W O R K P R O G R A M M I N G

707

Bind the Socket to an IPEndPoint

There is no difference in creating an IPEndPoint for either TCP or UDP. Because of the nature of UDP, you will probably use the IPEndPoint frequently. The reason is that you need an EndPoint class to send and receive data, and one of the easiest ways to create an EndPoint is to create an IPEndPoint and then typecast it to the EndPoint.

As you’ll recall from our earlier discussion, you will most likely use one of the following methods to create an IPEndPoint:

IPEndPoint^ iped = gcnew IPEndPoint(IPAddress::Any, portnumber);

IPEndPoint^ iped = gcnew IPEndPoint(IPAddress::Parse("127.0.0.1"), portnumber);

TCP and UDP have different purposes for binding to a socket. For TCP, you are creating one endpoint of a corridor between two specific endpoints. For UDP, on the other hand, you are creating a two-way door into your system from which you can communicate with any other system and any other system can communicate with your system.

All you need to know to send a package with another system is that system’s IPEndPoint and the communication protocol used by that system. The reverse is also true; for another system to communicate with your system, all it needs to know is your system’s IPEndPoint and your system’s communication protocol.

The communication protocol can be simple as the echo system (what I get, I will send back), as extremely complex as a multiplayer gaming system (passwords, multiple packet formats, system states, etc.), or anything in between.

By the way, to bind to a socket in UDP you simply call the following code:

socket->Bind(iped);

Receive a Message

One of the best aspects of UDP is that when you receive a message packet, it is the complete package. (You just have to remember that the order, the number, and even whether you get all the sent messages are always in question.)

Another good feature of the UDP receive method is that you are not restricted to only one source of messages but instead can receive a message from any UDP sender, as long as the sender knows the receiver’s IPEndPoint. Because of this, there is no need to spawn threads to handle all connections to the server. An IPEndPoint, and therefore a single thread, can handle all incoming messages from all clients.

The actual code for the ReceiveFrom() method that is used to receive messages using UDP is a bit more involved than that of the connected Receive() method, for two reasons.

First, you need to allocate a buffer to be populated by the ReceiveFrom() method. Be aware that if you specify a buffer that is too small, then the ReceiveFrom() method will fill as much data as it can in the buffer, discard all the extra unread data of the packet, and then throw a SocketException.

Second, due to the fact that the ReceiveFrom() method can get messages from any client, the method needs some way of providing the origin of the message. To accomplish this, an EndPoint is created and passed as a parameter to the ReceiveFrom() method. Then, when the ReceiveFrom() method is executed, the passed EndPoint receives the IPEndPoint of the sending client.

This may sound complex, but as you can see from the following code, it is anything but:

array<unsigned char>^ message = gcnew array<unsigned char>(1024); EndPoint^ Remote = (EndPoint^) gcnew IPEndPoint(IPAddress::Any, 0); int recv = socket->ReceiveFrom(message, Remote);