Ruediger R. Asche
Microsoft Developer Network Technology Group
Created: October 21, 1994
Revised: June 1, 1995 (redesigned class definitions; incorporated information on MFC sockets)
Click to open or copy the files in the CommChat sample application for this technical article.
This is Part 4 of the five-part trilogy "The Hitchhiker's Guide Through the Network Jungle," following "Communication with Class," "Garden Hoses at Work," and "Power Outlets in Action: Windows Sockets." This article describes the NetBIOS interface and explains how it can be encapsulated in a generic C++ networking interface.
This article is fourth in a series of technical articles that explore network programming with Visual C++™ and the Microsoft® Foundation Class Library (MFC). The series consists of the following articles:
"Communication with Class" (introduction and description of the CommChat sample)
"Garden Hoses at Work" (named pipes)
"Power Outlets in Action: Windows Sockets" (Microsoft Windows® sockets)
"Aristocratic Communication: NetBIOS" (NetBIOS)
"Plugs and Jacks: Network Interfaces Compared" (summary)
The CommChat sample application illustrates the concepts and implements the C++ objects discussed in these articles.
So far, we have looked at named pipes and sockets as application programming interface (API) sets that applications written for Win32® can utilize to transfer data back and forth between computers. Like named pipes and sockets, NetBIOS is a network interface; that is, a set of functions that applications can use to access networks.
It is important to make the distinction between interfaces and protocols: Named pipes, sockets, and NetBIOS (as well as NetDDE®, which will not be discussed in this article series) are interfaces that provide a means for applications to access networks; these interfaces do not define how the data is actually transferred over the network.
In contrast, the modules that are responsible for transferring data over a network implement transport protocols. As explained in the Windows NT™ Resource Kit, a transport protocol "packages data that is to be sent on the network in a way that the computer on the receiving end can understand." A number of different protocols are currently available—for example, TCP/IP, UDP/IP, XNS, NetBEUI, and IPX. I may discuss these protocols in upcoming articles, but I would like to emphasize that the network interfaces relieve you from having to worry about the details of the underlying protocol. In some interface implementations, it is possible to influence parameters that are specific to certain protocols; for example, the setsockopt socket function has a number of TCP/IP-specific parameters. In general, however, an interface can be implemented on top of any protocol installed on the two computers that wish to communicate. See the "Plugs and Jacks: Network Interfaces Compared" article for more information on how to associate protocols with interfaces.
This article describes the NetBIOS interface and explains how a CNetBIOS class can be implemented. Note that NetBIOS, unlike other network interfaces, is somewhat more than an interface because it imposes several restrictions on the underlying transport protocol. We will come back to that later.
NetBIOS is a specification for an interface that was designed by IBM. From the point of view of an application programmer, the most notable difference between NetBIOS and other interfaces (such as named pipes) seems to be that NetBIOS commands are not submitted to the operating system through a set of functions, but instead by passing the address of a data structure filled in by the application to the operating system. This structure is called a network control block (NCB) and encodes the command as well as command parameters.
However, it does not really matter what form the commands are passed in. The reason why NetBIOS works via an NCB instead of a functional interface is pretty much pure convenience. The first implementations of NetBIOS required the application to pass the commands to the operating system as a software interrupt; thus, the representation of the function call in an NCB to be passed to the software interrupt made sense.
Of course, the functionality of an interface is totally independent of how it is made available to applications. I have defined an "intermediate" C++ class for the NetBIOS interface that hides the NCB from the application programmer. That class is called CNCB, its prototype can be found in CNCB.H, and the implementation is in CNCB.CPP. This is what CNCB looks like:
class CNCB
{
private:
NCB m_NCB;
public:
// Constructor
CNCB();
// Helper function
void ClearNCB();
UCHAR GetLSN();
WORD GetLength();
void Fill(CNCB ncbSource);
void GetCommand();
// Name management services
UCHAR AddName(PSTR pName);
UCHAR AddGroupName(PSTR pName);
UCHAR DeleteName(PSTR pName);
UCHAR FindName();
// Data transfer services
UCHAR Call(PSTR pWe,PSTR pTheOther,UCHAR wSendTO,UCHAR wRecvTO);
UCHAR Listen(PSTR pWe,PSTR pTheOther,UCHAR wSendTO,UCHAR wRecvTO);
UCHAR Hangup(UCHAR wSessionNumber);
// Connectionless data transfer
UCHAR Cancel();
UCHAR Send(UCHAR wSessionNumber,LPSTR lpPacket, UINT wLength);
UCHAR SendNoAck();
UCHAR SendDatagram(UCHAR wSessionNumber,LPSTR lpPacket, WORD wLength);
UCHAR SendBroadcastDatagram();
UCHAR Receive(UCHAR wSessionNumber,LPSTR lpPacket, UINT wLength);
UCHAR ReceiveAny();
UCHAR ReceiveDatagram(UCHAR wSessionNumber,LPSTR lpPacket, WORD wLength);
UCHAR ReceiveBroadcastDatagram();
UCHAR ChainSend();
UCHAR ChainSendNoAck();
// General-purpose services
UCHAR Reset(UCHAR wSessions, UCHAR wNCBs);
UCHAR GetAdapterStatus(PSTR pName);
UCHAR GetSessionStatus(PSTR pName);
UCHAR EnumerateAdapters();
UCHAR StatusAlert();
UCHAR Action();
};
The NCB (whose structure is defined in the NB30.H header file that is provided with Visual C++™ version 2.0) is a private member, and all functions that can be called on an NCB are provided as public member functions. Let us look at a typical implementation of one of those functions:
UCHAR CNCB::Listen(PSTR pWe,PSTR pTheOther,WORD wSendTO,WORD wRecvTO)
{ClearNCB();
strncpy((char *)&m_NCB.ncb_name,pWe,MAXMACHINENAME);
strncpy((char *)&m_NCB.ncb_callname,pTheOther,MAXMACHINENAME);
m_NCB.ncb_rto = (UCHAR)wRecvTO;
m_NCB.ncb_sto = (UCHAR)wSendTO;
m_NCB.ncb_command = NCBLISTEN;
return (Netbios(&m_NCB));
};
All this function does is clear its private NCB (that is, fill it up with zeroes), fill in the appropriate parameters (note, in particular, the function code in the ncb_command member), and then call the Netbios system function.
Going through this intermediate C++ API layer allows us to focus on the important parameters to the individual services without worrying about NCBs.
The NetBIOS function set is described fairly well in the article "An Introduction to Network Programming Using the NetBIOS Interface," by Alok Sinha and Raymond Patch, in the March/April 1992 issue of the Microsoft Systems Journal (MSDN Library Archive, Books and Periodicals, Microsoft Systems Journal, Selections from Previous Issues). In this section, I will focus on the differences between NetBIOS and the other interfaces we have seen so far (named pipes and sockets).
All network interfaces deal with a common set of issues. I will talk about those issues and how they are resolved by the different interfaces in the article "Plugs and Jacks: Network Interfaces Compared," but let me provide a quick preview here. The first issue is name resolution: How does a process address another machine? Second, if the first problem is solved, how can a machine concurrently serve different communications? The third issue involves asynchronous vs. synchronous communications. Finally, some interfaces provide both connection-oriented and "connectionless" communication (allowing two machines to send data back and forth without explicitly establishing a communication).
How does NetBIOS address the problem of name resolution? NetBIOS, in a way, is the most dynamic of the three interfaces we have discussed so far in that it allows a process to assign itself a name by which it can be addressed. A central concept in the NetBIOS model is that of a name table: Each computer that has NetBIOS support keeps a table of names under which different applications can address the computer.
A good way to get a grip on the name table concept is to call the NetBIOS GetAdapterStatus function. This function, among other things, enumerates the name table entries. Here is a typical name table dump from my development machine, with the names changed to protect the innocent:
[EBP-015C]-ADAPTER_STATUS_BLOCK asStatus = {...}
-_ADAPTER_STATUS asb_header = {...}
+unsigned char adapter_address[6] = 0x0012f99c ""
unsigned char rev_major = 3 '\x03'
unsigned char reserved0 = 0 '\x00'
unsigned char adapter_type = 254 'þ'
unsigned char rev_minor = 0 '\x00'
unsigned short duration = 0
unsigned short frmr_recv = 0
unsigned short frmr_xmit = 0
unsigned short iframe_recv_err = 0
unsigned short xmit_aborts = 0
unsigned long xmit_success = 0
unsigned long recv_success = 0
unsigned short iframe_xmit_err = 0
unsigned short recv_buff_unavail = 0
unsigned short t1_timeouts = 0
unsigned short ti_timeouts = 0
unsigned long reserved1 = 0
unsigned short free_ncbs = 255
unsigned short max_cfg_ncbs = 255
unsigned short max_ncbs = 255
unsigned short xmit_buf_unavail = 0
unsigned short max_dgram_size = 0
unsigned short pending_sess = 0
unsigned short max_cfg_sess = 16
unsigned short max_sess = 16
unsigned short max_sess_pkt_size = 0
unsigned short name_count = 1
-_NAME_BUFFER asb_Names[16] = 0x0012f9d8
-_NAME_BUFFER [0] = {...}
+unsigned char name[16] = 0x0012f9d8 "BEAKER001! "
unsigned char name_num = 2 '\x02'
unsigned char name_flags = 4 '\x04'
-_NAME_BUFFER [1] = {...}
+unsigned char name[16] = 0x0012f9ea "BEAKER001 \x1F\x01"
unsigned char name_num = 1 '\x01'
unsigned char name_flags = 0 '\x00'
-_NAME_BUFFER [2] = {...}
+unsigned char name[16] = 0x0012f9fc "BEAKER001 \x02"
unsigned char name_num = 2 '\x02'
unsigned char name_flags = 0 '\x00'
-_NAME_BUFFER [3] = {...}
+unsigned char name[16] = 0x0012fa0e "BEAKER001 "
unsigned char name_num = 3 '\x03'
unsigned char name_flags = 0 '\x00'
-_NAME_BUFFER [4] = {...}
+unsigned char name[16] = 0x0012fa20 "DOMAIN1 "
unsigned char name_num = 4 '\x04'
unsigned char name_flags = 128 '\x80'
-_NAME_BUFFER [5] = {...}
+unsigned char name[16] = 0x0012fa32 "BEAKER001 \x03\x05"
unsigned char name_num = 5 '\x05'
unsigned char name_flags = 0 '\x00'
-_NAME_BUFFER [6] = {...}
+unsigned char name[16] = 0x0012fa44 "DOMAIN1 \x1E\x06\x80BEAKER001 \x03\a"
unsigned char name_num = 6 '\x06'
unsigned char name_flags = 128 '\x80'
-_NAME_BUFFER [7] = {...}
+unsigned char name[16] = 0x0012fa56 "BEAKER001 \x03\a"
unsigned char name_num = 7 '\a'
unsigned char name_flags = 0 '\x00'
-_NAME_BUFFER [8] = {...}
+unsigned char name[16] = 0x0012fa68 "BEAKER001! "
unsigned char name_num = 8 '\b'
unsigned char name_flags = 0 '\x00'
-_NAME_BUFFER [9] = {...}
+unsigned char name[16] = 0x0012fa7a ""
unsigned char name_num = 0 '\x00'
unsigned char name_flags = 0 '\x00'
-_NAME_BUFFER [10] = {...}
+unsigned char name[16] = 0x0012fa8c ""
unsigned char name_num = 0 '\x00'
unsigned char name_flags = 0 '\x00'
-_NAME_BUFFER [11] = {...}
+unsigned char name[16] = 0x0012fa9e ""
unsigned char name_num = 0 '\x00'
unsigned char name_flags = 0 '\x00'
-_NAME_BUFFER [12] = {...}
+unsigned char name[16] = 0x0012fab0 ""
unsigned char name_num = 0 '\x00'
unsigned char name_flags = 0 '\x00'
-_NAME_BUFFER [13] = {...}
+unsigned char name[16] = 0x0012fac2 ""
unsigned char name_num = 0 '\x00'
unsigned char name_flags = 0 '\x00'
-_NAME_BUFFER [14] = {...}
+unsigned char name[16] = 0x0012fad4 ""
unsigned char name_num = 0 '\x00'
unsigned char name_flags = 0 '\x00'
-_NAME_BUFFER [15] = {...}
+unsigned char name[16] = 0x0012fae6 ""
unsigned char name_num = 0 '\x00'
unsigned char name_flags = 0 '\x00'
In case you are interested in how I produced this name table dump: I added a GetAdapterStatus call to the code in NetBIOS.CPP, set a breakpoint to that line, traced until I hit the call to Netbios in CNCB.CPP, and after one more single step, copied and pasted the ADAPTER_STATUS_BLOCK variable from the locals window to another document. Slick, huh?
Anyway, we see here that the name table has nine entries, seven of which begin with the suspicious prefix BEAKER001. The other two entries are variations of the domain name (DOMAIN1) assigned to the machine. Two kinds of names can exist in a name table: unique names and group names. Unique names are verified over the entire network. If an application tries to add a unique name that is already registered somewhere on the network, the AddName call fails. Group names can exist in the name tables of multiple machines. They allow a machine to broadcast a message to all machines in the DOMAIN1 domain by simply broadcasting the message all over the net; a machine that has the appropriate entry in its name table can then pick up the message.
This is a very powerful feature of NetBIOS. It allows for fairly elaborate (possibly hierarchical or multi-level) implementations of domain-type structures. An implementation of LAN administration software can utilize the name table so that each machine registers a set of domain names and aliases that can be used by other pieces of the software to group machines logically.
A name table is a little more complicated than a simple table of names. We see that the BEAKER001 entry shows up a number of times. An entry in the NetBIOS name table consists of two parts: the name itself (a string that does not contain a wildcard character ["*"] and does not exceed 15 characters in length) and a one-byte service identifier. The service identifier distinguishes the name among several network services. For example, the machine name BEAKER001 could be used in a Microsoft LAN Manager environment to identify both a machine that can share logical directories for other machines to attach to, and an end point for a NetDDE conversation. One rule of NetBIOS addressing is that each component that wants to add an entry to a machine's name table must specify a unique name. Another rule is that no process can initiate a conversation without first adding a unique name explicitly to the name table (although in some implementations of NetBIOS, separate processes can share entries in the name table).
Note that this particular usage of NetBIOS names is implementation-dependent—other implementations may use name tables differently. The important thing to keep in mind is that NetBIOS requires names to be unique; otherwise, the AddName call on a machine name will fail.
This naming convention gets applications written for the NetBIOS interface into a certain kind of trouble. For example, I would like to have CommChat address a remote machine by its logical name assigned at startup time. However, a machine cannot simply add the logical name to the name table because entries for that name already exist in the table, as you can see in the dump above.
To work around this problem, the 16th character of the name is reserved to discriminate between different modules (for example, the NetDDE service, RAS, and the network redirector) that share the name. You will note that the name table entries for BEAKER001 in our name table dump have different 16th characters.
In CommChat, I do not use a unique value for the 16th character, but I use a similar approach: The name that a machine registers is the computer name (obtained with the GetComputerName function) followed by an exclamation point and padded with blanks. This approach cuts down the number of available characters in a machine name from 15 to 14, but it allows for unique names that do not interfere with present or future services sharing the machine name (unless a machine with the same name followed by the exclamation point is already on the net).
Now we know quite a lot about NetBIOS names. There is more, though. The network software still must know how to translate a logical name into a physical address and vice versa. However, what if a given machine has multiple adapters or several transport protocols that share the same adapter?
NetBIOS addresses this question by keeping a distinct name table for each adapter installed on a computer. The lana_number field in the NCB selects the adapter and the protocol used for transmission. Using distinct names in the name tables of adapters allows a machine to explicitly select specific adapters and/or protocols.
CommChat uses only the default adapter; therefore, lana_number is always set to 0 (the first adapter). A more flexible implementation of the CNCB class would require a lana_number to be passed to each NetBIOS command.
CommChat has a shortcoming with respect to multiple transport protocols and adapters: Because the server is set up to listen only on LANA_NUM 0, clients can establish communications with the server only if (a) their respective client code is bound to the same protocol as the server, and (b) the server's name has been entered into all the name tables on the server side. Using a new NetBIOS service in NetBIOS version 3.0 (NCB.Enum) would enable a server to register its name and listen on all installed adapters, and a client to enumerate the adapters on the server side to look for a specific name on all of the server's LAN addresses. Note that not all implementations of NetBIOS support the Enum service.
It would be easy to change CommChat to generate unique names for each machine. For example, when a machine starts an instance of CommChat, it could try to add a unique name (say, COMMCHAT0) to the name table. If the call to add the name fails due to a multiple name error, the machine could try COMMCHAT1, COMMCHAT2, and so on until it finds a unique address. The drawback of such an approach would be that a CommChat user would not necessarily know the names associated with other users. The naming scheme of NetBIOS, as you can see, is very flexible—if we were to implement the naming scheme I just suggested using, say, sockets, we would need to provide the software that translates those "custom" names into machine names or IP addresses.
A group of NetBIOS functions deals with name table management services. These functions let you manipulate the name table: add names, delete names, and query the name table.
Now that we've solved the first problem (addressing machines), the next problem is how to sort out multiple communications on the same machine.
Using NetBIOS, applications can distinguish between several concurrent communications through names, as we discussed earlier. A machine running NetBIOS can register many different names for remote machines to connect with. As we discussed in the previous section, you can even "overload" a name by adding a unique byte in the 16th character of the name field or by internally changing the name to make it unique (as CommChat does).
Like named pipes and sockets, NetBIOS allows several instances of the same communication type to be active: If two machines want to communicate with each other, one of them (the server) must have an outstanding listen command (this corresponds to the AwaitCommunicationAttempt member in the CCommunication class), and the other machine (the client) must submit a call command. When a communication is established, a local session number (LSN) is returned to each machine. The server can now go back into the listening state and accept another communication. When a third machine establishes a communication with the machine that has gone back to listening, new LSNs are returned to the server and the third machine.
Note once again that the call and listen commands are directed; that is, both specify the addresses of the communicating machines. NetBIOS offers a very high degree of control over the details of the communication because you can use the wildcard address "*" or any name registered in any name table on one of the participating machines, and you can select specific transport protocols.
NetBIOS calls can be submitted in asynchronous mode, where an asynchronous callback routine that is invoked as soon as the asynchronous command completes is supplied to the NCB. Asynchronous NetBIOS commands were a big issue under 16-bit Windows—although the non-preemptive nature of the operating system practically enforced the usage of asynchronous over synchronous commands, it also provided a number of challenges concerning the callback (for example, an asynchronous callback routine had to be explicitly page-locked in memory).
In the "Plugs and Jacks: Network Interfaces Compared" article, I will discuss the pros and cons of asynchronous vs. synchronous communications.
In addition to the standard Send and Receive calls, NetBIOS allows applications to transfer data over the network without having previously established a connection. This feature (called connectionless communication) is useful only for small amounts of data sent in a "one-shot" fashion and for messages delivered to a group of machines (namely, to machines that have registered the same group name). Connectionless communication employs the following datagram functions: Send Datagram, Receive Datagram, Send Broadcast Datagram, and Receive Broadcast Datagram. It is important to note that none of these commands is guaranteed to transmit the data correctly.
I already mentioned a number of things that we need to know to implement the CNetBIOS object. Most member functions can be mapped fairly easily to the NetBIOS API.
Opening a communication object is surprisingly easy. This is because NetBIOS does not require a communication object to be created explicitly—as soon as a NetBIOS name is registered with the NetBIOS library, the object is ready to accept connections. The most complicated piece of code is the one-shot initialization that resides in the Open code so that a possible failure of the AddName call can be propagated to the application. The code for CNetBIOS::Open is shown below.
Note The code below, like the sockets code, is a bit clumsy: We keep track of the number of objects created to make sure that only one one-shot initialization takes place. The code was originally designed for use with Visual C++ version 1.1, which did not support AFX classes in dynamic-link libraries (DLLs). Ideally, the class definitions for CCommunication and its derived classes would go in a DLL, and the one-shot initialization would take place in the DLL's entry function.
Another slip in the code is that the Reset function should be called only once per process. In an application that dynamically creates and deletes many CNetBIOS objects, it is possible for the Reset service to be called more than once. This problem would also be fixed implicitly in a DLL implementation of CNetBIOS.
BOOL CClientNetBIOS::Open(const char* pszFileName, UINT nOpenFlags,
CFileException* pError)
{
// first of all, register the new NetBIOS name and fail for good if this doesn't work
m_iStatusPending = STATUS_NOT_CONNECTED;
if (!bAddNameWorked) return FALSE;
if (iNBObjectCount == 0)
// the first object to be created initializes the machine name string...
// this way, we won't have to do it every time...
{
unsigned long iMachineNameLength;
GetComputerName(achLocalMachineName,&iMachineNameLength);
MakeNetBIOSName((char *)0,achLocalMachineName);
CNCB ncbNameAdder;
ncbNameAdder.Reset(0,0);
UINT uReturn = ncbNameAdder.AddName(achLocalMachineName);
if ((uReturn != NRC_GOODRET)) // && (uReturn != NRC_DUPNAME))
{
bAddNameWorked = FALSE;
return FALSE;
};
};
iNBObjectCount++;
if (!pszFileName)
{ // we are server
// nothing to do here, believe it or not!!! :-)
// the name table already contains the entry for our machine...
}
else
{
int iLen = strlen(pszFileName);
MakeNetBIOSName((char *)pszFileName,m_pszPaddedFileName);
m_thisNCB.Call(achLocalMachineName,m_pszPaddedFileName,(UCHAR)0,(UCHAR)0);
m_lsn = m_thisNCB.GetLSN();
};
return TRUE;
};
Once again, this particular way of initializing the library is not multithreading-safe and works with CommChat only because CommChat does not open and close CCommunication objects from separate threads. Before we look at the "real" initialization code, let's examine the corresponding implementation of CNetBIOS::Close:
void CNetBIOS::Close(void)
{
m_thisNCB.Hangup(m_lsn);
// This code is invoked to clean up after the last object...
iNBObjectCount--;
if (!iNBObjectCount)
{
CNCB ncbNameDeleter;
ncbNameDeleter.DeleteName(achLocalMachineName);
};
};
This code suffers from one very ugly shortcoming, which is (once again) related to the problem of several processes not being able to share information easily: The iNBObjectCount variable only counts how many CNCB objects are created and deleted in one process. If a NetBIOS client and server reside on the same machine, both the client and server process keep independent counts of iNBObjectCount. The second process that tries to register the same NetBIOS name will not succeed when it calls AddName, because the NetBIOS name cannot be registered twice on the same machine. However, you could change the CNetBIOS::Open code to succeed if the NetBIOS name has already been registered—you would simply uncomment the line:
&& (uReturn != NRC_DUPNAME))
from the code in CNetBIOS::Open. The problem comes into play in the CNetBIOS::Close call, because there is no way for a process to know if another process is still using the same NetBIOS name. Since CommChat was not designed to support communications between processes on the same machine, I have not addressed the issue of correct "global" initialization and cleanup. Be aware, though, that if your application needs to register the same NetBIOS name for several processes, you might have to interact with the registry, use shared kernel objects, or define shared data in a DLL to keep track of how many processes utilize a NetBIOS name.
Now, what is really involved in opening an object for communication, leaving the name registration aside? Surprisingly enough, the code needed for the server is zero—as soon as the name is registered, NetBIOS can establish a connection without any further work. Note that if we want to set any parameters (such as time-out values), we should probably do this at Open time.
The heart of connection-oriented communication under NetBIOS consists of the Call and Listen functions. The client, when attempting to open a communication, submits a Call call, passing the local machine name as the first parameter and the target machine name as the second parameter. This is called a directed call. The client could also pass a wildcard to the Call function so that any machine that is ready to accept calls can respond.
In the AwaitCommunicationAttempt code, the server has previously submitted a listen NetBIOS call, passing the wildcard string "*" as the name of the target machine (this means that the server is accepting a call from any machine). Both calls will block until a communication is established, as discussed earlier. Note that there is an option to initiate a communication in a nonblocking mode; however, as I discussed before, CommChat does not have to worry about nonblocking calls because it runs as a multithreaded application.
The implementatrion of CancelCommunicationAttempt is worth elaboration. This function maps to the NetBIOS NCBCANCEL function, but a cancel operation on an NCB is fairly generic. A number of NetBIOS calls can be canceled, so how does NetBIOS know which call to cancel? We must pass the address of the NCB to be canceled in the ncb_buffer member of another NCB that contains NCBCANCEL in the ncb_command member. In other words, an NCB that cancels another NCB must contain the address of the NCBs to be canceled in its buffer member. Before I show you the code of CNCB::Cancel to illustrate this process, let me point out that there is a slight difficulty in designing the CNCB class to support a generic cancel command: The code for CNCB::Cancel can cancel only the very last command that was submitted in the m_NCB member variable, because m_NCB gets reused every time a command is issued. Keeping track of every NCB to cancel would require creating a new instance of the CNCB class for every command and deleting that instance only when the request has been completed or canceled Note that there is no need to delete the "helper" CNCB explicitly because it is allocated as an automatic variable and therefore constructed and destroyed automatically.
UCHAR CNCB::Cancel()
{
CNCB cbCanceller;
cbCanceller.ClearNCB();
cbCanceller.m_NCB.ncb_buffer = (unsigned char *)&m_NCB;
cbCanceller.m_NCB.ncb_length = sizeof(NCB);
cbCanceller.m_NCB.ncb_command = NCBCANCEL;
return (Netbios(&cbCanceller.m_NCB));
};
Once a communication is established between two machines, the NetBIOS commands Send and Receive can be used to transfer data back and forth. When you look at the interface that CNCB exports, you will find several flavors of the send and receive functions. In this discussion, we will focus only on the "bare bones" Send and Receive commands.
Theoretically, CNetBIOS::Read and CNetBIOS::Write are easy to implement, as you can see below:
void CNetBIOS::Write(const void FAR* pBuf, UINT iCount)
{
m_thisNCB.Send(m_lsn,(char *)pBuf,iCount);
};
UINT CNetBIOS::Read(void FAR* lpBuf, UINT nCount)
{
if (m_thisNCB.Receive(m_lsn,(char *)lpBuf,nCount) != NRC_GOODRET)
return 0;
return (UINT)m_thisNCB.GetLength();
};
The problem, as usual, is to get the details right. NetBIOS limits data transmissions to 64K (the identifier that specifies the buffer length for both send and receive calls is of type WORD, which is 16 bits, thereby limiting the buffer to 64K). As a result, I found that chat communications worked just fine, but larger file transfers plainly did not work.
Fortunately, I designed CommChat in such a way that the file transfer protocol itself is able to handle problems with large data transfers. If a Read operation returns fewer characters than requested, a second Read is initiated with the remaining number of characters until all data is received. Unfortunately, CFile::Write (recall that CCommunication is derived from CFile) does not return a value, so it is not easy to figure out whether a write operation was successful. In CommChat, I solved this problem through structured exception handling: If a write operation fails, an exception is raised, which causes the protocol to retry the write operation with a smaller buffer. (See "Communication with Class" for details.)
Thus, the code in CNCB::Send and CNCB::Receive must do a little more than simply filling in the NCB and submitting the NetBIOS call. The code also checks the size of the buffer passed in against the size of a WORD. (I know it is ugly to hard-code the size of a WORD, but I haven't been able to find a nice, platform-independent way to determine the largest numeric value that fits into a variable of an arbitrary type.) If the buffer size is larger than the WORD size, the code trims it to a WORD (in the case of Receive) or raises an exception that is caught by the protocol (in the case of Send). Here is the code:
UCHAR CNCB::Send(WORD wSessionNumber,LPSTR lpPacket, UINT wLength)
{ClearNCB();
if (wLength > 0xffff) RaiseException(EXCEPTION_ACCESS_VIOLATION,0,0,NULL);
m_NCB.ncb_command = NCBSEND;
m_NCB.ncb_lsn = wSessionNumber;
m_NCB.ncb_length = wLength;
m_NCB.ncb_buffer = (unsigned char *)lpPacket;
return (Netbios(&m_NCB));
};
UCHAR CNCB::Receive(WORD wSessionNumber,LPSTR lpPacket, UINT wLength)
{ClearNCB();
m_NCB.ncb_command = NCBRECV;
m_NCB.ncb_lsn = wSessionNumber;
if (wLength > 0xffff) m_NCB.ncb_length = 0xffff; else
m_NCB.ncb_length = wLength;
m_NCB.ncb_buffer = (unsigned char *)lpPacket;
return (Netbios(&m_NCB));
};
As with sockets, I was very careful to follow good modularity practices and design the CNetBIOS class in a way that wouldn't affect the existing application architecture of CommChat. I changed only the COMMCHAT.CPP file to accommodate the new NetBIOS communication type.
That is the party line. If you compared this version of COMMCHAT.CPP to the version provided in the October edition of the MSDN Library, you would find that almost all changes affect menu handling and object assignment only (for example, the new file includes options for creating objects of type CNetBIOS and CSocket instead of only CNamedPipe). However, in the small print, you will notice that I had to modify a little bit more than that.
When I added NetBIOS support, I was fairly surprised to see that the chat communication worked fine, whereas a file transfer communication failed as soon as CommChat tried to access the file mapping that contained the file to be transferred. To make a long story short, in my original design of CommChat, the file to be transferred and its corresponding file mapping were both opened in read-only mode. Because NetBIOS internally probes the buffer that is passed to the NetBIOS Send command in read/write mode, the read-only option results in an access violation that causes NetBIOS to fail. When I changed the file mapping access to read/write, everything was fine again.
Using the intermediate class definition of CNCB provided for a straightforward implementation of CNetBIOS, although a number of detail issues (such as the 64K limit of data transfers) caused some grief. So far, the class definition for CCommunication has proven to be generic enough to abstract away the differences between the three interfaces and serve most multithreaded communication applications.
The last article in this series will compare the three interfaces we have discussed so far: named pipes, sockets, and NetBIOS.
Sinha, Alok, and Raymond Patch. "An Introduction to Network Programming Using the NetBIOS Interface." Microsoft Systems Journal 7 (March/April 1992): 61:80. (MSDN Library Archive, Books and Periodicals, Selections from Previous Issues)
Sinha, Alok, and Raymond Patch. "Developing Network-Aware Programs Using Windows 3.1 and NetBIOS." Microsoft Systems Journal 7 (July/August 1992): 61:80.