NetBIOS—最基础的网络API

传统的网络接口NetBIOS,他和Winsock类似,是一种与协议无关的网络API。

重定向器提供了与传输无关的文件I/O方式。

邮槽是一种简单的接口,可实现Windows机器之间的广播和单向数据通信。

命名管道可以建议一个双向管道,这种管道可以提供对Windows安全通信的支持。

一般的Windows平台和大部分应用程序都依赖于NetBIOS,特殊的有WindowsCE,它仅支持NetBIOS的名字解析。

Win32 NetBIOS接口类似DX,支持向后兼容。即,即使用户使用最新版本的NetBIOS,它也会支持早期版本NetBIOS的所有API接口。

OSI网络模型

“开放系统互连”OSI模型从高层次对网络系统进行了描述,将网络系统分为了7层。具体的我已在另一日志上做有说明,不再重复,NetBIOS主要是在会话层和传输层发挥作用。进行控制两主机间的通信,数据传输服务。

NetBIOS API的一大特点是协议无关性,也就意味着一个优秀的NetBIOS程序在大部分协议上都可以运行,然而它依旧有一个要求,进行通信的两台计算机必须存在一个相同的协议,否则依旧是无法运行的。

LANA编号

LAN配适器(LAN adapter)编号的意义是将传输协议与NetBIOS建立对应关系。LANA编号是对应网卡与传输协议的唯一组合。假设我们在某工作站上装了两张网卡,并且安装了两套协议,那么他就有四个LABA编号。

其中LANA0就是默认的LANA。其他的LANA编号是随机的,而且因为“即插即用”的关系,Win95.98中LANA编号是不可更改的,但是在WinNT中,系统进行了优化,允许我们对编号进行控制。

此时我们可以想象一件事,假若我们建立了一个NetBIOS应用程序,仅实现对LANA1的监听,那么假若本机LANA1和对方LANA1对应的协议不同的话,那么就无法通信,所以,健壮的NetBIOS应用应当使用全面的LANA监听,只要这两台计算机之间有一个相同的协议,立刻就允许通信。

NetBIOS名字

它包括五部分:“唯一名”(ID),“域组名”(IC),普通组名(IE),Internet组名(20),第16字符标识(MSBROWSE)

其实Micosft网络中的机器名就是NetBIOS名字中的“唯一名”,它不允许在网络上有重复,当用户登陆时,WINS服务器就会向所有登陆用户发送消息,查询是否有名称重复,则没有计算机进行回复,则允许使用该名字。NetBIOS名字中的“组名”是用来群发消息的,它不需要唯一性。

只要安装了TCP/IP协议的计算机,均可使用nbtstat命令行对本机和其他网络上注册的计算机进行检测获取其NetBios名称。其中的第16字符标识是用来区别WINS服务的类型的,是工作站?还是服务器?监视器……等。

NetBIOS特性

NetBIOS同时支持“面向连接”服务和“无连接”服务。

“面向连接”的服务,是允许两个客户机间建立一个对话,它是双向的,允许双方互相传送消息。它可以保证数据包的可靠性,但会浪费一些性能用来建立连接。

“无连接”的服务或是数据报服务,都是无需建立好连接的,仅仅是客户机将数据根据NetBIOS名字进行发包,对方是否能够接收到,都是不存在任何保障的,但是性能比较高,无需要消耗来建立连接。

重定向器

我们假如想访问网络上的一个文件,对它进行读写工作,那么我们需要通过一个将自己的I/O请求发送到网络,让网络中的某一物件将请求转发到远程设备,建立访问。而这个负责中转请求的就是“I/O重定向器”。

但是我们的重定向器是如何将普通的I/O请求“重定向”到远程设备的呢?通过UNC地址来引用远程文件的。

我们来理解下UCN路径,实际上它就是网络文件中对设备访问的一个统一规范。它的格式如下

\服务器IP \ 共享名 \ 文件名

我们可能会有疑问,为什么没见文件路径呢?就象E:\Mp3\Pop这样的文件路径在UCN中是不存在的,它已经将这一长串的路径简化为一个网络间的“共享名”。

邮槽

它是将客户机消息传送或广播给其他的服务器的一种通信媒介,他可以进行信息传输,甚至是组群的信息传输,但是由于是基于广播通信体系设计出来的,所以,它是单向的,所以也没有所谓的安全通信。

邮槽的通信是倚赖于重定向器的,在Win95,98中,消息是完全依靠“数据报”的形式发送的,而在Win NT,2000中这一规则改变了,当一个信息长度长于424字节时,则会要求建立一个“面向连接的”SMB会话。所以假如我们发送一个大于424字节的消息的话,Win NT,2000会将424字节之后的信息全部丢弃。所以,当我们需要进行比较长的信息交互的话,则可使用“面向连接的”命名管道传输。

当我们建立邮槽的应用程序时,需要包含Winbase.h头文件,之后建立与Kernel32.lib库的连接。

因为是单向设计,所以若我们在设计邮槽时,服务器方面和客户端方面所负责的方面是不同的。

服务器方面需要负责信息的接受读取,不必写入。

  1. CreateMailslot创建邮槽,传出句柄。
  2. ReadFile,使用该邮槽从客户机接收数据。
  3. CloseHandle释放邮槽句柄,关闭邮槽。

客户端则仅负责信息的写入。也是先创建邮槽,之后WriteFile进行写入,之后CloseHandle关闭。

平时邮槽服务器还可以添加以下两个函数,GetMailslotInfo来对数据进行轮询,动态的对信息缓冲区大小进行动态设置。

SerMailslotInfo则可以设置一个邮槽的超时值,根据这个时间可以防止读邮槽时出现的“凝结”状态。(此状态是由于读邮槽时,若服务器突然终止运行,邮槽被无限挂起)

命名管线(Named Pipes)

它是一种简单的进程间通信(IPC)机制,与邮槽不同的是,它建立了计算机之间的连接,并且支持可靠的,双向的数据通信。

命名管线是围绕Windows文件系统设计的一种机制,采用“命名管道文件系统NamedPipeFileSystem”(NPFS)接口创建,所以Client和Server端都可采用Win32文件系统API(ReadFile,WriteFile)来进行数据收发。

它与邮槽一样依赖于MSNP重定向器,用MSNP来进行命名管道的发送和接收。如此一来,NPFS也拥有了协议无关特性,而它也是通用命名规范UNC来进行标识的。

命名管道进行基本通信的模式有两种:字节模式和消息模式。

其中字节模式,是将消息做为一个连续的字节流模式,长期的在CS之间流动。

而消息模式,则是将消息做为一个单位包发出,每次在管道中发出一条消息,它就必须做为一个绝对完整的消息进行读取。

与邮槽一样,在使用命名管道编程的话,需要包含Winbase.h头文件,之后建立与Kernel32.lib库的连接。

而命名管道中信息是可以双向传递的,此时要区别客户端和服务器端,就看哪个负责创建命名管道。命名管道仅可以由服务器创建,而客户机仅能和已经拥有命名管道的服务器进行连接。但是值得注意的是,命名管道仅可以在Win2000,NT上创建,WIN95,98上无法创建命名管道的。但Win98,95可做为客户端与Win2000服务器正常NPFS连接。

命名管道的服务器负责
  1. CreateNamedPipe创建一个命名管道实例,获得句柄
  2. ConnectNamedPipe监听连接请求
  3. ReadFile,WriteFile接收数据,发送数据
  4. DisconnectNamedPipe关闭命名管道
  5. CloseHandle关闭命名管道实例句柄
命名管道的客户端负责
  1. WaitNamedPipe等待一个命名管道实例
  2. CreateFile建立与命名管道的连接
  3. WriteFile和ReadFile分别想服务器发送数据
  4. CloseHandle关闭命名管道

当然,实际上还有很多问题,例如,服务器可能支持多个命名管道,这时需要使用多线程,为提高效率,需要使用异步I/O,重叠式I/O等。

这个一会我会写些代码来进行补充说明,当然也会将上面概述的函数参数也介绍一下。

命名管道中还有一些附加的函数,例如CallNamedPipe和TransactNamedPipe函数,君可以有效的减轻一个应用程序的编码复杂性,另外几个GetNamedPipeInfo,SetNamedPipeHandleState,GetNamedPipeHandleState等函数,可以快速方便的获得命名管道信息,并对其进行修改。

只上我们说的种种,大多是协议无关,利用重定向器进行“间接”通信的方法,而Winsock技术则是进行直接通信的方式。

我先把这快的代码整理一下,之后我们继续Winsock的复习。基于一中网络传输协议,进行直接通信的技巧。

###测试建立多线程命名管道服务器源码

// NPFSServer.h

#pragma once

#include <windows.h>    // 包含了windows头文件,则无需再包含winbase.h
#include <stdio.h>
#include <conio.h>

// Fun:处理一个命名管道实例的线程
DWORD WINAPI PipeInstanceProc(LPVOID lpParameter);

//----------------------------------------------------------------------------------------------------------------------

//NPFSServer.cpp

/*
* 文件名称:NPFSServer.cpp
*
* 文件作用:测试建立多线程命名管道服务器
*
* 完成日期:2007.04.03
*/

#include "NPFSServer.h"


#define MAX_NUM_PIPES 5     // 宏定义命名管道最大个数为5个

void main( void ) 
{
HANDLE hThreadHandle; // 线程句柄
DWORD ThreadId;     // 线程编号

for( int i = 0; i < MAX_NUM_PIPES; i++ )
{
    // Fun:为每个命名管道实例创建一个线程,并调用其回调函数

    if ( ( hThreadHandle = CreateThread( NULL,      // WinNT后永久设NULL则可
              0,       // 线程初始化堆栈大小,一般设为0
              PipeInstanceProc, // 线程回调函数的指针,即函数名
              NULL,      // 传输给回调函数的参数,通过它实现对回调函数的控制 
              0,       // 线程创建完毕后的状态,0表示创建后立刻执行线程,CREATE_SUSPENDED表示暂时挂起,等待叫醒 
              &ThreadId     // 线程ID值的地址
             ) ) == NULL )
    {
     printf( "创建线程失败! %\n", GetLastError() );
     return;
    }

    CloseHandle( hThreadHandle );
}

printf( "按下任意键停止命名管道服务器。 \n");
getchar();
}

/*
*    函数名称: PipeInstanceProc
* 函数参数: (in)LPVOID:     附加信息
*     函数作用: 处理一个命名管道实例的线程
* 函数返回值:(DWORD)若顺利完成,则返回1;若获取域名失败,则返回0
*/
DWORD WINAPI PipeInstanceProc( LPVOID lpParameter )
{
HANDLE hPipeHandle;    // 命名管道的句柄
DWORD nBytesRead;     // 读取收到的信息字节数
DWORD nBytesWritten;    // 发出的写入信息字节数
CHAR cBuffer[256];

/*
* 函数名称: CreateNamedPipe
* 函数参数: (in)LPCTSTR       命名管道名字,UNC标准( \\IP地址\Pipe\唯一标识文件路径 )
*      (in)DWORD       命名管道模式,单双向,读写控制,安全模式( 均被宏定义好,可位或操作 )
*      (in)DWORD       命名管道读,写,等待模式( 均被宏定义好,可位或操作 )
*      (in)DWORD       命名管道最多可创建的实例句柄个数
*      (in)DWORD       命名管道输出缓冲区大小
*      (in)DWORD       命名管道输入缓冲区大小
*      (in)DWORD       命名管道默认超时时间
*      (in)LPSECURITY_ATTRIBUTES 命名管道安全描述符,若为NULL,则句柄不可继承的默认安全。
* 函数作用: 创建一个命名管道实例
* 函数返回值:(HANDLE)成功则返回命名管道实例的句柄,失败则返回值INVALID_HANDLE_VALUE
*/
if ( ( hPipeHandle = CreateNamedPipe("\\\\.\\PIPE\\Duzhi",
    PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE,
    MAX_NUM_PIPES, 0, 0, 1000, NULL ) ) == INVALID_HANDLE_VALUE )
{
    printf( "创建命名管道失败! %d\n", GetLastError() );
    return 0;
}

// 持续处理客户端信息
while( 1 ) 
{
    /*
    *    函数名称: ConnectNamedPipe
    * 函数参数: (in)HANDLE:     命名管道实例句柄
    *      (in)LPOVERLAPPED    是否锁定式命名管道
    *     函数作用: 建立命名管道连接并监听
    * 函数返回值:(bool)若顺利建立,则返回true;若顺利失败,则返回false
    */
    if ( ! ConnectNamedPipe( hPipeHandle, NULL) )
    {
     printf( "连接命名管道失败r %d\n", GetLastError() );
     CloseHandle( hPipeHandle );
     return 0;
    }

    // 从客户端消息处读取数据,并且原封不动的返回回去,直到客户端关闭
    while( ReadFile(hPipeHandle, cBuffer, sizeof(cBuffer),
     &nBytesRead,    NULL) > 0 )
    {
     printf( "从客户端收到 %d 字节数据\n", nBytesRead );

     if ( WriteFile(hPipeHandle, cBuffer, nBytesRead,
      &nBytesWritten, NULL) == 0 )
     {
      printf( "发送消息失败 %d\n", GetLastError() );
      CloseHandle(hPipeHandle);
      return 0;
     }
    }

    if (DisconnectNamedPipe( hPipeHandle ) == 0)
    {
     printf( "断开命名管道失败! %d\n", GetLastError() );
     CloseHandle( hPipeHandle );
     return 0;
    }
}

CloseHandle(hPipeHandle);
return 1;
}