Network Programming

Overview

The need for computers and devices to communicate across a network is one of the key ingredients of enterprise programming. In its relentless goal to simplify programming, the .NET Framework includes a slew of new networking classes that are logical, efficient, and consistent.

The only drawback to networking with .NET is that no single dominant model exists. In this chapter, you'll learn how to manage network interaction using sockets (recipes 8.8 to 8.11), but you won't learn about two higher-level distributed programming frameworks—Web Services and .NET Remoting—which have their own dedicated chapters later in this book. Typically, socket-based network programming is ideal for closed systems that don't require interoperability, where developers want to have complete flexibility to tailor communication and control the data before it hits the wire.

Of course, this chapter doesn't concentrate exclusively on socket programming. You'll also learn about Web interaction, such as downloading a Web page from the Internet (recipe 8.5) or a single piece of information (recipe 8.6). You'll also learn how to retrieve Web connectivity information for the current computer, look up Internet Protocol (IP) addresses and domain names, and ping another computer to gauge its response time. At the end of this chapter, two recipes (8.13 and 8.14) show how you can build on the Transmission Control Protocol (TCP) classes included with .NET to work with higher-level protocols such as Post Office Protocol 3 (POP3) for e-mail and File Transfer Protocol (FTP) for transferring files.

Get Web Connectivity Information for the Current Computer

Problem

You need to determine programmatically if the current computer can connect to the Internet.

Solution

Use the Microsoft Windows API function InternetGetConnectedState.

Discussion

The InternetGetConnectedState function returns True if the current computer is configured to access the Internet. It also returns a dwFlags parameter that specifies the type of connection using one (or more) of a series of constants.

The following Console application defines the InternetGetConnectedState function and uses it to test the current computer's connectivity:

Public Module GetInternetState ' Declare the API function. Private Declare Function InternetGetConnectedState Lib "wininet" _ (ByRef dwFlags As Long, ByVal dwReserved As Long) As Long ' Define the possible types of connections. Private Enum ConnectStates LAN = &H2 Modem = &H1 Proxy = &H4 Offline = &H20 Configured = &H40 RasInstalled = &H10 End Enum Public Sub Main() ' Get the connected status. Dim dwFlags As Long Dim Connected As Boolean = _ (InternetGetConnectedState(dwFlags, 0&) <> 0) If Connected Then Console.WriteLine("This computer is connected to the Internet.") ' Display all connection flags. Console.Write("Connection flags:") Dim ConnectionType As ConnectStates For Each ConnectionType In _ System.Enum.GetValues(GetType(ConnectStates)) If (ConnectionType And dwFlags) = ConnectionType Then Console.Write(" " & ConnectionType.ToString()) End If Next End If Console.ReadLine() End Sub End Module

A sample output is shown here:

This computer is connected to the Internet. Connection flags: LAN

Notice that the InternetGetConnectedState reflects how the computer is configured. It doesn't reflect whether the computer is configured correctly (in other words, whether the Internet connection is actually working).

Get the IP Address of the Current Computer

Problem

You want to retrieve the IP address of the current computer, perhaps to use later in networking code.

Solution

Use the System.Net.Dns class, which provides shared GetHostName and GetHostByName methods.

Discussion

The Dns class provides domain name resolution services. You can invoke its GetHostName to retrieve the host name for the current computer. You can then translate the host name into an IP address using GetHostByName.

Dim HostName As String Dim IPAddress As String ' Look up the host name and IP address. HostName = System.Net.Dns.GetHostName() IPAddress = System.Net.Dns.GetHostByName(HostName).AddressList(0).ToString() Console.WriteLine("Host name:" & HostName) Console.WriteLine("IP address:" & IPAddress)

Be aware that the GetHostByName method returns a list of usable IP addresses. In most cases, this address list will contain only one entry.

If you run this code, you'll see something like this:

Host name: fariamat IP address: 24.114.131.70

Look Up a Host Name for an IP Address

Problem

You want to determine the IP address for a computer based on its domain name by performing a Domain Name System (DNS) query.

Solution

Use the System.Net.Dns class, which wraps this functionality in the GetHostByName method.

Discussion

On the Web, publicly accessible IP addresses are often mapped to host names that are easier to remember using a network of DNS servers, which are a fundamental part of the Internet backbone. To perform a DNS lookup, the computer might contact its cache or a DNS sever (which might in turn forward the request to a DNS root server).

This entire process is transparent if you use the System.Net.Dns class, which allows you to retrieve the IP address for a host name by calling GetHostByName. Here's how you might retrieve the list of IP addresses mapped to http:// www.yahoo.com:

Dim IP As System.Net.IPAddress For Each IP In System.Net.Dns.GetHostByName("www.yahoo.com").AddressList Console.WriteLine(IP.AddressFamily.ToString()) Console.WriteLine(IP.ToString()) Next

Ping an IP Address

Problem

You want to check if a computer is online and gauge its response time.

Solution

Send a ping message.

Discussion

A ping message contacts a device at a specific IP address, sends a test message, and requests that the remote device respond by echoing back the packet. You can measure the time taken for a ping response to be received to gauge the connection latency between two computers.

Despite the simplicity of ping messages compared to other types of network communication, implementing a ping utility in .NET requires a significant amount of complex low-level networking code. The .NET class library doesn't have a prebuilt solution—instead, you must use raw sockets.

However, at least one developer has solved the ping problem. Lance Olson, a developer at Microsoft, has provided C# code that allows you to ping a host by name or IP address and measure the milliseconds taken for a response. This code has been adapted into a PingUtility component, which is available with the code in this book's sample files.

To use the ping utility, you must first add a reference to the PingUtility.dll assembly. You can then use the shared Pinger.GetPingTime method with an IP address or domain name. The GetPingTime method returns the number of milliseconds that elapse before a response is received.

Console.WriteLine("Milliseconds to contact www.yahoo.com: " & _ PingUtility.Pinger.GetPingTime("www.yahoo.com")) Console.WriteLine("Milliseconds to contact www.seti.org: " & _ PingUtility.Pinger.GetPingTime("www.seti.org")) Console.WriteLine("Milliseconds to contact the local computer: " & _ PingUtility.Pinger.GetPingTime("127.0.0.1"))

The ping test allows you to verify that other computers are online. It can also be useful if your application needs to evaluate several different remote computers that provide the same content and to determine which one will offer the lowest network latency for communication.

  Note

A ping attempt might not succeed if a firewall forbids it. For example, many heavily trafficked sites ignore ping requests because they're wary of being swamped by a flood of simultaneous pings that will tie up the server (in essence, a denial of service attack).

Download a File Using HTTP

Problem

You want to retrieve a file from the Web.

Solution

Use the HttpWebRequest class to create your request, the WebResponse class to retrieve the response from the Web server, and some form of reader (typically a StreamReader for HTML or text data or a BinaryReader for a binary file) to parse the response data.

Discussion

Downloading a file from the Web takes the following four basic steps:

  1. Use the shared Create method of the System.Net.WebRequest class to specify the page you want. This method returns a WebRequest-derived object, depending on the type of Uniform Resource Identifier (URI) you use. For example, if you use an HTTP URI (with the scheme http://), it will create an HttpWebRequest instance. If you use a file system URI (with the scheme file://), it will create a FileWebRequest instance.
  2. Use the GetResponse method of the HttpWebRequest object to return a WebResponse object for the page.
  3. Create a StreamReader or BinaryReader for the WebResponse stream.
  4. Perform any steps you need to with the stream, such as writing it to a file.

The following code is a test application that retrieves and displays the HTML of a Web page. For it to work, you must import both the System.Net and the System.IO namespaces.

Public Module DownloadTest Public Sub Main() Dim Url As String = "http://www.prosetech.com/index.html" ' Create the request. Dim PageRequest As HttpWebRequest = _ CType(WebRequest.Create(Url), HttpWebRequest) ' Get the response. ' This takes the most significant amount of time, particularly ' if the file is large, because the whole response is retrieved. Dim PageResponse As WebResponse = PageRequest.GetResponse() Console.WriteLine("Response received.") ' Read the response stream. Dim r As New StreamReader(PageResponse.GetResponseStream()) Dim Page As String = r.ReadToEnd() r.Close() ' Display the retrieved data. Console.Write(Page) Console.ReadLine() End Sub End Module

To deal efficiently with large files that need to be downloaded from the Web, you might want to use asynchronous techniques, as described in Chapter 7. You can also use the WebRequest.BeginGetResponse, which doesn't block your code and calls a callback procedure when the response has been retrieved.

Retrieve a Single Piece of Information from a Web Page

Problem

You want to extract a single piece of information from a Web page.

Solution

Use the WebResponse class to retrieve the stream, copy it to a string, and use a regular expression.

Discussion

You can extract information from a Web stream in several ways. You could read through the stream, use methods of the String class such as IndexOf, or apply a regular expression. The latter of these—using a regular expression—is the most flexible and powerful.

The first step is to create a regular expression that filters out the information you need. Recipe 1.17 provides several examples and a reference to basic regular expression syntax. For example, most Web pages include a text title that is stored in a

tag. To retrieve this piece of information, you use the following regular expression:

 

(?.*?)

This expression retrieves all the text between the opening and closing

tag and places it in a named group called match. The following sample code uses this regular expression to retrieve the title from a URL the user enters. It requires three namespace imports: System.Net, System.IO, and System.Text. RegularExpressions.

Public Module ExtractTitleTest Public Sub Main() Console.WriteLine("Enter a URL, and press Enter.") Console.Write(">") Dim Url As String = Console.ReadLine() Dim Page As String Try ' Create the request. Dim PageRequest As HttpWebRequest = _ CType(WebRequest.Create(Url), HttpWebRequest) ' Get the response. ' This takes the most significant amount of time, particularly ' if the file is large, because the whole response is retrieved. Dim PageResponse As WebResponse = PageRequest.GetResponse() Console.WriteLine("Response received.") ' Read the response stream. Dim r As New StreamReader(PageResponse.GetResponseStream()) Page = r.ReadToEnd() r.Close() Catch Err As Exception Console.WriteLine(Err.ToString()) Return End Try ' Define the regular expression. Dim TitlePattern As String = "

(?.*?)

" Dim TitleRegex As New Regex(TitlePattern, _ RegexOptions.IgnoreCase Or RegexOptions.Singleline) ' Find the title. Dim TitleMatch As Match = TitleRegex.Match(Page) ' Display the title. If TitleMatch.Success Then Console.WriteLine("Found title: " & _ TitleMatch.Groups("match").Value) End If Console.ReadLine() End Sub End Module

Here's the output for a test run that retrieves the title from the Yahoo! search engine:

Enter a URL, and press Enter. >http://yahoo.com Response received. Found title: Yahoo!

If the Web page is extremely large, this approach might not be efficient because the entire stream is copied to a string in memory. Another option is to read through the stream character-by-character and try to build up a match to a search pattern. This approach requires more custom code and is demonstrated in detail with text searching in a file in recipe 5.8.

  Note

Screen scraping solutions such as this one can be quite brittle. If the user interface for the Web site changes and the expected pattern is altered, you'll no longer be able to extract the information you need. If you have control over the Web site, you can implement a much more robust approach using a Web service to return the desired information. Web services also support the full set of basic data types, which prevents another possible source of errors.

Find All Links in a Web Page

Problem

You want to retrieve all the hyperlinks in a Web page (perhaps because you want to download those pages also).

Solution

Retrieve the page using WebResponse, and use a regular expression to search for URIs.

Discussion

Retrieving links in a Web page is conceptually quite easy but often more difficult in practice. The problem is that Web pages follow a semi-standardized format and tolerate a great deal of variance. For example, a hyperlink can be added in the href attribute of an anchor, the onclick attribute of a JavaScript element such as a button, and so on. The URI itself could be relative (in which case it needs to be interpreted relative to the current page), fully qualified (in which case it can have one of countless schemes, including http:// or file:// or mailto://), or it might just be a bookmark (an anchor tag with an href that starts with the # character). Dealing with these myriad possibilities isn't easy.

The first step is to craft a suitable regular expression. In this case, we'll consider only the links that are provided in the href attribute of an anchor tag. Here's one regular expression that retrieves all href values from a Web page:

hrefs*=s*(?:"(?[^"]*)"|(?S+))

Another option is to retrieve absolute paths only. The following line of code is a slightly less complicated regular expression that matches href values that start with http://.

hrefs*=s*"(?http://.*?)"

The following sample application uses the first option. It then manually checks the retrieved URIs to see if they are bookmarks (in which case they are discarded) and to determine if they're relative or absolute. If the bookmarks are relative paths, the System.Uri class is used with the current page Uri to transform them into fully qualified paths.

Public Module ExtractURITest Public Sub Main() Console.WriteLine("Enter a URL, and press Enter.") Console.Write(">") Dim Url As String = Console.ReadLine() Dim BaseUri As Uri Dim Page As String Try BaseUri = New Uri(Url) ' Create the request. Dim PageRequest As HttpWebRequest = _ CType(WebRequest.Create(Url), HttpWebRequest) ' Get the response. ' This takes the most significant amount of time, particularly ' if the file is large, because the whole response is retrieved. Dim PageResponse As WebResponse = PageRequest.GetResponse() Console.WriteLine("Response received.") ' Read the response stream. Dim r As New StreamReader(PageResponse.GetResponseStream()) Page = r.ReadToEnd() r.Close() Catch Err As Exception Console.WriteLine(Err.ToString()) Console.ReadLine() Return End Try ' Define the regular expression. Dim HrefPattern As String HrefPattern = "hrefs*=s*(?:""(?[^""]*)""|(?S+))" Dim HrefRegex As New Regex(HrefPattern, _ RegexOptions.IgnoreCase Or RegexOptions.Compiled) ' Find and display all the href matches. Dim HrefMatch As Match = HrefRegex.Match(Page) Do While HrefMatch.Success Dim Link As String = HrefMatch.Groups(1).Value If Link.Substring(0, 1) = "#" Then ' Ignore this match, it was just a bookmark. Else ' Attempt to determine if this is a fully-qualified link ' by comparing it against some known schemes. Dim Absolute As Boolean = False If Link.Length > 8 Then Dim Scheme As String Scheme = Uri.UriSchemeHttp & "://" If Link.Substring(0, Scheme.Length) = Scheme Then _ Absolute = True Scheme = Uri.UriSchemeHttps & "://" If Link.Substring(0, Scheme.Length) = Scheme Then _ Absolute = True Scheme = Uri.UriSchemeFile & "://" If Link.Substring(0, Scheme.Length) = Scheme Then _ Absolute = True End If ' (You could compare it against additional schemes here.) If Absolute Then Console.WriteLine(Link) Else Console.WriteLine(New Uri(BaseUri, Link).ToString()) End If End If HrefMatch = HrefMatch.NextMatch() Loop Console.ReadLine() End Sub End Module

This code investigates each URI by comparing it against a few common schemes. Another approach would be to try to instantiate a new System.Uri instance using the retrieved URI string. If the string is not an absolute path, an error would occur. You could catch the resulting exception and respond accordingly.

Here's the partial output for a sample test:

Enter a URL, and press Enter. >http://www.nytimes.com Response received. http://www.nytimes.com/pages/jobs/index.html http://www.nytimes.com/pages/realestate/index.html http://www.nytimes.com/pages/automobiles/index.html http://www.nytimes.com/pages/world/index.html http://www.nytimes.com/pages/national/index.html http://www.nytimes.com/pages/politics/index.html http://www.nytimes.com/pages/business/index.html http://www.nytimes.com/pages/technology/index.html http://www.nytimes.com/pages/science/index.html ...

Communicate Using TCP

Problem

You need to send data between two computers on a network using a TCP/IP connection.

Solution

Use the TcpClient and TcpListener classes.

Discussion

TCP is a reliable, connection-based protocol that allows two computers to communicate over a network. It provides built-in flow-control, sequencing, and error handling, which makes it very reliable and easy to program with.

To create a TCP connection, one computer must act as the server and start listening on a specific endpoint. (An endpoint is defined as an IP address, which identifies the computer and port number.) The other computer must act as a client and send a connection request to the endpoint where the first computer is listening. Once the connection is established, the two computers can take turns exchanging messages. .NET makes this process easy through its stream abstraction. Both computers simply write to and read from a NetworkStream to transmit data.

  Note

Even though a TCP connection always requires a server and a client, there's no reason an individual application can't be both. For example, in a peer-to-peer application, one thread is dedicated to listening for incoming requests (acting as a server) while another thread is dedicated to initiate outgoing connections (acting as a client).

Once a TCP connection is established, the two computers can send any type of data by writing it to the NetworkStream. However, it's a good idea to begin designing a networked application by defining constants that represent the allowable commands. Doing so ensures that your application code doesn't need to hardcode communication strings.

Public Class ServerMessages Public Const AcknowledgeOK As String = "OK" Public Const AcknowledgeCancel As String = "Cancel" Public Const Disconnect As String = "Bye" End Class Public Class ClientMessages Public Const RequestConnect As String = "Hello" Public Const Disconnect As String = "Bye" End Class

In this example, the defined vocabulary is very basic. You would add more constants depending on the type of application. For example, in a file transfer application, you might include a client message for requesting a file. The server might then respond with an acknowledgment and return file details such as the file size. These constants must be compiled into a separate class library assembly, which must be referenced by both the client and server. Both the client and the server will also need to import the System.Net, System.Net.Sockets, and System.IO namespaces.

The following code is a template for a basic TCP server. It listens at a fixed port, accepts the first incoming connection, and then waits for the client to request a disconnect. At this point, the server could call the AcceptTcpClient method again to wait for the next client, but instead, it simply shuts down.

Public Module TcpServerTest Public Sub Main() ' Create a new listener on port 8000. Dim Listener As New TcpListener(8000) Console.WriteLine("About to initialize port.") Listener.Start() Console.WriteLine("Listening for a connection...") Try ' Wait for a connection request, ' and return a TcpClient initialized for communication. Dim Client As TcpClient = Listener.AcceptTcpClient() Console.WriteLine("Connection accepted.") ' Retrieve the network stream. Dim Stream As NetworkStream = Client.GetStream() ' Create a BinaryWriter for writing to the stream. Dim w As New BinaryWriter(Stream) ' Create a BinaryReader for reading from the stream. Dim r As New BinaryReader(Stream) If r.ReadString() = ClientMessages.RequestConnect Then w.Write(ServerMessages.AcknowledgeOK) Console.WriteLine("Connection completed.") Do Loop Until r.ReadString() = ClientMessages.Disconnect Console.WriteLine() Console.WriteLine("Disconnect request received.") w.Write(ServerMessages.Disconnect) Else Console.WriteLine("Could not complete connection.") End If ' Close the connection socket. Client.Close() Console.WriteLine("Connection closed.") ' Close the underlying socket (stop listening for new requests). Listener.Stop() Console.WriteLine("Listener stopped.") Catch Err As Exception Console.WriteLine(Err.ToString()) End Try Console.ReadLine() End Sub End Module

The following code is a template for a basic TCP client. It contacts the server at the specified IP address and port. In this example, the loopback address (127.0.0.1) is used, which always points to the current computer. Keep in mind that a TCP connection requires two ports: one at the server end, and one at the client end. However, only the server port needs to be specified. The client port can be chosen dynamically at runtime from the available ports, which is what the TcpClient class will do by default.

Public Module TcpClientTest Public Sub Main() Dim Client As New TcpClient() Try Console.WriteLine("Attempting to connect to the server " & _ "on port 8000.") Client.Connect(IPAddress.Parse("127.0.0.1"), 8000) Console.WriteLine("Connection established.") ' Retrieve the network stream. Dim Stream As NetworkStream = Client.GetStream() ' Create a BinaryWriter for writing to the stream. Dim w As New BinaryWriter(Stream) ' Create a BinaryReader for reading from the stream. Dim r As New BinaryReader(Stream) ' Start a dialogue. w.Write(ClientMessages.RequestConnect) If r.ReadString() = ServerMessages.AcknowledgeOK Then Console.WriteLine("Connected.") Console.WriteLine("Press Enter to disconnect.") Console.ReadLine() Console.WriteLine("Disconnecting...") w.Write(ClientMessages.Disconnect) Else Console.WriteLine("Connection not completed.") End If ' Close the connection socket. Client.Close() Console.WriteLine("Port closed.") Catch Err As Exception Console.WriteLine(Err.ToString()) End Try Console.ReadLine() End Sub End Module

Here's a sample connection transcript on the server side:

About to initialize port. Listening for a connection... Connection accepted. Connection completed. Disconnect request received. Connection closed. Listener stopped.

And here's a sample connection transcript on the client side:

Attempting to connect to the server on port 8000. Connection established. Connected. Press Enter to disconnect. Disconnecting... Port closed.

Create a Multithreaded TCP Network Server

Problem

You want to create a TCP server that can simultaneously handle multiple TCP clients.

Solution

Every time a new client connects, start a new thread to handle the request.

Discussion

A single TCP endpoint (IP address and port) can serve multiple connections. In fact, the operating system takes care of most of the work for you. All you need to do is launch a worker object on the server that will handle the connection on a new thread.

For example, consider the basic TCP client and server classes shown in recipe 8.8. You can convert the server into a multithreaded server that supports multiple simultaneous connections quite easily. First create a class that will interact with an individual client:

Public Class ClientHandler Private Client As TcpClient Private ID As String Public Sub New(ByVal client As TcpClient, ByVal ID As String) Me.Client = client Me.ID = ID End Sub Public Sub Start() ' Retrieve the network stream. Dim Stream As NetworkStream = Client.GetStream() ' Create a BinaryWriter for writing to the stream. Dim w As New BinaryWriter(Stream) ' Create a BinaryReader for reading from the stream. Dim r As New BinaryReader(Stream) If r.ReadString() = ClientMessages.RequestConnect Then w.Write(ServerMessages.AcknowledgeOK) Console.WriteLine(ID & ": Connection completed.") Do Loop Until r.ReadString() = ClientMessages.Disconnect Console.WriteLine(ID & ": Disconnect request received.") w.Write(ServerMessages.Disconnect) Else Console.WriteLine(ID & ": Could not complete connection.") End If ' Close the connection socket. Client.Close() Console.WriteLine(ID & ": Client connection closed.") Console.ReadLine() End Sub End Class

Next modify the server code so that it loops continuously, creating new ClientHandler instances as required and launching them on new threads. Here's the revised code:

Dim ClientNum As Integer Do Try ' Wait for a connection request, ' and return a TcpClient initialized for communication. Dim Client As TcpClient = Listener.AcceptTcpClient() Console.WriteLine("Server: Connection accepted.") ' Create a new object to handle this connection. ClientNum += 1 Dim Handler As New ClientHandler(Client, "Client " & _ ClientNum.ToString()) ' Start this object working on another thread. Dim HandlerThread As New System.Threading.Thread( _ AddressOf Handler.Start) HandlerThread.IsBackground = True HandlerThread.Start() ' (You could also add the Handler and HandlerThread to ' a collection to track client sessions.) Catch Err As Exception Console.WriteLine(Err.ToString()) End Try Loop

The following code shows the server-side transcript of a session with two clients:

Server: About to initialize port. Server: Listening for a connection... Server: Connection accepted. Client 1: Connection completed. Server: Connection accepted. Client 2: Connection completed. Client 2: Disconnect request received. Client 2: Client connection closed. Client 1: Disconnect request received. Client 1: Client connection closed.

You might want to add additional code to the network server so that it tracks the current worker objects in a collection. Doing so would allow the server to abort these tasks if it needs to shut down and enforce a maximum number of simultaneous clients. For more information, see the multithreading recipes in Chapter 7.

Communicate Using UDP

Problem

You need to send data between two computers on a network using a User Datagram Protocol (UDP) stream.

Solution

Use the UdpClient class, and use two threads: one to send data, and the other to receive it.

Discussion

UDP is a connectionless protocol that doesn't include any flow control or error checking. Unlike TCP, UDP shouldn't be used where communication is critical. However, because of its lower overhead, UDP is often used for "chatty" applications where it's acceptable to lose some messages. For example, imagine you want to create a network where individual clients send information about the current temperature at their location to a server every few minutes. You might use UDP in this case because the communication frequency is high and the damage caused by losing a packet is trivial (because the server can just continue to use the last received temperature reading).

The UDP template application shown in the following code uses two threads: one to receive messages, and one to send them. To test this application, load two instances at the same time. On computer A, specify the IP address for computer B. On computer B, specify the address for computer A. You can then send text messages back and forth at will. (You can simulate a test on a single computer by using two different ports and the loopback IP address 127.0.0.1.)

Public Module UdpTest Private LocalPort As Integer Public Sub Main() ' Define endpoint where messages are sent. Console.WriteLine("Connect to IP: ") Dim IP As String = Console.ReadLine() Dim Port As Integer = 8800 Dim RemoteEndPoint As New IPEndPoint(IPAddress.Parse(IP), _ Port) ' Define local endpoint (where messages are received). LocalPort = 8800 ' Create a new thread for receiving incoming messages. Dim ReceiveThread As New System.Threading.Thread( _ AddressOf ReceiveData) ReceiveThread.IsBackground = True ReceiveThread.Start() Dim Client As New UdpClient() Try Dim Text As String Do Text = Console.ReadLine() ' Send the text to the remote client. If Text <> "" Then ' Encode the data to binary using UTF8 encoding. Dim Data() As Byte = _ System.Text.Encoding.UTF8.GetBytes(Text) ' Send the text to the remote client. Client.Send(Data, Data.Length, RemoteEndPoint) End If Loop Until Text = "" Catch Err As Exception Console.WriteLine(Err.ToString()) End Try Console.ReadLine() End Sub Private Sub ReceiveData() Dim Client As New UdpClient(LocalPort) Do Try ' Receive bytes. Dim Data() As Byte = Client.Receive(Nothing) ' Convert bytes to text using UTF8 encoding. Dim Text As String = System.Text.Encoding.UTF8.GetString(Data) ' Display the retrieved text. Console.WriteLine(">> " & Text) Catch Err As Exception Console.WriteLine(Err.ToString()) End Try Loop End Sub End Module

Note that UDP applications cannot use the NetworkStream abstraction that TCP applications can. Instead, they must convert all data to a stream of bytes using an encoding class, as described in recipe 1.15 and recipe 2.18.

Send a Broadcast Message

Problem

You want to send a message to every user on the local subnet.

Solution

Use the UdpClient class with the appropriate broadcast address.

Discussion

Broadcasts are network messages that are forwarded to all the devices on a local subnet. When a broadcast message is received, the client decides whether to discard (depending on whether any application is monitoring the appropriate port). Broadcasts can't travel beyond a subnet because routers block all broadcast messages. Otherwise, the network could be swamped in traffic.

To send a broadcast message, you use a broadcast IP address, which is the IP address that identifies the network and has all the host bits set to 1. For example, if the network is identified by the first three bytes (140.80.0), the broadcast address would be 140.80.0.255. Alternatively, you can set all bits to 1 (the address 255.255.255.255), which specifies the entire network. In this case, the broadcast message will still travel only inside the local subnet because routers won't allow it to pass.

Dim IP As String = "255.255.255.255" Dim Port As Integer = 8800 Dim RemoteEndPoint As New IPEndPoint(IPAddress.Parse(IP), _ Port) Dim Client As New UdpClient() Dim Data() As Byte = System.Text.Encoding.UTF8.GetBytes("Broadcast Message") ' Send the broadcast message. Client.Send(Data, Data.Length, RemoteEndPoint)

Send E Mail Through SMTP

Problem

You want to send an e-mail address using a Simple Mail Transfer Protocol (SMTP) server.

Solution

Use the SmtpMail and MailMessage classes in the System.Web.Mail namespace.

Discussion

The classes in the System.Web.Mail namespace provide a bare-bones wrapper for the Collaboration Data Objects for Windows 2000 (CDOSYS) component. They allow you to compose and send formatted e-mail messages using SMTP.

Using these types is easy. You simply create a MailMessage object, specify the sender and recipient e-mail address, and place the message content in the Body property:

Dim MyMessage As New MailMessage() MyMessage.To = "someone@somewhere.com" MyMessage.From = "me@somewhere.com" MyMessage.Subject = "Hello" MyMessage.Priority = MailPriority.High MyMessage.Body = "This is the message!"

If you want, you can send an HTML message by changing the message format and using HTML tags:

MyMessage.BodyFormat = MailFormat.Html MyMessage.Body = "

" & _ "

This is the message!

"

You can even add file attachments using the MailMessage.Attachments collection and the MailAttachment class.

Dim MyAttachment As New MailAttachment("c:mypic.gif") MyMessage.Attachments.Add(MyAttachment)

To send the message, you simply specify the SMTP server name and call the SmtpMail.Send method.

SmtpMail.SmtpServer = "test.mailserver.com" SmtpMail.Send(MyMessage)

However, there is a significant catch to using the SmtpMail class to send an e-mail message. This class requires a local SMTP server or relay server on your network. In addition, the SmtpMail class doesn't support authentication, so if your SMTP server requires a username and password, you won't be able to send any mail. To overcome these problems, you can use the CDOSYS component directly through COM Interop (assuming you have a server version of Windows or Microsoft Exchange). Alternatively, you might want to access Microsoft Outlook using COM Automation, as described in Chapter 19 (see recipe 19.6).

Here's an example that uses the CDOSYS component to deliver SMTP using a server that requires authentication:

Dim MyMessage As New CDO.Message() Dim Config As New CDO.Configuration() ' Specify the configuration. Config.Fields(cdoSendUsingMethod) = cdoSendUsingPort Config.Fields(cdoSMTPServer) = "test.mailserver.com" Config.Fields(cdoSMTPServerPort) = 25 Config.Fields(cdoSMTPAuthenticate) = cdoBasic Config.Fields(cdoSendUserName) = "username" Config.Fields(cdoSendPassword) = "password" ' Update the configuration. Config.Fields.Update() MyMessage.Configuration = Config ' Create the message. MyMessage.To = "someone@somewhere.com" MyMessage.From = "me@somewhere.com" MyMessage.Subject = "Hello" MyMessage.TextBody = "This is the message!" ' Send the CDOSYS Message MyMessage.Send()

  Note

Note that the SMTP protocol can't be used to retrieve e-mail. For this task, you need the POP3 (as described in recipe 8.13) or IMAP protocol.

For more information about using and configuring your own SMTP server, you can refer to the Microsoft introduction to Internet e-mail and mail servers at http://www.microsoft.com/TechNet/prodtechnol/iis/deploy/config/mail.asp.

Retrieve E Mail Through POP3

Problem

You want to retrieve messages from a POP3 mail server.

Solution

Create a dedicated class that sends POP3 commands over a TCP connection.

Discussion

POP3 is a common e-mail protocol used to download messages from a mail server. POP3, like many Internet protocols, defines a small set of commands that are sent as simple ASCII-encoded messages over a TCP connection (typically on port 110).

Here's a listing of a typical POP3 dialogue, starting immediately after the client makes a TCP connection:

Server sends:+OK <22689.1039100760@mail.prosetech.com> Client sends:USER Server sends:+OK Client sends:PASS Server sends:+OK Client sends:LIST Server sends:+OK Server sends: Clients sends:RETR Server sends:+OK Server sends: Client sends:QUIT Server sends:+OK

To add this functionality to your .NET applications, you can create a Pop3Client class that encapsulates all the logic for communicating with a POP3 server. Your application can then retrieve information about messages using the Pop3Client class. We'll explore this class piece by piece in this recipe. To see the complete code, download the recipes for this chapter in this book's sample files.

The first step is to define a basic skeleton for the Pop3Client class. It should store a TcpClient instance as a member variable, which will be used to send all network messages. You can also add generic Send and ReceiveResponse messages, which translate data from binary form into the ASCII encoding used for POP3 communication.

Public Class Pop3Client Inherits System.ComponentModel.Component ' The internal TCP connection. Private Client As New TcpClient() Private Stream As NetworkStream ' The connection state. Private _Connected As Boolean = False Public ReadOnly Property Connected() As Boolean Get Return _Connected End Get End Property Private Sub Send(ByVal message As String) ' Send the command in ASCII format. Dim MessageBytes() As Byte = Encoding.ASCII.GetBytes(message) Stream.Write(MessageBytes, 0, MessageBytes.Length) Debug.WriteLine(message) End Sub Private Function GetResponse() As String ' Build up the response string until the line termination ' character is found. Dim Character, Response As String Do Character = Chr(Stream.ReadByte()).ToString() Response &= Character Loop Until Character = Chr(13) Response = Response.Trim(New Char() {Chr(13), Chr(10)}) Debug.WriteLine(Response) Return Response End Function ' (Other code omitted.) End Class

You'll notice that the Pop3Client is derived from the Component class. This nicety allows you to add and configure an instance of the Pop3Client class at design time using Microsoft Visual Studio .NET.

You can also add constants for common POP3 commands. One easy way to add constants is to group them in a private class nested inside Pop3Client, as shown here:

' Some command constants. Private Class Commands ' These constants represent client commands. Public Const List As String = "LIST" & vbNewLine Public Const User As String = "USER " Public Const Password As String = "PASS " Public Const Delete As String = "DELE " Public Const GetMessage As String = "RETR " Public Const Quit As String = "QUIT" & vbNewLine ' These two constants represent server responses. Public Const ServerConfirm As String = "+OK" Public Const ServerNoMoreData As String = "." End Class

The next step is to create a basic method for connecting to the POP3 server and disconnecting from it. Because Pop3Client derives from Component, it indirectly implements IDisposable. Therefore, you can also override the Dispose method to ensure that connections are properly cleaned up when the class is disposed.

Public Sub Connect(ByVal serverName As String, ByVal userName As String, _ ByVal password As String) If Connected Then Me.Disconnect() ' Connect to the POP3 server ' (which is almost always at port 110). Client.Connect(serverName, 110) Stream = Client.GetStream() ' Check if connection worked. CheckResponse(GetResponse()) ' Send user name. Send(Commands.User & userName & vbNewLine) ' Check response. CheckResponse(GetResponse()) ' Send password. Send(Commands.Password & password & vbNewLine) ' Check response. CheckResponse(GetResponse()) _Connected = True End Sub Public Sub Disconnect() If Connected Then Send(Commands.Quit) CheckResponse(GetResponse()) _Connected = False End If End Sub Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) If disposing Then Disconnect() MyBase.Dispose(disposing) End Sub

  Note

Some mail servers will not allow the password to be transmitted in clear text. In this case, you will need to manually encrypt the password information first using the appropriate algorithm before you submit it to the Pop3Client class.

The Pop3Client class uses a private CheckResponse procedure, which verifies that the server's message is the excepted confirmation and throws an exception if it isn't.

Private Sub CheckResponse(ByVal response As String) If Not (response.Substring(0, 3) = Commands.ServerConfirm) Then Client.Close() _Connected = False Throw New ApplicationException("Response " & response & _ " not expected.") End If End Sub

The only remaining step is to implement three higher-level methods: GetMessageList, which the client calls to retrieve a list of message headers, GetMessageContent, which the client calls to retrieve the body of a single message, and DeleteMessage, which is typically used to remove a message after its content is downloaded.

To support the GetMessageList method, you need to create a simple class for storing message header information, which includes a message number and size in bytes.

Public Class MessageHeader Private _Number As Integer Private _Size As Integer Public ReadOnly Property Number() As Integer Get Return _Number End Get End Property Public ReadOnly Property Size() As Integer Get Return _Size End Get End Property Public Sub New(ByVal number As Integer, ByVal size As Integer) Me._Number = number Me._Size = size End Sub End Class

The GetMessageList method returns an array of MessageHeader objects. When the server returns a period (.) on a separate line, the list is complete.

Public Function GetMessageList() As MessageHeader() If Not Connected Then Throw New _ InvalidOperationException("Not connected.") Send(Commands.List) CheckResponse(GetResponse()) Dim Messages As New ArrayList() Do Dim Response As String = GetResponse() If Response = Commands.ServerNoMoreData Then ' No more messages. Return CType(Messages.ToArray(GetType(MessageHeader)), _ MessageHeader()) Else ' Create an EmailMessage object for this message. ' Include the header information. Dim Values() As String = Response.Split() Dim Message As New MessageHeader(Val(Values(0)), Val(Values(1))) Messages.Add(Message) End If Loop End Function

To retrieve the information for a single message, the client calls GetMessageContent with the appropriate message number. The message content includes headers that indicate the sender, recipient, and path taken, along with the message subject, priority, and body. A more sophisticated version of the Pop3Client might parse this information into a class that provides separate properties for these details.

Public Function GetMessageContent(ByVal messageNumber As Integer) As String If Not Connected Then Throw New _ InvalidOperationException("Not connected.") Send(Commands.GetMessage & messageNumber.ToString() & vbNewLine) CheckResponse(GetResponse) Dim Line, Content As String ' Retrieve all message text until the end point. Do Line = GetResponse() If Line = Commands.ServerNoMoreData Then Return Content Else Content &= Line & vbNewLine End If Loop End Function

Finally DeleteMessage removes a message from the server based on its message number.

Public Sub DeleteMessage(ByVal messageNumber As Integer) If Not Connected Then Throw New _ InvalidOperationException("Not connected.") Send(Commands.Delete & messageNumber.ToString() & vbNewLine) CheckResponse(GetResponse()) End Sub

You can test the Pop3Client class with a simple program such as the following one, which retrieves all the messages for a given account:

Public Module Pop3Test Public Sub Main() ' Get the connection information. Dim Server, Name, Password As String Console.Write("POP3 Server: ") Server = Console.ReadLine() Console.Write("Name: ") Name = Console.ReadLine() Console.Write("Password: ") Password = Console.ReadLine() Console.WriteLine() ' Connect. Dim POP3 As New Pop3Client() POP3.Connect(Server, Name, Password) ' Retrieve a list of message, and display the corresponding content. Dim Messages() As MessageHeader = POP3.GetMessageList() Console.WriteLine(Messages.Length().ToString() & " messages.") Dim Message As MessageHeader For Each Message In Messages Console.WriteLine(New String("-"c, 60)) Console.WriteLine("Message Number: " & Message.Number.ToString()) Console.WriteLine("Size: " & Message.Size.ToString()) Console.WriteLine() Console.WriteLine(POP3.GetMessageContent(Message.Number)) Console.WriteLine(New String("-"c, 60)) Console.WriteLine() Next Console.WriteLine("Press Enter to disconnect.") Console.ReadLine() POP3.Disconnect() End Sub End Module

The output for a typical session is as follows:

POP3 Server: mail.server.com Name: matthew Password: opensesame 1 messages. ------------------------------------------------------------------------------ Message Number: 1 Size: 1380 Return-Path: Delivered-To: somewhere.com%someone@somewhere.com Received: (cpmta 15300 invoked from network); 5 Dec 2002 06:57:13 -0800 Received: from 66.185.86.71 (HELO fep01-mail.bloor.is.net.cable.rogers.com) by smtp.c000.snv.cp.net (209.228.32.87) with SMTP; 5 Dec 2002 06:57:13 -0800 X-Received: 5 Dec 2002 14:57:13 GMT Received: from fariamat ([24.114.131.60]) by fep01-mail.bloor.is.net.cable.rogers.com (InterMail vM.5.01.05.06 201-253-122-126-106-20020509) with ESMTP id <20021205145711.SJDZ4718.fep01- mail.bloor.is.net.cable.rogers.com@fariamat> for ; Thu, 5 Dec 2002 09:57:11 -0500 Message-ID: <004c01c29c6f$186c5150$3c837218@fariamat> From: To: Subject: Test Message Date: Thu, 5 Dec 2002 10:00:48 -0500 MIME-Version: 1.0 Content-Type: text/plain; charset="iso-8859-1" Content-Transfer-Encoding: 7bit X-Priority: 3 X-MSMail-Priority: Normal X-Mailer: Microsoft Outlook Express 6.00.2600.0000 X-MIMEOLE: Produced By Microsoft MimeOLE V6.00.2600.0000 X-Authentication-Info: Submitted using SMTP AUTH LOGIN at fep01-- mail.bloor.is.net.cable.rogers.com from [24.114.131.60] at Thu, 5 Dec 2002 09:57:11 -0500 Status: RO X-UIDL: Pe9pStHkIFc7yAE Hi! This is a test message! ------------------------------------------------------------------------------ Press Enter to disconnect.

You can see the transcript of commands sent and received in the Debug output window in Visual Studio .NET.

Access an FTP Site

Problem

You want to retrieve or upload files from an FTP server.

Solution

Use a third-party component, or create a dedicated class that sends FTP commands over a TCP connection.

Discussion

FTP (File Transfer Protocol) is a common protocol used to upload and download files from a server. FTP, like many Internet protocols, defines a small set of commands that are sent as simple ASCII-encoded messages over a TCP connection (typically on port 21).

The following is a listing of a typical FTP dialogue, starting immediately after the client makes a TCP connection. Notice that every line sent from the server begins with an FTP response code.

Server sends: 220-FTP server ready. Server sends: 220-<< Server sends: 220- Server sends: 220->> Server sends: 220 This is a private system - No anonymous login Client sends: USER Server sends: 331 User OK. Password required Client sends: PASS Server sends: 230-User authenticated. Client sends: PASV Server sends: 227 Entering Passive Mode (66,185,95,103,166,76) Client sends: TYPE I Server sends: 200 TYPE is now 8-bit binary Client sends: RETR Server sends: 150 Accepted data connection < file transferred on separate connection > Server sends: 226-File successfully transferred Server sends: 226 0.001 seconds (measured here), 2.09 Mbytes per second Client sends: QUIT Server sends: 221-Goodbye. You uploaded 0 and downloaded 3 kbytes.

FTP is a fairly detailed protocol, and implementing a successful FTP client is not a trivial task. One challenge is that FTP works in two modes: active and passive. In active mode, the FTP server attempts to transfer files to a client by treating the client as a server. In other words, the client must open a new connection and wait for an incoming server request. This configuration won't work with most firewalls, which is why most FTP use is over passive connections. In passive mode, the server provides a new server connection that the client can connect to for downloading data. To switch to passive mode, you use the PASV command.

The remainder of this recipe presents a bare-bones FTP client that can log on and download files. If you want more sophisticated FTP functionality, such as the ability to browse the directory structure and upload files, you can use a commercial component or you can extend this code. For a reference of valid FTP commands and return codes, refer to http://www.vbip.com/winsock/winsock_ftp_ref_01.asp.

The FtpClient class works on a similar principle to the Pop3Client class in recipe 8.13. The basic outline defines recognized command constants and an internal TCP connection.

Public Class FtpClient Inherits System.ComponentModel.Component ' The internal TCP connection. Private Client As New TcpClient() Private Stream As NetworkStream ' The connection state. Private _Connected As Boolean = False Public ReadOnly Property Connected() As Boolean Get Return _Connected End Get End Property ' Some command constants. Private Class Commands Public Const User As String = "USER " Public Const Password As String = "PASS " Public Const Quit As String = "QUIT" & vbNewLine Public Const GetFile As String = "RETR " Public Const UsePassiveMode As String = "PASV" & vbNewLine Public Const UseBinary As String = "TYPE I" & vbNewLine Public Const UseAscii As String = "TYPE A" & vbNewLine End Class Private Enum ReturnCodes ServiceReady = 220 Accepted = 200 PasswordRequired = 331 UserLoggedIn = 230 EnteringPassiveMode = 227 StartingTransferAlreadyOpen = 125 StartingTransferOpening = 150 TransferComplete = 226 End Enum ' (Other code omitted.) End Class

Next we add private functions for sending and receiving data, as well as a helper function that verifies that a given response begins with an expected return code.

Private Function Send(ByVal message As String) As String ' Send the command in ASCII format. Dim MessageBytes() As Byte = Encoding.ASCII.GetBytes(message) Stream.Write(MessageBytes, 0, MessageBytes.Length) Debug.WriteLine(message) ' Return the response code. Return GetResponse() End Function Private Function GetResponse() As String ' Retrieve all the available lines. Dim Character As String Dim Response As String = "" Do Do Character = Chr(Stream.ReadByte()).ToString() Response &= Character Loop Until Character = Chr(10) Loop While Stream.DataAvailable Response = Response.Trim(New Char() {Chr(13), Chr(10)}) Debug.WriteLine(Response) Return Response End Function Private Function CheckCode(ByVal response As String, _ ByVal expectedCode As ReturnCodes) As Boolean Return Val(response.Substring(0, 3)) = expectedCode End Function

The next step is to add the basic methods for connecting and disconnecting to an FTP server:

Public Sub Connect(ByVal serverName As String, ByVal userName As String, _ ByVal password As String) If Connected Then Me.Disconnect() ' Connect to the POP3 server ' (which is almost always at port 21). Client.Connect(serverName, 21) Stream = Client.GetStream() ' Send user name. Dim Response As String Response = GetResponse() Response = Send(Commands.User & userName & vbNewLine) If CheckCode(Response, ReturnCodes.PasswordRequired) Then ' Send password. Response = Send(Commands.Password & password & vbNewLine) End If If Not (CheckCode(Response, ReturnCodes.UserLoggedIn)) _ And Not (CheckCode(Response, ReturnCodes.ServiceReady)) Then Throw New ApplicationException("Could not log in.") End If _Connected = True End Sub Public Sub Disconnect() If Connected Then If Not TransferClient Is Nothing Then TransferClient.Close() End If Send(Commands.Quit) _Connected = False End If End Sub

The most complicated part of FtpClient is the code needed for downloading files. FtpClient uses passive mode, which means it must use a separate TcpClient instance to download files.

' Second connection for retrieving a file. Private TransferClient As TcpClient Private TransferEndpoint As IPEndPoint

The private CreateTransferClient procedure instructs the FTP server to use passive mode, retrieves the new IP address and port that it should use, and initializes the TcpClient object accordingly:

Private Sub CreateTransferClient() Dim Response As String = Send(Commands.UsePassiveMode) If Not CheckCode(Response, ReturnCodes.EnteringPassiveMode) Then Throw New ApplicationException("Error entering passive mode.") End If ' The IP address and port number is appended to the response. ' Retrieve these details. Dim StartPos As Integer = Response.IndexOf("(") Dim EndPos As Integer = Response.IndexOf(")") Dim IPAndPort As String = Response.Substring(StartPos + 1, _ EndPos - StartPos - 1) Dim IPParts() As String = IPAndPort.Split(","c) Dim IP As String = IPParts(0) + "." + IPParts(1) + "." + _ IPParts(2) + "." + IPParts(3) Dim Port As Integer = Convert.ToInt32(IPParts(4)) * 256 + _ Convert.ToInt32(IPParts(5)) ' Create the data transfer connection. TransferClient = New TcpClient() TransferEndpoint = New IPEndPoint(IPAddress.Parse(IP), Port) End Sub

In addition, the private SetMode procedure sends a message to the server indicating whether the file is binary or ASCII-based.

Private Sub SetMode(ByVal binaryMode As Boolean) Dim Response As String If binaryMode Then Response = Send(Commands.UseBinary) Else Response = Send(Commands.UseAscii) End If If Not CheckCode(Response, ReturnCodes.Accepted) Then Throw New ApplicationException("Could not change mode.") End If End Sub

To download a file, your application calls the DownloadFile method with the filename and a Boolean variable indicating whether binary or ASCII mode should be used. The DownloadFile method uses SetMode and CreateTransferClient accordingly. Once the connection is made, the NetworkStream is returned to the client, who can read from it and close it when complete. Using a stream avoids the overhead of storing the retrieved data in memory (for example, in a byte array), which can be quite enormous if a file several megabytes large is being downloaded.

Public Function DownloadFile(ByVal filename As String, _ ByVal binaryMode As Boolean) As NetworkStream ' Create a connection to the second port in passive mode. CreateTransferClient() TransferClient.Connect(TransferEndpoint) SetMode(binaryMode) Dim Response As String = Send(Commands.GetFile & filename & vbNewLine) If Not CheckCode(Response, ReturnCodes.StartingTransferAlreadyOpen) _ And Not (CheckCode(Response, ReturnCodes.StartingTransferOpening)) Then Throw New ApplicationException("Could not open connection.") End If ' Let the client read data from the network stream. ' This is more efficient that creating and returning an ' intermediate byte array, but it also relies on the client ' to close the stream. Return TransferClient.GetStream() End Function

The filename can be a complete relative path, as long as you use the forward slash (/) character to separate directories. For example, images/mypic.gif is valid, but imagesmypic.gif is not.

When the transfer is complete, the client must call ConfirmDownloadComplete, which reads the confirmation message from the server and frees it up to serve new requests.

Public Sub ConfirmDownloadComplete() Dim Response As String = GetResponse() CheckCode(Response, ReturnCodes.TransferComplete) End Sub

Putting it all together is easy. The following Console application connects to an FTP server and attempts to download a file. You can watch a transcript of all sent and received messages in the Debug window.

Public Module FtpTest Public Sub Main() ' Connect to an FTP server. Dim FTP As New FtpClient() FTP.Connect("ftp.adobe.com", "anonymous", "me@somewhere.com") ' Request a file. Dim Stream As NetworkStream = FTP.DownloadFile("license.txt", True) ' Copy the data into file 1K at a time. Dim fs As New FileStream("c:license.txt", IO.FileMode.Create) Dim BytesRead As Integer Do Dim Bytes(1024) As Byte BytesRead = Stream.Read(Bytes, 0, Bytes.Length) fs.Write(Bytes, 0, BytesRead) Loop While BytesRead > 0 ' Close the network stream and the file. Stream.Close() fs.Close() ' Retrieve the server confirmation message. FTP.ConfirmDownloadComplete() Console.WriteLine("File transfer complete.") Console.WriteLine("Press Enter to disconnect.") Console.ReadLine() FTP.Disconnect() End Sub End Module

Категории