网络编程(二)
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库的连接。
因为是单向设计,所以若我们在设计邮槽时,服务器方面和客户端方面所负责的方面是不同的。
服务器方面需要负责信息的接受读取,不必写入。
- CreateMailslot创建邮槽,传出句柄。
- ReadFile,使用该邮槽从客户机接收数据。
- 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连接。
命名管道的服务器负责
- CreateNamedPipe创建一个命名管道实例,获得句柄
- ConnectNamedPipe监听连接请求
- ReadFile,WriteFile接收数据,发送数据
- DisconnectNamedPipe关闭命名管道
- CloseHandle关闭命名管道实例句柄
命名管道的客户端负责
- WaitNamedPipe等待一个命名管道实例
- CreateFile建立与命名管道的连接
- WriteFile和ReadFile分别想服务器发送数据
- 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;
}