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

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

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

718 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

void main()

{

IPEndPoint^ ipep = gcnew IPEndPoint(IPAddress::Any, 54321); UdpClient^ server = gcnew UdpClient(ipep);

Console::WriteLine("Waiting for client connection.");

array<unsigned char>^ message;

while(true)

{

IPEndPoint^ Remote = gcnew IPEndPoint(IPAddress::Any, 0); message = server->Receive(Remote);

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

Remote->ToString(), Encoding::ASCII->GetString(message, 0, message->Length));

server->Send(message, message->Length, Remote);

}

}

Listing 17-10. A UDP Client Using UdpClient

using namespace System; using namespace System::Net;

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

void main()

{

UdpClient^ client = gcnew UdpClient();

IPEndPoint^ Remote =

gcnew IPEndPoint(IPAddress::Parse("127.0.0.1"), 54321);

while (true)

{

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

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

array<unsigned char>^ message = Encoding::ASCII->GetBytes(input); client->Send(message, message->Length, Remote);

message = client->Receive(Remote); Console::WriteLine("[{0}] {1}",

Remote->ToString(),

Encoding::ASCII->GetString(message, 0, message->Length));

}

}

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

719

There is not much difference between client and server, is there? In Listing 17-10, I threw in the UdpClient class’s Send() method’s ability to auto-bind to a port, but you could have just as easily used a UdpClient constructor with more information so that the constructor itself would bind to the port. Just remember that if you do this, the client and IP address and the port pairs must be different.

Changing Socket Options

I guess I’m kind of obligated to cover socket options here, as I did mention them in the caution way up near the start of the chapter. In most programs you write, you will not normally have to worry about the options on a socket. In fact, nearly all of the options are beyond the scope of this book. But, on those occasions that the defaults need to be tweaked or retrieved, the Socket class provides you with the aptly named methods SetSocketOption() and GetSocketOption().

The SetSocketOption() method has four different overloads. The reason is that different options require different data types to be set. Thus, each overload provides one of these data types:

void SetSocketOption(SocketOptionLevel, SocketOptionName, Boolean)

void SetSocketOption(SocketOptionLevel, SocketOptionName, array<Byte>^)

void SetSocketOption(SocketOptionLevel, SocketOptionName, Int32)

void SetSocketOption(SocketOptionLevel, SocketOptionName, Object^)

As you can see, each of these methods has two parameters in common: SocketOptionLevel, which specifies what level of socket to apply the set to (IP, IPv6, Socket, Tcp, or Udp), and SocketOptionName, which specifies which option to set. There are quite a few options that you can tweak, if you feel adventurous. Personally, I only recall using Linger, which keeps the socket open if unsent data exists, and ReceiveTimeout, which specifies how long to wait on a receive command before giving up and throwing an exception.

The GetSocketOption() method is also overloaded but only three times:

object GetSocketOption(SocketOptionLevel, SocketOptionName)

void GetSocketOption(SocketOptionLevel, SocketOptionName, array<Byte>^ value)

array<Byte>^ Socket::GetSocketOption(SocketOptionLevel, SocketOptionName, Int32)

Just like the SetSocketOption() method, the first two parameters are SocketOptionLevel and SocketOptionName. In most cases, you use the first version of the GetSocketOption() method, but for those options that deal in byte arrays the other two versions are also available.

Listing 17-11 is an example of using the ReceiveTimeout option with UDP. You might find this option helpful if you want a simple way to help check that a package was sent successfully, by way of having the receiver of the package immediately send back an acknowledgment package. Since you have a timeout set on the ReceiveFrom() method, if the acknowledgment package doesn’t come back in a timely fashion you know one of two things: The package was never received or the acknowledgment package was lost. (I never said it would check that the package was sent successfully, but only that it would help in checking.)

Listing 17-11. A UDP Client with a Timeout

using namespace System; using namespace System::Net;

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

void main()

{

Socket^ socket = gcnew Socket(AddressFamily::InterNetwork, SocketType::Dgram, ProtocolType::Udp);

720 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

EndPoint^ Remote = gcnew IPEndPoint(IPAddress::Parse("127.0.0.1"), 54321);

if ((int)socket->GetSocketOption(SocketOptionLevel::Socket, SocketOptionName::ReceiveTimeout) < 5000)

{

socket->SetSocketOption(SocketOptionLevel::Socket, SocketOptionName::ReceiveTimeout, 5000 );

}

while (true)

{

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

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

array<unsigned char>^ message = Encoding::ASCII->GetBytes(input); socket->SendTo(message, Remote);

message = gcnew array<unsigned char>(1024); try

{

int recv = socket->ReceiveFrom(message, Remote); Console::WriteLine("[{0}] {1}",

Remote->ToString(), Encoding::ASCII->GetString(message, 0, recv));

}

catch (SocketException^)

{

Console::WriteLine("Receive failed with a time out."); Console::WriteLine("Make sure server is running.");

}

}

}

In the code, the use of GetSocketOption() is redundant as the default value is 0, but I wanted to show an example of it being used.

One thing that threw me is that the ReceiveFrom() method throws a SocketException if no socket is bound to the IPEndPoint it is expecting to receive data from. I first thought that the timeout was working, but when I extended the timeout value, the SocketException still happened immediately. It wasn’t until I had the server bind to the socket that the timeout started working properly.

Asynchronous Sockets

It is time to change gears and look at another way of coding network programs. In all of the previous examples when the program called a network function, the program blocked (stopped/suspended) until the network function returned or timed out. In many programs this is just fine, and with multithreading that’s usually all you need.

But there will come a time when you will need the program to not stop/suspend when it encounters a network function, and in those cases you will use asynchronous network functions. (In previous versions of .NET, you would refer to this as asynchronous socket functions, but with version 2 of the

.NET Framework TcpListener, TcpClient, and UdpClient were expanded to support asynchronous

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

721

functionality. Yeah, I know down in their depths these three are socket code as well, so if you want to be picky I guess you can use the term asynchronous socket function and be completely correct.)

Asynchronous functions cause the execution of the code to be broken into two threads. When an asynchronous function is called, the processing of the network functionality breaks off and runs in another thread, while the application continues to run on the original thread. Then when the network functionality thread completes, you process any results in a callback function.

There really isn’t anything that special about writing asynchronous network code; once you figure out how to do it for one asynchronous method, then you know how to do it for them all. The reason is that you code all asynchronous methods in almost exactly the same way.

Asynchronous methods are basically synchronous methods divided into two parts: the BeginXxx() method, which specifies the callback method and causes the thread to split, and the EndXxx() method, which processes the callback method when the network functionality completes.

Accepting Connections

As with synchronous connection-oriented code, you need to set up a Socket or TcpListener so that it can accept connections. There is no asynchronous method for the process of creating a socket or TcpListener; therefore, you use the same code as you did for your synchronous code. This makes sense because this code is not dependent on a remote client.

The first step, in which a server starts to communicate with a client (an extensive wait may occur while this communication process occurs), is the accept stage. You have three options when accepting connections:

The Socket class’s BeginAccept() method

The TcpListener class’s BeginAcceptSocket() method

The TcpListener class’s BeginAcceptTcpClient() method

All three of these methods have overloaded parameter sets similar to their synchronous equivalent, with the addition of two more parameters: a handle to the AsyncCallback method, which gets executed when the accept completes, and a handle to an Object class to hold information to pass from the Begin method to the End method. In addition, all three methods also return a handle to an IAsyncResult class. (You probably will not need to use this return value.)

To invoke the BeginAccept() method, you must first create a socket and the AsyncCallback method to handle the results of the accept operation. You have seen the steps to create a socket earlier (in our discussion of connection-oriented sockets), so I won’t repeat myself here. Creating an AsyncCallback, on the other hand, is new. The AsyncCallback has two constructors. Which you use depends on whether the actual callback method is a static method:

AsyncCallback^ method = gcnew AsyncCallback(&TcpServer::AcceptCB);

or a member method:

AsyncCallback^ method = gcnew AsyncCallback(server, &TcpServer::AcceptCB);

Normally, you will just embed this code directly in the BeginAccept() method call like this:

socket->BeginAcceptSocket(gcnew AsyncCallback(&TcpServer::AcceptCB), socket);

The actual callback method (AcceptCB in this case) looks like this:

void TcpServer::AcceptCB(IAsyncResult^ iar)

{

//...

}

722 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

where AcceptCB is declared as one of the following:

public:

void AcceptCB(IAsyncResult^ iar);

or

public:

static void AcceptCB(IAsyncResult^ iar);

When the BeginAccept() method is called, it creates a new thread to wait on the completion of a socket accept and then lets the original thread continue on its merry way. When the socket accept finally completes, the program now has two threads running concurrently: the original thread, plus the socket’s accept thread, which starts to execute (as far as you are concerned anyway) from the beginning of the callback method.

The first thing you would normally do in the callback method is get back the socket that the original BeginAccept() method was run on. You get this from the AsyncState property on the IAsyncResult parameter of the callback method. This value is there because you passed it as a parameter of the BeginAccept() method.

TcpListener^ tcpListener = (TcpListener^)iar->AsyncState;

Now that you have the original socket you can call the EndAccept() method to get the accepted socket and finish off the accept operation:

Socket^ client = tcpListener->EndAccept(iar);

Now comes the tricky part. Remember you have two threads running, but unlike synchronous sockets the main thread has no knowledge of the newly accepted client; therefore, the main thread cannot handle the socket sends or receives without jumping through some hoops (I’ve never explored how to do this but you are free to explore on your own).

What I do instead is use the new thread to handle the sends and receives and basically let the original thread do whatever it was doing. What ultimately happens is that a callback method throws off a chain of calls to other callbacks and then exits gracefully. However, the number of threads spawned can get large, and in the case of an error, you have to figure out what thread went wrong.

Connecting to a Connection

A client using asynchronous code must connect to a server just like its synchronous counterpart. The difference as I’m sure you suspect is that you will use the BeginConnect()/EndConnect() method pair instead of the Connect() method. Also, just like the server, there is no asynchronous method for the process of creating a socket or TcpClient; therefore, you use the same code as you did for your synchronous code.

The first step, in which a client starts to communicate with a server (again, an extensive wait may occur while this communication process occurs), is the connection stage. You have two options when connecting:

The Socket class’s BeginConnect() method

The TcpClient class’s BeginConnect() method

As I said in the beginning of this section, once you know how to use one asynchronous method you know how to use them all. Just like the BeginAccept() method, the BeginConnect() method has overloaded parameter sets similar to their synchronous equivalent with the addition of two more parameters: a handle to the AsyncCallback method and a handle to an Object class (in which you should place the socket handle). Both methods also return a handle to a c. For example:

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

723

IAsyncResult^ ret =

socket->BeginConnect(iep, gcnew AsyncCallback(&TcpClient::ConnectCB), socket);

When the connection operation completes, the callback method is executed (on its own thread):

void TcpClient::ConnectCB(IAsyncResult^ iar)

{

//...

}

The first thing you do is get the Socket that was used to call the BeginConnect() method. You get this from the AsyncState property on the IAsyncResult parameter of the callback method:

Socket^ socket = (Socket^)iar->AsyncState;

Next, you execute the EndConnect() method, usually in a try/catch block, to complete the connection process:

try

{

socket->EndConnect(iar);

}

catch (SocketException^ se)

{

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

}

Disconnecting from a Connection

Your client applications have available to them only one asynchronous disconnect method pair from which you can reconnect to other servers. As with all asynchronous methods, you initiate the disconnect with the Begin method, in this case BeginDisconnect(). The BeginDisconnect() takes three parameters—the Boolean value that you specify if the socket will be reused, a handle to the AsyncCallback method, and a handle to an Object class—and returns an IAsyncResult. (The last two methods and the return value should, by now, look fairly familiar.)

IAsyncResult^ ret =

socket->BeginDisconnect(true, gcnew AsyncCallback(&TcpClient::DisconnectCB), socket);

When the disconnect operation completes, the callback method is executed (on its own thread):

void TcpClient::DisconnectCB(IAsyncResult^ iar)

{

//...

}

The first thing you do (like with any other asynchronous callback) is get the Socket that was used to call the BeginDisconnect() method. You get this from the AsyncState property on the IAsyncResult parameter of the callback method:

Socket^ socket = (Socket^)iar->AsyncState;

Next, you execute the EndDisconnect() method, thus completing the disconnect process:

socket->EndDisconnect(iar);

724 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

Sending a Message

You have three options when it comes to sending messages asynchronously:

The Socket class’s BeginSend() method

The Socket class’s BeginSendTo() method

The UDPClient class’s BeginSend() method

All three of these methods have overloaded parameter sets similar to their synchronous equivalent, with the addition of two more parameters: a handle to the AsyncCallback method and a handle to an Object class. All three methods also return a handle to an IAsyncResult class. Here’s an example:

IAsyncResult^ ret =

client->BeginSend(msg, 0, msg->Length, SocketFlags::None,

gcnew AsyncCallback(&TcpServer::SendCB), client);

When the send operation completes, the callback is executed. Within the callback you will get the socket and then execute the EndSend() method:

void TcpServer::SendCB(IAsyncResult^ iar)

{

Socket^ client = (Socket^)iar->AsyncState; client->EndSend(iar);

}

Receiving a Message

Like the asynchronous send, the receive has three options:

The Socket class’s BeginReceive() method

The Socket class’s BeginReceiveFrom() method

The UDPClient class’s BeginReceive() method

All three of these methods have overloaded parameter sets similar to their synchronous equivalent, along with two more parameters: a handle to the AsyncCallback method and a handle to an Object class. All three methods also return a handle to an IAsyncResult class.

One thing that is different about asynchronous receive is that you should not pass the socket in the final parameter of the Socket class asynchronous methods. (Send the socket in the UdpClient version as you would normally.) Instead, you send a custom class that is made up of a handle to the socket and a handle to the message buffer to receive the message. Here is an example:

ref class StateObject

{

public:

property int bufSize; property Socket ^workSocket;

property array<unsigned char>^ message;

StateObject(Socket^ sock, int bufsize)

{

workSocket = sock; bufSize = bufsize;

message = gcnew array<unsigned char>(bufsize);

}

};

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

725

The reason for this is that the receive callback method needs both of these handles to run correctly. Here’s how you would call the BeginReceive() method:

StateObject^ so = gcnew StateObject(client, 1024); client->BeginReceive(so->message, 0, so->bufSize, SocketFlags::None,

gcnew AsyncCallback(&TcpServer::ReceiveCB), so);

Now, when the receive operation completes, the callback is executed just like any other asynchronous callback, but this time, instead of just grabbing the socket from the AsyncState property on the IAsyncResult parameter, you grab the StateObject and then get the socket and the message buffer from it:

void TcpServer::ReceiveCB(IAsyncResult^ iar)

{

StateObject^ so = (StateObject^)iar->AsyncState; Socket^ client = so->workSocket;

int rcv;

if ((rcv = client->EndReceive(iar)) > 0) // get message

{

//... the received data is in: so->message

}

else // connection closed

{

client->Close();

}

}

Asynchronous TCP Server

Okay, let’s take one last look at the TCP server in Listing 17-12. This time I’ve rewritten it so that it uses asynchronous methods. The functionality is exactly the same as the synchronous version. In fact, you can use the TCP clients that you wrote earlier to connect to it.

Personally, I find following the logic of asynchronous code a little more complex than that of synchronous and prefer not to use it. The only benefit I see of this version over my original is that you don’t have to maintain the threads of the program yourself.

The example program relies heavily on asynchronous callback chaining. Here is the basic outline of how the program runs:

1.The main program calls accept, then waits for a return key to end the program.

2.Accept calls send, receive, and then recalls accept. Finally the program exits and ends the thread.

3.Send ends without calling anything, thus ending the thread.

4.Receive either calls send and then recalls receive, or it closes the connection. Finally, the method ends, ending the thread.

What ultimately results is a threaded loop that accepts new clients and multiple threaded loops that receive messages for each client.

Listing 17-12. A TCP Server Asynchronous Style

using namespace System; using namespace System::Net;

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

726 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

using namespace System::Text;

ref class StateObject

{

public:

property int bufSize; property Socket ^workSocket;

property array<unsigned char>^ message;

StateObject(Socket^ sock, int bufsize)

{

workSocket = sock; bufSize = bufsize;

message = gcnew array<unsigned char>(bufsize);

}

};

ref class TcpServer

{

public:

static void AcceptCB(IAsyncResult^ iar); static void SendCB(IAsyncResult^ iar); static void ReceiveCB(IAsyncResult^ iar);

};

void TcpServer::AcceptCB(IAsyncResult^ iar)

{

TcpListener^ tcpListener = (TcpListener^)iar->AsyncState; Socket^ client = tcpListener->EndAcceptSocket(iar);

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

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

// Send socket successful connection message array<unsigned char>^ msg = Encoding::ASCII->GetBytes(

String::Format("Successful connection to the server on port {0}", clientEP->Port));

client->BeginSend(msg, 0, msg->Length, SocketFlags::None, gcnew AsyncCallback(&TcpServer::SendCB), client);

// Get message from client

StateObject^ so = gcnew StateObject(client, 1024); client->BeginReceive(so->message, 0, so->bufSize,

SocketFlags::None, gcnew AsyncCallback(&TcpServer::ReceiveCB), so);

// Get the next socket connection

Console::WriteLine("Waiting for client connections. [Return to Exit]"); tcpListener->BeginAcceptSocket(gcnew AsyncCallback(&TcpServer::AcceptCB),

tcpListener);

}

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

727

void TcpServer::SendCB(IAsyncResult^ iar)

{

Socket^ client = (Socket^)iar->AsyncState; client->EndSend(iar);

}

void TcpServer::ReceiveCB(IAsyncResult^ iar)

{

StateObject^ so = (StateObject^)iar->AsyncState; Socket^ client = so->workSocket;

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

int rcv;

if ((rcv = client->EndReceive(iar)) > 0) // get message

{

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

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

// echo message

client->BeginSend(so->message, 0, rcv, SocketFlags::None, gcnew AsyncCallback(&TcpServer::SendCB), client);

// set up for next receive

so = gcnew StateObject(client, 1024); client->BeginReceive(so->message, 0, so->bufSize,

SocketFlags::None, gcnew AsyncCallback(&TcpServer::ReceiveCB), so);

}

else // connection closed

{

client->Close();

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

}

}

void main()

{

TcpListener^ socket = gcnew TcpListener(IPAddress::Any, 12345); socket->Start();

Console::WriteLine("Waiting for client connections. [Return to Exit]"); socket->BeginAcceptSocket(gcnew AsyncCallback(&TcpServer::AcceptCB),

socket);

// Exit on return key Console::ReadLine();

}

I added comments to the code to help you walk through. As you can see, asynchronous network programming can get complex fast.

When you run TcpServer_Async.exe, you should get something like Figure 17-5.