看到这个标题,如果你想说谁会这么蛋疼,有网不好好上,那么说明你还是一个纯洁的少年。自动重拨的需求所在多有,主要是为了绕过各大网站对相同IP地址的重复请求次数限制等等。具体的我不说了,说多了说我教坏小孩子。我最近研究这个只是因为我想写个软件自动播放和下载某个网站的视频,but这个网站一天只让同个IP免费看五个视频,我又抠不愿意花钱,但是又特别想多看多载几部。什么,你要我把网站地址告诉你?还是算了吧,传播XX(se)OO(qing)是违法的知道不。
按惯例,先声明:技术知识浩瀚无垠,笔者但求浅尝辄止便心满意足,所以,本文并不确保描述的准确性,若有错误和不足之处请不吝赐教。
上网方式简介
趁此机会简单了解下各类网络接入方式。以下描述主要来自浅谈各种宽带上网的方法:
拨号上网:20世纪90年代刚有互联网的时候,老百姓上网使用最为普便的一种方式是拨号上网。只要用户拥有一台个人电脑、一个外置或内置的调制解调器(Modem)和一根电话线,再向本地ISP供应商申请自己的账号,或购买上网卡,拥有自己的用户名和密码后,然后通过拨打ISP的接入号连接到Internet上。那个时候,出差的人们常常会问宾馆能否拨号上网,然后问拨什么号,之后以缓慢的速度发送邮件或“畅游”网络。拨号方式理论上的最高速率56KBIT/S。除了速度慢外,同时只能进行一项工作,比如上网了电话就打不进来。
ISDN:准确地说应是N-ISDN,即窄带ISDN,它主要提供64kbit/s到2Mbit/s的接入能力、目前推向用户的ISDN业务是基本速率接口,即2B+D,每个B通道为64kbit/s,D通道为16kbit/s,ISDN在Internet接入方面比普通用户以56kbit/s的Modem上网具有无可比拟的优势。首先,由于ISDN提供了2B+D的功能,使得用户可以一面以64kbit/s的速度上网,一面又可以同时使用另一通道打电话而互不干扰,非常方便;其次,ISDN是数字式设备,因而接入速度相当快,不像Modem那样要有很长的建立时间,全数字的网络使建立只需几秒钟;最后,由于ISDN提供2B+D方式,使得用户可以绑定两个B通道上网,最高速率可达128kbit/s,比56k Modem的接入速率快2.2倍,真正可以做到网上冲浪。
ADSL:近几年最普遍的网络接入方式。长期以来通信用户的电话机经过"对绞铜线"的用户线连至市内交换局,进入公共交换的通信网(PSTN),接至对方用户的电话机,使双方得以互相会话。对绞铜线为传统的模拟电话提供300~3400Hz 的频带,为了适应电话用户使用低速数据通信,曾加装调制一解调器(modem),使速率33kb/s 和最高56kb/s 的数据信号能够通过模拟话音频带与对方实行数据通信。这就是拨号上网方式的基础。话音modem 只能提供56kb/s 的数据速率。为什么对绞铜线只能传输以56kb/s 为限度的数据呢?应该说,这不是对绞铜线传输能力的限度,而是通信网中的交换机有限制,它对电话通信只是分配一个话音频带。虽然用户的数据信息经过话音modem,交换机并不认出它是话音modem 传来的数据信号,而只是对它当作话音信号看待。对绞铜线本身并不限制定带数据信号的传输,只要避开窄带交换机,用户就可以把宽带数据信号送进通信网。因此我们说,用户线如避开了窄带的话音交换机,就可成为"数字用户线"(DSL,Digital Subscriber Line)。简单地说,ADSL可以利用现有的电话线网络,只要在线路两端加装ADSL设备,即可为用户提供高宽带服务,提供8MBIT/S的高速下行速率,远高于ISDN速率,而上行速率有1MBIT/S,是普通电话拔号MODEM的百倍以上,传输距离能达3-5KM。当然ADSL的商业化推进也经过了各方面多年的努力。有人会问,“我家用的就是ADSL,怎么每次还要弹出一个拨号界面让我输入账号密码呢?”实际上,ADSL本身是不需要拨号的,而是一种专线链接。拨号的目的仅仅是认证用户的身份并进行计费,因此叫做虚拟拨号。虚拟拨号需要用到虚拟拨号软件,常用的有WinPoET ,虚拟拨号软件在安装过程中会提示将拨号绑定到与ADSL Modem 相连的网卡。配置好以后,拨号形式与56Kbps模拟Modem完全一样,在对话框里输入ISP提供的用户名和密码就可以了。ADSL还涉及到PPPOE的概念,有空再研究吧。
光纤入户:很明显,最直观的区别就是传输介质不同,当然相关的接入技术肯定也是不同的,网速比ADSL又有了很大提升,就不赘述了。笔者安装光纤时,电信也给了一个类似Modem的东西,工作人员也把它叫做“猫”,其实和拨号方式用的Modem不是同个东西,但是本质还是主要用来处理信号转换。
ISDN没接触过就不说了,传统拨号和ADSL都可以通过调用拨号程序控制网络的断开与连接,如果ADSL入户时接入到路由器上,那么可以通过重启路由器的方式进行IP更换。光纤入户时常也接路由器(光猫原本自带无线路由功能,但是有时电信会把这个功能给锁掉让你用不了,很坑爹),但是账号和密码是保存在光猫里的,所以要重启的是光猫(似乎也有方法可以通过路由器进行拨号,笔者没研究过)。重启路由器和光猫很简单,调用设备管理页面的重启请求即可。下面我们主要讲如何通过自动拨号进行网络重连。
RAS概述
现如今,社会已经离不了网络,企事业单位对网络的需求也在不断扩大,各地分支机构都需要与总部随时进行信息交换。虽然通过Intemet能很好实现远程数据的传输,但需要总部与各地分支机构都拥有良好的上网条件和IP地址。在实际工作中,出于成本上的考虑,大多数中小型企业多采用传统的通过调制解调器和电话线直接拨号的方式来实现远程数据的传输(并不一定经由Internet,更类似于局域网)。
远程访问服务(Remote Access seryice)利用公共交换电话网络(PSTN)把远离局域网(LAN)的计算机连接到局域网上来,远程计算机(RAS客户机)可以获得LAN用户的所有服务和权限,并访问、共享该LAN上的资源。它所实施的网络互联架构,在提供网络互联的同时,可以集中发布各种应用程序,包括Web应用程序,以及几乎所有部署在台式机的应用程序。分析结果显示,维持RAS正常使用所需要的带宽相当低,用户端提供最低28Kbps快速访问。相对于传统的组网方案, RAS提供了更为快速、安全、灵活和可管理的远程应用接入方案,使人们可以随需应变,通过任何网络连接方式对企业进行随时随地的安全访问,为企业的 ERP 、 CRM 、 OA 等信息应用系统全面提速。RAS适用于行业用户使用,如:企业办公网络,政府办公网络、税收征收系统、电力系统、医疗领域等等。
远程访问服务支持拨号网络(DUN)连接与虚拟专用网络(VPN)连接两种不同的远程访问连接方式。拨号网络(DUN)连接就是远程客户机利用一个连接了电话线的调制解调器,通过拨号的方式呼叫远程计算机并建立连接。虚拟专用网络(VPN)是远程客户机使用基于TCP/IP协议的专门隧道协议(如PPTP、L2TP),通过虚拟专用网络服务器的虚拟端口,借助其他网络(如Intemet),实现一种逻辑上的直接连接。
VPN属于远程访问技术,简单地说就是利用公用网络架设专用网络。例如某公司员工出差到外地,他想访问企业内网的服务器资源,这种访问就属于远程访问。在传统的企业网络配置中,要进行远程访问,传统的方法是租用DDN(数字数据网)专线或帧中继,这样的通讯方案必然导致高昂的网络通讯和维护费用。对于移动用户(移动办公人员)与远端个人用户而言,一般会通过拨号线路(Internet)进入企业的局域网,但这样必然带来安全上的隐患。
让外地员工访问到内网资源,利用VPN的解决方法就是在内网中架设一台VPN服务器。外地员工在当地连上互联网后,通过互联网连接VPN服务器,然后通过VPN服务器进入企业内网。为了保证数据安全,VPN服务器和客户机之间的通讯数据都进行了加密处理。有了数据加密,就可以认为数据是在一条专用的数据链路上进行安全传输,就如同专门架设了一个专用网络一样,但实际上VPN使用的是互联网上的公用链路,因此VPN称为虚拟专用网络,其实质上就是利用加密技术在公网上封装出一个数据通讯隧道。有了VPN技术,用户无论是在外地出差还是在家中办公,只要能上互联网就能利用VPN访问内网资源,这就是VPN在企业中应用得如此广泛的原因。
P/Invoke
我们可以借助RASapi实现软件的自动拨号。RAS APl最早是在windows for workgroup 3.11中出现的,现已成为win32 API的一个组成部分。RAS API提供了专门用于实现RAS功能的函数,根据其具体的功能分别存放在API32.DLL、RASDLG.DLL和RASSAPI.DLL三个动态链接库中。其中,RASAPl32.DLL封装了RAS客户功能的函数,RASDLG.DLL封装了几个基于对话框的功能函数,RASSAPI.DLL则封装了RAS Server的一些管理函数。
RAS API将整个拨号网络视为RAS电话簿(Phonebook),而每一个连接视为电话簿条目(PhonebookEntry)。调用RasCreatePhonebookEntry创建新的连接,调用RasDial进行拨号,调用RasEnumEntries获得当前系统已有的所有连接,调用其它的RAS函数还可以分别实现获取或设置连接参数等功能。
作为.NETer,偶尔要借助P/Invoke调用Win32Api,以RASAPI中的核心函数RasDial为例,看用C#如何封装对该函数的调用。
还有个api名曰InternetDial,也能用来写拨号程序,就不知道它和RasApi的区别有哪些。MSDN里说道InternetDial does not support double-dial connections, SmartCard authentication, or connections that require registry-based certification. Starting on Windows Vista and Windows Server 2008, the WinINet dial-up functions use the RAS functions to establish a dial-up connection. WinINet supports the functionality documented in the RasDialDlg function. 所以我想现在还是用RAS比较靠谱一点。
首先在MSDN中查看RasDial的定义如下:
DWORD RasDial(
_In_ LPRASDIALEXTENSIONS lpRasDialExtensions,
_In_ LPCTSTR lpszPhonebook,
_In_ LPRASDIALPARAMS lpRasDialParams,
_In_ DWORD dwNotifierType,
_In_ LPVOID lpvNotifier,
_Out_ LPHRASCONN lphRasConn
);
然后我们要将Win32类型转为C#类型。由于 DWORD 是 4 字节的整数,因此我们可以使用 int 或 uint 作为 C# 对应类型。由于 int 是 CLS 兼容类型(可以用于所有 .NET 语言),以此比 uint 更常用,并且在多数情况下,它们之间的区别并不重要。以P或LP开头的类型一般都是指针,对应的C#类型需要为引用类型,如果作为参数使用,那么也可以使用值类型,前面加上ref/out关键字即可;还有一种方法,就是使用CLR中的System.IntPrt作为万能指针去对应任意Win32指针类型,它没有要求一开始就定义对应的数据类型,后面用到时再赋予即可(概念上有点类似于Object)。不像系统中的其他基类型,IntPtr 并没有固定的大小。相反,它在运行时的大小是依底层操作系统的正常指针大小而定的。这意味着在 32 位的 Windows 中,IntPtr 变量的宽度是 32 位的,而在 64 位的 Windows 中,实时编译器编译的代码会将 IntPtr 值看作 64 位的值。当在托管代码和非托管代码之间封送不透明指针时,这种自动调节大小的特点十分有用。
我们平时常说的32位、64位系统,这里的位数其实指的是最大寻址空间,即内存的最大大小,也就是说使用32位操作系统支持的内存也最多为2的32次方,就是4G。和数据类型占用的字节大小没有关系。
RASDIALEXTENSIONS是指向一个结构的指针,定义如下:
typedef struct _RADIALEXTENSIONS { DWORD dwSize; DWORD dwfOptions; HWND hwndParent; ULONG_PTR reserved; ULONG_PTR reserved1; RASEAPINFO RasEapInfo; BOOL fSkipPppAuth; RASDEVSPECIFICINFO RasDevSpecificInfo; } RASDIALEXTENSIONS, *PRASDIALEXTENSIONS;
根据上述,在c#中我们可以使用class替代它,由于这个结构是作为RasDial函数的一个参数传递,因此我们也可以使用struct替代,并在调用方法的时候在此参数前加上ref/out(若将参数作为一个整体操作,那么class也必须加上ref/out,避免出现实参/形参引用不一致的情况),当然,在方法调用的时候,我们也可以使用System.IntPrt指向之,略微麻烦一点的是,IntPrt需要手动为其分配非托管内存并写入数据,所幸借助System.Runtime.InteropServices.Marshal类的相关方法,这部分工作也很简单。
c#表示如下(RASDIALEXTENSIONS最后两个字段是Win7之后新增的,在编码的时候要注意):
[StructLayout(LayoutKind.Sequential, Pack = 4)] public struct RASDIALEXTENSIONS { public int size; public RDEOPT options; public IntPtr handle; public IntPtr reserved; public IntPtr reserved1; public RASEAPINFO eapInfo; #if (WIN7 || WIN8) public bool skipPppAuth; public RASDEVSPECIFICINFO devSpecificInfo; #endif }
WIN7、WIN8是在VS里自定义的条件编译符号,略过不谈。StructLayout特性指示结构字段在内存中如何分布,当导出到非托管代码时非托管对象亦按此布局。StructLayout有四个属性:
- LayoutKind,有三个枚举值:Sequential,对象的成员按照它们出现的顺序依次布局,并且同StructLayout的另一个属性Pack有关,详细规则可以参看内存对齐的规则以及作用,那篇文章中的#pragma pack()同这里的Pack作用一样;Explicit,对象的各个成员的精确位置被显式控制,每个成员必须使用 System.Runtime.InteropServices.FieldOffsetAttribute指示该字段在类型中的位置,当映射到C/C++的union时,该枚举值非常有用;Auto,运行时自动对象的成员选择适当的布局,可能会对字段顺序进行调整,使实例占用尽可能少的内存(当然前提是各个字段独享自己的内存,不同于union)。考虑到CPU读取效率,一般采用LayoutKind.Sequential。
- Pack,第1条已述。
- CharSet,指示字符是单字节or双字节,这主要是历史遗留问题。可以将之设为Auto,这样,在 Windows NT、Windows 2000、Windows XP 和 Windows Server2003 系列上,默认值为 System.Runtime.InteropServices.CharSet.Unicode;在 Windows 98 和 Windows Me 上,默认值为 System.Runtime.InteropServices.CharSet.Ansi。
- Size,指示类或结构的绝对大小。不常用,但是如果需要在结构末尾分配额外的空间,则可能会用到此属性。
结构体的ULONG_PTR等表示基元类型指针的字段,只能使用IntPtr映射;若字段有预定义的若干值表示有意义的状态指示等,则可以使用enum映射,如dwfOptions标记RasDial的某些扩展信息,这些标记可以用枚举值表示。
[Flags] public enum RDEOPT { None = 0x0, UsePrefixSuffix = 0x1, PausedStates = 0x2, IgnoreModemSpeaker = 0x4, //... }
RasDial还有个参数值得注意——lpvNotifier——虽然LPVOID类型表示这是个不透明指针,用IntPtr即可,但文档所述表明这是个回调函数参数,当 Win32 函数需要返回多项数据时,通常都是通过回调机制来实现的,开发人员将函数指针传递给函数。.Net中有个类型专门作为方法的引用——Delegate,所以用Delegate映射更精确更方便。
最终RasDial函数的C#封装版本如下:
[DllImport(NativeMethods.RasApi32Dll, CharSet = CharSet.Unicode)] private static extern int RasDial( IntPtr lpRasDialExtensions, string lpszPhonebook, IntPtr lpRasDialParams, RasNotifierType dwNotifierType, Delegate lpvNotifier, out RasHandle lphRasConn);
可以看到lpRasDialExtensions使用的类型是IntPtr,如前所述,我们要手动为其分配内存(非托管),并写入相应数据,关键代码如下:
1 try 2 { 3 IntPtr lpRasDialExtensions = IntPtr.Zero; 4 var extensions = new RASDIALEXTENSIONS(); 5 //根据StructLayout相关属性计算内存大小 6 int extensionsSize = Marshal.SizeOf(typeof(RASDIALEXTENSIONS)); 7 extensions.size = extensionsSize; 8 #if (WIN7 || WIN8) 9 extensions.devSpecificInfo.size = Marshal.SizeOf(typeof(RASDEVSPECIFICINFO)); 10 #endif 11 lpRasDialExtensions = Marshal.AllocHGlobal(extensionsSize); 12 Marshal.StructureToPtr(extensions, lpRasDialExtensions, true); 13 } 14 catch (Exception) 15 { 16 //... 17 } 18 finally 19 { 20 if (lpRasDialExtensions != IntPtr.Zero) 21 { 22 Marshal.FreeHGlobal(lpRasDialExtensions); 23 } 24 }
代码并不复杂,其中Marshal.StructureToPtr(object structure, IntPtr ptr, bool fDeleteOld)方法的第三个参数说明如下:
调用结束后记住要使用Marshal.FreeHGlobal释放非托管内存。
DotRas
以上代码来自于一个开源项目DotRas,虽然我并不提倡重复造轮子,但大概知道轮子怎么造总没有坏处。由于笔者家里条件不允许——光纤入户——so,我借用朋友的虚拟机(ADSL)进行DotRas的调用测试,主要代码如下:
1 //断开 2 private void btnHangUp_Click(object sender, RoutedEventArgs e) 3 { 4 if (_dataContext.SelectedRasConnection != null) 5 { 6 var conns = RasConnection.GetActiveConnections();//获取当前所有活动连接 7 var conn = conns.First(o => o.EntryId == _dataContext.SelectedRasConnection.EntryId); 8 if (conn != null) 9 { 10 RasIPInfo ipAddresses = (RasIPInfo)conn.GetProjectionInfo(RasProjectionType.IP); 11 tbTestInfo.Text += "_前_" + ipAddresses.IPAddress.ToString(); 12 conn.HangUp();//断开,断开后RasConnection.GetActiveConnections()返回值里就没它了 13 System.Threading.Thread.Sleep(10000); 14 DialUp(_dataContext.SelectedRasConnection.EntryName); 15 } 16 } 17 } 18 19 //拨号连接 20 private void DialUp(string entryname) 21 { 22 RasDialer dialer = new RasDialer(); 23 dialer.EntryName = entryname; 24 dialer.PhoneNumber = " "; 25 dialer.AllowUseStoredCredentials = true; 26 dialer.PhoneBookPath = RasPhoneBook.GetPhoneBookPath(RasPhoneBookType.AllUsers); 27 dialer.Timeout = 1000; 28 dialer.Dial(); 29 30 if (_dataContext.SelectedRasConnection != null) 31 { 32 var conns = RasConnection.GetActiveConnections(); 33 var conn = conns.First(o => o.EntryId == _dataContext.SelectedRasConnection.EntryId); 34 if (conn != null) 35 { 36 RasIPInfo ipAddresses = (RasIPInfo)conn.GetProjectionInfo(RasProjectionType.IP); 37 tbTestInfo.Text += "_后_" + ipAddresses.IPAddress.ToString(); 38 } 39 } 40 }
界面如图:
点断开后,果然远程桌面断开了:
10秒钟后,虚拟机重拨连接,再等待一段时间后(这个时间比较长有1到3分钟,远远没达到实用的标准,可能是花生壳域名重新解析的缘故;经朋友在本地测试,速度杠杠的),界面重新展现:
可以看到前后的IP是不一样的。
其它参考资料:
基于VC++的RAS拨号连接技术及其应用((武汉科技大学)何璐、陈和平、肖刚)
VPN——虚拟专用网络
Calling Win32 DLLs in C# with P/Invoke
C# 托管内存与非托管内存之间的转换(结合Unity3d的实际开发)