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

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

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

708 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

Notice that I use the IPEndPoint constructor to create an EndPoint. You must do this as the EndPoint class is abstract and you cannot directly create an instance of it.

To receive a message, you use one of the following overloaded ReceiveFrom() methods:

Socket.ReceiveFrom(array<unsigned char>^, EndPoint)

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

Socket.ReceiveFrom(array<unsigned char>^, int, SocketFlags, EndPoint)

Socket.ReceiveFrom(array<unsigned char>^, int, int, SocketFlags, EndPoint)

Again, each just expands upon the other. The first parameter is the unsigned char array of the message being received, and the last parameter is the EndPoint of the sender. The first added parameter is SocketFlags (most likely None); next is the size of the message to be received; and finally we have the start point within the unsigned char array (use this if you want to place the received message someplace other than the actual start of the message array).

Just like the connected Receive() method, the ReceiveFrom() method returns the number of bytes received. But unlike the connected Receive() method, the unconnected ReceiveFrom() method does not receive any message when a client closes its IPEndPoint. Since this is the case, if you need your server (or client) to be aware of the demise of its opposite IPEndPoint, you must send some type of message to notify the server or client of this fact.

Send a Message

Just as when receiving a message, to send a message you need an EndPoint. To acquire an EndPoint, you will most likely use one created from scratch using an IPEndPoint constructor:

EndPoint^ Remote = gcnew IPEndPoint(IPAddress::Parse("127.0.0.1"), 54321); array<unsigned char>^ message = Encoding::ASCII->GetBytes("Message"); socket->SendTo(message, Remote);

or use an EndPoint received from a ReceiveFrom() method:

socket->ReceiveFrom(inMessage, Remote);

array<unsigned char>^ outMessage = Encoding::ASCII->GetBytes("Message"); socket->SendTo(outMessage, Remote);

Kind of convenient, don’t you think?

One cool thing about the UDP SendTo() method is that you can send it to many different EndPoints. Thus, you can use the same block of code to send the same message to multiple clients (or servers).

The SendTo() method overloads are exactly the same as with the ReceiveFrom() method:

Socket.SendTo(array<unsigned char>^, EndPoint)

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

Socket.SendTo(array<unsigned char>^, int, SocketFlags, EndPoint)

Socket.SendTo(array<unsigned char>^, int, int, SocketFlags, EndPoint)

Once again, each just extends from the other. The first parameter is the unsigned char array of the message being received; the last parameter is the EndPoint of the destination of the message. The first added parameter is SocketFlags (most likely None); next is the size of the message to be sent; and next is 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).

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

709

Example UDP Server

Now that we have all the pieces, let’s take a look at Listing 17-3, another example of an echo server but this time using connectionless UDP.

Listing 17-3. A UDP Server That Accepts Multiple Concurrent Clients

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);

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

socket->Bind(ipep);

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

while(true)

{

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

int recv = socket->ReceiveFrom(message, Remote);

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

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

socket->SendTo(message, recv, SocketFlags::None, Remote);

}

}

The first thing you’ll probably notice is that the code contains no special logic to handle multiple concurrent clients. The second thing you’ll notice is that there is no logic to handle missing, duplicate, or wrong-order messages. As I mentioned earlier, I usually ignore the problems since I don’t use UDP when message reliability is needed. If it is, I use TCP.

Also note that there is no way to exit the main loop other than killing the application or pressing Ctrl-C on the console. This is also by design (and to make the example simple) as killing the app works fine for me as a way to kill the server.

When you run UdpServer.exe, you should get something like Figure 17-3.

Figure 17-3. The UDP server in action

710 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

UDP Client Example

No new code is required to create a UDP client, so I’ll just dive directly into the Echo client console application shown in Listing 17-4.

Listing 17-4. A UDP Client

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);

//IPEndPoint^ ipep = gcnew IPEndPoint(IPAddress::Any, 54322);

//socket->Bind(ipep);

EndPoint^ 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); socket->SendTo(message, Remote);

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

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

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

}

}

The first thing that should jump out at you from this code is that there is no bind to an IPEndPoint. In the example, there is no need since the first method call used by the socket class is the SendTo() method. This method has a handy built-in feature: It does the bind for you. Once you call the SendTo() method, all subsequent sends and receives will come through the randomly generated IPEndPoint assigned by that SendTo() method.

There is nothing stopping you from binding the socket yourself. Well, actually, I take that back. There is. You cannot bind twice to the same IPEndPoint. So you must use a unique IP address (or port) for each client and server. Either method is easy if clients and servers are on different machines. On the same machine, I recommend just using unique ports as things get a little trickier for IP addresses, especially if you have only one NIC, because you need to use specific IP addresses like 192.168.1.102 for the one IP and 127.0.0.1 for the other.

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

711

Another thing that should stand out in the previous program is that the client must know the specific IPEndPoint, bound by the server, that it is connecting with. Without this, the client cannot connect to the server.

Using Connect() with UDP

What if you are always sending and receiving from the same EndPoint? It seems a little redundant to continually send and receive the same address over and over. Well, you are in luck; UDP provides the ability to “sort of” connect to an EndPoint using a socket class Connect() method:

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

The Connect() method does not cause a true connection but instead allows you to use the Send() and Receive() methods, which don’t require the repeated use of an EndPoint. The syntax of the Send() and Receive() methods is the same as what is shown here in connection-oriented sockets.

Listing 17-5 shows a connected UDP client echo application.

Listing 17-5. A UDP Client Using Connect()

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);

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

socket->Connect(Remote);

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->Send(message);

message = gcnew array<unsigned char>(1024); int recv = socket->Receive(message);

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

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

}

}

712 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

As you can see, the code in Listing 17-4 is functionally equivalent to that in Listing 17-5; both can send to and receive messages from the same server. The only difference is that using the Connect() method in Listing 17-5 has allowed us to use the simplified Send()/Receive() method syntax instead of the (slightly more complex) SendTo()/ReceiveFrom() method syntax, at the expense of the socket being able to talk to only a single preset EndPoint.

When you run UdpClient.exe, you should get something like Figure 17-4.

Figure 17-4. The UDP client in action

Socket Helper Classes and Methods

Okay, I’ve shown you the hard way to create connection-oriented and connectionless network code. Let’s see if there is an easier way of doing the same thing—maybe at the expense of a little (usually unneeded) control.

TcpListener

Since the code to establish a TCP server connection is almost always the same no matter the implementation, the .NET Framework has provided TcpListener, a class that simplifies the whole process.

The TcpListener constructor has two overloads (there is a third but it is marked as obsolete), each providing a different way of determining the IPEndPoint that the TCP connection will be established on:

TcpListener(IPAddress^ address, int port)

TcpListener(IPEndPoint^ ipep)

The first overload allows you to pass the IP address and the port on which you want to make the connection. The second constructor allows you to build the IPEndPoint yourself and pass it into the

TcpListener class.

Once you have an instance to a TcpListener object, you must start the listener up with the aptly named Start() method.

Now you are ready to accept socket connections on the IPEndPoint using the AcceptSocket() method. Listing 17-6 is the main method of a simplified version of Listing 17-1 using the TcpListener helper class. I did not include the TcpServer class’s code as it is identical to that of Listing 17-1.

Listing 17-6. A TCP Server’s Main Method Using TcpListener

using namespace System; using namespace System::Net;

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

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

713

//... TcpServer class

void main()

{

TcpServer^ server = gcnew TcpServer();

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

while(true)

{

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

Thread ^thr = gcnew Thread(

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

}

}

Cleans up the code nicely, doesn’t it? But we’re not done with the simplifications.

TcpClient

TCP communication is via a stream, right? So why not allow sending and receiving of messages to be handled as a stream instead of using the TCP Send() and Receive() methods? The TcpClient provides this functionality by providing a stream interface to TCP messages.

Just to confuse things, you can (and probably will) use the TcpClient on both the client and the server, as the code to set up the connection as a stream works equally well in both instances. The only real difference is that on a server you will accept a TcpClient using the AcceptTcpClient() method instead of the AcceptSocket() method like this:

TcpClient^ client = socket->AcceptTcpClient();

While on the client, you will create your own instance of it.

While creating an instance of TcpClient, you have the option of just using the constructor to connect to the server or using the Connect() method later on. The overloads to both are nearly the same; the main difference I see is that the Connect() method allows you to make the connection at a different time than when creating the instance of TcpClient.

TcpClient()

TcpClient(AddressFamily^)

TcpClient(IPEndPoint^)

TcpClient(String^ hostname, Int32 port)

The first two constructors don’t provide the ability to immediately connect to a server for the obvious reason that the server’s address has not been specified. The difference between these two constructors is that the second constructor allows TcpClient to use version 6 IP addresses by passing an address family of InterNetworkV6.

The second and third constructors will automatically attempt to connect to the server specified by the passed parameter. You have already seen the IPEndPoint, so let’s move on to the last constructor. This neat little constructor allows you to pass either the IP address or the DNS host name (sweet! if you ask me), and the port to connect on. A DNS host name is the more human-friendly name you

714 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

type in when you are using Internet Explorer, Firefox, or whatever browser you prefer—for example, www.managedcpp.net or www.procppcli.net (just a little plug for my C++/CLI Web site).

As I said earlier, the Connect() method takes very similar parameters:

Connect(IPEndPoint^)

Connect(IPAddress^ addr, Int32 port)

Connect(array<IPAddress^>^ addrs, Int32 port)

Connect(String^ hostname, Int32 port)

All the parameters passed to the Connect() method should be familiar to you except the third overload. With this overload, Connect() is expecting an array of IPAddresses. Why is this overload needed, you might ask? The reason is it works perfectly with the static

Dns::ResolveToAddresses(String^ hostname) method, which returns an array of IPAddresses. This static method is helpful in that it allows you to give it a DNS host name and it spits out all IP addresses associated with it.

Okay, now that you are connected, you can use the TcpClient class’s GetStream() method (why not a property?) to provide a Stream object from which you can access the TCP port as a stream of data:

TcpClient^ client = gcnew TcpClient(); client->Connect("www.procppcli.net", 12345); NetworkStream^ ns = client->GetStream();

TCP Helper Class Example

Listing 17-7 and Listing 17-8 show how you can use TCPListener and TcpClient to communicate in a client-server fashion using strictly streams of String objects.

Listing 17-7. A TCP Server Implementing Helper Classes

using namespace System; using namespace System::IO; using namespace System::Net;

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

ref class TcpServer

{

public:

void ProcessThread(Object ^clientObj);

};

void TcpServer::ProcessThread(Object ^clientObj)

{

TcpClient^ client = (TcpClient^)clientObj;

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

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

StreamWriter^ writer = gcnew StreamWriter(client->GetStream());

StreamReader^ reader = gcnew StreamReader(client->GetStream());

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

715

writer->WriteLine("Successful connection to the server on port {0}", clientEP->Port);

writer->Flush();

String^ msg; while (true)

{

try

{

msg = reader->ReadLine();

Console::WriteLine("Port[{0}] {1}", clientEP->Port, msg);

writer->WriteLine(msg); writer->Flush();

}

catch (IOException^)

{

break; // connection lost

}

}

client->Close();

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

}

void main()

{

TcpServer^ server = gcnew TcpServer();

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

while(true)

{

Console::WriteLine("Waiting for client connection."); TcpClient^ client = socket->AcceptTcpClient();

Thread ^thr = gcnew Thread(

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

}

}

As you can see from the code, all sending and receiving of data is of type String. What’s more interesting is that I am able to use standard WriteLine() and ReadLine() methods to handle communication over the Internet! The following two lines make this possible:

StreamWriter^ writer = gcnew StreamWriter(client->GetStream());

StreamReader^ reader = gcnew StreamReader(client->GetStream());

These lines create a StreamWriter and StreamReader object (which I covered in Chapter 8) from the NetworkStream object returned by the TcpClient class’s GetStream() method.

716 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

All that socket stuff is now (almost) completely hidden. There are only two catches: First, you need to flush the messages manually using the Flush() method, or the messages stay in the stream’s buffer until the buffer is full. Second, you need to catch an IOException from the ReadLine() and WriteLine() methods. When this exception happens, you can assume that the network connection has been closed and you can go ahead and close things up.

Listing 17-8. A TCP Client Implementing Helper Classes

using namespace System; using namespace System::IO; using namespace System::Net;

using namespace System::Net::Sockets;

void main()

{

TcpClient^ server; StreamWriter^ writer; StreamReader^ reader; String^ msg;

try

{

server = gcnew TcpClient("127.0.0.1", 12345);

writer = gcnew StreamWriter(server->GetStream()); reader = gcnew StreamReader(server->GetStream());

}

catch (SocketException^ se)

{

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

return;

}

msg = reader->ReadLine(); Console::WriteLine(msg);

while (true)

{

Console::Write("Message ('q' to quit): "); msg = Console::ReadLine();

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

try

{

writer->WriteLine(msg); writer->Flush();

msg = reader->ReadLine(); Console::WriteLine(msg);

}

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

717

catch (IOException^)

{

break; // connection lost

}

}

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

}

Okay, I sort of fibbed. There is a third catch. To simplify the client, you should also use a StreamWriter and StreamReader, as shown in Listing 17-8.

Notice that the client also places the WriteLine() and ReadLine() methods within a try/catch block. In most cases, a server should not come down with clients attached, but there are no rules saying it can’t. Thus, if the WriteLine() or ReadLine() method throws an IOException, you can assume that the server has severed its connection and that you need to close the client connection. One bonus of TcpClient is that it shuts down gracefully on its own and therefore doesn’t even provide a Shutdown() method like the Socket class does.

UdpClient

Since there is a TcpClient, you must be thinking that there has to be a UdpClient (and you’d be right). The UdpClient simplifies the already simple UDP socket in two ways, though there already isn’t much left to simplify.

One area that is made easier is that you don’t need to worry about binding; the myriad of UdpClient constructors handles it for you. Here is a list of the constructors available to you:

UdpClient()

UdpClient(AddressFamily^)

UdpClient(Int32 port)

UdpClient(IPEndPoint^)

UdpClient(Int32 port, AddressFamily^)

UdpClient(String^ hostname, Int32 port)

We have examined all these parameters already in some form in the constructor already covered, but I’ll recap them so you don’t have to go searching for them. The AddressFamily can be either InterNetwork (version 4 IP address) or InterNetworkV6 (version 6 IP address). The port parameter is an integer from 0 to 65535, but you should not use 0–1024 as these numbers are reserved as well-known ports. IPEndPoint is a combination of the IP address and the port. Finally, hostname is the human-friendly(ish) name you give to an IP address, like the one you type into Internet Explorer.

The other benefit of using UdpClient is that you no longer have to worry about not receiving the whole message package by allocating too small a buffer. With UdpClient the Receive() method returns the buffer; all you have to do is provide a handle to return it to.

Listings 17-9 and 17-10 contain all the basic code needed for a UDP client-server application using UdpClient.

Listing 17-9. A UDP Server That Accepts Multiple Concurrent Clients Using UdpClient

using namespace System; using namespace System::Net;

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