I l @ ve RuBoard |
The two main aspects of network programming define where data is actually transmitted to (or sent from) and transmitting that data in a manner that a receiving application can understand. Data transmission is concerned with formats and protocols. This chapter (and the rest of this book, actually) will assume that the underlying network protocol being used is TCP/IP, the protocol of the Internet. Other protocols might be layered over it ”HTTP and SOAP, for example ”but TCP/IP is the lowest common denominator of many modern networked systems. Even though TCP/IP is the underlying protocol, the format of the data broadcast over TCP/IP is largely up to the applications doing the sending and receiving. Any scheme that is used must be portable, taking into account such issues as binary data and big endian versus little endian computers. Defining the source and destinations of data is all about managing and creating endpoints . Endpoints are the metaphorical software plugs and sockets that connect computers together. Because they can identify the source of any request, endpoints can also feature in the implementation of security. The Sockets API was created many years ago for C programmers who were building applications running under UNIX. Since then, sockets have been ported and adapted for use by a variety of other operating systems, including the Microsoft Windows family. Microsoft's latest incarnation of the socket library can be found in the System.Net.Sockets namespace in the .NET Framework Class Library. Even if you're familiar with sockets and socket programming, it's still worthwhile to look at how to use them with .NET because Microsoft has added a selection of features, type safety, and object-oriented wrappers. As mentioned earlier, the protocol underpinning sockets is TCP/IP. However, TCP/IP is actually a family of transport protocols that are often used in conjunction with other transport protocols. The protocol type requested at run time by the processes at both ends of the wire determines the specific protocol used. (The sending and receiving process must use the same protocol type.) Examples of transports that sockets can use include Transmission Control Protocol (TCP), User Datagram Protocol (UDP), Internetwork Packet Exchange (IPX), Sequenced Packet Exchange (SPX), Gateway-to-Gateway Protocol (GGP), and Internet Control Message Protocol (ICMP). TCP is the most common transport protocol used for handling connection-oriented sockets, and UDP is its counterpart in the connectionless world, so we'll concentrate on these two. The JDK has its own socket library in the java.net package. Microsoft has provided an implementation of this package in vjslib.dll. If you're careful, you can use JDK sockets and .NET sockets in the same class, but be sure to use the appropriate namespace qualifiers ” System.Net.Sockets.Socket and java.net.Socket . Connection-Oriented SocketsSockets support two modes of operation: connection-oriented and connectionless. We'll examine connectionless sockets shortly. Connection-oriented sockets operate by establishing a virtual circuit, or continuous connection, between a process that sends data (referred to as the client in this model) and the process that receives and processes the data (referred to as the server ). In a typical connection-oriented configuration, the server process attaches to a well-known endpoint and waits for clients to contact it. An endpoint has two elements: the Internet Protocol (IP) address of the computer running the process and a port number. (See the IP Addresses and Ports sidebar if you're not familiar with these concepts.) The server sleeps until a client calls. When a call occurs, the server can optionally examine the identity of the computer executing the client process, which is transmitted as part of the client request by TCP. If it determines that the client computer is valid, it can spawn a dedicated thread to service the client. The main server thread continues to wait for the next client to connect, and so on.
IP addresses are maintained centrally by the Internet Network Information Center (InterNIC) and its local subsidiaries. Organizations that want to connect to the Internet must obtain an IP address (or, more commonly, a range of addresses) from InterNIC. In many cases, however, organizations access the Internet through an Internet service provider (ISP). An ISP has a pool of IP addresses obtained from InterNIC, and it leases an address to each client computer that connects to the Internet through it. The duration of the lease is determined by the ISP, but it is often limited to a few hours. On expiration, the client computer can disconnect and reconnect if necessary. Most ISPs charge a small fee for this service. One implication of this mechanism is that the IP address of a given client computer is not always the same, and conversely two different computers might be assigned the same IP address at different points in time. This is fine for client computers, but it's not so good for servers. (It makes it difficult for a client to locate a server if its address changes frequently.) For this reason, some ISPs also offer hosting facilities on their computers, renting space and processing power for server applications that need to reside at an IP address that does not vary. The IP address uniquely identifies the computer, but many server processes might be running on that computer, so there must be a means of discovering a process as well as a computer. For connections that use sockets, each server process listens on a port. You can think of a port as an integer that identifies the server process to a client. To communicate with a server, a client must specify the IP address of the server computer and the port the server process is listening on. The TCP/IP specification reserves a number of well-known ports for specific system processes. For example, port 23 is usually occupied by the Telnet service, which provides remote terminal connection services to the host computer, and port 80 is reserved for the HTTP service, which receives Web requests from clients. In fact, the TCP specification reserves all ports below 1024, so you should not use these for your own server applications. Connection-Oriented ServersThe CakeServer class (available in the file CakeServer.jsl in the CakeSizeServer project) is an example of a simple connection-oriented server. It is an extension of the example shown in earlier chapters that figures out how many people a cake of a given size , shape, and filling will feed. This time, however, the server performs the service remotely, using sockets. The main method creates a socket for clients to connect to. SocketserverSocket=newSocket(AddressFamily.InterNetwork, SocketType.Stream,ProtocolType.Tcp); The parameters of the Socket constructor include an AddressFamily , a SocketType , and a ProtocolType . These are all enumerations, and there are many combinations that you can specify, but not all combinations are legal. The AddressFamily parameter indicates the addressing scheme used by the socket. This affects how addresses will be interpreted. Options include NetBios , DecNet , Banyan , Sna , and InterNetwork , among a plethora of others. The CakeServer class uses InterNetwork , which indicates that it will specify an IP (version 4.0) address. (An InterNetworkV6 option is also available for IP version 6.0 addresses, but it is not fully supported by version 1.0 of the .NET Framework Class Library.) The SocketType parameter specifies how the socket will be used. The value Stream indicates that the socket should support a reliable two-way continuous connection between the client and the server. The ProtocolType parameter helps to determine which of the many transport protocols in the TCP/IP suite will be used. The example uses Tcp . You can use only a protocol that is valid for the specified address family and protocol type; otherwise , the common language runtime will throw a System.Net.Sockets.SocketException at run time. We won't describe every possible combination of parameters in this book; we'll simply create a connection-oriented Internet socket over TCP using the values InterNetwork , Stream , and Tcp . After you fashion a socket, you establish an endpoint that you can bind it to. In theory, different address families can use different formats to specify an endpoint. The .NET Framework Class Library defines the generic EndPoint class in the System.Net namespace, which holds the functionality common to endpoints for all address families. You're unlikely to ever use an EndPoint object directly. You're more likely to use a specialized class that inherits from it and contains the additional functionality peculiar to an address family. IPEndPoint is just such a class; you use it to manage IP endpoints. (In fact, it is currently the only specialized EndPoint class, although others might be added in the future.) An IPEndPoint comprises an IP address and a port number. The IP address used by a server is typically that of the computer the server process is running on. You might know the name of your computer, but the name is not the same as its IP address. Remember that an IP address can change, so it's not a good idea to hard-code it into an application. In contrast, the name is less likely to change. The .NET Framework Class Library offers the ability to look up the IP address of a computer given its name, using the Domain Name System (DNS) ”a DNS server maintains a dynamic database of computer names and IP addresses. The Dns class in the System.Net namespace contains a selection of useful methods , one of which is Resolve . This returns an IPHostEntry object for a specified computer, from which you can extract its IP address. If you want to make the application independent of the computer name as well, you can also invoke Dns.GetHostName , which returns the name of the local computer. An added complication of DNS is that a single computer can have several aliases, so the IPHostEntry class actually contains an array of addresses in a property called AddressList (which is accessed using get_AddressList from J#). The CakeServer class uses the first matching IP address that it finds: IPHostEntryipHostInfo=Dns.Resolve(Dns.GetHostName()); IPAddressipAddress=ipHostInfo.get_AddressList()[0]; Note If you've used sockets in the past, you might be more familiar with the GetHostByName method than with the Resolve method. ( GetHostByName is available as getByName in java.net in the JDK.) The Resolve method is actually a wrapper around GetHostByName and the GetHostByAddress method (which looks up an IP address using dotted-quad notation). You can use Resolve to return an IPHostEntry object that represents a computer given either its name or to return a string containing its address in dotted-quad format. Resolve calls the appropriate GetHostByName or GetHostByAddress method for you. The port number is a positive integer. The IPEndPoint class exposes two static fields called MinPort (0) and MaxPort (65535). These fields indicate the lower and upper values that you can use for the port number. You can use any value in this range that is not already in use on the local computer, but you should refrain from using any value less than 1024 if you want to avoid clashing with any of the reserved system ports. Our example uses the static value cakePort (4000) defined in the CakeServerUtils class (in the CakeUtils.jsl sample file in the CakeUtils project, which has been added to the CakeSizeServer solution for your convenience): publicclassCakeServerUtils { publicstaticfinalintcakePort=4000; } Note The original Berkeley UNIX socket libraries, on which most subsequent versions are based, let you associate a port number with a service name and store it in a system file, along with other service/port pairs. You could then execute the getservbyname method, which took the name of a service as its argument and returned the corresponding port. Sadly, this feature is also not currently available with .NET (or with the JDK, for that matter). The IPEndPoint constructor creates an endpoint that you can attach to the socket using the Bind method: IPEndPointipEp=newIPEndPoint(IPAddress.Loopback, CakeServerUtils.cakePort); serverSocket.Bind(ipEp); Next, the socket has to be placed in the listening state. In this mode of operation, the socket can start to wait for incoming client requests. These requests are held in an internal queue until the application services them. You must specify the maximum length of this queue when you put the socket into listening mode using the Listen method: serverSocket.Listen(5); This statement sets the queue length to 5. If more clients try to connect at a rate that causes the queue to overflow, their connection attempts will be aborted with a SocketException . Note The queue length is not the same as the maximum number of clients that the server can handle at the same time; it is the number of clients that have tried to connect but that the server has not yet accepted. Clients that are currently being handled aren't included in this number. Having placed the socket in listen mode, the server has nothing else to do until a client sends a request. It can sit and wait for a client request by executing the Accept method: CakeServerserver=newCakeServer(); //Waitforaclienttoconnect server.clientSocket=serverSocket.Accept(); The Accept method sleeps until a client connects. The original socket is reserved for clients to connect to, so when a client contacts the server, a new private communications channel is opened up just for that client. (In JDK terms, this is a java.net.ServerSocket object.) The value returned by Accept is a reference to a new socket created especially for conversing with the client. When the client transmits data to the server, it will appear on this new socket. The server should send any response to the client through this socket. In our example, the application creates a new instance of the CakeServer class before issuing the call to Accept . The CakeServer class contains a Socket instance variable called clientSocket , and the value returned by Accept is used to populate this variable: publicclassCakeServer { privateSocketclientSocket; }
For example, to reject requests from the computer named MOCKTURTLE on our network, you can use the following code fragment: SocketclientSocket; SocketserverSocket; clientSocket=serverSocket.Accept(); IPEndPointclientEndPoint=(IPEndPoint)clientSocket.get_RemoteEndPoint(); IPAddressclientAddress=clientEndPoint.get_Address(); if(clientAddress.Equals(Dns.Resolve("MOCKTURTLE").get_AddressList()[0])) { //Denyaccess clientSocket.Close(); } else { //Processtherequest } If you have a list of computers that you want to allow or deny access to, another strategy is to use a hash table ( System.Collections.Hashtable ) of computer names and query that hash table to look for a match. The CakeServer class also contains a method called handleClientRequest : privatevoidhandleClientRequest() { } The server uses this method as the callback for a thread that it creates to service the client: System.Threading.Threadrunner=new System.Threading.Thread(newThreadStart(server.handleClientRequest)); runner.Start(); In this way, the server can handle multiple clients simultaneously . The main server thread loops back, creates another CakeServer object, and then waits for a new client to connect. In the meantime, the handleClientRequest method running on the new thread takes care of the client. The handleClientRequest method is where the business logic of the server lies. A client application transmits three items of data to the server, which describe the size, shape, and filling of a prospective cake, respectively. These are passed as integers over the socket. However, a minor issue arises here. Sockets act as conduits for unstructured streams of data. You can read the data sent down a socket using the Receive method, but this data appears as an array of unsigned bytes: ubyte[]data=newubyte[4]; clientSocket.Receive(data,4,SocketFlags.None); The parameters of the Receive method include an array to read the data into, and two optional parameters: the number of bytes to read, and a flag. The Receive method will block if insufficient data is available to satisfy the request ”in this case, waiting until 4 bytes (the length of an integer) have been sent. If you omit this parameter, the Receive call will use the size of the array specified as the first parameter. The SocketFlags parameter determines the nature of the receive operation. Normally, reads from a socket are destructive and function on a queued basis ”that is, when you read from a socket, the data you've read is removed and the data is read in the same order in which it was written. The SocketFlags parameter lets you change this policy. If you specify SocketFlags.Peek , the data will be copied from the socket but not removed. A client can send urgent data that jumps to the head of the queue by using the SocketFlags.OutOfBand flag, and a server can read this urgent data with the same SocketFlags.OutOfBand value. If no out-of- band data is available, the receive operation will block. You can combine flags using the bitwise or operator. Our example specifies SocketFlags.None , which clears all flags and uses the default behavior. The application itself is responsible for parsing the raw data received into meaningful information. The server uses the static buildInt method of the CakeServerUtils class to convert the array of 4 bytes into an integer: publicstaticintbuildInt(ubyte[]data)throwsSystem.Exception { returndata[0]+256*data[1]+65536*data[2]+16777216*data[3]; } Note The implementation of the FeedsHowMany method in the previous chapters used short parameters. In this chapter, we'll temporarily switch to using int parameters because doing so will make the buildInt and buildArray (shown below) methods more interesting. The handleClientRequest method repeats this process two more times, extracting the shape and filling of the cake from the socket before calling the static FeedsHowMany method of the CakeInfo class. This is the same method we used in earlier chapters: intnumEaters=CakeInfo.FeedsHowMany(diameter,shape,filling); The value returned, numEaters , is sent back to the client. In the same way that data can be read from a socket only as an array of bytes, you can transmit only an array of bytes down a socket. The application uses the static CakeServerUtils.buildArray method to convert the numEaters integer into an array of four ubyte values: data=CakeServerUtils.buildArray(numEaters); The buildArray method itself performs the converse operation of buildInt using a combination of modulus , division, and right-shift operations to split an integer into its constituent bytes: publicstaticubyte[]buildArray(intintData) { ubyte[]data=newubyte[4]; data[0]=(ubyte)(intData%256); intData>>=8; data[1]=(ubyte)(intData%256); intData>>=8; data[2]=(ubyte)(intData%256); data[3]=(ubyte)(intData/256); returndata; } The server transmits the data back to the client using the Send method. The parameters are the same as those used by Receive : clientSocket.Send(data,4,SocketFlags.None); You'll observe that when the method terminates (or an exception occurs), it executes the Shutdown method: clientSocket.Shutdown(SocketShutdown.Both); This method disables sending and/or receiving data over a socket in a graceful manner. (The underlying protocol causes an exchange of messages between both ends of the socket, allowing the server to notify the client that the socket is about to disappear.) Using the value SocketShutdown.Both disables sending and receiving, but you can be more selective by specifying SocketShutdown.Send or SocketShutdown.Receive to simply prevent sending or receiving data. If you've totally shut down a socket, you should also close it and release any resources held: clientSocket.Close(); If the client attempts to send or receive any more data over a socket that has been successfully closed, the common language runtime will throw a SocketException .
You can change this behavior by setting the NoDelay option of the socket using the SetSocketOption method: clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.NoDelay,1); The arguments to the SetSocketOption method are a level, an option, and a value. The level determines how the option is applied. Many options operate on the socket as a whole; others are used by specific protocols. If you specify an invalid level for a given option, the common language runtime will throw a runtime exception. Our example sets the NoDelay option to 1, which enables it and prevents packet sharing. Setting it to 0 disables it. On other occasions, you might find that a large send operation blocks while data packets are assembled and transmitted to the receiver. It is possible for the Send method to block, especially if the server has previously sent data to the client but the client has not yet received it. (It might be busy doing something else, causing a backlog in the internal processing of its network buffers.) You can prevent an indefinite delay by setting the SendTimeout option: clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout,1000); This example sets the timeout for the socket to 1000 ms (1s). If the timeout occurs, the sending application will receive a SocketException . Like the Send method, the Close method of a socket can also block. Normally, a socket will not close until all its data has been sent. You can force the socket to close after a given period of time by setting its linger option. You do this by creating a System.Net.Sockets.LingerOption object and populating it with information about the specified timeout, and then calling the SetSocketOption method to apply it: LingerOptionsocketOpt=newLingerOption(true,5); clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Linger,socketOpt); These statements create a LingerOption object that enables lingering for 5 seconds before timing out. At that point, any data that has not been sent will be lost. You can specify a timeout of 0 to close the socket immediately and discard all untransmitted data. By default, lingering is disabled. This is not as dangerous as it sounds: Even though the Close method will terminate immediately, the socket itself will be closed gracefully in the background and data that has already been transmitted should not be lost.
Connection-Oriented ClientsWriting a client application that uses sockets is less involved than writing a server. A typical client application will create a socket and connect it to the server. Once the connection is established, the client can send the server any data it needs and then wait for a response. The CakeClient class in the sample file CakeClient.jsl in the CakeSizeClient project is just such a client for the CakeServer class. The CakeClient class is a simple test harness that asks the CakeServer class how many people a 14-inch hexagonal fruit cake will feed: intrequestedSize=14; intrequestedShape=CakeShape.Hexagonal; intrequestedFilling=CakeFilling.Fruit; Like the server, the client creates a connection-oriented socket based on TCP: SocketcakeServerSocket=newSocket(AddressFamily.InterNetwork, SocketType.Stream,ProtocolType.Tcp); The next task is to connect this socket to the appropriate endpoint on the server machine. To do this, you first need the IP address of the server computer. The program asks the user for the name of the computer in question and stores it in the variable serverComputer . The CakeClient looks up the IP address of the server computer, using a similar technique to that used by the server for finding its own address: IPHostEntryserverAddresses=Dns.Resolve(serverComputer); IPAddressserverAddress=serverAddresses.get_AddressList()[0]; Dns.Resolve raises the SocketException "No such host is known" if the named computer cannot be located. To connect to the server, the client can create an endpoint structure that specifies the address and port of the server process and then call the Connect method using this endpoint as the parameter: IPEndPointipEndpoint=newIPEndPoint(serverAddress, CakeServerUtils.cakePort); cakeServerSocket.Connect(ipEndpoint); If the server has not yet executed a Listen call, the common language runtime will throw a SocketException , informing the client that "No connection could be made because the target machine actively refused it." If the server is accepting calls, the Connect method will block until the Accept method in the server has instantiated the new socket as described earlier. At this point, the client can send data to the server. In the CakeClient , the data transmitted comprises three integers: the size of the cake, the shape, and the filling. These integers are converted into ubyte arrays by the same buildArray utility method used by the server, and then sent to the server in sequence: ubyte[]data=newubyte[4]; data=CakeServerUtils.buildArray(requestedSize); cakeServerSocket.Send(data,4,SocketFlags.None); As you might recall, when the server has received all three integers, it calls the FeedsHowMany method and passes the value returned back to the client through the socket. The client waits for this reply using the Receive method: cakeServerSocket.Receive(data,4,SocketFlags.None); The data passed back is converted to an integer and displayed, and then the client closes the socket and terminates. To execute these applications, you start the server (execute the CakeSizeServer.exe program) and then run the client (CakeClient.exe). When you're prompted by the client, type the name (or IP address) of the computer running the server program. The results are shown in Figures 9-1 and 9-2. Figure 9-1. Output of the CakeServer program
Figure 9-2. Output of the CakeClient program
The TcpListener and TcpClient ClassesThe Socket class provides an implementation of sockets that is reasonably faithful to the original UNIX model, except that it is based on objects and methods rather than functions. However, if you find yourself writing a lot of socket code, you'll be repeatedly implementing the same common idioms ”creating a socket using a valid set of arguments, creating an endpoint comprising a machine address and a port number, binding the socket to the endpoint, setting the request queue length, waiting for requests. Also, if you're approaching J# from a pure Java angle and have never before programmed sockets using C or C++, you might find the Socket class a little low-level compared to what you're used to ”after all, the JDK has the java.net.ServerSocket class, which hides all of this stuff. To give you the same convenience in J#, Microsoft has created a pair of classes named TcpListener and TcpClient that abstract out much of this routine work. They are located in the System.Net.Sockets namespace. These classes are basically just wrappers around the Socket class. The TcpListener class is used for creating socket server applications, and the TcpClient is its counterpart for building socket clients. In many ways, the TcpListener class is analogous to the ServerSocket class of the JDK. The following code shows the main method of the CakeServer class, which is written using a TcpListener rather than a raw socket. (This code is available in CakeServer.jsl in the TcpCakeSizeServer project.) The TcpListener constructor expects a port number as its argument. The constructor creates an Internet, connection-oriented, TCP socket (IP version 4.0) and binds it to an endpoint comprising the IP address of the local machine and the specified port. If you need more control, you can use the TcpListener class, which offers a second constructor that takes a handcrafted IPEndPoint parameter or a third constructor that takes an IPAddress and a port number. The Start method puts the TcpListener into listen mode. One difference between a TcpListener and a Socket is that you cannot set the queue length, although you can query the Pending property (use get_Pending from J#), which returns a Boolean indicating whether there are any pending connections. The AcceptSocket method blocks the listener until a client connects, returning a socket for communicating with the client when it does so. The Stop method, in the finally block, closes down the listener and stops it from accepting any more requests: publicstaticvoidmain(String[]args) { //UseaTCPlistenerratherthanasocket TcpListenerserverListener=null; try { //CreateaTCPsocket,andbindittoanendpointcomprising //theIPaddressofthelocalmachine,andcakePort serverListener=newTcpListener(CakeServerUtils.cakePort); //Startlistening serverListener.Start(); //Loopforever,waitingforandservicingclients while(true) { CakeServerserver=newCakeServer(); //Waitforaclienttoconnect server.clientSocket=serverListener.AcceptSocket(); //Spawnathreadtohandletheclientrequest System.Threading.Threadrunner=newSystem.Threading.Thread(newThreadStart(server.handleClientRequest)); runner.Start(); } } catch(System.Exceptione) { Console.WriteLine("Exception: " +e); } finally { //Tidyup if(serverListener!=null) serverListener.Stop(); } } You cannot directly access the socket contained in a TcpListener object. The TcpListener class exposes the Server property, which returns a reference to the underlying socket, but this method is protected and is available only to code inside classes that are inherited from TcpListener . However, you can query the endpoint created for the socket using the LocalEndpoint property, which is returned as a generic EndPoint object. You should cast it to an IPEndPoint if you want to examine address and port information: TcpListenerserverListener; IPEndPointipEndpoint=(IPEndPoint)serverListener.get_LocalEndpoint(); This new version of the CakeServer will operate perfectly well with the existing client, which was developed using sockets. However, for the sake of completeness, the following fragment shows another implementation of the main method of the client that uses a TcpClient object to replace the socket. This code is available in the file CakeClient.jsl in the TcpCakeClient project. A TcpClient is closer to what a programmer familiar with the JDK would think of as a java.net.Socket . The TcpClient has three constructors. The one used here specifies the name of the server computer to attach to and the port that the server process is listening on. This constructor will create a client socket and then use DNS to obtain the IP address of the server, create an endpoint, and connect the socket to that endpoint. The TcpClient class uses a NetworkStream object to actually send and receive data. This is one of the stream classes discussed in Chapter 8 that can perform asynchronous I/O using a thread from the thread pool. Our example performs synchronous I/O because there is little else the client can do while it's waiting to receive a reply from the server, but in serious client applications you might find it beneficial to use the asynchronous BeginRead and BeginWrite methods of this object. The GetStream method of the TcpClient class returns a reference to this stream. The client code executes the Write method to send data to the server. (The parameters are a buffer containing the data, an offset into the buffer indicating the starting point of the data, and the length of the data.) Similarly, the client uses the Read method to perform a blocking read of the response from the server. The Close method of the TcpListener class closes the client socket and terminates the connection to the server: publicstaticvoidmain(String[]args) { //Findouthowmanypeoplea14" hexagonalfruitcakewillfeed intrequestedSize=14; intrequestedShape=CakeShape.Hexagonal; intrequestedFilling=CakeFilling.Fruit; TcpClientcakeClient=null; try { Console.WriteLine("Typeinthenameoftheservermachine"); StringserverComputer=Console.ReadLine(); //CreateaTcpClienttocommunicatewithCakeServer cakeClient=newTcpClient(serverComputer,CakeServerUtils.cakePort); //Getahandleonthestreamfortransmittingandreceivingdata NetworkStreamdataStream=cakeClient.GetStream(); //Transmittheparametersforthecakedownthesocket //Firstthesize ubyte[]data=newubyte[4]; data=CakeServerUtils.buildArray(requestedSize); dataStream.Write(data,0,4); //Thentheshape data=CakeServerUtils.buildArray(requestedShape); dataStream.Write(data,0,4); //Andfinallythefilling data=CakeServerUtils.buildArray(requestedFilling); dataStream.Write(data,0,4); //Waitforthereplyfromtheserver dataStream.Read(data,0,4); //Convertthereplyintoaninteger intnumEaters=CakeServerUtils.buildInt(data); //Displaytheresult Console.WriteLine("Thiscakewillfeed " +numEaters); //Closethesocket,andfinish cakeClient.Close(); } catch(System.Exceptione) { Console.WriteLine("Exceptioncommunicatingwithserver: " +e); if(cakeClient!=null) cakeClient.Close(); } } Interoperability with JDK SocketsJDK sockets and .NET sockets are noninterchangeable. For instance, you cannot cast a JDK socket to a .NET socket, or vice versa. However, at the network level there is a high degree of interoperability. A .NET TcpClient object can use its NetworkStream to transmit data to a JDK ServerSocket , and a JDK client socket can communicate with a TcpListener object without any problem. The sample JDKCakeClient class (in the JDKCakeClient project) illustrates a sample JDK client that will work with the TcpListener implementation of the CakeServer application (in the TcpCakeSizeServer project). For convenience, the class also contains the various constants and utility methods that were previously supplied by the CakeUtils assembly, mainly because you cannot access .NET assemblies when you use the JDK compiler. The main method is remarkably similar to the TcpClient implementation of the CakeClient class. The main difference, apart from using a java.net.Socket object, is that the I/O is performed using DataInputStream and DataOutputStream objects rather than the single NetworkStream object implemented by .NET. Although a Visual J#.NET project is supplied, you can also compile the JDKCakeClient class and execute it using the JDK. Data Transmission IssuesEven though .NET sockets and JDK sockets can work together, whether the data transmitted from one socket is actually understood by the application at the other end is another matter. You might wonder why the JDKCakeClient class jumps through various hoops, converting integers into byte arrays and vice versa when the DataOutputStream class has a perfectly good writeInt method that will transmit a 4-byte integer without further ado. Well, the answer is that the writeInt method sends an integer as 4 bytes, with the most significant byte first. When the CakeServer class reads the data from the socket and converts it back to an integer, it expects the least significant byte first. This is the classic big endian/little endian problem. We could arguably have said that the buildInt and buildArray methods expect the data in big endian format, which would fix the problem for JDK clients, but other operating systems and languages use little endian encoding, and programs developed on those platforms would not be able to pass data to the CakeServer class. For both ends of the socket to interpret the data passed over the network in the same way, they should agree on a common representation for that data. In the CakeServer example, this is achieved by using the same routines to encode and decode the data at each end of the wire. The original UNIX sockets library includes a set of four functions that convert long and short integers to and from a neutral representation called the network byte order . These functions are htonl (host to network long), htons (host to network short), ntohl (network to host long), and ntohs (network to host short). If you transmit native integer data over a socket, you should first convert it to network-byte ordering. The receiving end should convert it back to host-byte ordering. The .NET Framework Class Library has implementations of these as static methods in the IPAddress class. They're called HostToNetworkOrder and NetworkToHostOrder ; they're overloaded and can operate on long , short , and int values. Note The network-byte ordering used by the sockets library is actually big endian. This means that the implementations the htons , ntohs , htonl , and ntohl functions on a UNIX machine that also uses a big endian architecture are actually null operations. The same functions on a computer running Windows might need to do some real work because Windows is a little endian operating system (the JDK excepted). Note, however, that unlike the JDK DataInputStream and DataOutputStream classes, neither the System.Net.Sockets.Socket class nor the System.Net.Sockets.NetworkStream class provides a prepackaged means of sending or receiving any type of data other than raw arrays of ubyte . You're welcome to subclass the NetworkStream class and implement your own methods for transmitting other types. Microsoft is not being lazy: It omitted this functionality because there's a better way to send chunks of data in a safe manner. Binary data has often posed a problem for socket-based systems. Some organizations have actually forbidden developers from sending binary numeric data over a socket, so everything has to be converted to an agreed Unicode character representation (that is, to a string), transmitted, and then decoded at the other end. If you're considering doing this, bear in mind that the sending operation is not necessarily atomic and that transmitting a large block of data might require several receives at the other end to retrieve it all. Similarly, if Nagle coalescing has not been disabled, several small data items might be retrieved with a single receive operation. You must clearly define the data boundaries ”in essence, what you must do is define your own type marshaling mechanism. While this can occasionally be fun to do, life is too short to spend a lot of time writing libraries of marshaling routines. (Ask anyone on the COM development team at Microsoft.) Java has its own in-built means of bundling data and unbundling it again: serialization. Serialization is a mechanism for converting objects into a format that can be transmitted over a data stream and then reassembled into a copy of the original object at the other end. Native Java serialization can be used only by native Java applications ”a program written in C# will not understand the format of a Java serialized stream. The .NET Framework Class Library implements a more extensible mode of serialization that can be used by any language that executes under the auspices of the common language runtime, as well as some other selected environments. We'll discuss serialization in more detail in Chapter 10. Connectionless SocketsConnection-oriented sockets are essential if you want to make certain that data that is sent is actually received. The TCP protocol is quite good at ensuring that packets of data are not lost and that large multipacket data items are assembled in the correct order before being handed over to a receiving socket. This reliability comes at a cost, though, and establishing a TCP connection can consume considerable resources and time. In some situations, speed is more important than 100 percent accuracy. Take audio streaming, for example. Not every packet of audio data has to arrive at the receiving application that's outputting the resulting sounds, as long as most of the packets do. Missing packets will result in an occasional glitch or crackle. A human listener should be able to cope with this and would certainly find the result more pleasing than if the audio player were to halt while the retransmission of missing packets is requested ”or worse still, if the audio player were to report an exception every time a packet appeared out of sequence. Connectionless sockets are based on UDP. In this protocol, a sending application does not establish a direct connection to a receiving application. (This model uses the terms sender and receiver to identify the processes because it is more peer-to-peer-oriented than the client/server form implemented by connection-oriented sockets.) Instead, the sender just fires off a packet of data to an endpoint on a computer that it hopes is listening. If a receiver is listening, a process connected to that endpoint will receive the data, although the sender will not necessarily know it. Likewise, if the packet disappears without a trace or if no process is connected to the destination endpoint ”or even if the computer that should be listening is down ”the sender will be unaware. The sender just dispatches the data in the hope that it will arrive. This mechanism is sometimes referred to as "Send and Pray." Caution Do not confuse connectionless sockets with asynchronous communication mechanisms such as message queues. With connectionless sockets, if no process is listening on an endpoint, any data sent to that endpoint will evaporate into the ether . The data will not be held pending in any queue and will not be retrievable if the listening process starts up later. The PriceSender class (in the sample file CakePriceSender.jsl in the CakePriceSender project) is an example of a sending application. It periodically generates a message with information about offers on cakes, which it sends to a receiving application called PriceListener (in the file CakePriceListener.jsl in the CakePriceListener project). If the receiving application is listening, it will display the data in the message. Looking at the PriceSender class first, the initial difference between this and the original CakeServer class is the statement that creates the socket: Socketsender=null; sender=newSocket(AddressFamily.InterNetwork,SocketType.Dgram, ProtocolType.Udp); Although the address family is the same as before, the type of socket is different, as is the transport protocol. Connectionless sockets send individual datagrams (which are like telegrams but contain data) rather than establishing a continual stream, and they use UDP. The statements that create a local endpoint and bind the socket to that endpoint are identical to the earlier example (although the variables have different names): IPHostEntrysenderHostInfo=Dns.Resolve(Dns.GetHostName()); IPAddresssenderAddress=senderHostInfo.get_AddressList()[0]; IPEndPointsenderEndpoint=newIPEndPoint(senderAddress,senderPort); sender.Bind(senderEndpoint); Datagrams should to be sent to a named destination. The program prompts for the name of the computer running the receiving process, uses DNS to obtain its IP address, and then constructs an endpoint comprising this address and a port that the receiving process will be listening on. The endpoint is stored in variable receiverEndpoint . The program then enters a loop, simulating the periodic generation of messages and sending them out. In this case, all the program does is sleep for 10 seconds and then send the same string out each time. Tip The program shows a convenient way to convert a string into an array of bytes using Unicode encoding. The System.Text namespace contains several classes you can use for encoding text in different ways. The UnicodeEncoding class has the GetBytes method, which will convert a string into an array of Unicode byte-pairs (UTF-16). This is similar to the getBytes method of the java.lang.String class in the JDK. The various encoding classes also provide a GetString method that will convert an encoded byte array back into a string. You use the SendTo method to send a datagram. The parameters include the data to be sent, as a ubyte array (there is no direct equivalent to the java.net.DatagramPacket class of the JDK), and the endpoint to send the data to. SendTo is overloaded, and you can optionally specify the length of the data and a starting offset into the array as well as a selection of socket flags (just like the Send method). The PriceListener class in the CakePriceListener project creates a UDP socket and binds it to an endpoint on the local computer, using the same port that the sending process dispatches messages to. Having created the socket, the PriceListener class enters a loop, waiting for messages to appear and then printing them on the console. You wait for UDP messages by executing the ReceiveFrom method. This method is overloaded in a similar manner to SendTo , but at a minimum you should specify a ubyte array big enough to hold the received data (the common language runtime will throw an exception if the array is too small to hold the entire datagram) and an endpoint. The endpoint will be filled in with the details of the sending computer ”the IP address and the port. You must instantiate this endpoint variable before calling ReceiveFrom . The PriceListener class uses the values IPAddress.Any for the IP address and 0 for the port ”they will be overwritten when data is received, but you have to specify values for these parameters in the constructor, and these are good placeholders. Also notice that the variable is declared as an EndPoint rather than as an IPEndPoint (although it is instantiated using an IPEndPoint constructor). This is because the ReceiveFrom method expects an EndPoint and not an IPEndPoint , and in this situation the common language runtime cannot downcast the variable. EndPointsenderEndpoint=newIPEndPoint(IPAddress.Any,0); The ReceiveFrom method will block until a datagram arrives. Remember that datagrams are not queued; any datagrams that arrive while the PriceListener program is not listening will be lost. It is also possible for the sender to send an empty message. The ReceiveFrom method returns the number of bytes actually received (as does the Receive method, actually). You should check this value before processing the results because there might not be any! Assuming that some data actually does arrive, the program will convert it into a string (the sender sent it as an array of Unicode bytes, so the receiver must convert it back again) and prints it: Console.WriteLine(System.Text.Encoding.get_Unicode().GetString(cakePriceData)); Even though this example shows the principles of sending and receiving using UDP, it is not very realistic. A receiving application is much more likely to perform some task or other while waiting for data to arrive rather than sit in a blocked state waiting for data. We'll describe techniques for addressing this situation shortly. If you compile and run the CakePriceSender application, you'll be prompted for the name of the receiving computer, and then the program will go off in its loop of sleeping and generating messages. It doesn't matter that the receiving application isn't running ”the messages will just fall into the proverbial black hole. When you execute the CakePriceListener application, it will receive any messages that are sent after it started listening, as shown in Figure 9-3. (You might get a couple of messages immediately, but that would be due to the internal buffering used by sockets.) Figure 9-3. The CakePriceListener application
The System.Net.Sockets namespace also contains the UdpClient class. This is analogous to the TcpClient class, but it is for handling UDP sockets. Its counterpart in the JDK world is the java.net.DatagramSocket class. Like the TcpClient class, UdpClient automates much of the work involved in setting up a UDP socket and binding it to an endpoint. The overloaded constructor allows you to bind to an endpoint attached to a port on a local or remote machine. Once the endpoint has been created, you can use the Receive method to wait for data from a sender. Receive returns an array of ubyte and expects an IPEndPoint ( not an EndPoint ) parameter that identifies the sender. The main method shown below shows an alternative implementation of the PriceListener class. (This is available in the file UdpListener.jsl in the UdpListener project.) publicclassPriceListener { //Localportforreceiving privatestaticintreceiverPort=4002; //Maximumlengthofmessagesthatcanbereceived privatestaticintmessageLength=100; publicstaticvoidmain(String[]args) { //DefineaUdpClientforreceiving UdpClientreceiver=null; try { //CreatetheUDPlistener receiver=newUdpClient(receiverPort); //Loopforever,waitingformessagesandprintingthem while(true) { ubyte[]cakePriceData=newubyte[messageLength]; //Createanendpointwhichwillbepopulatedwiththeaddress //ofthesender IPEndPointsenderEndpoint=newIPEndPoint(IPAddress.Any,0); //Receivedatafromthesender(thismayblockorreturnzero //bytes) cakePriceData=receiver.Receive(senderEndpoint); if(cakePriceData.length>0) { //ConverttheubytearrayofUnicodecharactersinto //aStringanddisplayit Console.WriteLine(System.Text.Encoding.get_Unicode(). GetString(cakePriceData)); } } } catch(System.Exceptione) { Console.WriteLine("Exception: " +e); } finally { //Tidyup if(receiver!=null) receiver.Close(); } } } There is no UdpListener class (to match TcpListener ) because it is unnecessary. Instead, you can use the Connect method of UdpClient to attach to a remote endpoint and use the Send method to transmit data. Blocking and Nonblocking SocketsWhen you keep them simple, sockets are suspiciously easy to program against. However, as everyone knows , the world is not a simple place, and many real-world situations will be more complex than the CakeSizeServer and CakeClient scenario. Also, networks are fragile. Even though the TCP transport used by connection-oriented sockets does all it can to guarantee delivery of data, and in the correct sequence, the protocol cannot recover from situations such as a broken network or a computer crash. However, an application will know when such a failure has occurred because of errors raised in the sending or receiving application if a connection is unexpectedly terminated . For example, if a process is blocked while waiting to receive data on a socket and the sending process (or something else, such as a network failure) breaks the circuit, the receiver will trip a socket exception with the message "An existing connection was forcibly closed by the remote host." The same exception will occur if you try to send data down a socket when the receiver has closed it. The message is not specific enough to figure out exactly what the problem is, but at least you know that something nasty has happened at the other end of the wire, and the application can perform some sort of error recovery ”or at least record a diagnostic somewhere! A more interesting situation occurs if the client and server somehow get out of synch. When a server is executing the Receive method, it will block until the specified number of bytes of data have arrived. If they do not arrive, the Receive method might block indefinitely. There is also the issue of variable-length data: Sometimes there will be a need for a client or a server to send data of unspecified length, so a mechanism must be agreed upon that allows the receiving end of a socket to determine when all the data has been received. If the client gets itself into the state in which it is performing a receive operation on the same socket at the same time as the server (because it thinks it has sent all the data that the server is expecting and is waiting for a reply, but the server has not realized this and is still waiting for more data), both ends will wait for each other forever. To prevent such a situation from occurring, sockets have a Blocking property, which you can access using the set_Blocking and get_Blocking methods from J#. By default, this property is set to true . You can change it to false to create a nonblocking socket: SocketclientSocket=...; clientSocket.set_Blocking(false); The socket will no longer block if there is no data for it to read. Instead, calls to the Receive method will terminate immediately, sucking whatever data is available from the socket. The Receive method passes back the number of bytes read as the return value: intnumBytes=serverSocket.Receive(...); While this approach looks promising , it can lead to problems. For example, what should a server do if it reads zero bytes? Should it just loop until something does appear? What if it receives an incomplete message from the client? (The Send operation is not guaranteed to be atomic, especially if the client is sending a large amount of data.) How would the server know? Another problem is that issuing an Accept method call on a nonblocking socket will throw a SocketException with the message "A non-blocking socket operation could not be completed immediately" if no client is attempting to connect at that precise instant. And a client that connects to a server socket that is not currently open and accepting requests will also get an exception: "No connection could be made because the target machine actively refused it." An alternative to using a nonblocking socket is to set a receive timeout, in milliseconds , using SetSocketOption : SocketclientSocket; clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout,5000); If a timeout occurs, the common language runtime will throw a SocketException : "A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond." If you're using timeouts, you must be prepared to catch exceptions such as this, handle them elegantly, and recover if possible. |
I l @ ve RuBoard |