看到这个标题,如果你想说谁会这么蛋疼,有网不好好上,那么说明你还是一个纯洁的少年。自动重拨的需求所在多有,主要是为了绕过各大网站对相同IP地址的重复请求次数限制等等。具体的我不说了,说多了说我教坏小孩子。我最近研究这个只是因为我想写个软件自动播放和下载某个网站的视频,but这个网站一天只让同个IP免费看五个视频,我又抠不愿意花钱,但是又特别想多看多载几部。什么,你要我把网站地址告诉你?还是算了吧,传播XX(se)OO(qing)是违法的知道不。
按惯例,先声明:技术知识浩瀚无垠,笔者但求浅尝辄止便心满意足,所以,本文并不确保描述的准确性,若有错误和不足之处请不吝赐教。
上网方式简介
趁此机会简单了解下各类网络接入方式。以下描述主要来自百度文档《浅谈各种宽带上网的方法》,有兴趣的朋友可以自行百度搜索文库。
拨号上网:20世纪90年代刚有互联网的时候,老百姓上网使用最为普便的一种方式是拨号上网。只要用户拥有一台个人电脑、一个外置或内置的调制解调器(modem)和一根电话线,再向本地ISP供应商申请自己的账号,或购买上网卡,拥有自己的用户名和密码后,然后通过拨打ISP的接入号连接到Internet上。那个时候,出差的人们常常会问宾馆能否拨号上网,然后问拨什么号,之后以缓慢的速度发送邮件或“畅游”网络。拨号方式理论上的最高速率56KBIT/S。除了速度慢外,同时只能进行一项工作,比如上网了电话就打不进来。
结构体的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,如前所述,我们要手动为其分配内存(非托管),并写入相应数据,关键代码如下:
try { IntPtr lpRasDialExtensions = IntPtr.Zero; var extensions = new RASDIALEXTENSIONS(); //根据StructLayout相关属性计算内存大小 int extensionsSize = Marshal.SizeOf(typeof(RASDIALEXTENSIONS)); extensions.size = extensionsSize; #if (WIN7 || WIN8) extensions.devSpecificInfo.size = Marshal.SizeOf(typeof(RASDEVSPECIFICINFO)); #endif lpRasDialExtensions = Marshal.AllocHGlobal(extensionsSize); Marshal.StructureToPtr(extensions, lpRasDialExtensions, true); } catch (Exception) { //... } finally { if (lpRasDialExtensions != IntPtr.Zero) { Marshal.FreeHGlobal(lpRasDialExtensions); } }
代码并不复杂,其中Marshal.StructureToPtr(object structure, IntPtr ptr, bool fDeleteOld)方法的第三个参数说明如下:
假设 ptr 指向非托管内存块。此内存块的布局由相应的托管类 structure 描述。StructureToPtr将字段值从结构封送到指针。假设 ptr 块包含引用字段,该字段指向当前包含“abc”的字符串缓冲区。假设托管端上相应的字段是包含“vwxyz”的字符串。如果不另行通知它,StructureToPtr将分配一个新的非托管缓冲区来保存“vwxyz”,并将它挂钩到ptr 块。这将丢弃旧缓冲区“abc”使之漂移而不将其释放回非托管堆。最后,您将得到一个孤立的缓冲区,它表示在代码中存在内存泄漏。如果将 fDeleteOld 参数设置为真,则StructureToPtr 在继续为“vwxyz”分配新缓冲区之前释放保存“abc”的缓冲区。
调用结束后记住要使用Marshal.FreeHGlobal释放非托管内存。
DotRas
以上代码来自于一个开源项目DotRas,虽然我并不提倡重复造轮子,但大概知道轮子怎么造总没有坏处。由于笔者家里条件不允许——光纤入户——so,我借用朋友的虚拟机(ADSL)进行DotRas的调用测试,主要代码如下:
//断开 private void btnHangUp_Click(object sender, RoutedEventArgs e) { if (_dataContext.SelectedRasConnection != null) { var conns = RasConnection.GetActiveConnections();//获取当前所有活动连接 var conn = conns.First(o => o.EntryId == _dataContext.SelectedRasConnection.EntryId); if (conn != null) { RasIPInfo ipAddresses = (RasIPInfo)conn.GetProjectionInfo(RasProjectionType.IP); tbTestInfo.Text = "_前_" ipAddresses.IPAddress.ToString(); conn.HangUp();//断开,断开后RasConnection.GetActiveConnections()返回值里就没它了 System.Threading.Thread.Sleep(10000); DialUp(_dataContext.SelectedRasConnection.EntryName); } } } //拨号连接 private void DialUp(string entryname) { RasDialer dialer = new RasDialer(); dialer.EntryName = entryname; dialer.PhoneNumber = " "; dialer.AllowUseStoredCredentials = true; dialer.PhoneBookPath = RasPhoneBook.GetPhoneBookPath(RasPhoneBookType.AllUsers); dialer.Timeout = 1000; dialer.Dial(); if (_dataContext.SelectedRasConnection != null) { var conns = RasConnection.GetActiveConnections(); var conn = conns.First(o => o.EntryId == _dataContext.SelectedRasConnection.EntryId); if (conn != null) { RasIPInfo ipAddresses = (RasIPInfo)conn.GetProjectionInfo(RasProjectionType.IP); tbTestInfo.Text = "_后_" ipAddresses.IPAddress.ToString(); } } }
界面如图:
点断开后,果然远程桌面断开了:
10秒钟后,虚拟机重拨连接,再等待一段时间后(这个时间比较长有1到3分钟,远远没达到实用的标准,可能是花生壳域名重新解析的缘故;经朋友在本地测试,速度杠杠的),界面重新展现:
可以看到前后的IP是不一样的。
,