|
|
|
|
||||||||||||
Chapter 3: The Base Class LibraryJust as any other language does, C# needs a runtime library it can depend on. C#'s runtime is the .NET framework's Base Class Library (BCL). The BCL is the basic runtime library required in all CLI implementations and includes all the classes in the System namespace, except for the following:
The BCL helps enable your programming work by extending the CLR's execution and compilation support with an extensive library of classes, collections, and system APIs that are used by all programming languages and compilers on .NET. Programmers using multiple languages no longer need to learn multiple programming models and runtime libraries, and code and data can be shared among applications. The BCL is also extensible. In .NET, you can inherit from and extend classes even if you don't have the original source code, as long as the class is not marked sealed in its manifest. At runtime, your derived class is the same as it would be if you had developed the original class yourself. Architecture and ProfilesThe .NET platform is built using a layered architecture, as shown in Figure 3.1. The CLR provides the basic environment and then the BCL builds on that; additional libraries, applications, and development tools build out to meet particular needs. Figure 3.1. The .NET platform includes the concept of profiles, predefined configurations of the BCL, the CLR, and certain additional libraries that enable specific functionality. A profile is tailored to the needs of a particular hardware environment or application, such as a mobile phone or home appliance. The only currently defined profiles are the Kernel and Compact profiles. The Kernel profile is the very minimum required of a conforming implementation of the CLI. The Kernel profile doesn't require support for floating-point math, complex arrays, reflection, or remoting. The Kernel profile contains only the BCL and compiler and execution facilities. The Compact profile adds XML, Networking and Reflection to the services available in the Kernel profile. The Compact profile is intended to enable small-footprint .NET applicationsfor mobile devices, appliances, and similar equipment that might suffer under resource limitations. Strings and Regular ExpressionsC# gains extensive support for string manipulation by exposing the string capabilities in the CLR. Strings have flexible formatting options, as well as comparison and search functions using the classes in System.Text.RegularExpressions. Strings can be used in many ways. Listing 3.1 shows several of them. Listing 3.1 String Manipulation Examplesusing System.Text;
.
.
.
string s1, s2;
StringBuilder sb;
// simple initialization; the strings are assigned references to
// the literal objects.
s1 = "This is a test.";
s2 = "this is a test.";
sb = new StringBuilder();
// case sensitive/insensitive compare
if ( string.Compare( s1, s2, true ) == 0 )
sb.Append( "The strings are equivalent." );
else
sb.Append( "The strings are not equivalent." );
char c = s1[7]; // gets the char at position 7
char [] ar = new char[20];
s1.CopyTo( 0, ar, 0, s1.Length ); // copies range of chars
CharEnumerator ce = s1.GetEnumerator(); // enumeration
while ( ce.MoveNext() )
{
sb.Append( ce.Current );
}
if ( s1.EndsWith( "test." ) ) // checks end of string
sb.Append( " The string is a test." );
s2 = (string)s1.Clone(); // copies a reference so that
// s2 now points to s1's string
int i = s1.IndexOf("is"); // returns 2
i = s1.LastIndexOf("is"); // returns 5
string s3 = s1.Insert(5, "value"); // has to make a new string
// strings are immutable
sb.Append(" " + s3);
s3 = s1.PadLeft(20, ' '); // right aligns s1 in a 20-char
// wide space.
s1 = s3.Trim(); // removes padding again; can also
// TrimLeft(), TrimRight()
s3 = s1.ToLower(); // convert to lower case
The CLR also provides the System.Text.RegularExpressions namespace, which contains flexible pattern-matching classes. Many of the searching methods in strings (such as EndsWith()) provide functionality that is easily implemented using a regular expression. Listing 3.2 shows some uses of Regex functionality. Listing 3.2 Using Regular Expressions 1: // illustrates using custom capture processing
2: protected string MatchEval( Match m )
3: {
4: // append a newline and return
5: return m.ToString() + "\n";
6: }
7:
8: private void DemoRegexp()
9: {
10: StringBuilder sb = new StringBuilder();
11: string s1, s2;
12: bool b;
13:
14: // Set up to strings; these are used throughout the following
15: // code.
16: s1 = "This is a test?";
17: s2 = "this is a test.";
18:
19: Regex r = new Regex("test");
20:
21: int i = r.Match( s1 ).Index; // equiv to s1.IndexOf( literal )
22:
23: r = new Regex("test.$");
24: b = r.IsMatch(s1, 0); // same result as
25: // s1.EndsWith( literal )
26:
27: s2 = "This is a really long, aimless sentence that " +
28: "has no real purpose but to illustrate using regular expressions.";
29:
30: // separate a sentence into individual words
31: r = new Regex(@"\S*[ \.]");
32: MatchCollection mc = r.Matches(s2, 0);
33: foreach ( Match m in mc )
34: {
35: // Process each word here; I just show it on the screen
36: System.Windows.Forms.MessageBox.Show(
37: m.Captures[0].ToString(), "Capture" );
38: }
39:
40: // place all the words in the sentence on their own line
41: s2 = r.Replace( s2, new MatchEvaluator( MatchEval ) );
42: System.Windows.Forms.MessageBox.Show(s2, "Capture");
43: }
Regular expressions have long been a part of computerized text processing. I first encountered regular expressions while working on Unix in 1990, and they were nothing new then. The examples in Listing 3.2 only scratch the surface of their power, but they illustrate the key concepts. A regular expression is a pattern, which can contain both literal text and special characters that are interpreted as any number of character classes. You pass the pattern to the Regex constructor, as in line 19 of Listing 3.2. This pattern is a simple literal stringthe word "test." I then use the Regex object's Match() method to look for that pattern in the string s1 at line 21. Line 23 uses a different pattern, making use of two character classes: the period and the dollar symbol. Regex interprets the period as a wildcard indicating any single character; the dollar symbol is likewise interpreted as the end of a line in a multiline input string, and as the end of the string. Therefore, the pattern "test.$" will match the word "test" if it is separated by one character from the end of a line or the end of the string; the Boolean value stored to b in line 24 will therefore be true because the word "test" is the last in the string s1, with one character (the question mark) separating it from the end of the string. Lines 31 to 38 demonstrate a more complicated expression and the Regex library's capability to find more than one match at a time. The pattern in line 31 looks for sequences of characters ending with either a space or the end of a line or the string. The @ before the pattern string makes it easier to write the string itself by turning off C#'s escape character processing (otherwise, each time I use a \, I would have to use two of them). The first item in the string is \S*. \S is an escape sequence that Regex interprets as matching any non-whitespace character. The asterisk tells it to match any number of them. The next item is a character list in brackets, [ \.]. Regex will look at the position of the character list for any one of the characters within the bracketsin this case, a space and a period (the latter escaped so it is interpreted as a literal character instead of a wildcard). The result of the pattern is to tell Regex to look for any sequence of non-whitespace characters ending either in a space or a period. The result is that it will match each individual word in the sentence. Having established the search pattern, line 32 uses the pattern to search the string for matches, but instead of just the first match, the Regex.Matches() method returns all matches in the string as a MatchCollection object. Each Match object contains a collection called Captures, which contains the text that matched the pattern. For a simple pattern like this one, there will be only one capture, but you can code more complex expressions that can match the text in multiple ways. In the end, the message box (lines 36 and 37) shows each word in the sentence in turn. Regular expressions don't have to be just for finding text; they also provide a way to transform it by using the Replace() method on the Regex instance. You can provide a string to use to replace text matching the pattern or take further control over the process by providing a delegate method that processes each match, or both. The code at lines 41 and 42 use the delegate method to hook the MatchEval() function into the replacement process. Line 41 invokes r.Replace() on the string s2, passing a MatchEvaluator delegate that is initialized with a reference to the MatchEval() function. The Regex instance r invokes the delegate with each match in the source string; whatever the MatchEval() function returns replaces the matched text in the text that Replace() returns. In this case, the search matches each word, and MatchEval() replaces each match with the match and a newline character (line 5). The resulting string is displayed in a dialog box, as shown in Figure 3.2. Figure 3.2 This has just been an introduction to regular expressions in .NET. Entire books have been written on the subject of regular expressions, but there are more topics to cover in this one. You can easily find more information on the Internet or learn about them with the book Sams Teach Yourself Regular Expressions in 24 Hours by Alexia Prendergrast, ISBN 0-672-31936-5. CollectionsCollections and containers are important tools for programmers, and .NET comes with its share of library classes, including ordered and unordered lists, stack, queue, and dictionary types. Table 3.1 lists the basic collections provided by the .NET framework. Table 3.1 NET Container Classes
Listing 3.3 shows using these classes. Listing 3.3 Using Basic Collections 1: public class KeyValue
2: {
3: string key;
4: string val;
5:
6: public KeyValue( string newKey, string newValue )
7: {
8: key = newKey;
9: val = newValue;
10: }
11:
12: public override string ToString()
13: {
14: return "Class KeyValue: Key: " + key + " Value: " + val;
15: }
16:
17: public string GetKey() { return key; }
18: public string GetValue() { return val; }
19: }
20:
21: class CollectionDemo
22: {
23: static void Main(string[] args)
24: {
25: KeyValue [] kv = new KeyValue[]
26: {
27: new KeyValue( "1", "value 1" ),
28: new KeyValue( "2", "value 2" ),
29: new KeyValue( "3", "value 3" ),
30: new KeyValue( "4", "value 4" ),
31: new KeyValue( "5", "value 5" )
32: };
33:
34: bool[] b = { true, false, false, true, true, false };
35:
36: ArrayList al = new ArrayList();
37: foreach ( KeyValue k in kv )
38: {
39: // add to array
40: al.Add( k );
41: }
42:
43: Hashtable ht = new Hashtable();
44: Queue q = new Queue();
45: Stack s = new Stack();
46:
47: Console.WriteLine( "\nArrayList:" );
48: foreach ( KeyValue k in al )
49: {
50: Console.WriteLine( k );
51:
52: // add to hashtable
53: ht.Add( k.GetKey(), k.GetValue() );
54:
55: // add to queue
56: q.Enqueue( k );
57:
58: // add to stack
59: s.Push( k );
60: }
61:
62: // lookup by key
63: Console.WriteLine( "\nHashtable:" );
64: Console.WriteLine( ht["4"] );
65:
66: // objects return in order they were added
67: Console.WriteLine( "\nQueue:" );
68: while ( q.Count > 0 )
69: {
70: Console.WriteLine( q.Dequeue() );
71: }
72:
73: // objects returned in opposite order
74: Console.WriteLine( "\nStack:" );
75: while ( s.Count > 0 )
76: {
77: Console.WriteLine( s.Pop() );
78: }
79:
80: Console.WriteLine( "\nBitArray:" );
81: BitArray ba = new BitArray(b);
82: foreach ( bool bv in ba )
83: {
84: Console.WriteLine( bv );
85: }
86:
87: // add to sorted list in random order
88: SortedList sl = new SortedList();
89: int [] order = { 3, 4, 1, 2, 0 };
90: foreach ( int i in order )
91: sl.Add( kv[i].GetKey(), kv[i].GetValue() );
92:
93: // the objects come back out sorted by key
94: Console.WriteLine( "\nSortedList:" );
95: foreach ( DictionaryEntry de in sl )
96: Console.WriteLine( de.Key + ", " + de.Value );
97: }
98: }
To demonstrate using collections requires a set of objects to store in them, so Listing 3.3 begins by declaring a KeyValue type to store string pairs. The type overrides the Object.ToString() to facilitate writing the objects out to the screen. Lines 25 to 32 create and populate an array of five of these objects. To test the BitArray, line 34 declares an additional array of bool values. The first collection in the example is the ArrayList declared at line 36. The ArrayList combines the behaviors of arrays and lists, providing an unordered bag of objects that can be accessed by using array syntax, as in al[3], or by using an enumerator as in the example. Lines 37 to 41 add each of the KeyValue objects into the array, and then the foreach loop starting on line 48 writes each of them out to the console. The foreach that iterates the values in the ArrayList puts those same values into a Hashtable, a Queue, and a Stack. The Hashtable contains any object and uses the GetHashCode() method to obtain an integer hash value for each item's key. You can override this function to provide a custom hash calculation or, as Listing 3.3 does, depend on the implementation provided by Object. You can retrieve objects from the Hashtable using array notation and a key value, as demonstrated in line 64. For efficiency, Hashtable bins objects based on their hash code and then searches only in the appropriate bin for the requested key's value. The Queue is a collection to store objects by order of insertion and retrieve them in the same order. You use the Enqueue() method to add objects to the collection, as at line 56, and then Dequeue() to retrieve the next object from the collection, as at line 70. The Stack object functions in the opposite manner. Objects come out of the stack in the opposite order from which they went in. You use the Stack's Push() method to add an object onto the stack and Pop() to take it off. Lines 74 to 78 demonstrate retrieving the objects off the stack. The BitArray collection stores bit values that you access as bool elements. In Listing 3.3, the array of bool declared at line 34 initializes the BitArray ba on line 81. You can also declare the size of the array in the object's constructor. You access individual elements of the array using array notation with an integer index. BitArray objects have the additional capability to perform binary AND, NOT, OR and XOR operations. For example, the following code masks off the first two bits of the first example by using an AND operation: bool[] b2 = { false, false, true, true, true, true };
BitArray ba2 = ba.And( new BitArray( b2 ) );
The last collection to discuss, the SortedList, orders its contents based on the value of the items' keys. In the example, lines 88 through 91 create a SortedList instance and then add the test elements into the list in random order. The output from the code retrieving the items, however, displays them in order as follows: SortedList: 1, value 1 2, value 2 3, value 3 4, value 4 5, value 5 As with other collections, you can access the items in a SortedList by using its indexer and the object's key value: string str = (string)sl["1"]; These are the basic collections provided by the framework. However, other collections are implemented throughout the framework for specific items. These other collection types implement the ICollection interface and are listed in the documentation for that interface in the .NET Framework Reference. SerializationSerialization is the process of assembling a representation of an object's state in a form that can be persisted to a disk file or sent across the network and from which the object can be re-created in another context. .NET supports serialization by separating the destination (file, network connection, and so on), the rendering (translating the object to and from a stream of bytes), and the object itself. Translating from an object to a stream and back again is handled by objects called formatters. These are contained in namespaces that are children of the System.Runtime.Serialization.Formatters namespace. .NET comes with two formatters out of the box: one that records a binary copy of objects (BinaryFormatter) and one that stores the object into a SOAP envelope (SoapFormatter). The binary formatter renders the object into an efficient binary representation that can be read quickly; this is a good solution for saving to disk or for transmitting objects across the network between similar platforms. The SOAP representation, on the other hand, can be read and used on any platform, so it might be useful in heterogeneous systems but not without a cost; the difference in size between the binary and SOAP versions of an object are significant. Listing 3.4 demonstrates using the serialization classes to write objects out to disk and read them back in. Listing 3.4 Serializing with Formatters 1: using System;
2: using System.IO;
3: using System.Collections;
4: using System.Runtime.Serialization;
5: using System.Runtime.Serialization.Formatters.Binary;
6: using System.Runtime.Serialization.Formatters.Soap;
7: .
8: .
9: .
10: [Serializable]
11: class StreetAddress
12: {
13: public int id;
14: public string name, street1, street2, city, state, zip;
15:
16: public StreetAddress()
17: {
18: name = street1 = street2 = city = state = zip = "";
19: id = 0;
20: }
21:
22: public StreetAddress(int inId, string inName, string inStreet1,
23: string inStreet2,
24: string inCity, string inState, string inZip)
25: {
26: id = inId;
27: name = inName;
28: street1 = inStreet1;
29: street2 = inStreet2;
30: city = inCity;
31: state = inState;
32: zip = inZip;
33: }
34: }
35:
36: class Serializer
37: {
38: static void Main(string[] args)
39: {
40: ArrayList addresses = new ArrayList(10);
41:
42: // create a list of addresses
43: for ( int id = 0; id < 10; id++ )
44: {
45: addresses.Add( new StreetAddress(id, "AName",
46: "123 Main St.", "Ste. 800",
47: "Anywhere", "AK", "12345") );
48: }
49:
50: // write the information out as XML
51: IFormatter soapFmt = new SoapFormatter();
52: Stream s = File.Open( "outfile.xml", FileMode.Create );
53: soapFmt.Serialize( s, addresses );
54: s.Close();
55:
56: // write the information out in binary form
57: IFormatter binFmt = new BinaryFormatter();
58: s = File.Open("outfile.bin", FileMode.Create);
59: binFmt.Serialize( s, addresses );
60:
61: s.Close();
62:
63: // reopen and read the data back in
64: s = File.Open( "outfile.bin", FileMode.Open );
65: addresses = binFmt.Deserialize( s ) as ArrayList;
66:
67: for ( int i = 0; i < addresses.Count; i++ )
68: Console.WriteLine(
69: ((StreetAddress)addresses[i]).id.ToString() + " " +
70: ((StreetAddress)addresses[i]).name);
71: }
72: }
The first class declared in Listing 3.4, StreetAddress, holds typical address information. StreetAddress is marked with the Serializable attribute so it can be serialized. The Serializer class creates a collection of these objects in lines 40 through 48. It then uses a SoapFormatter to write a SOAP version of the address list and a BinaryFormatter to write a binary version. All that is necessary is to create the formatter (lines 51 and 57), create a stream for it to write the information to (lines 52 and 58), and then invoke the formatter's Serialize() method. Most objects contain more than just the values that make up their state; they usually also contain references to other types, which must also be re-created when the object is deserialized. The formatter takes care of inspecting the object and identifying the references it holds. It walks through the reference graph and serializes each object onto the stream. With the necessary information written into the stream, re-creating the object out of the stream only requires using the Deserialize() method on the same kind of formatter that was originally used to write the stream. The Serializer class reads the array of addresses back in from the binary file by first opening it as a stream at line 64 and then calling the BinaryFormatter.Deserialize() method on line 65. Because the Deserialize() method returns Object, I added the as ArrayList to type the result back to an ArrayList reference. Input and OutputEven with a freshly minted application platform, some of the same old needs remain. One of these is the need to move information between your program and other entities. The .NET platform provides stream- and random-access I/O classes and the System.Console class, the members of which are used by console applications to access the standard input and output streams. In addition to the expected file and console I/O, .NET provides classes for memory, string, and network streams. Listing 3.5 shows the most basic of file operations in C#. Listing 3.5 Using Basic I/O1: // Create a file with some data in it. 2: string str = "This is some text."; 3: FileStream fs = File.Create( "testfile.txt" ); 4: byte [] buff; 5: 6: // get the bytes for each character in str 7: // and write to the file. 8: buff = Encoding.Unicode.GetBytes( str ); 9: fs.Write( buff, 0, buff.Length ); 10: 11: fs.Close(); Listing 3.5 uses a FileStream object to create a text file and then write a string value to it. The static File.Create() method creates a file on disk and returns a FileStream attached to it. Lines 8 gets the underlying bytes from the string, and line 9 writes those bytes to the stream. Finally, line 11 closes the FileStream. Working directly with byte arrays is okay when you need that level of control, but often it is more work than it's worth. Therefore, the BCL has higher-level classes to facilitate more streamlined I/O. Listing 3.6 demonstrates accomplishing the same result as Listing 3.5, but uses the StreamWriter class to simplify the code. Listing 3.6 Using Simpler I/O Using StreamWriter string str = "This is some text.";
StreamWriter sw = new StreamWriter("testfile.txt", false);
sw.Write( str );
sw.Close();
The StreamWriter is intended to aid you in reading and writing string values on a stream. Using the StreamWriter also has the advantage of writing the Unicode preamble (0xFEFF) to the text file automatically, enabling the OS and other programs to automatically recognize the file's encoding and byte order. If you need instead to write ASCII text, you can specify the encoding to the StreamWriter: StreamWriter sw = new StreamWriter("testfile.txt", false,
System.Text.Encoding.ASCII);
.NET supports stream composition to further facilitate I/O operations. For example, the BinaryWriter class enables you to read and write basic value types without going through the intervening steps necessary to obtain their raw data representation. BinaryWriter defines Write() methods for the base value types and for byte and char arrays. The following code writes a byte array to a sample file: byte [] ar = new byte[10]; // initialize with values 0 to 9 for ( byte i = 0; i < 10; ar[i] = i, i++ ); FileStream fs = File.Create( "testfile.dat" ); BinaryWriter bw = new BinaryWriter( fs ); bw.Write( ar ); bw.Close(); To write a character array is no more difficult: string str = "This is some text."; char [] cha = str.ToCharArray(); FileStream fs = File.Create( "testfile.dat" ); BinaryWriter bw = new BinaryWriter( fs ); bw.Write( cha ); bw.Close(); Reading from a stream involves using the symmetrically defined reader classes. Reading a string, for example, looks like the following: string str; StreamReader sr = File.OpenText( "testfile.txt" ); str = sr.ReadToEnd(); Console.WriteLine(str); sr.Close(); All the foregoing examples have been synchronous, meaning that control does not return to your code until the operation completes; however, you will often want to use an asynchronous model, particularly with console or network operations. To enable asynchronous operation, you construct a FileStream object with the appropriate parameters and then use the BeginRead() method to start the actual I/O operation. BeginRead() accepts as one of its parameters a delegate that you initialize with the address of a callback method. The method is called when the I/O is completed. You can also pass a state variable in to BeginRead(), which will then be passed to the callback method. Listing 3.7 demonstrates these features. Listing 3.7 Using Basic I/O 1: struct ReadInfo
2: {
3: public FileStream fs;
4: public byte [] ba;
5: public long bufSz;
6: public ManualResetEvent ev;
7: }
8:
9: static void ReadCallback( IAsyncResult res )
10: {
11: ReadInfo ri = (ReadInfo)res.AsyncState;
12: for ( int i = 0; i < ri.bufSz; i++ )
13: Console.Write( ri.ba[i] );
14:
15: ri.fs.Close();
16: ri.ev.Set();
17: }
18:
19: static void Main(string[] args)
20: {
21: // Create the callback delegate
22: AsyncCallback ac = new AsyncCallback( ReadCallback );
23: ReadInfo ri = new ReadInfo();
24:
25: // open the file
26: ri.fs = new FileStream(
27: "testfile.dat", // path
28: FileMode.Open, // open mode
29: FileAccess.Read, // access requested
30: FileShare.None, // sharing?
31: 256, // buffer size
32: true // isAsync?
33: );
34:
35: // set up other variables in the state struct
36: ri.bufSz = ri.fs.Length;
37: ri.ba = new byte [ri.bufSz];
38: ri.ev = new ManualResetEvent( false );
39:
40: // Start the read operation
41: ri.fs.BeginRead(
42: ri.ba, // buffer
43: 0, // offset to start
44: (int)ri.fs.Length, // n bytes to read
45: ac, // callback
46: ri // state object
47: );
48:
49: // Wait for the child thread.
50: ri.ev.WaitOne();
51: }
To hold the various pieces of information related to the I/O operationthe stream, buffer, buffer size, and the synchronization objectthe code first declares the ReadInfo structure with appropriate methods. The next step is to write the method that will receive the data when the read is complete, the function ReadCallback(). Finally, the code in Main() ties it all together. Line 22 creates an AsyncCallback with ReadCallback() as its target, and line 23 instantiates the state object for the operation. Lines 26 to 33 create the new FileStream with appropriate parameters, the most important for the demonstration being the last; this bool value, set to true, tells the FileStream to set up for asynchronous operation. With the FileStream opened, I use its Length property to size the buffer in the ri structure in lines 36 and 37 and then create a synchronization event to signal when the operation is complete. With all that preamble, the actual read operation starts with the BeginRead() call at line 41. This passes the read buffer, an offset into the buffer at which to start reading (which is zero in this case), the number of bytes to read, the callback delegate, and the state structure. All the main thread does after that is wait for the synchronization event. The call to BeginRead() returns immediately, but of course, the read itself does not happen instantaneously. A separate thread of execution completes the operation, which is the reason for the ManualResetEvent. While the main thread is waiting on the event, the system completes the read and then calls into ReadCallback() with the result. This function writes out the data from the file (lines 11 to 13), closes the file (line 15), and then signals the event. The main thread then resumes execution at line 51, from which it exits normally. Although it requires more setup to work, in many instances, particularly in server or distributed applications, asynchronous I/O is necessary. It does not make sense for small data blocks, but for larger or unpredictably sized items, using asynchronous operations can keep you from tying up your application for extended periods while waiting for I/O to complete. Network CommunicationNetwork communication in C# follows the same model as traditional languages do, but with fewer details for the user to manage. Socket programming can still be difficult, but wrapper classes facilitate common tasks. The System.Net* namespaces are not technically part of the Base Class Libraries, but I include them in this discussion because communication is so important today. The framework and Windows support a number of protocols, but I focus on Internet Protocol (IP) and sockets programming because most readers needing to communicate across the network will use IP-based technology. The two protocols in common use are the Transmission Control Protocol (TCP) and the User Datagram Protocol (UDP). With each of these protocols, a datagram is sent from a client identified by its IP address and a port number, a logical identifier that identifies a program or service on a computer. A Web server, for example, will usually listen to requests on port 80. UDP provides connectionless, unreliable delivery of packets between computers. UDP is called unreliable because it does not include any mechanism for maintaining a connection between computers or assuring delivery of data to the recipient. UDP doesn't even guarantee that the receiving program will receive information in the same order it was sent. The program using UDP to communicate has to provide any error checking or receipt mechanism it needs. TCP, on the other hand, is a connection-oriented, assured-delivery protocol that ensures that the data sent by the program reaches the intended recipient in order, or that the application is notified if not. As packets are received on the destination computer, they are organized into the same order as they were sent, and acknowledgements are transmitted back to the sender as each sequential packet is recognized. If the sender doesn't receive an acknowledgement, it retransmits the packet that was lost. Eventually, if too many errors occur, TCP reports an error back to the application that is trying to send the data. This method of communication is often easier to work with than UDP, but it also uses more network resources to exchange the same amount of information. SocketsA socket is an endpoint of communication between two programs that can carry information in either direction between the programs using it. To establish communication by using sockets, you must specify five things: the IP addresses of the sender and the receiver, the port number each is using, and the protocol used to communicate (normally TCP or UDP). The first program wanting to communicate creates a socket and binds it to a port number and protocol on its host. To establish communication, the second program connects to the first using a socket it has created itself. When the connection is made (with TCP) or a packet is sent (with UDP), the five parts of the channel's identity have been specified: the host and port on each end of the channel and the protocol used between them. When using the UDP protocol, no persistent connection exists between the communicating applications; a packet is sent with the hope it will be received. With TCP, on the other hand, a virtual circuit is established between the two computers that persists until the participating applications shut it down. That virtual circuit is serviced by the TCP providers on each end exchanging status messages to manage traffic flowing between the two computers. Communicating with SocketsIn most communications using sockets, one application starts and begins listening on a socket. Another application later starts and sends information to the first, already-running application. For convenience, I'll refer to the first application as the server and the second as the client. The first step in establishing the communication for both the client and the server is to create a Socket and bind it to the address family, address, and port with which the socket will be associated. What happens next depends on the kind of connection you plan to use. For connection-oriented TCP sockets (stream sockets), the server next places the socket into a listening state and waits for connections. On the client end, the communicating program issues a connection request identifying the server and port to which it wants to connect. After the connection is made, the programs can communicate using the socket. Connectionless UDP sockets require no further setup. Because no persistent connection exists between computers, the server enters its listening state, and the client sends the message. No acknowledgement is returned, therefore the operation returns immediately. Listing 3.8 shows how to send and receive datagram messages. Listing 3.8 Communicating with a Datagram Socket 1: using System;
2: using System.Net;
3: using System.Net.Sockets;
4: 5: namespace Comm
5: {
6: class Communication
7: {
8: const int sendPort = 20000;
9: const int bufSize = 256;
10: void Start( string ipremote )
11: {
12: Socket soc;
13: int bytesMoved = 0; // number of bytes transferred
14: byte [] buf = new byte[bufSize]; // data buffer
15: // Set up the local socket address
16: IPEndPoint localEp = new IPEndPoint( IPAddress.Any, sendPort );
17: // create a datagram socket
18: soc = new Socket(
19: AddressFamily.InterNetwork,
20: SocketType.Dgram,
21: ProtocolType.Udp );
22: // bind the endpoint to the socket
23: soc.Bind( localEp );
24: if ( ipremote == "" )
25: {
26: // server mode - listen
27: bytesMoved = soc.Receive( buf );
28: }
29: else
30: {
31: // client side - transmit
32: IPEndPoint remote =
33: new IPEndPoint( IPAddress.Parse(ipremote), sendPort);
34: for ( int i = 0; i < bufSize; buf[i] = (byte)i, i++ );
35: bytesMoved = soc.SendTo( buf, remote );
36: }
37: Console.WriteLine( "Transferred {0} bytes.", bytesMoved );
38: }
39:
40: static void Main(string[] args)
41: {
42: Communication comm = new Communication();
43: // If IP to connect to is specified on the command line,
44: // connect as client; otherwise listen as server
45: if ( args.Length == 1 )
46: {
47: comm.Start( args[0] );
48: }
49: else
50: comm.Start( "" );
51: }
52: }
53: }
Listing 3.8 imports the System, System.Net, and System.Net.Sockets namespaces. Although most of the classes needed to work with sockets are declared in System.Net.Sockets, several classes in System.Net are also necessary. The program's entry point is the Main() function, beginning at line 40. The program is invoked in either a client or server mode, depending on whether you specify an IP address on the command line. Main() invokes the Start() function using the IP address or an empty string, and then Start() does the appropriate work. The Start() function sends a block of data to another computer if it receives an IP address in its remote parameter or waits to receive data if not. The first step, at line 16, is to create an IPEndPoint containing the local computer's IP address and the port the program will use to communicate. Lines 18 to 21 create a new IP datagram socket that will use UDP to send information. The socket's setup is completed by binding the endpoint to it using Socket.Bind() at line 23.
Line 24 selects the program's behavior based on whether an IP was supplied. If the program is in server mode, line 27 calls Socket.Receive() to listen for incoming information. When data arrive, the library writes them into the buf buffer and returns the number of bytes that were received. On the other hand, if the program is in client mode, it creates a new endpoint with the address and port to send the data to (line 32), fills in the buffer with some data to send (line 34), and then sends the information (line 35). That's all you need to send UDP datagrams. Life would be simpler if that was all you needed for network programming. However, UDP is simple because it has no control mechanism to manage delivery. The next example, Listing 3.9, uses TCP to |