先说清楚一件事:代理是挂在进程上的,不是挂在窗口上
上周帮朋友排查一个易语言写的数据查询工具,十个窗口同时跑,他以为给程序设了代理就万事大吉,结果一查出口,十个窗口全是同一个IP。问题不在代理,在于归属搞错了——易语言里不管是发HTTP请求的网络命令,还是窗口里内嵌的浏览器控件,底下走的都是Windows同一套网络通道(WinINet),而这条通道的代理设置,默认整个进程共用一份。
换句话说,同一个进程里开多少个窗口,它们都是"一个身子上的十个脑袋",出口必然相同。想让每个窗口各走各的IP,只有两条路:要么在发请求的那一刻单独指定代理(请求级),要么让每个窗口独占一个进程,进程启动时绑好自己的代理(进程级)。下面两种写法都给出完整代码,先从简单的说起。
动手之前,把IP备齐
多窗口方案对IP有个硬要求:一次能拿到好几个,而且互不重复。有的服务一次只吐一个,提取一次还得等半天,窗口开着干等。我自己的做法是用神龙HTTP的API提取——它有300多个城市的精准定位,提取链接里可以直接指定城市和数量,返回格式选"每行一个",拿回来就是现成的列表,易语言里一个分割文本就拆好了。想给不同窗口分不同城市的IP,在链接里把城市排好就行。
认证方式上给个实在建议:能用白名单就用白名单——把发起请求的服务器IP加进白名单,提取到的代理就是纯"IP:端口"格式,代码里完全不用碰账号密码。别小看这一步,账号密码认证在多进程场景下要多写好几段传凭据的代码,能省则省。
还有个细节:如果窗口要长时间挂着跑,提取时注意选长效静态IP。短效动态IP的寿命只有几分钟到几十分钟,跑到一半失效,窗口就悄悄断流了。
写法一:请求级绑定——发请求时临时指定
如果窗口只是发请求、拿数据、显示结果,不涉及内嵌浏览器,请求级是最省事的路子。不用装任何第三方模块,声明几个系统自带的WinINet接口就够:
.版本 2
.DLL命令 InternetOpenA, 整数型, "wininet.dll", "InternetOpenA"
.参数 名称, 文本型
.参数 访问类型, 整数型
.参数 代理地址, 文本型
.参数 例外列表, 文本型
.参数 标志, 整数型
.DLL命令 InternetOpenUrlA, 整数型, "wininet.dll", "InternetOpenUrlA"
.参数 会话句柄, 整数型
.参数 网址, 文本型
.参数 附加头, 文本型
.参数 头长度, 整数型
.参数 标志, 整数型
.参数 上下文, 整数型
.DLL命令 InternetReadFile, 逻辑型, "wininet.dll", "InternetReadFile"
.参数 句柄, 整数型
.参数 缓冲区, 字节集
.参数 想读字节数, 整数型
.参数 实际读数, 整数型, 传址
.DLL命令 InternetCloseHandle, 逻辑型, "wininet.dll", "InternetCloseHandle"
.参数 句柄, 整数型
核心就一处:访问类型填3(走代理),第三个参数直接放"IP:端口"。封装成函数,哪个窗口用哪个IP,传参就行:
.子程序 带代理取网页, 文本型, , 走指定代理抓一个页面,代理传空则直连
.参数 代理, 文本型, , 格式:IP:端口,留空直连
.参数 网址, 文本型
.局部变量 会话, 整数型
.局部变量 链接, 整数型
.局部变量 缓冲区, 字节集
.局部变量 已读, 整数型
.局部变量 结果, 文本型
.如果 (代理 = “”)
会话 = InternetOpenA (“MyTool”, 1, “”, “”, 0) ' 1 = 直连
.否则
会话 = InternetOpenA (“MyTool”, 3, 代理, “”, 0) ' 3 = 走代理
.如果结束
.如果真 (会话 = 0)
返回 (“”)
.如果真结束
链接 = InternetOpenUrlA (会话, 网址, “”, 0, 0, 0)
.如果真 (链接 = 0)
InternetCloseHandle (会话)
返回 (“”)
.如果真结束
缓冲区 = 取空白字节集 (4096)
.判断循环首 (InternetReadFile (链接, 缓冲区, 4096, 已读) 且 已读 > 0)
结果 = 结果 + 到文本 (取字节集左边 (缓冲区, 已读))
.判断循环尾 ()
InternetCloseHandle (链接)
InternetCloseHandle (会话)
返回 (结果)
测试时网址随便填一个能回显IP的查询页,返回里出现的应该是代理IP而不是本机IP。如果页面是UTF-8编码,中文部分可能显示乱码,但IP数字看得清,要完整中文就再做一步编码转换。装了易语言常用网络模块的同学,模块里的网页访问命令同样留了代理参数位,填法一个意思;本文全部用系统自带接口,零依赖,复制就能跑。
写法二:进程级绑定——整个进程都走这个代理
窗口里要是嵌了浏览器控件,请求级就管不到它了——浏览器控件自己走自己的通道。这时候要用InternetSetOption把代理焊死在进程上,进程里后续所有网络动作,包括浏览器控件加载页面,全部照办:
.版本 2
.DLL命令 GlobalAlloc, 整数型, "kernel32.dll", "GlobalAlloc"
.参数 标志, 整数型
.参数 字节数, 整数型
.DLL命令 复制文本, 整数型, "kernel32.dll", "lstrcpyA"
.参数 目标地址, 整数型
.参数 源文本, 文本型, 传址
.DLL命令 InternetSetOptionA, 逻辑型, "wininet.dll", "InternetSetOptionA"
.参数 句柄, 整数型
.参数 选项, 整数型
.参数 缓冲区, 整数型
.参数 长度, 整数型
.数据类型 代理结构
.成员 访问类型, 整数型
.成员 代理指针, 整数型
.成员 例外指针, 整数型
.子程序 绑定进程代理, 逻辑型, , 当前进程的所有网络请求都走这个代理
.参数 代理, 文本型, , 格式:IP:端口
.局部变量 信息, 代理结构
.局部变量 代理指针, 整数型
代理指针 = GlobalAlloc (64, 取文本长度 (代理) + 1) ' 64 = 申请一块自动清零的固定内存
复制文本 (代理指针, 代理)
信息.访问类型 = 3 ' 3 = 走代理
信息.代理指针 = 代理指针
信息.例外指针 = 0
InternetSetOptionA (0, 38, 取变量数据地址 (信息), 12) ' 38 = 设置本进程代理
InternetSetOptionA (0, 39, 0, 0) ' 39 = 广播设置变更
InternetSetOptionA (0, 37, 0, 0) ' 37 = 立刻刷新
返回 (真)
这个函数有个使用前提,记住了能少走半天弯路:必须在进程发出第一个网络请求之前调用。所以把它放在窗口"创建完毕"事件的第一行最稳。要是进程已经发过请求再绑,通道可能还记着旧路线,重启进程才保险。
多窗口独立IP:一个窗口一个进程
原理捋顺了,多窗口就是个体力活:主程序负责提取IP、派活;窗口端负责领IP、绑代理、干活。两个角色可以写成同一个程序——带参数启动就是窗口端,不带参数就是主程序;也可以干脆分成两个exe,后者更清晰,下面按两个exe讲。
主程序的活儿就两步:提取,然后挨个拉起窗口端,把IP塞进命令行参数:
.版本 2
.子程序 按钮_开窗口_被单击
.局部变量 提取链接, 文本型
.局部变量 返回文本, 文本型
.局部变量 ip列表, 文本型, , "0"
.局部变量 i, 整数型
.局部变量 窗口端路径, 文本型
' 在神龙HTTP个人中心生成提取链接,返回格式选“每行一个”
提取链接 = “你的API提取链接”
返回文本 = 带代理取网页 (“”, 提取链接) ' 代理传空就是直连
ip列表 = 分割文本 (返回文本, #换行符, )
窗口端路径 = 取运行目录 () + “\窗口端.exe”
.计次循环首 (取数组成员数 (ip列表), i)
' 跳过空行和提示文字,只放行带端口的行
.如果真 (寻找文本 (ip列表 [i], “:”, , 假) > 0)
运行 (窗口端路径 + “ ” + ip列表 [i], 假, )
.如果真结束
.计次循环尾 ()
窗口端启动后第一件事,是从命令行把IP接住、绑上,然后才轮到窗口干活:
.版本 2
.子程序 __启动窗口_创建完毕
.局部变量 参数文本, 文本型
参数文本 = 取命令行 ()
.如果真 (寻找文本 (参数文本, “:”, , 假) ≤ 0)
信息框 (“请从主程序带代理参数启动”, 0, “提示”)
结束 ()
.如果真结束
绑定进程代理 (参数文本) ' 上一节的函数,原样搬过来
标签_本窗IP.标题 = “本窗口代理:” + 参数文本
' 往下的浏览器控件、请求代码,全都自动走这个IP
这样跑起来,主程序点一次按钮,提取到几个IP就开几个窗口端进程,每个进程绑一个IP,互不掺和。小提醒:窗口位置可以在窗口端里按启动顺序左右错开,不然十几个窗口全叠在屏幕中间,看着费劲。
两个容易忽略的点:一是同一进程里再开第二个浏览器窗口,两个窗口还是同一个IP,别在这里绕弯路;二是如果非要用账号密码认证的代理,命令行传参就得连账号密码一起传,格式自己约定好再解析,用白名单认证则完全没这个烦恼。
验收和排错:盯着出口IP看
验收标准很简单:让每个窗口启动后自己请求一次IP回显页,把结果显示在标题栏或标签上。十个窗口亮出十个不一样的IP,这事才算成。跑不顺的时候,对照下面这张表基本能定位:
| 现象 | 多半的原因 | 处理办法 |
|---|---|---|
| 显示的还是本机IP | 绑定时机太晚,进程已发过请求 | 把绑定挪到创建完毕第一行,或重启进程 |
| 请求一直超时 | IP已失效或端口不通 | 重新提取一批,代码里加超时和重试 |
| 提示需要认证 | 代理是账号密码认证,代码没传凭据 | 改用白名单认证,代码里只写IP和端口 |
| 几个窗口IP相同 | 提取结果里有重复 | 提取时加大数量,或选不重复的返回格式 |
再提醒一句长跑的情况:短效动态IP到期后窗口会悄悄断流,程序里最好加个心跳检测——请求失败就重新提取,临时要换个地区的IP,在提取链接里指定城市就行;然后把旧子进程关掉、带着新参数再拉起来。要是窗口需要一开一整天,直接上长效静态IP省心得多,神龙HTTP这两类产品线都有,按任务时长选即可。
常见问题
Q:一个进程里开多个窗口,能让每个窗口的IP都不一样吗?
不能。代理设置挂在进程上,同一进程里的所有窗口共用一份。想独立只能一个窗口一个进程,用命令行传参的方式把IP带进去,这也是最省事的方案。
Q:账号密码认证的代理在易语言里怎么用?
请求级写法里,网络命令的参数表一般都留了账号、密码的位置,照着填就行;进程级写法要再调InternetSetOption传凭据(选项43是账号、44是密码),繁琐一些。多窗口场景更建议统一用白名单认证,把服务器IP加进白名单后,代理就是纯IP加端口,代码干净很多。
Q:IP用一会儿就失效,窗口断了怎么办?
短效动态IP本来就有寿命,这是正常现象。两条路:要么程序里加检测,失败就领新IP重启子进程;要么任务时间长就换长效静态IP,一个IP挂一整天不带掉的。
Q:窗口开多了,程序会不会很卡?
每个窗口端是独立进程,内存占用随数量线性涨,普通办公机开二三十个没什么压力,再多建议换好点的机器。另外控制好每个窗口的请求节奏,加随机间隔,别用同一个IP高频请求同一个页面,对目标网站和代理配额都友好。






