Windows Internet Overview (WinInet)
As previously mentioned, WinInet is a set of helper API functions that eases the creation of Internet client applications that use the more popular Internet protocols such as HTTP and FTP. What this basically means is that WinInet enables you to write programs over these protocols without having to worry about the underlying socket details. Just for the record, WinInet sits on top of Winsock.
To get a better idea of how WinInet interacts with Winsock, Figure 2.1 shows how it fits into the TCP/IP OSI model described in Chapter 1.
Figure 2.1. WinInet and the TCP/IP OSI layers
You can see that WinInet resides in the session layer of the protocol stack, talking directly with the transport layer to send information. Instead of working with TCP directly and creating sockets to read and write over a particular protocol, your application uses a WinInet session. HTTP and FTP sessions created with WinInet have a special kind of handle type, HINTERNET, which is synchronized and can be shared among multiple threads.
To use the WinInet library with your application, you need to include the header wininet.h, and link with the wininet.lib library.
Handling WinInet Errors
To get the last error from any WinInet API call, you can use the standard system GetLastError() function to return the error code for the last function that failed. More specific error definitions are located in wininet.h if you want to look up more information about an error code. If you require a more "human friendly" description of an error, or extended error information, you can simply call the InternetGetLastResponseInfo() function:
BOOL InternetGetLastResponseInfo(LPDWORD lpdwError,
LPTSTR lpszBuffer, LPDWORD lpdwBufferLength);
The lpdwError parameter is a pointer to a variable that will contain the error code of the last function. The lpszBuffer and lpdwBufferLength parameters are used to describe the buffer that will contain the error text. When calling InternetGetLastResponseInfo(), set lpdwBufferLength to the size of lpszBuffer. If the buffer is too small, the function will return FALSE, with an error code of ERROR_INSUFFICIENT_BUFFER. The size of the buffer that is required will then be stored in lpdwBufferLength. If the function succeeds, it will return TRUE and the error description will be in the buffer pointed to by lpszBuffer.
If you are using the HTTP protocol over WinInet, Pocket PC also supports displaying a system error dialog box into which a user can enter data (such as a username or password) if an HTTP request fails. Table 2.1 shows the HTTP errors that are returned from GetLastError() by using the InternetErrorDlg() function.
Table 2.1. Errors Supported by InternetErrorDlg() Error Name
Dialog Box Displayed
ERROR_INTERNET_HTTP_TO_HTTPS_ON_REDIR
Notification that the user is crossing secure/nonsecure zones
ERROR_INTERNET_INCORRECT_PASSWORD
User/Password dialog box
ERROR_INTERNET_INVALID_CA
Notification of an invalid security certificate authority
ERROR_INTERNET_POST_IS_NON_SECURE
Notification about passing data through a nonsecure connection
ERROR_INTERNET_SEC_CERT_CN_INVALID
Notification that the SSL Common Name is invalid
ERROR_INTERNET_SEC_CERT_DATE_INVALID
Notification that the SSL certificate has expired
The function is defined as follows:
DWORD InternetErrorDlg(HWND hWnd, HINTERNET hRequest,
DWORD dwError, DWORD wFlags, LPVOID *lppvData);
The hWnd parameter is the handle to a parent window for the dialog box and can be NULL. The next parameter, hRequest, is a HINTERNET handle to the Internet connection that was used to make the HttpSendRequest() that failed, and dwError is the error code that was returned from the GetLastErrror() function. The wFlags parameter specifies the action that the function should take (and can be one or a combination of the following values):
FLAGS_ERROR_UI_FILTER_FOR_ERRORS examines the HTTP header for additional errors.
FLAGS_ERROR_UI_FLAGS_CHANGE_OPTIONS saves the results of the dialog box in the HINTERNET handle that was passed in.
FLAGS_ERROR_UI_FLAGS_GENERATE_DATA inspects the hRequest handle for any additional information for the error.
FLAGS_ERROR_UI_SERIALIZE_DIALOGS enables the serialization of multiple password requests.
Finally, lppvData contains any additional data that is returned from the error dialog box. Using the FLAGS_ERROR_UI_FLAGS_GENERATE_DATA flag will cause lppvData to be filled with error-specific data, such as invalid certificates, and FLAGS_ERROR_UI_SERIALIZE_DIALOGS will return an INTERNET_AUTH_NOTIFY_DATA structure with user/password data.
The InternetErrorDlg() function's return value will tell you how the user interacted with the dialog box. If the function was successful, it will return ERROR_SUCCESS; if the user decided to cancel the dialog box, an ERROR_CANCELLED result is returned. If the function needs to retry any part of the original request (such as rechecking a password), you would then be returned the value ERROR_INTERNET_FORCE_RETRY.
Internet Connections and Proxies
In order to use WinInet (and Winsock, for that matter), you first need an active Internet connection. Because Pocket PC does not support the desktop InternetAutodial() function, which would automatically establish a connection if you were not online already, you have to use the Pocket PC Connection Manager to establish an Internet connection (see Chapter 7).
Proxy servers are also supported by the WinInet APIs. A proxy server is typically used as both a speed enhancement and a security measure for internal networks. While a proxy prevents your network from unauthorized access, a user/password combination is usually required when making Internet requests over the proxy server. To specify that you want to use a proxy server for a current Internet session, you need to set the INTERNET_OPEN_TYPE_PROXY flag when you use InternetOpen() to start your Internet session. You can then use the InternetSetOption() function (see the section "Internet Options") with the flags INTERNET_OPTION_PROXY_USERNAME and INTERNET_OPTION_PROXY_PASSWORD to set your authorization; otherwise, Pocket PC will attempt to use your default server login information instead. If you receive an error when accessing proxy resources, you can use the InternetErrorDlg() function to have users re-enter their username and password.
Currently, WinInet supports the CERN and SOCKS proxies for HTTP and TIS gateway proxies for FTP requests. You can also use a CERN proxy for FTP requests as long as the request is converted to an HTTP address or made by opening a connection with a URL.
Uniform Resource Locators (URLs)
If you have ever used a Web browser (and I'm sure you have or you would not be reading this), then you have already seen and used Uniform Resource Locators, otherwise known as URLs. A URL is a string that represents the location of a particular resource (a file, a directory, and so on) that is somewhere on the Internet. The accepted syntax for a well-formed URL address is as follows:
[protocol]://[username:password]@[servername]:[port]/[resource]
For example, a well-formed URL could be something as simple as http://www.microsoft.com, or it might appear as something more complicated, such as the following:
ftp://anonymous:anonymous@ftpsite.com:21/files/somefile.wav
If you look at the individual parts of a URL, you can see that the address contains all the information you need to create an application for working with this resource. Normally, if you want to parse the address, you need to perform a great deal of string manipulation for all of the possible combinations.
Fortunately, WinInet provides functions that enable you to create, parse, combine, and convert (also called canonicalize) URL addresses.
Because a URL can contain numerous "parts," both of the functions that create and break up a URL address use the URL_COMPONENTS structure. Before examining the specific WinInet URL functions, let's examine this structure:
typedef struct {
DWORD dwStructSize;
LPWSTR lpszScheme;
DWORD dwSchemeLength;
INTERNET_SCHEME nScheme;
LPWSTR lpszHostName;
DWORD dwHostNameLength;
INTERNET_PORT nPort;
LPWSTR lpszUserName;
DWORD dwUserNameLength;
LPWSTR lpszPassword;
DWORD dwPasswordLength;
LPWSTR lpszUrlPath;
DWORD dwUrlPathLength;
LPWSTR lpszExtraInfo;
DWORD dwExtraInfoLength;
} URL_COMPONENTS, * LPURL_COMPONENTS;
The URL_COMPONENTS structure contains all the individual pieces of a URL:
dwStructSize is the size of the URL_COMPONENTS structure.
lpszScheme and dwSchemeLength are the address and length, respectively, of the string buffer containing the scheme (or protocol) name. nScheme is the value of an INTERNET_SCHEME enumeration that specifies the protocol scheme.
lpszHostName and dwHostNameLength are the address and length, respectively, of the string buffer containing the host address.
nPort is the server port.
lpszUserName and dwUserNameLength are the address and length, respectively, of the string buffer containing the user name.
lpszPassword and dwPasswordLength are the address and length, respectively, of the string buffer containing the password.
lpszUrlPath and dwUrlPathLength are the address and length, respectively, of the string buffer containing the URL path.
lpszExtraInfo and dwExtraInfoLength are the address and length, respectively, of the string buffer containing any extra URL information, such as additional anchor links (e.g., #anchor).
The nScheme member uses the INTERNET_SCHEME enumerator to determine the appropriate value for the protocol specified in the URL address. The enumerator is defined as follows:
typedef enum {
INTERNET_SCHEME_PARTIAL = -2,
INTERNET_SCHEME_UNKNOWN = -1,
INTERNET_SCHEME_DEFAULT = 0,
INTERNET_SCHEME_FTP,
INTERNET_SCHEME_GOPHER,
INTERNET_SCHEME_HTTP,
INTERNET_SCHEME_HTTPS,
INTERNET_SCHEME_FILE,
INTERNET_SCHEME_NEWS,
INTERNET_SCHEME_MAILTO,
INTERNET_SCHEME_SOCKS,
INTERNET_SCHEME_JAVASCRIPT,
INTERNET_SCHEME_VBSCRIPT,
INTERNET_SCHEME_FIRST = INTERNET_SCHEME_FTP,
INTERNET_SCHEME_LAST = INTERNET_SCHEME_VBSCRIPT
} INTERNET_SCHEME, *LPINTERNET_SCHEME;
Now that you have defined all of the components of a URL address, let's take a look at what is required to create and parse this string.
To create a new URL string, you call the following function:
BOOL InternetCreateUrl(LPURL_COMPONENTS lpUrlComponents,
DWORD dwFlags, LPWSTR lpszUrl, LPDWORD lpdwUrlLength);
The function takes the passed-in pointer to a URL_COMPONENTS structure, the lpUrlComponents parameter, and constructs a new URL that is placed in the string buffer to which lpszUrl points. The dwFlags parameter controls certain aspects of the URL's creation, and can be a combination of the flags found in Table 2.2.
Table 2.2. InternetCreateUrl() Flags Flag
Description
ICU_ESCAPE
Converts all escape sequences into characters
ICU_USERNAME
Uses the default system username
Finally, the lpdwUrlLength parameter contains a pointer to a DWORD value, which is the size of the lpszUrl buffer. If the function successfully returns, lpdwUrlLength will contain the size of the new URL address string; otherwise, it will contain the required size of the buffer if it is too small. Finally, if you don't require a particular component piece, just make that member of URL_COMPONENTS a NULL value.
You can create a URL with the following:
URL_COMPONENTS url;
TCHAR tchURL[1024] = TEXT("\0");
DWORD dwLength = 1024;
BOOL fSuccess = FALSE;
// Setup the URL_COMPONENTS structure
memset(&url, 0, sizeof(URL_COMPONENTS));
url.dwStructSize = sizeof(URL_COMPONENTS);
url.lpszScheme = TEXT("http");
url.lpszHostName = TEXT("www.microsoft.com");
url.nScheme = INTERNET_SCHEME_HTTP;
url.nPort = 80;
url.lpszUrlPath = TEXT("index.htm");
// Create the URL
fSuccess = InternetCreateUrl(&url, ICU_ESCAPE, tchURL,
&dwLength);
The corresponding output URL (the value of tchURL) from the preceding code would look as follows:
http://www.microsoft.com/index.htm
Now that you've seen what's required to assemble a URL, you can use the InternetCrackUrl() function to perform the inverse operation—that is, take a string and parse it into a URL_COMPONENTS structure:
BOOL InternetCrackUrl(LPCWSTR lpszUrl, DWORD dwUrlLength,
DWORD dwFlags, LPURL_COMPONENTS lpUrlComponents);
When using InternetCrackUrl(), you first need to prepare the URL_COMPONENTS structure that will receive the parts of the string you pass in. Make sure that you initialize the structure's dwStructSize parameter to the size of URL_COMPONENTS, and that you set the corresponding length member value of the part you want to parse to a nonzero value, as shown in the following example:
URL_COMPONENTS urlCracked;
memset(&urlCracked, 0, sizeof(URL_COMPONENTS));
urlCracked.dwStructSize = sizeof(URL_COMPONENTS);
// Set which parts of URL_COMPONENTS we want to have
// returned
urlCracked.dwHostNameLength = 1;
urlCracked.dwSchemeLength = 1;
urlCracked.dwUrlPathLength = 1;
Once the structure has been set up, you can call InternetCrackUrl(). Set the lpszUrl parameter to the string of the URL, and its length in the dwUrlLength parameter. The dwFlags parameter can be one of the values shown in Table 2.3.
Finally, lpUrlComponents is a pointer to your URL_COMPONENTS structure.
Table 2.3. InternetCrackUrl() Flags Flag
Description
ICU_DECODE
Converts all encoded characters into their normal form
ICU_ESCAPE
Converts all escape sequences into their corresponding characters
You can crack the previously created URL in the following way:
fSuccess = InternetCrackUrl(tchURL, lstrlen(tchURL)*sizeof(TCHAR),
0, &urlCracked);
When the function returns, the urlCracked structure will contain all the parts of the passed-in URL parameter.
To combine two separate URL parts, such as a base URL and a relative URL, into one string, you can use the InternetCombineUrl() function:
BOOL InternetCombineUrl(LPCWSTR lpszBaseUrl, LPCWSTR
lpszRelativeUrl, LPWSTR lpszBuffer, LPDWORD lpdwBufferLength,
DWORD dwFlags);
The first two parameters are the URLs you want to combine—first the base URL, followed by the relative URL. The lpszBuffer parameter is a pointer to a string buffer that will hold your new URL, and lpdwBufferLength contains a pointer to the buffer size. If the buffer is too small, the required size will be in lpszBufferLength when the function fails. Finally, the dwFlags parameter can be one of the following:
ICU_BROWSER_MODE prevents the encoding or decoding of any characters following the # or ? characters.
ICU_DECODE coverts all escape sequences to characters.
ICU_ENCODE_SPACES_ONLY encodes spaces. Spaces are encoded as %20.
ICU_NO_ENCODE prevents the conversion of any unsafe characters.
ICU_NO_META prevents the removal of any meta-sequences from your URLs.
If the function succeeds, it will return TRUE.
Finally, to canonicalize a URL (which converts the URL into a "safe" form), you can use the "InternetCanonicalizeUrl" function:
BOOL InternetCanonicalizeUrl(LPCWSTR lpszUrl, LPWSTR lpszBuffer,
LPDWORD lpdwBufferLength, DWORD dwFlags);
The lpszUrl parameter is the URL you want to convert. The lpszBuffer points to a string buffer that will hold the converted URL and its length, as specified by lpdwBufferLength. If the function fails because the buffer is too small, lpdwBufferLength will point to the length of the required buffer size. The last parameter, dwFlags, is the same as that in the InternetCombineUrl() function.
Internet Cache (Temporary Internet Files)
The temporary Internet cache is a storage area on your device that captures all data received over the network via WinInet transactions. Web pages downloaded with Pocket Internet Explorer, as well as files downloaded via FTP or HTTP, are stored here. When a request is made to download an Internet resource, WinInet first checks the cache to see if it already exists. If it does, then the data is retrieved from the cache instead, enabling an overall faster transfer (it's already on your device), and providing you with the capability to access resources when you are not connected to the network. All WinInet functions will store data (for both FTP and HTTP sessions) in the cache, unless you specify the INTERNET_FLAG_NO_CACHE_WRITE flag when downloading data.
WinInet's APIs enable you to enumerate items in the cache, set cached item data, and delete cache entries. Most of the functions use an INTERNET_CACHE_ENTRY_INFO structure to represent a cached item's information:
typedef struct _INTERNET_CACHE_ENTRY_INFO {
DWORD dwStructSize;
LPWSTR lpszSourceUrlName;
LPWSTR lpszLocalFileName;
DWORD CacheEntryType;
DWORD dwUseCount;
DWORD dwHitRate;
DWORD dwSizeLow;
DWORD dwSizeHigh;
FILETIME LastModifiedTime;
FILETIME ExpireTime;
FILETIME LastAccessTime;
FILETIME LastSyncTime;
LPBYTE lpHeaderInfo;
DWORD dwHeaderInfoSize;
LPWSTR lpszFileExtension;
union {
DWORD dwReserved;
DWORD dwExemptDelta;
};
} INTERNET_CACHE_ENTRY_INFO, * LPINTERNET_CACHE_ENTRY_INFO;
Table 2.4 describes the members of INTERNET_CACHE_ENTRY_INFO.
Table 2.4. INTERNET_CACHE_ENTRY_INFO Members Member
Description
dwStructSize
Specifies the size of the INTERNET_CACHE_ENTRY_INFO structure.
lpszSourceUrlName
Specifies the URL name of the file.
lpszLocalFileName
Specifies the local filename.
CacheEntryType
Specifies the type of cache entry. If the file was downloaded from the Internet, the value is 0. If the file is a cookie or history entry, it is a combination of EDITED_CACHE_ENTRY, NORMAL_CACHE_ENTRY, and STICKY_CACHE_ENTRY.
dwUseCount
Specifies the user count of the cache entry.
dwHitRate
Specifies how many times the cache entry was used.
dwSizeLow
Specifies the low order of the cache file size.
dwSizeHigh
Specifies the high order of the cache file size.
LastModifiedTime
Specifies when the file was last modified in GMT format.
ExpireTime
Specifies when this cache file will expire in GMT format.
LastAccessTime
Specifies when the cache file was last accessed.
LastSyncTime
Specifies when the cache file was last synchronized.
lpHeaderInfo
Points to a buffer that contains the header information for the cached file.
dwHeaderInfoSize
Specifies the size of the buffer used in lpHeaderInfo.
lpszFileExtension
Points to a buffer that contains the cached file's extension.
dwReserved
Must be 0.
dwExemptDelta
Specifies the exemption time from the last accessed time, in seconds.
For cache entries that are history or cookie files, the CacheEntryType member can be a combination of two values. The EDITED_CACHE_ENTRY value is used for entries that have been changed since the entry was originally downloaded, the NORMAL_CACHE_ENTRY value is for normal entries, and the STICKY_CACHE_ENTRY value is used for entries that are persistent and ignore the dwExemptDelta member.
Finding Out What's in the Cache
Enumerating entries that are in the local Internet cache is similar to the FindFirstFile() and FindNextFile() APIs that are used on local files. To "walk" through the cache and receive an INTERNET_CACHE_ENTRY_INFO structure for each entry, you'll use the FindFirstUrlCacheEntry() and FindNextUrlCacheEntry() functions:
HANDLE FindFirstUrlCacheEntry(LPCWSTR lpszUrlSearchPattern,
LPINTERNET_CACHE_ENTRY_INFO lpFirstCacheEntryInfo,
LPDWORD lpdwFirstCacheEntryInfoBufferSize);
BOOL FindNextUrlCacheEntry(HANDLE hEnumHandle,
LPINTERNET_CACHE_ENTRY_INFO lpNextCacheEntryInfo,
LPDWORD lpdwNextCacheEntryInfoBufferSize);
To walk the cache files, you first call FindFirstUrlCacheEntry(). The lpszUrlSearchPattern parameter can be set to NULL (to return all entries), "visited:" (to return only URLs), or "cookie:" (to return only Web site cookie information). Next, the lpFirstCacheEntryInfo entry should point to an initialized INTERNET_CACHE_ENTRY_INFO structure. Make sure you set the structure's dwStructSize member variable to the size of INTERNET_CACHE_ENTRY_INFO before calling the function. The last parameter, lpdwFirstCacheEntryInfoBufferSize, should point to a DWORD that is the size of the lpFirstCacheEntryInfo parameter.
After calling FindFirstUrlCacheEntry(), the function should return a valid handle that you can use to walk through the rest of the cache files by passing it into the first parameter of the FindNextUrlCacheEntry() function. If it fails, you will be returned a NULL value.
To continue enumerating through the Internet cache, you repeatedly call FindNextUrlCacheEntry() to get each cached item until it returns FALSE. Once it does, you can close your enumerator by calling the following:
BOOL FindCloseUrlCache(HANDLE hEnumHandle);
Therefore, to walk through all the cache files, you can simply do the following:
INTERNET_CACHE_ENTRY_INFO *piCacheInfo = NULL;
HANDLE hCacheHandle = NULL;
DWORD dwCacheInfoSize = 1024;
BOOL fSuccess = FALSE;
// Create a buffer, which will be of 1024 bytes for the
// INTERNET_CACHE_ENTRY_INFO structure. This is larger than
// the normal size (80 bytes), to make sure it's large
// enough for most cache data.
piCacheInfo = (INTERNET_CACHE_ENTRY_INFO *)LocalAlloc(LPTR,
dwCacheInfoSize);
if(!piCacheInfo)
return FALSE;
piCacheInfo->dwStructSize = dwCacheInfoSize;
hCacheHandle = FindFirstUrlCacheEntry(NULL, piCacheInfo,
&dwCacheInfoSize);
if(!hCacheHandle) {
if(GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
// Under normal circumstances, we would have to reallocate
// a larger buffer, and try to make the call again.
return FALSE;
}
return FALSE;
}
// By this point, we should have a good handle, so let's
// start enumerating
do {
dwCacheInfoSize = 1024;
memset(piCacheInfo, 0, dwCacheInfoSize);
piCacheInfo->dwStructSize = dwCacheInfoSize;
// Walk through to the next entry
fSuccess = FindNextUrlCacheEntry(hCacheHandle,
piCacheInfo, &dwCacheInfoSize);
if(!fSuccess) {
if(GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
// Under normal circumstances, we would have to
// reallocate a larger buffer and try to make
// the call again.
}
break;
}
} while(fSuccess);
LocalFree(piCacheInfo);
FindCloseUrlCache(hCacheHandle);
return 0;
While walking through all of the files in the cache is interesting, it's typically more useful to directly interact with a cache entry for a particular URL:
BOOL GetUrlCacheEntryInfo(LPCWSTR lpszUrlName,
LPINTERNET_CACHE_ENTRY_INFO lpCacheEntryInfo,
LPDWORD lpdwCacheEntryInfoBufferSize);
The lpszUrlName parameter specifies the URL for which you want to retrieve information from the local cache, and the rest of the parameters are the same as the FindFirstUrlCacheEntry() function.
Once you have found a particular cache entry you are interested in, you need to lock the cache file in order to do anything with it. To do so, you can call RetrieveUrlCacheEntryFile():
BOOL RetrieveUrlCacheEntryFile(LPCWSTR lpszUrlName,
LPINTERNET_CACHE_ENTRY_INFO lpCacheEntryInfo,
LPDWORD lpdwCacheEntryInfoBufferSize, DWORD dwReserved);
The parameters are the same as for GetUrlCacheEntryInfo(). If the function is successful, your cached data file is now locked, and will not be removed by other processes. You can now do whatever you want with the file, but remember to unlock it before the cache manager can access it again.
To unlock a cache file, use the UnlockUrlCacheEntryFile() function, which is defined as follows:
BOOL UnlockUrlCacheEntryFile(LPCWSTR lpszUrlName, DWORD dwReserved);
The lpszUrlName parameter is the URL of the cache file, and dwReserved must be set to 0.
Creating a Cached File
Although using the WinInet functions to download files through HTTP and FTP will automatically add files to the cache, you may sometimes need to manually add a new file. Doing so is basically a two-step process: You need to first specify where to store the entry in the cache:
BOOL CreateUrlCacheEntry(LPCWSTR lpszUrlName, DWORD
dwExpectedFileSize, LPCWSTR lpszFileExtension,
LPWSTR lpszFileName, DWORD dwReserved);
The first parameter, lpszUrlName, is the URL for the file you want to put in the cache. The dwExpectedFileSize parameter is the size of the file, or 0 if you don't currently know it. You will also pass in the lpszFileExtension parameter, which is a buffer containing the extension of the file you are storing. The lpszFileName parameter should point to a buffer that is at least the length of MAX_PATH. This parameter will receive the cache path and name for your file when the function returns. Finally, dwReserved is set to 0.
Now that you have a cache file path (returned in lpszFileName), you can get the file you want to put in the cache using whatever method you want. Once you have the entire file, all you need to do to store it in the cache is call the following:
BOOL CommitUrlCacheEntry(LPCWSTR lpszUrlName, LPCWSTR
lpszLocalFileName, FILETIME ExpireTime, FILETIME
LastModifiedTime, DWORD CacheEntryType, LPWSTR
lpHeaderInfo, DWORD dwHeaderSize, LPCWSTR
lpszFileExtension, DWORD dwReserved);
As you've seen before, the lpszUrlName parameter is the URL of the file you are sending to the cache. The lpszLocalFileName parameter should be the set to the same name that you received from calling the CreateUrlCacheEntry() function, which was set in its lpszFileName parameter. The ExpireTime and LastModifiedTime parameters are time values for the newly cached file. CacheEntryType can be set to STICKY_CACHE_ENTRY if you want to make this file persistent in the cache; otherwise, set it to 0. The lpHeaderInfo and dwHeaderSize parameters can be used to set any additional header information for your file. Finally, the lpszFileExtension parameter should point to a buffer specifying the current file's extension; and dwReserved can be set to 0.
Deleting Cache Entries
Deleting an entry in the cache is as simple as calling the DeleteUrlCacheEntry() function:
BOOL DeleteUrlCacheEntry(LPCWSTR lpszUrlName);
The only parameter, lpszUrlName, represents the URL for the cached file you want to delete.
Cache Groups
The final topic to cover regarding caching is cache groups. Basically, a cache group is a set of several cache entries that relate to one another and are represented by a group identifier. For example, suppose you have an application that reads data from multiple Web sites—you might want to use a cache group to identify all of the entries from one particular site. Later, when your program is offline, you could easily retrieve any entries you are interested in for that site from the cache group, rather than enumerating all of the cache entries.
Before you can add cache entries to a group, you must first create a new cache group identifier by using the function CreateUrlCacheGroup(), which is defined as follows:
GROUPID CreateUrlCacheGroup(DWORD dwFlags, LPVOID lpReserved);
The only option that you can use with the dwFlags parameter is CACHEGROUP_FLAG_GIDONLY, which creates a unique group identifier, rather than the actual group. Most of the time, you can pass in 0 here. The second parameter, lpReserved, must be set to NULL.
If it is successful, the function will return a new group identifier that you can use to add cache entries to the group. CreateUrlCacheGroup() will return FALSE if it fails.
Now that you have a group ID, you can add and remove cache entries to and from the new group. This is done by using the function SetUrlCacheEntryGroup():
BOOL SetUrlCacheEntryGroup(LPCWSTR lpszUrlName, DWORD
dwFlags, GROUPID GroupId, LPBYTE pbGroupAttributes,
DWORD cbGroupAttributes, LPVOID lpReserved);
The first parameter is the familiar lpszUrlName, which is the URL for the cache entry. The dwFlags parameter specifies whether you are adding or removing an entry from the group. Use the flag INTERNET_CACHE_GROUP_ADD to add a file, and INTERNET_CACHE_GROUP_REMOVE to remove it. The GroupId parameter should be set to the group ID for which you want to add or remove the file. The last three parameters must all be set to NULL.
If you need to delete the cache group, you can call the following:
BOOL DeleteUrlCacheGroup(GROUPID GroupId, DWORD dwFlags,
LPVOID lpReserved);
Finally, if you want to enumerate all the files in a particular group, you can use the FindFirstUrlCacheEntryEx() and FindNextUrlCacheEntryEx() functions. These work the same as the FindFirstUrlCacheEntry() and FindNextUrlCacheEntry() functions, with the addition of a few new parameters. When you are finished enumerating, you must call FindCloseUrlCache():
HANDLE FindFirstUrlCacheEntryEx(LPCWSTR lpszUrlSearchPattern,
DWORD dwFlags, DWORD dwFilter, GROUPID GroupId,
LPINTERNET_CACHE_ENTRY_INFO lpFirstCacheEntryInfo,
LPDWORD lpdwFirstCacheEntryInfoBufferSize,
LPVOID lpGroupAttributes, LPDWORD pcbGroupAttributes,
LPVOID lpReserved);
BOOL FindNextUrlCacheEntryEx(HANDLE hEnumHandle,
LPINTERNET_CACHE_ENTRY_INFO lpFirstCacheEntryInfo,
LPDWORD lpdwFirstCacheEntryInfoBufferSize, LPVOID
lpGroupAttributes, LPDWORD pcbGroupAttributes,
LPVOID lpReserved);
The parameters are basically the same as what you previously defined for FindFirstUrlCacheEntry() and FindNextUrlCacheEntry(). The only real addition is the GroupId parameter, which specifies the group for which you want to enumerate the entries. lpGroupAttributes, pcbGroupAttributes, and lpReserved should all be set to NULL.
Internet Options
Although you can set several available options for an Internet session, you need only two functions to manipulate them: InternetSetOption() and InternetQueryOption(). Let's look at their definitions:
BOOL InternetSetOption(HINTERNET hInternet, DWORD dwOption,
LPVOID lpBuffer, DWORD dwBufferLength);
BOOL InternetQueryOption(HINTERNET hInternet, DWORD
dwOption, LPVOID lpBuffer, LPDWORD lpdwBufferLength);
When working with Internet session options, the first parameter always specifies the Internet handle; the second parameter, dwOption, specifies what option you will be working with (see Table 2.5). The last two parameters depend on whether you're getting or setting an option value. If you are setting an option value, lpBuffer will be a pointer to a buffer that contains the option setting, and dwBufferLength will specify its size. When getting option values, lpBuffer will be a pointer to a buffer that receives the option data, and lpdwBufferLength will be a pointer to a variable that contains the length of lpBuffer. When the function returns, if the buffer wasn't large enough, you will get the error ERROR_INSUFFICIENT_BUFFER, and lpdwBufferLength will contain the size, in bytes, that you need to get the option data.
Table 2.5. Pocket PC Internet Options Option Name
Get/Set
Description
INTERNET_OPTION_CALLBACK
Get
The address of the callback for this handle, specified as a DWORD
INTERNET_OPTION_CONTEXT_VALUE
Both
The context value associated with this handle, specified as a DWORD pointer
INTERNET_OPTION_CONNECT_TIMEOUT
Both
The timeout value, in milliseconds, before a connection request is cancelled, specified as an unsigned long
INTERNET_OPTION_CONNECT_RETRIES
Both
The number of times a request will attempt to resolve and connect before failing, specified as an unsigned long
INTERNET_OPTION_SEND_TIMEOUT
Both
The timeout value, in milliseconds, before a send request is cancelled, specified as an unsigned long
INTERNET_OPTION_RECEIVE_TIMEOUT
Both
The timeout value, in milliseconds, before a receive request is cancelled, specified as an unsigned long
INTERNET_OPTION_READ_BUFFER_SIZE
Both
The size, in bytes, of the read buffer, specified as an unsigned long
INTERNET_OPTION_WRITE_BUFFER_SIZE
Both
The size, in bytes, of the write buffer, specified as an unsigned long
INTERNET_OPTION_USERNAME
Both
The username associated with the session handle, specified as a LPWSTR
INTERNET_OPTION_PASSWORD
Both
The password associated with the session handle, specified as a LPWSTR
INTERNET_OPTION_PROXY
Both
Information about the current proxy associated with the session handle, specified as an INTERNET_PROXY_INFO structure
INTERNET_OPTION_PROXY_PASSWORD
Both
The current proxy password, specified as a LPWSTR
INTERNET_OPTION_PROXY_USERNAME
Both
The current proxy username, specified as a LPWSTR
INTERNET_OPTION_USER_AGENT
Both
The User-Agent header that is used for HTTP requests, specified as a LPWSTR
INTERNET_OPTION_SETTINGS_CHANGED
Both
Notifies the system that an option value has changed, which will force Pocket PC to reload values from the registry
INTERNET_OPTION_HANDLE_TYPE
Get
The type of Internet connection associated with the session handle (FTP, HTTP, HTTPS), specified as an unsigned long
INTERNET_OPTION_PARENT_HANDLE
Get
The parent handle to this handle as a HINTERNET
Both functions will return TRUE if they succeed, or FALSE if they fail.
The INTERNET_OPTION_PROXY option uses an INTERNET_PROXY_INFO structure that specifies the current proxy settings for an Internet session handle:
typedef struct {
DWORD dwAccessType;
LPCWSTR lpszProxy;
LPCWSTR lpszProxyBypass;
} INTERNET_PROXY_INFO, *LPINTERNET_PROXY_INFO;
The dwAccessType member contains the current access method for the handle, which is the same as what was defined previously for the InternetOpen() function. The lpszProxy field will contain the name of the proxy sever associated with the session handle, and lpszProxyBypass will be NULL.
Differences between Windows and Pocket PC WinInet
Note several minor differences between the desktop version of WinInet and what is currently supported on Pocket PC devices:
There is no direct support for autodialing an Internet connection. The InternetAutodial(), InternetAutodialHangup(), InternetGetConnectedState(), InternetHangup(), and InternetGoOnline() functions are not currently supported on Pocket PC.
The gopher protocol is not supported on Windows CE.
Windows CE does not support multiple proxy servers. You can specify only a single proxy server when establishing an Internet connection.
Several Internet options are not supported on Pocket PC. Consult Table 2.5 for the Internet options currently supported on Pocket PC.
InternetGetCookie() does not support "named" cookie values.
The FtpCommand() function is not supported on Pocket PC.
The FtpGetFile() and FtpPutFile() functions always return the ERROR_INVALID_PARAMETER error. This is a known bug and is documented in Microsoft KB article Q312039 (found at http://support.microsoft.com/).
Windows CE does not support proxy bypass lists.
Monday, December 10, 2007
Socket Options
Socket Options
You can set several socket options to further control the behavior of an individual socket. Some of these attributes are used to query information about a socket, such as its current state, while other options are used to change how a socket interacts with the network. For example, if you wanted to send a UDP datagram that is broadcast to the entire network, you would enable the SO_BROADCAST socket option on that particular socket.
Socket options can affect either the socket itself or the protocol that it is using. For example, the option TCP_NODELAY is designed to toggle the Nagle algorithm when using the TCP protocol, and will produce an error on a socket that is currently UDP-based. Note that options that begin with SO_ are generic items for the socket layer, whereas those that begin with TCP_ or IP_ are for the underlying protocol. In addition, you need to verify that some of the generic socket options, such as SO_BROADCAST, are supported by the underlying socket protocol for your socket. In other words, if you try to create a broadcast socket on one that was created to use the TCP protocol, it will also fail.
The function that you use to get socket options is getsockopt() and is defined as follows:
int getsockopt (SOCKET s, int level, int optname, char *optval,
int *optlen);
To set socket options, you use the setsockopt() function:
int setsockopt (SOCKET s, int level, int optname, const
char *optval, int optlen);
Both take almost the exact same parameters. The s parameter is the socket for which you want to get or set an option. The next parameter, level, defines what level in the OSI model the option will affect. On Pocket PC, this can be SOL_SOCKET, IPPROTO_TCP, or IPPROTO_IP. The available socket options described in Table 1.3 will help you determine the proper level for the option you want to manipulate. The optname field identifies the option you want to get or set. The last two parameters work a bit differently depending on whether you are getting or setting a socket option value. If you are using getsockopt(), the optval parameter points to the value of the option, and optlen is a pointer to the size of the buffer to which optval points. When setting values using setsockopt(), optval points to the new value you want to set, and optlen is the size of the optval buffer.
Table 1.3. Socket Options Level
Option Name
Type
Get/Set
Description
SOL_SOCKET
SO_ACCEPTCONN
BOOL
Get
Is the socket listening?
SO_BROADCAST
BOOL
Both
Allows broadcast messages on the socket
SO_DONTLINGER
BOOL
Both
Enables or disables immediate return from closesocket()
SO_KEEPALIVE
BOOL
Both
Sends keep-alive messages
SO_LINGER
struct linger
Both
Enables or disables immediate return from closesocket()
SO_OOBINLINE
BOOL
Get
Out-of-band data is in the normal data stream
SO_REUSEADDR
BOOL
Both
Enables or disables the reuse of a bound socket
SO_SECURE
DWORD
Both
Enables or disables SSL encryption on the socket
SO_SNDBUF
int
Both
Size of the buffer allocated for sending data
SO_TYPE
int
Get
Socket type
IPPROTO_TCP
TCP_NODELAY
BOOL
Both
Turns on/off the Nagle algorithm
IPPROTO_IP
IP_MULTICAST_TTL
int
Both
Time to live for a multicast packet
IP_MULTICAST_IF
unsigned long
Both
Address of the outgoing multicast interface
IP_ADD_MEMBERSHIP
struct ip_mreg
Set
Adds socket to a multicast group
IP_DROP_MEMBERSHIP
struct
Set
Removes socket from a multicast group
The SO_LINGER option is somewhat related to the SO_DONTLINGER option in that when it is disabled, SO_DONTLINGER is enabled. Both of the "linger" options determine how the socket should react when the closesocket() function call is made and there is additional data in the TCP send buffer. SO_LINGER uses a LINGER structure to set its state, which is defined as follows:
struct linger {
u_short l_onoff;
u_short l_linger;
}
The l_onoff parameter determines if linger is currently on or off. When SO_DONTLINGER is set to TRUE, l_onoff is 0 (i.e., don't linger). When l_onoff is enabled and set to 1, the l_linger field specifies the time to wait, in seconds, before the socket is closed.
Broadcast Sockets
Use the SO_BROADCAST socket option with care. It is generally considered bad practice to flood a network with data, especially when using a device such as a Pocket PC for which bandwidth and network resources are crucial. It does have some practical uses, such as discovering devices on a subnet, or sending information to a wide group of devices at once, so there can be some benefit to using broadcast sockets.
Broadcasting is only available for sockets that are on UDP, as TCP sockets are not capable of transmitting broadcast messages. It is important to ensure that your broadcast packets are small enough that datagram fragmentation doesn't occur, which means you should not exceed 512 bytes for your message. When setting up your SOCKADDR_IN structure, both the client using recvfrom() and the sender using sendto() should configure their functions to send/receive from the same port, and they can use the address INADDR_BROADCAST to designate the target address as a broadcast message.
Secure Sockets
Pocket PC devices also support the capability to use the device's built-in security to create secure sockets using SSL 2.0 and 3.0. When using the SO_SOSECURE socket option, set the DWORD value you are passing in for the optval parameter to SO_SEC_SLL.
Blocking and Nonblocking Sockets
When a socket is initially created, it is in blocking mode, which means that your program will not return from a blocking function until it has completed its operation. For example, when using a TCP socket, if you call the accept() function, your program will appear to "hang" until an incoming connection has arrived. Another example would be the connect() function, which will not return until either it has connected to its destination or an error has occurred. This can be rather unnerving on a device such as Pocket PC, as it will appear as if the device has "locked up" until it returns from the blocking function.
On a Windows-based system, this is solved by using the asynchronous Winsock functions, which provide Windows notification messages when a socket event has occurred. Unfortunately, these are not available on Pocket PC devices; instead, you need to put the sockets into nonblocking mode if you want them to return immediately. When a socket is set to nonblocking mode, any call to a blocking function will immediately return to you with a SOCKET_ERROR result. Calling WSAGetLastError() will return the error code WSAWOULDBLOCK, which means that you will need to check the socket again at some point to see whether the operation has completed. This can be done by using the select() function, rather than repeatedly calling into the nonblocking function to see whether it has completed.
To change the blocking mode of a socket, you can call the following function:
int ioctlsocket (SOCKET s, long cmd, u_long *argp);
The first parameter is the socket for which you want to change the mode. The cmd parameter is used to specify what operation you want to perform on the socket, and can be either FIONBIO or FIONREAD. Setting cmd to FIONBIO will set the socket's blocking mode to nonblocking if argp is set to 0; otherwise, setting argp to a positive value will put the socket into blocking mode. The FIONREAD command will return the number of bytes that are currently in the receive queue for the socket to the pointer passed in for the argp parameter.
For example, you could use the following if you wanted to change a socket from blocking to nonblocking:
// Create a connection-oriented socket
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Check to see if we have a valid socket
if(s == INVALID_SOCKET) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Make it non-blocking
DWORD dwNonBlocking = 0;
if(ioctlsocket(s, FIONBIO, &dwNonBlocking) != 0) {
int iSocketError = WSAGetLastError();
return FALSE;
}
Once you have placed the socket into nonblocking mode, you can use the select() function to see if any data is available on the socket. It is extremely important that you use this method of notification on your nonblocking sockets to determine their completion, as continuously polling the socket is a major drain of device resources.
The select() function is defined as follows:
int select (int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timeval *timeout);
The first parameter, nfds, is ignored. The next three parameters, readfds, writefds, and exceptfds, are pointers to a collection of socket sets (FD_SET), which are used for determining whether it can be read or written to, and whether there is out-of-band data. An FD_SET collection is an internal structure that is used to maintain a collection of sockets that you want select to monitor. Winsock provides several macros for adding and removing sockets from an FD_SET collection:
FD_ZERO(*set)— Initializes the set to NULL
FD_CLR(s, *set)— Removes the socket s from the set
FD_ISSET(s, *set)— Determines whether socket s is a part of the set, and returns TRUE if so
FD_SET(s, *set)— Adds socket s to the set
For example, if you want to monitor a socket to determine when it is safe to write to it, you can simply add your socket to the writefds set by calling the FD_SET macro.
Finally, the timeout parameter of the select() function is a pointer to a timeval structure (defined in winsock.h):
struct timeval {
long tv_sec;
long tv_usec;
};
The tv_sec field specifies how long the select() function will wait in seconds, and the tv_usec field indicates the number of milliseconds. If these two fields are set to 0, select() will return immediately. If the pointer timeout is NULL, it will wait indefinitely; otherwise, select() will wait the specified number of seconds and milliseconds defined in this structure.
Select will return 0 if a timeout has occurred, or a SOCKET_ERROR if there is an error. Otherwise, the select function won't return until a specific socket event has happened, and the return value will be the number of sockets on which the event has occurred.
Sockets in the readfds set will be identified under the following conditions:
There is data in the receive queue.
The connection has been lost, closed, or reset.
The socket is in listen() mode and a client is attempting to connect. This will allow accept() to succeed.
Sockets in the writefds set will be identified under these circumstances:
There is data to be sent on the socket.
A connect() has been accepted by a server.
Finally, sockets that are included in the exceptfds set are identified under the following conditions:
A connect() has failed from the server.
Out-of-band data is available for reading.
[ Team LiB ]
Support Functions
Several extremely useful Winsock support functions can help you get information about peers, make DNS queries, and convert between various data formats that are supported with Winsock.
Connected Peer Functions
To get address information for a connection that a socket is currently connected with, you can use the getpeername() function:
int getpeername (SOCKET s, struct sockaddr *name, int *namelen);
The first parameter is the socket for which you want information, the name parameter is a pointer to a SOCKADDR structure (which will be a SOCKADDR_IN for TCP/IP), and namelen is a pointer to its length.
To retrieve address information for the local interface of a connected socket, the following function is defined:
int getsockname (SOCKET s, struct sockaddr *name, int *namelen);
The getsockname() function takes the same parameters as the getpeername() function, except that the name parameter will return local address information, rather than the remote connection.
Host Names
To get your device's host name, you can call the aptly named gethostname() function:
int gethostname (char *name, int namelen);
The name parameter is a pointer to a character buffer that will receive the name, and namelen is the length of the name buffer specified.
To set the device's host name, use the sethostname() function, which is defined as follows:
int sethostname(char *pName, int cName);
This function takes two parameters. The first is a pointer to a buffer that contains the new host name, and the second is the cName parameter, which specifies the buffer's length.
Name Resolution
The inet_addr function is used to convert an IP "dot" address (e.g., 192.158.0.0) into an unsigned long value:
unsigned long inet_addr (const char *cp);
It takes a single parameter, a pointer to a character buffer cp containing the IP "dot" address string.
If you want to perform the inverse function, use inet_ntoa(), which will convert an address to a string:
char * inet_ntoa (struct in_addr in);
Here, you pass in an address structure; typically, a SOCKADDR_IN that defines the address you want to convert.
The next set of support functions we will look at deals with host name resolution. All Winsock functions dealing with host addresses and names use a HOSTENT data structure. This structure contains all the available information about an individual host. It is defined as follows:
struct hostent {
char *h_name;
char **h_aliases;
short h_addrtype;
short h_length;
char **h_addr_list;
};
The h_name field contains the official name of the host. The h_aliases field points to an array of alternative host name string pointers that are terminated by a NULL pointer value. The h_addrtype field is the type of address being returned, and will be either AF_INET or AF_IRDA. The h_length field is the length in bytes of each address in the h_addr_list array. The h_addr_list array is a null-terminated array of network addresses in network byte order.
To get host information for a network device by name, use the following:
struct hostent *gethostbyname(const char *name);
The only parameter needed is the name of the host for which you want information.
Use the following to get information about a host by address:
struct hostent *gethostbyaddr(const char *addr, int len, int type);
Here, you need to pass in a bit more information about the host. The first parameter is the address structure, with information about the host you want to query. This will typically be the value that is returned from calling the inet_addr() function. The len parameter is the length in bytes of the address, and the type parameter is the type of address—either AF_INET or AF_IDRA.
Byte Order
The final set of support functions deals with converting values from host byte order to network byte order.
To convert a long value to/from host byte order to network byte order, use the following functions:
u_long htonl (u_long hostlong);
u_long ntohl (u_long netlong);
Convert a short value to/from host byte order to network byte order as follows:
u_short htons (u_short hostshort);
u_short ntohs (u_short netshort);
[ Team LiB ]
You can set several socket options to further control the behavior of an individual socket. Some of these attributes are used to query information about a socket, such as its current state, while other options are used to change how a socket interacts with the network. For example, if you wanted to send a UDP datagram that is broadcast to the entire network, you would enable the SO_BROADCAST socket option on that particular socket.
Socket options can affect either the socket itself or the protocol that it is using. For example, the option TCP_NODELAY is designed to toggle the Nagle algorithm when using the TCP protocol, and will produce an error on a socket that is currently UDP-based. Note that options that begin with SO_ are generic items for the socket layer, whereas those that begin with TCP_ or IP_ are for the underlying protocol. In addition, you need to verify that some of the generic socket options, such as SO_BROADCAST, are supported by the underlying socket protocol for your socket. In other words, if you try to create a broadcast socket on one that was created to use the TCP protocol, it will also fail.
The function that you use to get socket options is getsockopt() and is defined as follows:
int getsockopt (SOCKET s, int level, int optname, char *optval,
int *optlen);
To set socket options, you use the setsockopt() function:
int setsockopt (SOCKET s, int level, int optname, const
char *optval, int optlen);
Both take almost the exact same parameters. The s parameter is the socket for which you want to get or set an option. The next parameter, level, defines what level in the OSI model the option will affect. On Pocket PC, this can be SOL_SOCKET, IPPROTO_TCP, or IPPROTO_IP. The available socket options described in Table 1.3 will help you determine the proper level for the option you want to manipulate. The optname field identifies the option you want to get or set. The last two parameters work a bit differently depending on whether you are getting or setting a socket option value. If you are using getsockopt(), the optval parameter points to the value of the option, and optlen is a pointer to the size of the buffer to which optval points. When setting values using setsockopt(), optval points to the new value you want to set, and optlen is the size of the optval buffer.
Table 1.3. Socket Options Level
Option Name
Type
Get/Set
Description
SOL_SOCKET
SO_ACCEPTCONN
BOOL
Get
Is the socket listening?
SO_BROADCAST
BOOL
Both
Allows broadcast messages on the socket
SO_DONTLINGER
BOOL
Both
Enables or disables immediate return from closesocket()
SO_KEEPALIVE
BOOL
Both
Sends keep-alive messages
SO_LINGER
struct linger
Both
Enables or disables immediate return from closesocket()
SO_OOBINLINE
BOOL
Get
Out-of-band data is in the normal data stream
SO_REUSEADDR
BOOL
Both
Enables or disables the reuse of a bound socket
SO_SECURE
DWORD
Both
Enables or disables SSL encryption on the socket
SO_SNDBUF
int
Both
Size of the buffer allocated for sending data
SO_TYPE
int
Get
Socket type
IPPROTO_TCP
TCP_NODELAY
BOOL
Both
Turns on/off the Nagle algorithm
IPPROTO_IP
IP_MULTICAST_TTL
int
Both
Time to live for a multicast packet
IP_MULTICAST_IF
unsigned long
Both
Address of the outgoing multicast interface
IP_ADD_MEMBERSHIP
struct ip_mreg
Set
Adds socket to a multicast group
IP_DROP_MEMBERSHIP
struct
Set
Removes socket from a multicast group
The SO_LINGER option is somewhat related to the SO_DONTLINGER option in that when it is disabled, SO_DONTLINGER is enabled. Both of the "linger" options determine how the socket should react when the closesocket() function call is made and there is additional data in the TCP send buffer. SO_LINGER uses a LINGER structure to set its state, which is defined as follows:
struct linger {
u_short l_onoff;
u_short l_linger;
}
The l_onoff parameter determines if linger is currently on or off. When SO_DONTLINGER is set to TRUE, l_onoff is 0 (i.e., don't linger). When l_onoff is enabled and set to 1, the l_linger field specifies the time to wait, in seconds, before the socket is closed.
Broadcast Sockets
Use the SO_BROADCAST socket option with care. It is generally considered bad practice to flood a network with data, especially when using a device such as a Pocket PC for which bandwidth and network resources are crucial. It does have some practical uses, such as discovering devices on a subnet, or sending information to a wide group of devices at once, so there can be some benefit to using broadcast sockets.
Broadcasting is only available for sockets that are on UDP, as TCP sockets are not capable of transmitting broadcast messages. It is important to ensure that your broadcast packets are small enough that datagram fragmentation doesn't occur, which means you should not exceed 512 bytes for your message. When setting up your SOCKADDR_IN structure, both the client using recvfrom() and the sender using sendto() should configure their functions to send/receive from the same port, and they can use the address INADDR_BROADCAST to designate the target address as a broadcast message.
Secure Sockets
Pocket PC devices also support the capability to use the device's built-in security to create secure sockets using SSL 2.0 and 3.0. When using the SO_SOSECURE socket option, set the DWORD value you are passing in for the optval parameter to SO_SEC_SLL.
Blocking and Nonblocking Sockets
When a socket is initially created, it is in blocking mode, which means that your program will not return from a blocking function until it has completed its operation. For example, when using a TCP socket, if you call the accept() function, your program will appear to "hang" until an incoming connection has arrived. Another example would be the connect() function, which will not return until either it has connected to its destination or an error has occurred. This can be rather unnerving on a device such as Pocket PC, as it will appear as if the device has "locked up" until it returns from the blocking function.
On a Windows-based system, this is solved by using the asynchronous Winsock functions, which provide Windows notification messages when a socket event has occurred. Unfortunately, these are not available on Pocket PC devices; instead, you need to put the sockets into nonblocking mode if you want them to return immediately. When a socket is set to nonblocking mode, any call to a blocking function will immediately return to you with a SOCKET_ERROR result. Calling WSAGetLastError() will return the error code WSAWOULDBLOCK, which means that you will need to check the socket again at some point to see whether the operation has completed. This can be done by using the select() function, rather than repeatedly calling into the nonblocking function to see whether it has completed.
To change the blocking mode of a socket, you can call the following function:
int ioctlsocket (SOCKET s, long cmd, u_long *argp);
The first parameter is the socket for which you want to change the mode. The cmd parameter is used to specify what operation you want to perform on the socket, and can be either FIONBIO or FIONREAD. Setting cmd to FIONBIO will set the socket's blocking mode to nonblocking if argp is set to 0; otherwise, setting argp to a positive value will put the socket into blocking mode. The FIONREAD command will return the number of bytes that are currently in the receive queue for the socket to the pointer passed in for the argp parameter.
For example, you could use the following if you wanted to change a socket from blocking to nonblocking:
// Create a connection-oriented socket
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Check to see if we have a valid socket
if(s == INVALID_SOCKET) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Make it non-blocking
DWORD dwNonBlocking = 0;
if(ioctlsocket(s, FIONBIO, &dwNonBlocking) != 0) {
int iSocketError = WSAGetLastError();
return FALSE;
}
Once you have placed the socket into nonblocking mode, you can use the select() function to see if any data is available on the socket. It is extremely important that you use this method of notification on your nonblocking sockets to determine their completion, as continuously polling the socket is a major drain of device resources.
The select() function is defined as follows:
int select (int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timeval *timeout);
The first parameter, nfds, is ignored. The next three parameters, readfds, writefds, and exceptfds, are pointers to a collection of socket sets (FD_SET), which are used for determining whether it can be read or written to, and whether there is out-of-band data. An FD_SET collection is an internal structure that is used to maintain a collection of sockets that you want select to monitor. Winsock provides several macros for adding and removing sockets from an FD_SET collection:
FD_ZERO(*set)— Initializes the set to NULL
FD_CLR(s, *set)— Removes the socket s from the set
FD_ISSET(s, *set)— Determines whether socket s is a part of the set, and returns TRUE if so
FD_SET(s, *set)— Adds socket s to the set
For example, if you want to monitor a socket to determine when it is safe to write to it, you can simply add your socket to the writefds set by calling the FD_SET macro.
Finally, the timeout parameter of the select() function is a pointer to a timeval structure (defined in winsock.h):
struct timeval {
long tv_sec;
long tv_usec;
};
The tv_sec field specifies how long the select() function will wait in seconds, and the tv_usec field indicates the number of milliseconds. If these two fields are set to 0, select() will return immediately. If the pointer timeout is NULL, it will wait indefinitely; otherwise, select() will wait the specified number of seconds and milliseconds defined in this structure.
Select will return 0 if a timeout has occurred, or a SOCKET_ERROR if there is an error. Otherwise, the select function won't return until a specific socket event has happened, and the return value will be the number of sockets on which the event has occurred.
Sockets in the readfds set will be identified under the following conditions:
There is data in the receive queue.
The connection has been lost, closed, or reset.
The socket is in listen() mode and a client is attempting to connect. This will allow accept() to succeed.
Sockets in the writefds set will be identified under these circumstances:
There is data to be sent on the socket.
A connect() has been accepted by a server.
Finally, sockets that are included in the exceptfds set are identified under the following conditions:
A connect() has failed from the server.
Out-of-band data is available for reading.
[ Team LiB ]
Support Functions
Several extremely useful Winsock support functions can help you get information about peers, make DNS queries, and convert between various data formats that are supported with Winsock.
Connected Peer Functions
To get address information for a connection that a socket is currently connected with, you can use the getpeername() function:
int getpeername (SOCKET s, struct sockaddr *name, int *namelen);
The first parameter is the socket for which you want information, the name parameter is a pointer to a SOCKADDR structure (which will be a SOCKADDR_IN for TCP/IP), and namelen is a pointer to its length.
To retrieve address information for the local interface of a connected socket, the following function is defined:
int getsockname (SOCKET s, struct sockaddr *name, int *namelen);
The getsockname() function takes the same parameters as the getpeername() function, except that the name parameter will return local address information, rather than the remote connection.
Host Names
To get your device's host name, you can call the aptly named gethostname() function:
int gethostname (char *name, int namelen);
The name parameter is a pointer to a character buffer that will receive the name, and namelen is the length of the name buffer specified.
To set the device's host name, use the sethostname() function, which is defined as follows:
int sethostname(char *pName, int cName);
This function takes two parameters. The first is a pointer to a buffer that contains the new host name, and the second is the cName parameter, which specifies the buffer's length.
Name Resolution
The inet_addr function is used to convert an IP "dot" address (e.g., 192.158.0.0) into an unsigned long value:
unsigned long inet_addr (const char *cp);
It takes a single parameter, a pointer to a character buffer cp containing the IP "dot" address string.
If you want to perform the inverse function, use inet_ntoa(), which will convert an address to a string:
char * inet_ntoa (struct in_addr in);
Here, you pass in an address structure; typically, a SOCKADDR_IN that defines the address you want to convert.
The next set of support functions we will look at deals with host name resolution. All Winsock functions dealing with host addresses and names use a HOSTENT data structure. This structure contains all the available information about an individual host. It is defined as follows:
struct hostent {
char *h_name;
char **h_aliases;
short h_addrtype;
short h_length;
char **h_addr_list;
};
The h_name field contains the official name of the host. The h_aliases field points to an array of alternative host name string pointers that are terminated by a NULL pointer value. The h_addrtype field is the type of address being returned, and will be either AF_INET or AF_IRDA. The h_length field is the length in bytes of each address in the h_addr_list array. The h_addr_list array is a null-terminated array of network addresses in network byte order.
To get host information for a network device by name, use the following:
struct hostent *gethostbyname(const char *name);
The only parameter needed is the name of the host for which you want information.
Use the following to get information about a host by address:
struct hostent *gethostbyaddr(const char *addr, int len, int type);
Here, you need to pass in a bit more information about the host. The first parameter is the address structure, with information about the host you want to query. This will typically be the value that is returned from calling the inet_addr() function. The len parameter is the length in bytes of the address, and the type parameter is the type of address—either AF_INET or AF_IDRA.
Byte Order
The final set of support functions deals with converting values from host byte order to network byte order.
To convert a long value to/from host byte order to network byte order, use the following functions:
u_long htonl (u_long hostlong);
u_long ntohl (u_long netlong);
Convert a short value to/from host byte order to network byte order as follows:
u_short htons (u_short hostshort);
u_short ntohs (u_short netshort);
[ Team LiB ]
Connectionless (UDP) Sockets
Connectionless (UDP) Sockets
Connectionless sockets (the User Datagram Protocol, or UDP) are your other option for transferring data between two networked devices. These are typically used in applications that require little overhead and that want to achieve higher network throughput, such as multimedia streaming protocols. Another advantage in using UDP is that it is capable of transmitting data to multiple endpoints simultaneously because a connection is not bound to a single address. Because UDP transfers datagrams (message packets) instead of a connected stream, these connections are considered unreliable and connectionless. However, don't mistake the term unreliable for low quality—unreliable in this context means only that the protocol does not guarantee that your data packets will ever arrive at your destination. Moreover, there is no sequenced order in which they are guaranteed to arrive, nor any notification if a packet never arrives.
You can compare using UDP datagrams to checking in several pieces of luggage (your packets) at the airport at the same time. Even though they boarded the plane in some order (the packets going out over the network), you're pretty sure that they'll arrive at their destination. Once you get off the plane and attempt to claim your luggage, you're not exactly sure what order they'll be unloaded in (packets arriving at the endpoint), but you can be relatively certain that they'll get there in one piece. Unfortunately, once in a while, something does get lost and never is seen again.
If you are planning to use UDP as your method to send data, it's probably a good idea to have your application either send some sort of acknowledgment that it received a datagram, or provide some way to reassemble packets in a predetermined order by using some sort of sequence—such as a packet number or timestamp—in your datagram message. This can ensure some amount of reliability with the protocol.
Figure 1.4 shows the process for creating both client and server UDP socket connections and how data flows between both network endpoints.
Figure 1.4. Socket process for connectionless clients and servers
Before you can send or receive UDP packets, whether you are the client or the server, you need to create a socket to talk to using the socket() function, and pass the SOCK_DGRAM and IPPROTO_IDP parameters:
SOCKET sUDPSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
Once you have created your sockets, you also need to bind() the socket to the interface on which you want to receive data. This is done exactly the same way as using a TCP connection.
Now that we have our sockets ready, let's take a look at what is required to both send and receive datagram packets. Remember that even though we have bound the socket, we don't need to call the listen() or accept() functions, as we are going to be operating in a connectionless manner, which enables us to send or receive packets to any device on the network.
Sending a UDP Datagram
Once you have created your sockets, sending a packet over UDP is fairly straightforward. To send a message, you need to call the sendto() function, which is defined as follows:
int sendto (SOCKET s, const char *buf, int len, int flags,
const struct sockaddr *to, int tolen);
You might notice that the parameters are similar to the send() function.
The s parameter is the socket on which we want to send data, which was created using the socket() function. The buf parameter is a pointer to a buffer that contains the data we want to send, and its length is specified in the len parameter. The flags parameter is used to affect the way the data is sent, and can be 0 or MSG_DONTROUTE, which specifies that the data should not be routed. Typically, this parameter will be set to 0. The to parameter contains a pointer to a SOCKADDR_IN address structure with the packet's destination address. You can also construct a broadcast packet (sending it to every machine on the network, which is usually not advised), and you can use the address INADDR_BROADCAST if you have set the socket option to broadcast mode (see the section "Socket Options"). Finally, the tolen parameter specifies the length of the to address.
The sendto() function will return the number of bytes it has transferred, or a SOCKET_ERROR if there was a problem sending the datagram.
The following example sends a UDP datagram:
// Create a connectionless socket
SOCKET sUDPSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// Check to see if we have a valid socket
if(sUDPSocket == INVALID_SOCKET) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Set up the target device address. For this sample,
// we are assuming it is a machine at 192.168.0.1, and on
// port 40040
SOCKADDR_IN sTargetDevice;
memset(&sTargetDevice, 0, sizeof(SOCKADDR_IN));
sTargetDevice.sin_family = AF_INET;
sTargetDevice.sin_port = htons(40040);
sTargetDevice.sin_addr.s_addr = inet_addr("192.168.0.1");
// Send a datagram to the target device
char cBuffer[1024] = "Test Buffer";
int nBytesSent = 0;
int nBufSize = strlen(cBuffer);
nBytesSent = sendto(sUDPSocket, cBuffer, nBufSize, 0,
(SOCKADDR *) &sTargetDevice,
sizeof(SOCKADDR_IN));
// Close the socket
closesocket(sUDPSocket);
Receiving a UDP Datagram
To have your application receive a UDP packet, you need to call the recvfrom() function, which will block until data has arrived from a client (or it can return immediately if in nonblocking mode; see "Socket Options"):
int recvfrom (SOCKET s, char *buf, int len, int flags,
struct sockaddr *from, int *fromlen);
Notice that the parameters are very similar to those described for the recv() function. The first parameter, s, is the socket on which we want to receive data. Next, buf is a pointer to a buffer for the incoming data, and its size is specified by the len parameter. The flags parameter must be set to 0. Finally, the from parameter contains a pointer to the SOCKADDR_IN structure, which contains information about the device that sent the data. A pointer to its length is in the fromlen field.
If the packet is received successfully, recvfrom() will return a 0; otherwise, a SOCKET_ERROR will occur.
The following example shows how to receive a UDP datagram packet:
// Create a connectionless socket
SOCKET sUDPSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// Check to see if we have a valid socket
if(sUDPSocket == INVALID_SOCKET) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Setup a bind on the socket, telling us what port and
// adapter to receive datagrams on. Assume we are listening
// on port 40040
SOCKADDR_IN sReceiveFromAddr;
memset(&sReceiveFromAddr, 0, sizeof(SOCKADDR_IN));
sReceiveFromAddr.sin_family = AF_INET;
sReceiveFromAddr.sin_port = htons(40040);
sReceiveFromAddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sUDPSocket, (SOCKADDR *)&sReceiveFromAddr,
sizeof(SOCKADDR_IN)) ==
SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Receive a datagram from another device
char cBuffer[1024] = "";
int nBytesRecv = 0;
int nBufSize = strlen(cBuffer);
int nReceiveAddrSize = 0;
// Get the datagram
nBytesRecv = recvfrom(sUDPSocket, cBuffer, nBufSize, 0,
(SOCKADDR *) &sReceiveFromAddr,
&nReceiveAddrSize);
// Close the socket
closesocket(sUDPSocket);
WSACleanup();
One final note regarding the sending and receiving of UDP data: You can also transfer data by using the connect(), send(), and recv() functions. Transmitting UDP data this way is considered a somewhat "directed" connectionless transfer, and should be used only if you plan to communicate with one other device during a session (i.e., all packets are sent to the same address). To do this, after your UDP socket has been created, call the connect() function with SOCKADDR_IN set to the machine you want to establish a session with. No real connection will be established, but you can use the send() and recv() functions to transfer data with the associated address.
TCP versus UDP: What Should Your App Use?
Deciding which transport protocol to use can be the hardest part of developing TCP/IP-based applications. The general rule of thumb is to use TCP unless your specific application calls for bandwidth sensitivity or congestion control, which can be relevant issues on Pocket PC devices now that wireless connections are becoming more accessible. UDP should be used in the following situations: when writing applications that require the capability to broadcast data to multiple devices (IP Multicast however, is much less bandwidth-intensive); when writing real-time multimedia applications that can afford to drop packets when congestion occurs; or for extremely small transmissions that require acknowledgment only.
TCP (stream)-based applications are considered much more reliable, and TCP's guarantee of message delivery combined with relatively low overhead makes it an extremely flexible and robust protocol. My best advice is to consider carefully the type of data that you will be sending, as well as your available bandwidth and network conditions, before making your decision.
[ Team LiB ]
Internet Control Message Protocol (ICMP)
The Internet Control Message Protocol (ICMP) is a "support" protocol that is used for sending informational, control, and error messages between network endpoints. It was originally designed to give network routers a way to deliver errors to the network layer of the OSI model so it could decide how to handle the error. ICMP uses a form of IP datagrams known as raw sockets to send information between hosts and servers. Pocket PC, however, does not allow you to explicitly create raw sockets; rather, it uses a set of functions that enable you to send limited ICMP ping messages.
ICMP pinging can be extremely useful when performing functions such as network diagnostics, as it can be used to test network connections, routing, and your TCP/IP stack. To use the ICMP functions, you should include the files icmpapi.h, ipexport.h, and winsock.h in your project's source, as well as link with both the icmplib.lib and winsock.lib libraries.
In order to send an ICMP message, you must first obtain an ICMP handle by calling the following function:
HANDLE IcmpCreateFile (void);
No parameters are required, and an ICMP handle will be returned. If an error has occurred, you will receive an INVALID_HANDLE_VALUE and you can use the GetLastError() function to obtain more information about why it failed.
Once you have a valid ICMP handle, you can send your ICMP request by calling the IcmpSendEcho() function:
DWORD IcmpSendEcho (HANDLE IcmpHandle,
IPAddr DestinationAddress,
LPVOID RequestData, WORD RequestSize,
PIP_OPTION_INFORMATION RequestOptions,
LPVOID ReplyBuffer,
DWORD ReplySize, DWORD Timeout);
For the first parameter, we pass in the handle to the newly created ICMP packet. The DestinationAddress parameter is an unsigned long value specifying the target IP address. You can get this by using one of the IP support functions, such as inet_addr(), described in the section "Name Resolution," later in this chapter. If you want to pass any additional data along with the ICMP message, you can pass a pointer to your buffer using the RequestData parameter, and set its size with the RequestSize field. You will also want to make sure that you don't try to stuff large messages into this buffer, as there is a limit of 8KB on most systems.
The next parameter, RequestOptions, is a pointer to an IP_OPTION_INFORMATION structure:
struct ip_option_information {
unsigned char Ttl;
unsigned char Tos;
unsigned char Flags;
unsigned char OptionsSize;
unsigned char FAR *OptionsData;
};
The IP_OPTION_INFORMATION structure is used to configure your echo request. The Ttl (time to live) parameter determines the amount of time that the packet will be around before it expires. Next, the Tos field specifies the type of service for the ICMP packet, which should be set to 0. The only option that the Flags parameter supports on Pocket PC is IP_FLAG_DF, which instructs ICMP not to fragment the message. Finally, OptionsSize is set to the size of the OptionsData parameter, which is a pointer to any additional options. Currently, Pocket PC supports only IP_OPT_RR, which records the record route; and IP_OPT_TS, which records the timestamp.
The next parameters in IcmpSendEcho() are ReplyBuffer and ReplySize. The ReplyBuffer is a pointer to an array of ICMP_ECHO_REPLY structures that contains responses to our ICMP echo message from each machine to which our request was sent. It is important to ensure that the buffer you have allocated to receive the reply is set to be the size of ICMP_ECHO_REPLY plus eight additional bytes, which are used for any additional error information. The last parameter is Timeout, which is the amount of time IcmpSendEcho() will wait in milliseconds before failing.
IcmpSendEcho() will return the number of packets that are waiting in the ReplyBuffer if it is successful; otherwise, it will return 0. The ICMP_ECHO_REPLY response structure is defined as follows:
struct icmp_echo_reply {
IPAddr Address;
unsigned long Status;
unsigned long RoundTripTime;
unsigned short DataSize;
unsigned short Reserved;
void FAR *Data;
struct ip_option_information Options;
};
Each network node with which we communicated during our echo request will respond with an ICMP_ECHO_REPLY package. The first field contains the Address of the machine that responded. The Status field should return IP_SUCCESS if successful; otherwise, it will contain an error code (defined in ipexport.h). Next, RoundTripTime contains the amount of time in milliseconds that it took for our request to get there. The DataSize field contains the size of the data, returned to us in the Data field. The last field, Options, is an IP_OPTIONS_STRUCTURE containing any options of the returning packet.
Finally, when you are finished sending your echo requests, you can close and clean up your ICMP packets by calling into the IcmpCloseHandle() function, which is defined as follows:
BOOL IcmpCloseHandle(HANDLE IcmpHandle);
The only parameter, IcmpHandle, is the ICMP session handle that you have been using. If ICMP closes successfully, this function will return TRUE. FALSE will be returned to you if an error has occurred.
The following example shows how we can use the ICMP functions to ping another network endpoint to determine whether our Internet connection is active (similar to the desktop ping utility):
// Initialize Winsock
WSADATA wsaData;
memset(&wsaData, 0, sizeof(WSADATA));
if(WSAStartup(MAKEWORD(1,1), &wsaData) != 0)
return FALSE;
// Create a new ICMP message session
HANDLE hIcmpSession = IcmpCreateFile();
if(hIcmpSession == INVALID_HANDLE_VALUE)
return FALSE;
// Convert the target address to a network address
HOSTENT *hostServer = gethostbyname("www.furrygoat.com");
if(hostServer == NULL) {
int dwError = WSAGetLastError();
return FALSE;
}
DWORD dwTargetAddress = *((u_long*)hostServer>h_addr_list[0]);
// Setup the option_information structure
IP_OPTION_INFORMATION ipOptions;
memset(&ipOptions, 0, sizeof(IP_OPTION_INFORMATION));
ipOptions.Ttl = 32;
ipOptions.Flags = IP_FLAG_DF;
// Send our request
BYTE bOutPacket[32];
BYTE bInPacket[1024];
int nTrace = IcmpSendEcho(hIcmpSession, dwTargetAddress, bOutPacket,
sizeof(bOutPacket), &ipOptions, bInPacket, 1024, 5000);
if(nTrace == 0) {
DWORD dwError = GetLastError();
IcmpCloseHandle(hIcmpSession);
return 0;
}
ICMP_ECHO_REPLY *pER = (PICMP_ECHO_REPLY)bInPacket;
for(int i =0;i TCHAR tchOutput[512] = TEXT("\0");
struct in_addr sReplyAddr;
sReplyAddr.S_un.S_addr = (IPAddr)pER->Address;
wsprintf(tchOutput, TEXT("Reply from %hs"),
inet_ntoa(sReplyAddr));
MessageBox(NULL, tchOutput, TEXT("Ping"), MB_OK);
pER++;
}
IcmpCloseHandle(hIcmpSession);
[ Team LiB ]
Connectionless sockets (the User Datagram Protocol, or UDP) are your other option for transferring data between two networked devices. These are typically used in applications that require little overhead and that want to achieve higher network throughput, such as multimedia streaming protocols. Another advantage in using UDP is that it is capable of transmitting data to multiple endpoints simultaneously because a connection is not bound to a single address. Because UDP transfers datagrams (message packets) instead of a connected stream, these connections are considered unreliable and connectionless. However, don't mistake the term unreliable for low quality—unreliable in this context means only that the protocol does not guarantee that your data packets will ever arrive at your destination. Moreover, there is no sequenced order in which they are guaranteed to arrive, nor any notification if a packet never arrives.
You can compare using UDP datagrams to checking in several pieces of luggage (your packets) at the airport at the same time. Even though they boarded the plane in some order (the packets going out over the network), you're pretty sure that they'll arrive at their destination. Once you get off the plane and attempt to claim your luggage, you're not exactly sure what order they'll be unloaded in (packets arriving at the endpoint), but you can be relatively certain that they'll get there in one piece. Unfortunately, once in a while, something does get lost and never is seen again.
If you are planning to use UDP as your method to send data, it's probably a good idea to have your application either send some sort of acknowledgment that it received a datagram, or provide some way to reassemble packets in a predetermined order by using some sort of sequence—such as a packet number or timestamp—in your datagram message. This can ensure some amount of reliability with the protocol.
Figure 1.4 shows the process for creating both client and server UDP socket connections and how data flows between both network endpoints.
Figure 1.4. Socket process for connectionless clients and servers
Before you can send or receive UDP packets, whether you are the client or the server, you need to create a socket to talk to using the socket() function, and pass the SOCK_DGRAM and IPPROTO_IDP parameters:
SOCKET sUDPSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
Once you have created your sockets, you also need to bind() the socket to the interface on which you want to receive data. This is done exactly the same way as using a TCP connection.
Now that we have our sockets ready, let's take a look at what is required to both send and receive datagram packets. Remember that even though we have bound the socket, we don't need to call the listen() or accept() functions, as we are going to be operating in a connectionless manner, which enables us to send or receive packets to any device on the network.
Sending a UDP Datagram
Once you have created your sockets, sending a packet over UDP is fairly straightforward. To send a message, you need to call the sendto() function, which is defined as follows:
int sendto (SOCKET s, const char *buf, int len, int flags,
const struct sockaddr *to, int tolen);
You might notice that the parameters are similar to the send() function.
The s parameter is the socket on which we want to send data, which was created using the socket() function. The buf parameter is a pointer to a buffer that contains the data we want to send, and its length is specified in the len parameter. The flags parameter is used to affect the way the data is sent, and can be 0 or MSG_DONTROUTE, which specifies that the data should not be routed. Typically, this parameter will be set to 0. The to parameter contains a pointer to a SOCKADDR_IN address structure with the packet's destination address. You can also construct a broadcast packet (sending it to every machine on the network, which is usually not advised), and you can use the address INADDR_BROADCAST if you have set the socket option to broadcast mode (see the section "Socket Options"). Finally, the tolen parameter specifies the length of the to address.
The sendto() function will return the number of bytes it has transferred, or a SOCKET_ERROR if there was a problem sending the datagram.
The following example sends a UDP datagram:
// Create a connectionless socket
SOCKET sUDPSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// Check to see if we have a valid socket
if(sUDPSocket == INVALID_SOCKET) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Set up the target device address. For this sample,
// we are assuming it is a machine at 192.168.0.1, and on
// port 40040
SOCKADDR_IN sTargetDevice;
memset(&sTargetDevice, 0, sizeof(SOCKADDR_IN));
sTargetDevice.sin_family = AF_INET;
sTargetDevice.sin_port = htons(40040);
sTargetDevice.sin_addr.s_addr = inet_addr("192.168.0.1");
// Send a datagram to the target device
char cBuffer[1024] = "Test Buffer";
int nBytesSent = 0;
int nBufSize = strlen(cBuffer);
nBytesSent = sendto(sUDPSocket, cBuffer, nBufSize, 0,
(SOCKADDR *) &sTargetDevice,
sizeof(SOCKADDR_IN));
// Close the socket
closesocket(sUDPSocket);
Receiving a UDP Datagram
To have your application receive a UDP packet, you need to call the recvfrom() function, which will block until data has arrived from a client (or it can return immediately if in nonblocking mode; see "Socket Options"):
int recvfrom (SOCKET s, char *buf, int len, int flags,
struct sockaddr *from, int *fromlen);
Notice that the parameters are very similar to those described for the recv() function. The first parameter, s, is the socket on which we want to receive data. Next, buf is a pointer to a buffer for the incoming data, and its size is specified by the len parameter. The flags parameter must be set to 0. Finally, the from parameter contains a pointer to the SOCKADDR_IN structure, which contains information about the device that sent the data. A pointer to its length is in the fromlen field.
If the packet is received successfully, recvfrom() will return a 0; otherwise, a SOCKET_ERROR will occur.
The following example shows how to receive a UDP datagram packet:
// Create a connectionless socket
SOCKET sUDPSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// Check to see if we have a valid socket
if(sUDPSocket == INVALID_SOCKET) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Setup a bind on the socket, telling us what port and
// adapter to receive datagrams on. Assume we are listening
// on port 40040
SOCKADDR_IN sReceiveFromAddr;
memset(&sReceiveFromAddr, 0, sizeof(SOCKADDR_IN));
sReceiveFromAddr.sin_family = AF_INET;
sReceiveFromAddr.sin_port = htons(40040);
sReceiveFromAddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sUDPSocket, (SOCKADDR *)&sReceiveFromAddr,
sizeof(SOCKADDR_IN)) ==
SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Receive a datagram from another device
char cBuffer[1024] = "";
int nBytesRecv = 0;
int nBufSize = strlen(cBuffer);
int nReceiveAddrSize = 0;
// Get the datagram
nBytesRecv = recvfrom(sUDPSocket, cBuffer, nBufSize, 0,
(SOCKADDR *) &sReceiveFromAddr,
&nReceiveAddrSize);
// Close the socket
closesocket(sUDPSocket);
WSACleanup();
One final note regarding the sending and receiving of UDP data: You can also transfer data by using the connect(), send(), and recv() functions. Transmitting UDP data this way is considered a somewhat "directed" connectionless transfer, and should be used only if you plan to communicate with one other device during a session (i.e., all packets are sent to the same address). To do this, after your UDP socket has been created, call the connect() function with SOCKADDR_IN set to the machine you want to establish a session with. No real connection will be established, but you can use the send() and recv() functions to transfer data with the associated address.
TCP versus UDP: What Should Your App Use?
Deciding which transport protocol to use can be the hardest part of developing TCP/IP-based applications. The general rule of thumb is to use TCP unless your specific application calls for bandwidth sensitivity or congestion control, which can be relevant issues on Pocket PC devices now that wireless connections are becoming more accessible. UDP should be used in the following situations: when writing applications that require the capability to broadcast data to multiple devices (IP Multicast however, is much less bandwidth-intensive); when writing real-time multimedia applications that can afford to drop packets when congestion occurs; or for extremely small transmissions that require acknowledgment only.
TCP (stream)-based applications are considered much more reliable, and TCP's guarantee of message delivery combined with relatively low overhead makes it an extremely flexible and robust protocol. My best advice is to consider carefully the type of data that you will be sending, as well as your available bandwidth and network conditions, before making your decision.
[ Team LiB ]
Internet Control Message Protocol (ICMP)
The Internet Control Message Protocol (ICMP) is a "support" protocol that is used for sending informational, control, and error messages between network endpoints. It was originally designed to give network routers a way to deliver errors to the network layer of the OSI model so it could decide how to handle the error. ICMP uses a form of IP datagrams known as raw sockets to send information between hosts and servers. Pocket PC, however, does not allow you to explicitly create raw sockets; rather, it uses a set of functions that enable you to send limited ICMP ping messages.
ICMP pinging can be extremely useful when performing functions such as network diagnostics, as it can be used to test network connections, routing, and your TCP/IP stack. To use the ICMP functions, you should include the files icmpapi.h, ipexport.h, and winsock.h in your project's source, as well as link with both the icmplib.lib and winsock.lib libraries.
In order to send an ICMP message, you must first obtain an ICMP handle by calling the following function:
HANDLE IcmpCreateFile (void);
No parameters are required, and an ICMP handle will be returned. If an error has occurred, you will receive an INVALID_HANDLE_VALUE and you can use the GetLastError() function to obtain more information about why it failed.
Once you have a valid ICMP handle, you can send your ICMP request by calling the IcmpSendEcho() function:
DWORD IcmpSendEcho (HANDLE IcmpHandle,
IPAddr DestinationAddress,
LPVOID RequestData, WORD RequestSize,
PIP_OPTION_INFORMATION RequestOptions,
LPVOID ReplyBuffer,
DWORD ReplySize, DWORD Timeout);
For the first parameter, we pass in the handle to the newly created ICMP packet. The DestinationAddress parameter is an unsigned long value specifying the target IP address. You can get this by using one of the IP support functions, such as inet_addr(), described in the section "Name Resolution," later in this chapter. If you want to pass any additional data along with the ICMP message, you can pass a pointer to your buffer using the RequestData parameter, and set its size with the RequestSize field. You will also want to make sure that you don't try to stuff large messages into this buffer, as there is a limit of 8KB on most systems.
The next parameter, RequestOptions, is a pointer to an IP_OPTION_INFORMATION structure:
struct ip_option_information {
unsigned char Ttl;
unsigned char Tos;
unsigned char Flags;
unsigned char OptionsSize;
unsigned char FAR *OptionsData;
};
The IP_OPTION_INFORMATION structure is used to configure your echo request. The Ttl (time to live) parameter determines the amount of time that the packet will be around before it expires. Next, the Tos field specifies the type of service for the ICMP packet, which should be set to 0. The only option that the Flags parameter supports on Pocket PC is IP_FLAG_DF, which instructs ICMP not to fragment the message. Finally, OptionsSize is set to the size of the OptionsData parameter, which is a pointer to any additional options. Currently, Pocket PC supports only IP_OPT_RR, which records the record route; and IP_OPT_TS, which records the timestamp.
The next parameters in IcmpSendEcho() are ReplyBuffer and ReplySize. The ReplyBuffer is a pointer to an array of ICMP_ECHO_REPLY structures that contains responses to our ICMP echo message from each machine to which our request was sent. It is important to ensure that the buffer you have allocated to receive the reply is set to be the size of ICMP_ECHO_REPLY plus eight additional bytes, which are used for any additional error information. The last parameter is Timeout, which is the amount of time IcmpSendEcho() will wait in milliseconds before failing.
IcmpSendEcho() will return the number of packets that are waiting in the ReplyBuffer if it is successful; otherwise, it will return 0. The ICMP_ECHO_REPLY response structure is defined as follows:
struct icmp_echo_reply {
IPAddr Address;
unsigned long Status;
unsigned long RoundTripTime;
unsigned short DataSize;
unsigned short Reserved;
void FAR *Data;
struct ip_option_information Options;
};
Each network node with which we communicated during our echo request will respond with an ICMP_ECHO_REPLY package. The first field contains the Address of the machine that responded. The Status field should return IP_SUCCESS if successful; otherwise, it will contain an error code (defined in ipexport.h). Next, RoundTripTime contains the amount of time in milliseconds that it took for our request to get there. The DataSize field contains the size of the data, returned to us in the Data field. The last field, Options, is an IP_OPTIONS_STRUCTURE containing any options of the returning packet.
Finally, when you are finished sending your echo requests, you can close and clean up your ICMP packets by calling into the IcmpCloseHandle() function, which is defined as follows:
BOOL IcmpCloseHandle(HANDLE IcmpHandle);
The only parameter, IcmpHandle, is the ICMP session handle that you have been using. If ICMP closes successfully, this function will return TRUE. FALSE will be returned to you if an error has occurred.
The following example shows how we can use the ICMP functions to ping another network endpoint to determine whether our Internet connection is active (similar to the desktop ping utility):
// Initialize Winsock
WSADATA wsaData;
memset(&wsaData, 0, sizeof(WSADATA));
if(WSAStartup(MAKEWORD(1,1), &wsaData) != 0)
return FALSE;
// Create a new ICMP message session
HANDLE hIcmpSession = IcmpCreateFile();
if(hIcmpSession == INVALID_HANDLE_VALUE)
return FALSE;
// Convert the target address to a network address
HOSTENT *hostServer = gethostbyname("www.furrygoat.com");
if(hostServer == NULL) {
int dwError = WSAGetLastError();
return FALSE;
}
DWORD dwTargetAddress = *((u_long*)hostServer>h_addr_list[0]);
// Setup the option_information structure
IP_OPTION_INFORMATION ipOptions;
memset(&ipOptions, 0, sizeof(IP_OPTION_INFORMATION));
ipOptions.Ttl = 32;
ipOptions.Flags = IP_FLAG_DF;
// Send our request
BYTE bOutPacket[32];
BYTE bInPacket[1024];
int nTrace = IcmpSendEcho(hIcmpSession, dwTargetAddress, bOutPacket,
sizeof(bOutPacket), &ipOptions, bInPacket, 1024, 5000);
if(nTrace == 0) {
DWORD dwError = GetLastError();
IcmpCloseHandle(hIcmpSession);
return 0;
}
ICMP_ECHO_REPLY *pER = (PICMP_ECHO_REPLY)bInPacket;
for(int i =0;i
struct in_addr sReplyAddr;
sReplyAddr.S_un.S_addr = (IPAddr)pER->Address;
wsprintf(tchOutput, TEXT("Reply from %hs"),
inet_ntoa(sReplyAddr));
MessageBox(NULL, tchOutput, TEXT("Ping"), MB_OK);
pER++;
}
IcmpCloseHandle(hIcmpSession);
[ Team LiB ]
Streaming (TCP) Sockets
Streaming (TCP) Sockets
Streaming (or connection-oriented) sockets are probably the most commonly used type of communication transport protocol over TCP/IP that you will use. TCP sockets provide you with a reliable, nearly error-free data pipe between two endpoints, both of which can send and receive streams of bytes back and forth, without data being lost or duplicated. A good analogy would be to compare a TCP socket connection to a telephone call between devices—one calls another (the phone number being the server device's IP address, and its phone extension being the server device's IP port). Once the other device picks up, a conversation can proceed between the two devices, with both transmitting and receiving data. Finally, the call is completed, and both sides hang up. The connection thus made is known as a communications session (this is the same session described previously in the OSI model).
This type of connection between devices is also sometimes referred to as a client/server model. One device, known as the client, creates a socket, connects to the server, and then begins sending and receiving data. On the other side, the server creates a socket and listens for an incoming connection from the client. Once a connection is initiated, the server accepts the connection, and then starts to send and receive data to and from the incoming client. The data that the client and server send back and forth is completely up to you; however, several well-known communication protocols have already been established, such as HTTP or FTP.
Figure 1.2 shows the process for creating both client and server TCP socket connections and how data flows between both network endpoints.
Figure 1.2. Socket process for connection-oriented clients and servers
Creating a Socket
The first step in establishing a network connection via Winsock is to create a socket. A socket is a data type, similar to a file handle, that identifies a unique descriptor that allows access to your network object. What the actual descriptor identifies is not specifically detailed in the Winsock specification; rather, it is determined by the specific Winsock implementation, so we don't really know what that value means. For our purposes, the actual contents of the descriptor are not important. What is important is the understanding that a socket is what you use to access your network connection.
To create a socket, you use the socket() function, which is defined as follows:
SOCKET socket (int af, int type, int protocol);
The af parameter specifies the protocol's address family, which determines what type of socket will be created. Pocket PC supports either the AF_INET or AF_IRDA socket types. If you wanted to create a socket for infrared communications, you would use AF_IRDA (see Chapter 5); otherwise, for normal TCP/IP usage, you would use AF_INET. The type parameter is the protocol's communication type, and can be either SOCK_STREAM or SOCK_DGRAM. To create a TCP connection-oriented socket, use SOCK_STREAM. When creating a connectionless UDP socket, use SOCK_DGRAM (see the section "Connectionless (UDP) Sockets"). You must use SOCK_STREAM if you are creating a socket to be used for infrared communications. The final parameter, protocol, specifies which protocol to use with the socket. If you want to specify the TCP protocol, you use the value IPPROTO_TCP. Conversely, IPPROTO_UDP specifies the UDP protocol.
When the function returns, you will receive either a new socket handle or the error INVALID_SOCKET. If you want to find out why you could not create a socket, use the WSAGetLastError() function described previously.
The following code shows how to create a connection-oriented socket:
// Create a connection-oriented socket
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Check to see if we have a valid socket
if(s == INVALID_SOCKET) {
int iSocketError = WSAGetLastError();
return FALSE;
}
Connecting to a Server (from the Client)
Once you have created a socket, you can use it to establish a connection to a server. This is done by using the connect() function call:
int connect (SOCKET s, const struct sockaddr *name, int namelen);
The first parameter, s, specifies the socket descriptor that was returned from the socket function. The name parameter is the socket address structure, SOCKADDR_IN, which identifies the server to which we are attempting to connect (see "TCP/IP Addresses"). The namelen parameter is the length of the buffer used for the name parameter.
If you are successful in establishing a connection to the server specified by the name parameter, the function will return a 0; otherwise, a SOCKET_ERROR will occur. To find out more information about why a connection could not be established, call WSAGetLastError(). Remember that you cannot call connect() on a socket that is already connected.
Once a connection has been established, the socket is ready to send and receive data. Note that if a connection is broken during the course of communications between client and server, your application will need to discard the old socket and create a new one if it needs to reestablish communications.
The following example shows how to connect with a server:
// First, get the host information
HOSTENT *hostServer = gethostbyname("www.microsoft.com");
if(hostServer == NULL) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Set up the target device address structure
SOCKADDR_IN sinServer;
memset(&sinServer, 0, sizeof(SOCKADDR_IN));
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(80);
sinServer.sin_addr =
*((IN_ADDR *)hostServer>h_addr_list[0]);
// Connect with a valid socket
if(connect(s, (SOCKADDR *)&sinServer, sizeof(sinServer)) ==
SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Do something with the socket
closesocket(s);
Sending and Receiving Data
Now that we have established a connection to a server, we are ready to send and receive data between the two network endpoints. On a connection-oriented socket, data can be transmitted in either direction, so both client and server can use the same methods to communicate data over the wire.
To transmit data on a connected socket, you use the send() function, which is defined as follows:
int send (SOCKET s, const char *buf, int len, int flags);
The s parameter is the same socket handle that we previously used with the connect function, and was originally created using the socket() function. The buf parameter is a pointer to a buffer that contains the data we want to send, and its length is specified in the len parameter. The final parameter, flags, is used to affect the way the data is sent, and can be 0 or MSG_DONTROUTE, which specifies that the data should not be routed. Typically, this parameter will be set to 0, as MSG_DONTROUTE is used only for testing or routing messages.
When the send function returns, it will return the number of actual bytes that were sent over the network, or a SOCKET_ERROR if there was some problem in transmitting the data.
To receive data on a socket, you use the recv() function:
int recv (SOCKET s, char *buf, int len, int flags);
Again, s indicates the socket on which we want to receive data. The second parameter, buf, is the buffer that will receive the data; and its size is specified by the len parameter. Finally, the flags parameter must be set to 0.
The return value for the recv() function is either the number of bytes received or 0, if the connection has been closed. You may also get a SOCKET_ERROR if an error has occurred.
Note that both the send() and recv() functions do not always read or write the exact amount of data you have requested. This is because TCP/IP allocates a limited amount of buffer space for both the outgoing and incoming data queues, and it typically fills up rather quickly. For example, if you request a 10MB file from a Web site, your incoming data queue will block (see the section "Socket Options") until you have read the data from the queue (using the recv() function). The same applies when transmitting, so you need to manually ensure that all your outgoing data has been sent. For example, to send a buffer over TCP:
// Send a request to the server
char cBuffer[1024] = "";
int nBytesSent = 0;
int nBytesIndex = 0;
// Set up the buffer to send
sprintf(cBuffer, "GET / HTTP/1.0\r\n\r\n");
int nBytesLeft = strlen(cBuffer);
// Send the entire buffer
while(nBytesLeft > 0) {
nBytesSent = send(s, &cBuffer[nBytesIndex], nBytesLeft, 0);
if(nBytesSent == SOCKET_ERROR)
break;
// See how many bytes are left. If we still need to send, loop
nBytesLeft -= nBytesSent;
nBytesIndex += nBytesSent;
}
The following example shows how to use TCP sockets to create a basic client that will connect to a Web page, send a command, and receive the Web site's default HTML page. When it has completed, it will display its contents in a message box. The actual buffer that is returned from the request is shown in Figure 1.3.
Figure 1.3. HTTP response from our request
// Initialize Winsock
WSADATA wsaData;
memset(&wsaData, 0, sizeof(WSADATA));
if(WSAStartup(MAKEWORD(1,1), &wsaData) != 0)
return FALSE;
// Create a connection-oriented socket
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Check to see if we have a valid socket
if(s == INVALID_SOCKET) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Get the host information
HOSTENT *hostServer = gethostbyname("www.microsoft.com");
if(hostServer == NULL) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Set up the target device address structure
SOCKADDR_IN sinServer;
memset(&sinServer, 0, sizeof(SOCKADDR_IN));
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(80);
sinServer.sin_addr =
*((IN_ADDR *)hostServer>h_addr_list[0]);
// Connect
if(connect(s, (SOCKADDR *)&sinServer, sizeof(sinServer)) ==
SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Send a request to the server
char cBuffer[1024] = "";
int nBytesSent = 0;
int nBytesIndex = 0;
// Set up the buffer to send
sprintf(cBuffer, "GET / HTTP/1.0\r\n\r\n");
int nBytesLeft = strlen(cBuffer);
// Send the entire buffer
while(nBytesLeft > 0) {
nBytesSent = send(s, &cBuffer[nBytesIndex], nBytesLeft, 0);
if(nBytesSent == SOCKET_ERROR)
break;
// See how many bytes are left. If we still need to send, loop
nBytesLeft -= nBytesSent;
nBytesIndex += nBytesSent;
}
// Get the response
TCHAR tchResponseBuffer[1024] = TEXT("\0");
char cResponseBuffer[1024] = "";
BOOL fBreak = FALSE;
int nBytesReceived = 0;
while(!fBreak) {
nBytesReceived = recv(s, &cResponseBuffer[0], 1024, 0);
if(nBytesReceived == SOCKET_ERROR)
break;
// Convert the data from ANSI to Unicode
mbstowcs(tchResponseBuffer, cResponseBuffer, nBytesReceived);
// Show the MessageBox
MessageBox(NULL, tchResponseBuffer, TEXT("Web Output"), MB_OK);
// Check to see if this is the end of the HTTP response by
// looking for \r\n\r\n
if(_tcsstr(tchResponseBuffer, TEXT("\r\n\r\n")))
fBreak = TRUE;
// Clear the buffers
memset(tchResponseBuffer, 0, 1024);
memset(cResponseBuffer, 0, 1024);
}
closesocket(s);
WSACleanup();
Receiving an Incoming Connection (Server)
The only real difference between transferring data between a client and a server stream connection is how the connection is established (a client makes the connection, a server listens for the connection). Otherwise, both use send() and recv() to transfer data between the two. Now that we have looked at the client, let's examine how we can create an application that services incoming connection requests (made by a client's call to the connect() function). The first thing we need to do is create a socket, in the same way you would a client, by calling the socket() function.
Once we have created a socket, instead of connecting to a server, we need to put our new socket into a state in which it can listen for incoming connections. To do this, we need to bind the newly created socket with a local address. Create this association by using the bind() function:
int bind (SOCKET s, const struct sockaddr *addr, int namelen);
The first parameter, s, is the handle to a new socket created by the socket() function, which will be the socket on which you want to wait for connections. The addr parameter is a pointer to an address buffer, which is determined by the protocol you want to use, and specifies protocol-specific address information. If you want to use the standard TCP/IP protocol, then you will want to use a SOCKADDR_IN buffer (see the section "TCP/IP Addresses"). If you are using infrared, you will use SOCKADDR_IRDA instead (see Chapter 5). Finally, namelen is the size of the address structure being passed in the addr parameter.
If there are no errors, bind() will return 0; otherwise, a SOCKET_ERROR will occur.
For example, the following binds a TCP connection on port 80 to a socket for all IP addresses on the device:
SOCKADDR_IN sListener;
memset(&sListener, 0, sizeof(SOCKADDR_IN));
// Set up the port to bind on
sListener.sin_family = AF_INET;
sListener.sin_port = htons(80);
sListener.sin_addr.s_addr = htonl(INADDR_ANY);
// Create a TCP socket
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(s == INVALID_SOCKET)
return FALSE;
// Bind to the socket
if(bind(s, (SOCKADDR *)&sListener, sizeof(sListener)) ==
SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
You may notice that I used the IP address INADDR_ANY instead of a specific adapter's IP address. Using INADDR_ANY enables us to bind our socket to all available IP addresses on our device, so that incoming connections on any interface will be accepted by our socket.
Once the socket has been bound to some address (or addresses), we need to put the socket into listening mode. This will actually enable the socket to wait for incoming connections:
int listen (SOCKET s, int backlog);
The parameter s is the bound socket. The backlog parameter specifies the size of the queue for pending incoming connections, and typically is set to SOMAXCONN (on Pocket PC, this is currently limited to two connections). The backlog queue is used when there are several simultaneous incoming connections. When the queue is full, all other requests will be refused until a connection request is removed from the queue by the accept() function.
If there is an error, the listen() function will return SOCKET_ERROR; otherwise, it will return 0.
Finally, to get the socket of the incoming connection, we need to call the accept() function, which is defined as follows:
SOCKET accept (SOCKET s, struct sockaddr *addr, int *addrlen);
The first parameter is the socket that we have previously placed into listening mode. The next parameter, addr, is a buffer that receives either a SOCKADDR_IN or SOCKADDR_IRDA structure, depending on the protocol used by the socket, which contains information about the incoming connection. The last parameter, addrlen, indicates the size of the structure addr.
You might notice that the accept() function does not return immediately. This is because accept() is a blocking function, which means that it won't return until a client makes a connection or the listening socket is destroyed (you can also set a socket option to put it into nonblocking mode, which is discussed in the section "Socket Options"). When accept() finally returns, it will return either a new socket handle for the incoming client, or a SOCKET_ERROR. All further communications with the client should be done using this new socket handle, while the original socket continues to listen for more incoming connections.
The following example listens for incoming TCP server connections for a client that is requesting a Web page using HTTP, and returns a basic response to the request:
// Initialize Winsock
WSADATA wsaData;
memset(&wsaData, 0, sizeof(WSADATA));
if(WSAStartup(MAKEWORD(1,1), &wsaData) != 0)
return FALSE;
// Create a connection-oriented socket
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Check to see if we have a valid socket
if(s == INVALID_SOCKET) {
int iSocketError = WSAGetLastError();
return FALSE;
}
SOCKADDR_IN sListener;
memset(&sListener, 0, sizeof(SOCKADDR_IN));
// Setup the port to bind on
sListener.sin_family = AF_INET;
sListener.sin_port = htons(80);
sListener.sin_addr.s_addr = htonl(INADDR_ANY);
// Bind to the socket
if(bind(s, (SOCKADDR *)&sListener, sizeof(sListener)) ==
SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Listen for incoming connections
if(listen(s, SOMAXCONN) == SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Wait for a connection
SOCKADDR_IN sIncomingAddr;
memset(&sIncomingAddr, 0, sizeof(SOCKADDR_IN));
int iAddrLen = sizeof(SOCKADDR_IN);
SOCKET sIncomingSocket = accept(s, (SOCKADDR *)
&sIncomingAddr, &iAddrLen);
if(sIncomingSocket == SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// We have an incoming socket request
char cResponseBuffer[1024] = "";
int nBytesReceived = 0;
// Get a basic request. In reality, we would want to check
// the HTTP request to see if it's valid, but let's just
// send a simple response.
nBytesReceived = recv(sIncomingSocket, &cResponseBuffer[0],
1024, 0);
if(nBytesReceived == SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Send out a response
char cBuffer[1024] = "";
int nBytesSent = 0;
int nBytesIndex = 0;
// Setup the buffer to send
sprintf(cBuffer, &"HTTP/1.0 200 OK\r\n\r\nTest
Response\r\n\r\n");
int nBytesLeft = strlen(cBuffer);
// Send the entire buffer
while(nBytesLeft > 0) {
nBytesSent = send(sIncomingSocket, &cBuffer[nBytesIndex],
nBytesLeft, 0);
if(nBytesSent == SOCKET_ERROR)
break;
// See how many bytes are left. If we still need to send, loop
nBytesLeft -= nBytesSent;
nBytesIndex += nBytesSent;
}
// Close the sockets
closesocket(sIncomingSocket);
closesocket(s);
WSACleanup();
Closing a Socket
Once you are finished using a socket, whether you are on a server or a client, you must release the device resources that are associated with that socket.
Before you actually close the socket, you should call the shutdown() function. You can destroy a socket by directly closing it, but you are better off calling shutdown() first because it ensures that all data in the TCP/IP transfer queue is sent or received before the socket is closed:
int shutdown (SOCKET s, int how);
The handle to the socket, s, is the first parameter we pass in to this function. The how parameter specifies how subsequent socket functions are processed on this socket, and can be set to SD_RECEIVE, SD_SEND, or SD_BOTH. Setting this to SD_RECEIVE will prevent any further recv function calls from being completed, and SD_SEND will prevent any further send calls. Obviously, SD_BOTH will stop sending and receiving for the socket (however, all data already queued will be processed).
If there are no errors, shutdown() will return 0. Once a socket has been shutdown(), you cannot use it again, except to close it with the closesocket() function:
int closesocket (SOCKET s);
The only parameter that closesocket() takes is the handle to the socket descriptor you want to close.
Streaming (or connection-oriented) sockets are probably the most commonly used type of communication transport protocol over TCP/IP that you will use. TCP sockets provide you with a reliable, nearly error-free data pipe between two endpoints, both of which can send and receive streams of bytes back and forth, without data being lost or duplicated. A good analogy would be to compare a TCP socket connection to a telephone call between devices—one calls another (the phone number being the server device's IP address, and its phone extension being the server device's IP port). Once the other device picks up, a conversation can proceed between the two devices, with both transmitting and receiving data. Finally, the call is completed, and both sides hang up. The connection thus made is known as a communications session (this is the same session described previously in the OSI model).
This type of connection between devices is also sometimes referred to as a client/server model. One device, known as the client, creates a socket, connects to the server, and then begins sending and receiving data. On the other side, the server creates a socket and listens for an incoming connection from the client. Once a connection is initiated, the server accepts the connection, and then starts to send and receive data to and from the incoming client. The data that the client and server send back and forth is completely up to you; however, several well-known communication protocols have already been established, such as HTTP or FTP.
Figure 1.2 shows the process for creating both client and server TCP socket connections and how data flows between both network endpoints.
Figure 1.2. Socket process for connection-oriented clients and servers
Creating a Socket
The first step in establishing a network connection via Winsock is to create a socket. A socket is a data type, similar to a file handle, that identifies a unique descriptor that allows access to your network object. What the actual descriptor identifies is not specifically detailed in the Winsock specification; rather, it is determined by the specific Winsock implementation, so we don't really know what that value means. For our purposes, the actual contents of the descriptor are not important. What is important is the understanding that a socket is what you use to access your network connection.
To create a socket, you use the socket() function, which is defined as follows:
SOCKET socket (int af, int type, int protocol);
The af parameter specifies the protocol's address family, which determines what type of socket will be created. Pocket PC supports either the AF_INET or AF_IRDA socket types. If you wanted to create a socket for infrared communications, you would use AF_IRDA (see Chapter 5); otherwise, for normal TCP/IP usage, you would use AF_INET. The type parameter is the protocol's communication type, and can be either SOCK_STREAM or SOCK_DGRAM. To create a TCP connection-oriented socket, use SOCK_STREAM. When creating a connectionless UDP socket, use SOCK_DGRAM (see the section "Connectionless (UDP) Sockets"). You must use SOCK_STREAM if you are creating a socket to be used for infrared communications. The final parameter, protocol, specifies which protocol to use with the socket. If you want to specify the TCP protocol, you use the value IPPROTO_TCP. Conversely, IPPROTO_UDP specifies the UDP protocol.
When the function returns, you will receive either a new socket handle or the error INVALID_SOCKET. If you want to find out why you could not create a socket, use the WSAGetLastError() function described previously.
The following code shows how to create a connection-oriented socket:
// Create a connection-oriented socket
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Check to see if we have a valid socket
if(s == INVALID_SOCKET) {
int iSocketError = WSAGetLastError();
return FALSE;
}
Connecting to a Server (from the Client)
Once you have created a socket, you can use it to establish a connection to a server. This is done by using the connect() function call:
int connect (SOCKET s, const struct sockaddr *name, int namelen);
The first parameter, s, specifies the socket descriptor that was returned from the socket function. The name parameter is the socket address structure, SOCKADDR_IN, which identifies the server to which we are attempting to connect (see "TCP/IP Addresses"). The namelen parameter is the length of the buffer used for the name parameter.
If you are successful in establishing a connection to the server specified by the name parameter, the function will return a 0; otherwise, a SOCKET_ERROR will occur. To find out more information about why a connection could not be established, call WSAGetLastError(). Remember that you cannot call connect() on a socket that is already connected.
Once a connection has been established, the socket is ready to send and receive data. Note that if a connection is broken during the course of communications between client and server, your application will need to discard the old socket and create a new one if it needs to reestablish communications.
The following example shows how to connect with a server:
// First, get the host information
HOSTENT *hostServer = gethostbyname("www.microsoft.com");
if(hostServer == NULL) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Set up the target device address structure
SOCKADDR_IN sinServer;
memset(&sinServer, 0, sizeof(SOCKADDR_IN));
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(80);
sinServer.sin_addr =
*((IN_ADDR *)hostServer>h_addr_list[0]);
// Connect with a valid socket
if(connect(s, (SOCKADDR *)&sinServer, sizeof(sinServer)) ==
SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Do something with the socket
closesocket(s);
Sending and Receiving Data
Now that we have established a connection to a server, we are ready to send and receive data between the two network endpoints. On a connection-oriented socket, data can be transmitted in either direction, so both client and server can use the same methods to communicate data over the wire.
To transmit data on a connected socket, you use the send() function, which is defined as follows:
int send (SOCKET s, const char *buf, int len, int flags);
The s parameter is the same socket handle that we previously used with the connect function, and was originally created using the socket() function. The buf parameter is a pointer to a buffer that contains the data we want to send, and its length is specified in the len parameter. The final parameter, flags, is used to affect the way the data is sent, and can be 0 or MSG_DONTROUTE, which specifies that the data should not be routed. Typically, this parameter will be set to 0, as MSG_DONTROUTE is used only for testing or routing messages.
When the send function returns, it will return the number of actual bytes that were sent over the network, or a SOCKET_ERROR if there was some problem in transmitting the data.
To receive data on a socket, you use the recv() function:
int recv (SOCKET s, char *buf, int len, int flags);
Again, s indicates the socket on which we want to receive data. The second parameter, buf, is the buffer that will receive the data; and its size is specified by the len parameter. Finally, the flags parameter must be set to 0.
The return value for the recv() function is either the number of bytes received or 0, if the connection has been closed. You may also get a SOCKET_ERROR if an error has occurred.
Note that both the send() and recv() functions do not always read or write the exact amount of data you have requested. This is because TCP/IP allocates a limited amount of buffer space for both the outgoing and incoming data queues, and it typically fills up rather quickly. For example, if you request a 10MB file from a Web site, your incoming data queue will block (see the section "Socket Options") until you have read the data from the queue (using the recv() function). The same applies when transmitting, so you need to manually ensure that all your outgoing data has been sent. For example, to send a buffer over TCP:
// Send a request to the server
char cBuffer[1024] = "";
int nBytesSent = 0;
int nBytesIndex = 0;
// Set up the buffer to send
sprintf(cBuffer, "GET / HTTP/1.0\r\n\r\n");
int nBytesLeft = strlen(cBuffer);
// Send the entire buffer
while(nBytesLeft > 0) {
nBytesSent = send(s, &cBuffer[nBytesIndex], nBytesLeft, 0);
if(nBytesSent == SOCKET_ERROR)
break;
// See how many bytes are left. If we still need to send, loop
nBytesLeft -= nBytesSent;
nBytesIndex += nBytesSent;
}
The following example shows how to use TCP sockets to create a basic client that will connect to a Web page, send a command, and receive the Web site's default HTML page. When it has completed, it will display its contents in a message box. The actual buffer that is returned from the request is shown in Figure 1.3.
Figure 1.3. HTTP response from our request
// Initialize Winsock
WSADATA wsaData;
memset(&wsaData, 0, sizeof(WSADATA));
if(WSAStartup(MAKEWORD(1,1), &wsaData) != 0)
return FALSE;
// Create a connection-oriented socket
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Check to see if we have a valid socket
if(s == INVALID_SOCKET) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Get the host information
HOSTENT *hostServer = gethostbyname("www.microsoft.com");
if(hostServer == NULL) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Set up the target device address structure
SOCKADDR_IN sinServer;
memset(&sinServer, 0, sizeof(SOCKADDR_IN));
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(80);
sinServer.sin_addr =
*((IN_ADDR *)hostServer>h_addr_list[0]);
// Connect
if(connect(s, (SOCKADDR *)&sinServer, sizeof(sinServer)) ==
SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Send a request to the server
char cBuffer[1024] = "";
int nBytesSent = 0;
int nBytesIndex = 0;
// Set up the buffer to send
sprintf(cBuffer, "GET / HTTP/1.0\r\n\r\n");
int nBytesLeft = strlen(cBuffer);
// Send the entire buffer
while(nBytesLeft > 0) {
nBytesSent = send(s, &cBuffer[nBytesIndex], nBytesLeft, 0);
if(nBytesSent == SOCKET_ERROR)
break;
// See how many bytes are left. If we still need to send, loop
nBytesLeft -= nBytesSent;
nBytesIndex += nBytesSent;
}
// Get the response
TCHAR tchResponseBuffer[1024] = TEXT("\0");
char cResponseBuffer[1024] = "";
BOOL fBreak = FALSE;
int nBytesReceived = 0;
while(!fBreak) {
nBytesReceived = recv(s, &cResponseBuffer[0], 1024, 0);
if(nBytesReceived == SOCKET_ERROR)
break;
// Convert the data from ANSI to Unicode
mbstowcs(tchResponseBuffer, cResponseBuffer, nBytesReceived);
// Show the MessageBox
MessageBox(NULL, tchResponseBuffer, TEXT("Web Output"), MB_OK);
// Check to see if this is the end of the HTTP response by
// looking for \r\n\r\n
if(_tcsstr(tchResponseBuffer, TEXT("\r\n\r\n")))
fBreak = TRUE;
// Clear the buffers
memset(tchResponseBuffer, 0, 1024);
memset(cResponseBuffer, 0, 1024);
}
closesocket(s);
WSACleanup();
Receiving an Incoming Connection (Server)
The only real difference between transferring data between a client and a server stream connection is how the connection is established (a client makes the connection, a server listens for the connection). Otherwise, both use send() and recv() to transfer data between the two. Now that we have looked at the client, let's examine how we can create an application that services incoming connection requests (made by a client's call to the connect() function). The first thing we need to do is create a socket, in the same way you would a client, by calling the socket() function.
Once we have created a socket, instead of connecting to a server, we need to put our new socket into a state in which it can listen for incoming connections. To do this, we need to bind the newly created socket with a local address. Create this association by using the bind() function:
int bind (SOCKET s, const struct sockaddr *addr, int namelen);
The first parameter, s, is the handle to a new socket created by the socket() function, which will be the socket on which you want to wait for connections. The addr parameter is a pointer to an address buffer, which is determined by the protocol you want to use, and specifies protocol-specific address information. If you want to use the standard TCP/IP protocol, then you will want to use a SOCKADDR_IN buffer (see the section "TCP/IP Addresses"). If you are using infrared, you will use SOCKADDR_IRDA instead (see Chapter 5). Finally, namelen is the size of the address structure being passed in the addr parameter.
If there are no errors, bind() will return 0; otherwise, a SOCKET_ERROR will occur.
For example, the following binds a TCP connection on port 80 to a socket for all IP addresses on the device:
SOCKADDR_IN sListener;
memset(&sListener, 0, sizeof(SOCKADDR_IN));
// Set up the port to bind on
sListener.sin_family = AF_INET;
sListener.sin_port = htons(80);
sListener.sin_addr.s_addr = htonl(INADDR_ANY);
// Create a TCP socket
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(s == INVALID_SOCKET)
return FALSE;
// Bind to the socket
if(bind(s, (SOCKADDR *)&sListener, sizeof(sListener)) ==
SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
You may notice that I used the IP address INADDR_ANY instead of a specific adapter's IP address. Using INADDR_ANY enables us to bind our socket to all available IP addresses on our device, so that incoming connections on any interface will be accepted by our socket.
Once the socket has been bound to some address (or addresses), we need to put the socket into listening mode. This will actually enable the socket to wait for incoming connections:
int listen (SOCKET s, int backlog);
The parameter s is the bound socket. The backlog parameter specifies the size of the queue for pending incoming connections, and typically is set to SOMAXCONN (on Pocket PC, this is currently limited to two connections). The backlog queue is used when there are several simultaneous incoming connections. When the queue is full, all other requests will be refused until a connection request is removed from the queue by the accept() function.
If there is an error, the listen() function will return SOCKET_ERROR; otherwise, it will return 0.
Finally, to get the socket of the incoming connection, we need to call the accept() function, which is defined as follows:
SOCKET accept (SOCKET s, struct sockaddr *addr, int *addrlen);
The first parameter is the socket that we have previously placed into listening mode. The next parameter, addr, is a buffer that receives either a SOCKADDR_IN or SOCKADDR_IRDA structure, depending on the protocol used by the socket, which contains information about the incoming connection. The last parameter, addrlen, indicates the size of the structure addr.
You might notice that the accept() function does not return immediately. This is because accept() is a blocking function, which means that it won't return until a client makes a connection or the listening socket is destroyed (you can also set a socket option to put it into nonblocking mode, which is discussed in the section "Socket Options"). When accept() finally returns, it will return either a new socket handle for the incoming client, or a SOCKET_ERROR. All further communications with the client should be done using this new socket handle, while the original socket continues to listen for more incoming connections.
The following example listens for incoming TCP server connections for a client that is requesting a Web page using HTTP, and returns a basic response to the request:
// Initialize Winsock
WSADATA wsaData;
memset(&wsaData, 0, sizeof(WSADATA));
if(WSAStartup(MAKEWORD(1,1), &wsaData) != 0)
return FALSE;
// Create a connection-oriented socket
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Check to see if we have a valid socket
if(s == INVALID_SOCKET) {
int iSocketError = WSAGetLastError();
return FALSE;
}
SOCKADDR_IN sListener;
memset(&sListener, 0, sizeof(SOCKADDR_IN));
// Setup the port to bind on
sListener.sin_family = AF_INET;
sListener.sin_port = htons(80);
sListener.sin_addr.s_addr = htonl(INADDR_ANY);
// Bind to the socket
if(bind(s, (SOCKADDR *)&sListener, sizeof(sListener)) ==
SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Listen for incoming connections
if(listen(s, SOMAXCONN) == SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Wait for a connection
SOCKADDR_IN sIncomingAddr;
memset(&sIncomingAddr, 0, sizeof(SOCKADDR_IN));
int iAddrLen = sizeof(SOCKADDR_IN);
SOCKET sIncomingSocket = accept(s, (SOCKADDR *)
&sIncomingAddr, &iAddrLen);
if(sIncomingSocket == SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// We have an incoming socket request
char cResponseBuffer[1024] = "";
int nBytesReceived = 0;
// Get a basic request. In reality, we would want to check
// the HTTP request to see if it's valid, but let's just
// send a simple response.
nBytesReceived = recv(sIncomingSocket, &cResponseBuffer[0],
1024, 0);
if(nBytesReceived == SOCKET_ERROR) {
int iSocketError = WSAGetLastError();
return FALSE;
}
// Send out a response
char cBuffer[1024] = "";
int nBytesSent = 0;
int nBytesIndex = 0;
// Setup the buffer to send
sprintf(cBuffer, &"HTTP/1.0 200 OK\r\n\r\nTest
Response\r\n\r\n");
int nBytesLeft = strlen(cBuffer);
// Send the entire buffer
while(nBytesLeft > 0) {
nBytesSent = send(sIncomingSocket, &cBuffer[nBytesIndex],
nBytesLeft, 0);
if(nBytesSent == SOCKET_ERROR)
break;
// See how many bytes are left. If we still need to send, loop
nBytesLeft -= nBytesSent;
nBytesIndex += nBytesSent;
}
// Close the sockets
closesocket(sIncomingSocket);
closesocket(s);
WSACleanup();
Closing a Socket
Once you are finished using a socket, whether you are on a server or a client, you must release the device resources that are associated with that socket.
Before you actually close the socket, you should call the shutdown() function. You can destroy a socket by directly closing it, but you are better off calling shutdown() first because it ensures that all data in the TCP/IP transfer queue is sent or received before the socket is closed:
int shutdown (SOCKET s, int how);
The handle to the socket, s, is the first parameter we pass in to this function. The how parameter specifies how subsequent socket functions are processed on this socket, and can be set to SD_RECEIVE, SD_SEND, or SD_BOTH. Setting this to SD_RECEIVE will prevent any further recv function calls from being completed, and SD_SEND will prevent any further send calls. Obviously, SD_BOTH will stop sending and receiving for the socket (however, all data already queued will be processed).
If there are no errors, shutdown() will return 0. Once a socket has been shutdown(), you cannot use it again, except to close it with the closesocket() function:
int closesocket (SOCKET s);
The only parameter that closesocket() takes is the handle to the socket descriptor you want to close.
Winsock
A Quick Look at TCP/IP
To better understand how Winsock works on Pocket PC, it is important to understand how TCP/IP itself is constructed. As mentioned previously, TCP/IP stands for Transmission Control Protocol/Internet Protocol, which typically represents the Internet Protocol Suite. The TCP/IP protocol suite is actually a collection of several protocols that form the basis of Internet communications. Table 1.1 describes the TCP/IP protocol suite.
Table 1.1. The TCP/IP Protocol Suite Protocol
Usage
IP
The Internet Protocol is responsible for the addressing, routing and packetization of data. It basically moves data between computers.
TCP
The Transport Control Protocol is a reliable connection—based data transport.
UDP
The User Datagram Protocol is a connectionless-based data transport that does not guarantee packets will arrive at their destination.
ICMP
The Internet Control Message Protocol handles error and control messages.
ARP
The Address Resolution Protocol performs IP address to hardware address translation for outgoing packets.
Developing applications that use Winsock on a Pocket PC device will require you to work with and understand the various TCP/IP protocols in order to effectively use networking in your application.
A Breakdown of TCP/IP
When you look at the various parts of TCP/IP, it is typically represented in the International Standards Organization/Open Systems Interconnect (ISO/OSI) network model, which divides networks into various layers depending on individual functionality (see Figure 1.1). The combination of each layer provides what is commonly known as the TCP/IP protocol stack. Note that the data-link layer and the physical layer are not technically part of the Internet Protocol Suite; this is what enables TCP/IP to talk over a variety of network types and mediums.
Figure 1.1. The ISO/OSI model
Let's take a quick look at each of the layers:
Physical: The physical layer is essentially the network hardware. This layer talks directly to the media (wireless, twisted pair cable, and so on) that is carrying the network data.
Data Link: The data-link layer (which is sometimes also called the network driver layer) manages requests to the network interface, and transfers data between the physical and network layers. Most importantly, it also hides the network's physical implementation from the network layer. This ensures that the protocol (in our case, TCP/IP) doesn't care about the specifics of what network technology is being used (such as Ethernet versus WiFi)—it simply passes data back and forth.
Network: The network layer is the core of TCP/IP. It is responsible for the addressing, routing, and packetization of data that it has received from the transport layer over the Internet Protocol (IP), and for passing it to the data-link layer. The network layer also provides the mapping of IP addresses to hardware addresses via the Address Resolution Protocol (ARP); the processing of error and control messages through the Internet Control Message Protocol (ICMP); and the control of multicast communications through the Internet Group Membership Protocol (IGMP).
Transport: The transport layer is what applications will typically talk to when running Winsock applications, and is sometimes called a low-level connection. It provides the transfer of data between a source and destination on the network. Pocket PC supports two transfer protocols: the Transfer Control Protocol (TCP) and the User Datagram Protocol (UDP). TCP is a connection-based protocol that reliably streams data from source to destination, whereas UDP is an unreliable, connectionless protocol that uses packets to send data from point A to point B.
Session: The session layer essentially creates a connection, controls it, and then finally closes the communications session. It is also sometimes referred to as a high-level connection, and it starts the upper layer of the OSI model. A session is usually defined by a specific application protocol, such as Telnet, for establishing and maintaining a connection between a client and a server.
Presentation: The presentation layer basically handles the formatting of data that two applications will use during the course of a session. Transfer protocols, such as FTP, SMTP, and HTTP, are considered part of the presentation layer.
Application: The application layer is what interacts with the user. It is the user interface for your application.
TCP/IP Addresses
In order for a device to talk on a TCP/IP network, it needs a valid network "address." TCP/IP on Pocket PC currently supports IP version 4 (IPv4), whereby network addresses are represented as a 32-bit number, which can be broken down into a host address and a subnet address. Typically, a user can recognize an IPv4 address by what's more commonly known as dotted notation, such as 192.168.0.1.
You should be aware of the following address types:
Unicast Address: A unicast address is typically what is assigned to your device, either through a hard-coded IP address, or one that is assigned to you via a DHCP server. This is the unique address that is used to identify your device on a network.
Broadcast Address: While not recommended, if you need to broadcast data to every device on a local network, you can send data to the address 255.255.255.255. Sending data here is considered a limited broadcast because most routers will never forward packets sent to this address. A directed broadcast address can also be used to send data to all devices on a specific subnet, such as 192.168.0.x, by sending data to 192.168.0.255. In order send data packets over a broadcast address, you need to set a socket option to accept broadcast communications, as well as use the UDP protocol as described later in the section "Connectionless (UDP) Sockets."
Loopback Address: Any network address that begins with the number 127 is considered a loopback address for the device. Typically, the address 127.0.0.1 is used to communicate with itself.
Multicast Address: The best way to think of a multicast address is to think of a "group" address. When you are a part of a multicast group, all of the devices that have joined that group will receive data that is sent to the group address. Multicast groups use a reserved address range from 224.0.0.0 through 239.255.255.255.
Whenever a Winsock function requires a TCP/IP address, it is specified by using the SOCKADDR_IN structure, which is defined as follows:
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
The sin_family parameter represents the address family, and must be set to AF_INET to use IP addressing (AF_IRDA is available for infrared, which is covered in Chapter 5). The next parameter, sin_port, specifies the communications port that you want your protocol, whether TCP or UDP, to talk on. The port typically represents a well-known service, such as HTTP or FTP. The sin_addr parameter specifies the IP address and can be either a local address or a remote address, depending on how the field is being used by the Winsock operation. The last parameter, sin_zero, is unused and serves as padding for the standard SOCKADDR structure.
Ports
As mentioned above, a TCP/IP port represents a specific communications port used by an application protocol to identify a service, such as HTTP or FTP. While you can certainly use any port for your own application, take care not to use the ones that are already defined by the Internet Assigned Numbers Authority (IANA), which are known as well-known ports (see Table 1.2).
Table 1.2. Well-Known Ports Protocol
Port
File Transfer Protocol (FTP)
21
Telnet Protocol
23
Simple Mail Transfer Protocol (SMTP)
25
Trivial File Transfer Protocol (TFTP)
69
Gopher Protocol
70
Finger Protocol
79
Hypertext Transfer Protocol (HTTP)
80
Post Office Protocol (POP3)
110
A list of up-to-date assigned port numbers is available at http://www.iana.org/assignments/port-numbers.
Ports are divided into three basic categories:
Well-known ports (0–1023): These are controlled by the IANA and are reserved for well-known ports.
Registered ports (1024–49151): These ports are listed and controlled by IANA, but they can also be used for normal applications.
Dynamic or private ports (49152–65535): The IANA does not track any applications that use ports in this range.
Therefore, for example, if you are writing an application that needs to read data from a Web server, you will use the well-known port 80. If you need to develop a custom application, it is safe for you to use any port in the range of 1024–49151.
If you are interested in more in-depth details about TCP/IP, IP addressing, and TCP/IP ports, I highly recommend that you read RFC 1180, "A TCP/IP Tutorial," and RFC 1122, "Requirements for Internet Hosts—Communication Layers."
Winsock Basics
Now that we have covered the basics of TCP/IP, let's look at actual Winsock APIs that your application will use to communicate over the network with. To use the Winsock functions in your applications, make sure you include winsock.h in your source, and that you link with the winsock.lib library.
Initializing and Cleaning Up
The first thing to do in order to use Winsock is to make sure that the correct version of winsock.dll is loaded into memory. Even though Pocket PC currently supports only a subset of Winsock version 1.1, it still needs to be initialized in the same way as its desktop counterpart before it is used. This is done by calling the WSAStartup() function, which is defined as follows:
int WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData);
The wVersionRequired parameter specifies which version of Winsock you want to load. For Pocket PC, this value needs to be version 1.1, which can be created by using the MAKEWORD(1,1) macro. The lpWSAData parameter is a pointer to a WSAData structure that WSAStartup() will fill in with information about the version of Winsock that is loaded:
typedef struct WSAData {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR *lpVendorInfo;
} WSADATA;
Both the wVersion and wHighVersion parameters will return the current version of Winsock that is actually loaded into memory, which will be 0x0101, as only 1.1 is supported. The szDescription and szSystemStatus parameters are not used on Pocket PC and are NULL. The iMaxSockets parameter indicates the recommended maximum number of sockets that an application can actually open. There is no guarantee that your application will be able to open this many sockets. The iMaxUdpDg parameter specifies the largest size of a UDP datagram packet. If this value is 0, there is no limit in this Winsock version. Finally, the lpVendorInfo parameter has a pointer to optional vendor-specific information.
WSAStartup() will return 0 if it succeeds; otherwise, it will return an error code.
Before your application closes, you should call the function WSACleanup(), which is defined as follows:
int WSACleanup(void);
However, note that this function doesn't actually do anything, and is there only to maintain compatibility with desktop applications that have been ported to Windows CE.
Winsock Errors
If an error occurs when calling a Winsock function (except WSAStartup(), which will return an error code), most functions will return the standard Winsock SOCKET_ERROR (defined in winsock.h as -1).
To obtain more information about the error that occurred, you can call the function WSAGetLastError() to find out why the function failed:
int WSAGetLastError (void);
The return value will be the error code for the last network error that occurred. If you need to set the last error for any reason (or to set it to 0), you can use the function WSASetLastError():
void WSASetLastError (int iError);
The parameter iError specifies the new error code.
You can find definitions for the individual Winsock error codes in the winsock.h header file.
Differences between Windows and Pocket PC Winsock Implementations
Besides the fact that Pocket PC supports only a subset of Winsock version 1.1, note a few other minor differences compared to the desktop implementation:
There are no asynchronous socket calls. There is no support in Pocket PC for desktop function calls such as WSAAsyncSelect() for notification of socket events.
There are no service name APIs. The desktop functions getservbyname() and getservbyport() are not supported on Pocket PC.
There are no protocol name APIs. The desktop functions getprotobyname() and getprotobynumber() are not supported on Pocket PC.
Unicode and ASCII. Although data that you send over TCP (and UDP messages) can be Unicode or ASCII (Winsock doesn't care, as long as corresponding support exists on both ends and everything is handled properly), most of the Winsock support functions support ASCII parameters only. You need to convert the Unicode strings to ASCII to use them.
Blocking versus nonblocking. Sockets are in blocking mode by default. You can set a socket to nonblocking mode by setting the correct socket option, as described in the section "Socket Options."
Several socket options are not supported. IP_MULTICAST_LOOP, SO_ACCEPTCONN, SO_RCVLOWAT, SO_SNDLOWAT, SO_SNDTIMEO, and the SO_TYPE socket options are not supported on Pocket PC.
IrDA supports only TCP stream sockets. Infrared communication via Winsock (covered in Chapter 5) supports using TCP sockets only.
Applications can drop UDP packets. The internal UDP queue buffer size is set to 2. This is a known bug and is documented in Microsoft KB article Q290206.
No raw socket support. There currently is no way to create a raw socket. However, you can use the ICMP support functions described in the section "Internet Control Message Protocol (ICMP)" to send ping data.
Finally, note that Windows CE.NET (Windows CE v4.x) now supports Winsock v2.0.
TCP/IP, ActiveSync, and Pocket PC Emulation Issues
While TCP/IP communications are a great way of enabling a Pocket PC device to network with a desktop, a server, or another Pocket PC device, accessing incoming TCP/IP connections can sometimes be a bit tricky if you are not using a network card or a wireless connection. This is typically true during development stages, when devices are often cradled to a desktop and you use the emulation environment.
Neither the emulator nor the device should have any problems establishing connections to a TCP/IP server (in essence, when the device/emulation is acting as a client or initiating a connection), but external resources will not be able to find the device when the device/emulation itself is waiting for incoming connections (i.e., it's acting as a server).
These problems are caused by the way IP addresses are assigned. The emulator uses a private IP address, and ActiveSync uses a Network Address Table (NAT) for cradled communications. Both render it unreachable from the outside world. You can, however, use the "localhost" address on the device itself to establish a connection within the device. For example, if you are writing an application to respond to HTTP requests, and the application is running in the emulation environment, you could use Pocket Internet Explorer inside the emulator to go to http://localhost to access your server.
This should not affect your device when using a dial-up connection, a wireless connection, or a network card.
To better understand how Winsock works on Pocket PC, it is important to understand how TCP/IP itself is constructed. As mentioned previously, TCP/IP stands for Transmission Control Protocol/Internet Protocol, which typically represents the Internet Protocol Suite. The TCP/IP protocol suite is actually a collection of several protocols that form the basis of Internet communications. Table 1.1 describes the TCP/IP protocol suite.
Table 1.1. The TCP/IP Protocol Suite Protocol
Usage
IP
The Internet Protocol is responsible for the addressing, routing and packetization of data. It basically moves data between computers.
TCP
The Transport Control Protocol is a reliable connection—based data transport.
UDP
The User Datagram Protocol is a connectionless-based data transport that does not guarantee packets will arrive at their destination.
ICMP
The Internet Control Message Protocol handles error and control messages.
ARP
The Address Resolution Protocol performs IP address to hardware address translation for outgoing packets.
Developing applications that use Winsock on a Pocket PC device will require you to work with and understand the various TCP/IP protocols in order to effectively use networking in your application.
A Breakdown of TCP/IP
When you look at the various parts of TCP/IP, it is typically represented in the International Standards Organization/Open Systems Interconnect (ISO/OSI) network model, which divides networks into various layers depending on individual functionality (see Figure 1.1). The combination of each layer provides what is commonly known as the TCP/IP protocol stack. Note that the data-link layer and the physical layer are not technically part of the Internet Protocol Suite; this is what enables TCP/IP to talk over a variety of network types and mediums.
Figure 1.1. The ISO/OSI model
Let's take a quick look at each of the layers:
Physical: The physical layer is essentially the network hardware. This layer talks directly to the media (wireless, twisted pair cable, and so on) that is carrying the network data.
Data Link: The data-link layer (which is sometimes also called the network driver layer) manages requests to the network interface, and transfers data between the physical and network layers. Most importantly, it also hides the network's physical implementation from the network layer. This ensures that the protocol (in our case, TCP/IP) doesn't care about the specifics of what network technology is being used (such as Ethernet versus WiFi)—it simply passes data back and forth.
Network: The network layer is the core of TCP/IP. It is responsible for the addressing, routing, and packetization of data that it has received from the transport layer over the Internet Protocol (IP), and for passing it to the data-link layer. The network layer also provides the mapping of IP addresses to hardware addresses via the Address Resolution Protocol (ARP); the processing of error and control messages through the Internet Control Message Protocol (ICMP); and the control of multicast communications through the Internet Group Membership Protocol (IGMP).
Transport: The transport layer is what applications will typically talk to when running Winsock applications, and is sometimes called a low-level connection. It provides the transfer of data between a source and destination on the network. Pocket PC supports two transfer protocols: the Transfer Control Protocol (TCP) and the User Datagram Protocol (UDP). TCP is a connection-based protocol that reliably streams data from source to destination, whereas UDP is an unreliable, connectionless protocol that uses packets to send data from point A to point B.
Session: The session layer essentially creates a connection, controls it, and then finally closes the communications session. It is also sometimes referred to as a high-level connection, and it starts the upper layer of the OSI model. A session is usually defined by a specific application protocol, such as Telnet, for establishing and maintaining a connection between a client and a server.
Presentation: The presentation layer basically handles the formatting of data that two applications will use during the course of a session. Transfer protocols, such as FTP, SMTP, and HTTP, are considered part of the presentation layer.
Application: The application layer is what interacts with the user. It is the user interface for your application.
TCP/IP Addresses
In order for a device to talk on a TCP/IP network, it needs a valid network "address." TCP/IP on Pocket PC currently supports IP version 4 (IPv4), whereby network addresses are represented as a 32-bit number, which can be broken down into a host address and a subnet address. Typically, a user can recognize an IPv4 address by what's more commonly known as dotted notation, such as 192.168.0.1.
You should be aware of the following address types:
Unicast Address: A unicast address is typically what is assigned to your device, either through a hard-coded IP address, or one that is assigned to you via a DHCP server. This is the unique address that is used to identify your device on a network.
Broadcast Address: While not recommended, if you need to broadcast data to every device on a local network, you can send data to the address 255.255.255.255. Sending data here is considered a limited broadcast because most routers will never forward packets sent to this address. A directed broadcast address can also be used to send data to all devices on a specific subnet, such as 192.168.0.x, by sending data to 192.168.0.255. In order send data packets over a broadcast address, you need to set a socket option to accept broadcast communications, as well as use the UDP protocol as described later in the section "Connectionless (UDP) Sockets."
Loopback Address: Any network address that begins with the number 127 is considered a loopback address for the device. Typically, the address 127.0.0.1 is used to communicate with itself.
Multicast Address: The best way to think of a multicast address is to think of a "group" address. When you are a part of a multicast group, all of the devices that have joined that group will receive data that is sent to the group address. Multicast groups use a reserved address range from 224.0.0.0 through 239.255.255.255.
Whenever a Winsock function requires a TCP/IP address, it is specified by using the SOCKADDR_IN structure, which is defined as follows:
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
The sin_family parameter represents the address family, and must be set to AF_INET to use IP addressing (AF_IRDA is available for infrared, which is covered in Chapter 5). The next parameter, sin_port, specifies the communications port that you want your protocol, whether TCP or UDP, to talk on. The port typically represents a well-known service, such as HTTP or FTP. The sin_addr parameter specifies the IP address and can be either a local address or a remote address, depending on how the field is being used by the Winsock operation. The last parameter, sin_zero, is unused and serves as padding for the standard SOCKADDR structure.
Ports
As mentioned above, a TCP/IP port represents a specific communications port used by an application protocol to identify a service, such as HTTP or FTP. While you can certainly use any port for your own application, take care not to use the ones that are already defined by the Internet Assigned Numbers Authority (IANA), which are known as well-known ports (see Table 1.2).
Table 1.2. Well-Known Ports Protocol
Port
File Transfer Protocol (FTP)
21
Telnet Protocol
23
Simple Mail Transfer Protocol (SMTP)
25
Trivial File Transfer Protocol (TFTP)
69
Gopher Protocol
70
Finger Protocol
79
Hypertext Transfer Protocol (HTTP)
80
Post Office Protocol (POP3)
110
A list of up-to-date assigned port numbers is available at http://www.iana.org/assignments/port-numbers.
Ports are divided into three basic categories:
Well-known ports (0–1023): These are controlled by the IANA and are reserved for well-known ports.
Registered ports (1024–49151): These ports are listed and controlled by IANA, but they can also be used for normal applications.
Dynamic or private ports (49152–65535): The IANA does not track any applications that use ports in this range.
Therefore, for example, if you are writing an application that needs to read data from a Web server, you will use the well-known port 80. If you need to develop a custom application, it is safe for you to use any port in the range of 1024–49151.
If you are interested in more in-depth details about TCP/IP, IP addressing, and TCP/IP ports, I highly recommend that you read RFC 1180, "A TCP/IP Tutorial," and RFC 1122, "Requirements for Internet Hosts—Communication Layers."
Winsock Basics
Now that we have covered the basics of TCP/IP, let's look at actual Winsock APIs that your application will use to communicate over the network with. To use the Winsock functions in your applications, make sure you include winsock.h in your source, and that you link with the winsock.lib library.
Initializing and Cleaning Up
The first thing to do in order to use Winsock is to make sure that the correct version of winsock.dll is loaded into memory. Even though Pocket PC currently supports only a subset of Winsock version 1.1, it still needs to be initialized in the same way as its desktop counterpart before it is used. This is done by calling the WSAStartup() function, which is defined as follows:
int WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData);
The wVersionRequired parameter specifies which version of Winsock you want to load. For Pocket PC, this value needs to be version 1.1, which can be created by using the MAKEWORD(1,1) macro. The lpWSAData parameter is a pointer to a WSAData structure that WSAStartup() will fill in with information about the version of Winsock that is loaded:
typedef struct WSAData {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR *lpVendorInfo;
} WSADATA;
Both the wVersion and wHighVersion parameters will return the current version of Winsock that is actually loaded into memory, which will be 0x0101, as only 1.1 is supported. The szDescription and szSystemStatus parameters are not used on Pocket PC and are NULL. The iMaxSockets parameter indicates the recommended maximum number of sockets that an application can actually open. There is no guarantee that your application will be able to open this many sockets. The iMaxUdpDg parameter specifies the largest size of a UDP datagram packet. If this value is 0, there is no limit in this Winsock version. Finally, the lpVendorInfo parameter has a pointer to optional vendor-specific information.
WSAStartup() will return 0 if it succeeds; otherwise, it will return an error code.
Before your application closes, you should call the function WSACleanup(), which is defined as follows:
int WSACleanup(void);
However, note that this function doesn't actually do anything, and is there only to maintain compatibility with desktop applications that have been ported to Windows CE.
Winsock Errors
If an error occurs when calling a Winsock function (except WSAStartup(), which will return an error code), most functions will return the standard Winsock SOCKET_ERROR (defined in winsock.h as -1).
To obtain more information about the error that occurred, you can call the function WSAGetLastError() to find out why the function failed:
int WSAGetLastError (void);
The return value will be the error code for the last network error that occurred. If you need to set the last error for any reason (or to set it to 0), you can use the function WSASetLastError():
void WSASetLastError (int iError);
The parameter iError specifies the new error code.
You can find definitions for the individual Winsock error codes in the winsock.h header file.
Differences between Windows and Pocket PC Winsock Implementations
Besides the fact that Pocket PC supports only a subset of Winsock version 1.1, note a few other minor differences compared to the desktop implementation:
There are no asynchronous socket calls. There is no support in Pocket PC for desktop function calls such as WSAAsyncSelect() for notification of socket events.
There are no service name APIs. The desktop functions getservbyname() and getservbyport() are not supported on Pocket PC.
There are no protocol name APIs. The desktop functions getprotobyname() and getprotobynumber() are not supported on Pocket PC.
Unicode and ASCII. Although data that you send over TCP (and UDP messages) can be Unicode or ASCII (Winsock doesn't care, as long as corresponding support exists on both ends and everything is handled properly), most of the Winsock support functions support ASCII parameters only. You need to convert the Unicode strings to ASCII to use them.
Blocking versus nonblocking. Sockets are in blocking mode by default. You can set a socket to nonblocking mode by setting the correct socket option, as described in the section "Socket Options."
Several socket options are not supported. IP_MULTICAST_LOOP, SO_ACCEPTCONN, SO_RCVLOWAT, SO_SNDLOWAT, SO_SNDTIMEO, and the SO_TYPE socket options are not supported on Pocket PC.
IrDA supports only TCP stream sockets. Infrared communication via Winsock (covered in Chapter 5) supports using TCP sockets only.
Applications can drop UDP packets. The internal UDP queue buffer size is set to 2. This is a known bug and is documented in Microsoft KB article Q290206.
No raw socket support. There currently is no way to create a raw socket. However, you can use the ICMP support functions described in the section "Internet Control Message Protocol (ICMP)" to send ping data.
Finally, note that Windows CE.NET (Windows CE v4.x) now supports Winsock v2.0.
TCP/IP, ActiveSync, and Pocket PC Emulation Issues
While TCP/IP communications are a great way of enabling a Pocket PC device to network with a desktop, a server, or another Pocket PC device, accessing incoming TCP/IP connections can sometimes be a bit tricky if you are not using a network card or a wireless connection. This is typically true during development stages, when devices are often cradled to a desktop and you use the emulation environment.
Neither the emulator nor the device should have any problems establishing connections to a TCP/IP server (in essence, when the device/emulation is acting as a client or initiating a connection), but external resources will not be able to find the device when the device/emulation itself is waiting for incoming connections (i.e., it's acting as a server).
These problems are caused by the way IP addresses are assigned. The emulator uses a private IP address, and ActiveSync uses a Network Address Table (NAT) for cradled communications. Both render it unreachable from the outside world. You can, however, use the "localhost" address on the device itself to establish a connection within the device. For example, if you are writing an application to respond to HTTP requests, and the application is running in the emulation environment, you could use Pocket Internet Explorer inside the emulator to go to http://localhost to access your server.
This should not affect your device when using a dial-up connection, a wireless connection, or a network card.
Subscribe to:
Comments (Atom)