写这篇文章其实源于之前做了几个爬虫,当然,不能称为纯粹的爬虫,因为他们不单是无限制的爬网页,大多数需要实现一定的业务逻辑,而最关键的就是登陆的过程以及获取登陆以后的cookie了。
其实第一个方法可以不用到截包,因为完全可以自己用应用程序模拟出浏览器的所有行为来登陆,比较麻烦的就是验证码识别,可以把验证码抓回来人工输入,也可以找网上的识别lib,但是,一般成功率不太可能到100%。
但是第一个方式往往比较麻烦,一方面登陆的过程往往是防范比较严格的地方,自己写程序模拟过程比较艰辛,二是经过自己的应用程序模拟的东西不够灵活,因为浏览器不可能共享自己应用程序的cookie,所以只能把自己的应用程序当做是浏览器,那么在网页里实现的东西可能也需要做,无形中增加了难度。
所以接下来另外的方式就是截包了。所谓的截包,就是不影响浏览器正常工作的情况下把数据包“偷”出来。本人见识短浅,就目前windows下的截包比较常用的方式应该有两个,一个是TCP层的截包,比较著名的库是winpcap,而在他之上著名的截包应用是wireshark,如果用c#调用,那还有个sharppcap是基于winpcap的;另一种方式应该是HTTP层的截包,在这方面做得比较好的工具有fiddler,httpwatcher等。
- 先说TCP层的截包(即winpcap),这个主要适合的情况有有线局域网和大多数无线局域网。可惜winpcap在windows系列得不到3g无线网卡(通常是modem而不是真正的网卡)的包。
调用winpcap的教程网上有很多,我这里着重说一下sharppcap,在codeproject地址是http://www.codeproject.com/Articles/12458/SharpPcap-A-Packet-Capture-Framework-for-NET。这是一个优秀的project,至少能让c#程序员轻松的调用winpcap的api。
总结起来,sharppcap主要调用步骤有4步。
1.找到所有可用网卡设备
/* Retrieve the device list */
CaptureDeviceList devices = CaptureDeviceList.Instance;
/*If no device exists, print error */
if (devices.Count < 1)
{
log.Warn("No device found on this machine");
return null;
}
if (devices.Count == 1)
{
return devices[0];
}
/* Scan the list printing every entry */
foreach (ICaptureDevice dev in devices)
{
......
}
2.开始截包,这里要注意的是DeviceMode.Promiscuous是winpcap的混杂模式,据说无线网截包时候不能使用该模式,可以换成normal什么的
if (device != null)
{
log.Info(string.Format("Found device. device name is {0}", device.Name));
device.OnPacketArrival +=
new PacketArrivalEventHandler(device_OnPacketArrival);
// Open the device for capturing
device.Open(DeviceMode.Promiscuous, readTimeoutMilliseconds);
// tcpdump filter to capture only TCP/IP packets
changeFilter("host " + ConfigurationRepository.SystemConfiguration.Host);
device.StartCapture();
}
3.编写截包事件,其实这里可以拿到IP packet的,就是TCP下层的Packet,而TcpPacket就是把ip的包拿出来做了一次过滤解析,拿到IP packet有什么好处呢?你一定会注意到,Tcp协议里面其实并没有存放ip地址,他的前20个字节里面并没有出现ip地址,这对于习惯了用ip标示主机的我们来说是不太方便的,但是如果你转到ip协议一看,前二十字节就有ip地址,这个大概就是ip协议存在的目的了,而tcp协议存放的是端口(这也是为什么说是端到端的协议),其实利用端口也可以达到同样的目的,因为浏览器一般是在客户端随机开一个端口,只要你拿到并记下来这个端口,其实大多数情况下也可以当做ip来用了,因为相对来说也是唯一的。
private static void device_OnPacketArrival(object sender, CaptureEventArgs e)
{
DateTime time = e.Packet.Timeval.Date;
int len = e.Packet.Data.Length;
Packet packet = Packet.ParsePacket(e.Packet.LinkLayerType, e.Packet.Data);
TcpPacket tcpPacket = TcpPacket.GetEncapsulated(packet);
try
{
......这里就是业务逻辑
}
catch (Exception ex)
{
log.Error(ex);
}
}
private static void device_OnPacketArrival(object sender, CaptureEventArgs e)
{
DateTime time = e.Packet.Timeval.Date;
int len = e.Packet.Data.Length;
Packet packet = Packet.ParsePacket(e.Packet.LinkLayerType, e.Packet.Data);
TcpPacket tcpPacket = TcpPacket.GetEncapsulated(packet);
try
{
......这里就是业务逻辑
}
catch (Exception ex)
{
log.Error(ex);
}
}
截包这里完了以后看业务的需要,是否还需要包重组,这个在正常的tcp应用中都是又操作系统来实现的,相对来说也比较复杂,但是在截包的时候,由于包是无序到来的,所以有时候需要自己进行tcp包重组,重组算法可以参考这个:http://www.rosoo.net/a/201012/10653.html, 个人觉得这是一篇好文,基本上已经把tcp协议吃透了想出来的算法,比较全面,作为应用,只要实现就行了。关于重组的内核代码可以参考这个:http://blog.dccmx.com/2011/03/libnids-tcp-reassembly/。实际上,只要不是特别细致的应用,是可以简化设计的。
4. 最后就是关闭截包了,这里一般是业务触发某个事件来关闭截包。如下代码段
if (device != null)
{
try
{
if (device.Started)
device.StopCapture();
}catch(Exception e)
{
log.Error("Can't stop capture", e);
}
finally
{
device.Close();
}
}
总的来说,这种方式比较繁琐的地方在于tcp包重组,这里需要充分了解tcp协议。
- 但是当3g上网卡出现后,上面的方式就不能用了,因为windows并不把modem当做一块网卡,遇到这种情况也有几种办法可以解决,一个是截取usb传输的包;另一个就是把截包推迟到上层。针对HTTP协议,可以把截包推迟到应用层。而这个思想有些类似fiddler,fiddler的实现原理就是把系统的代理切换到自己的一个服务,然后由自己转发,如果这个过程自己写,还是有点麻烦,但是稍微查一下,可以发现fiddler就提供几种方式来做这个事,第一个方式是以开发Fiddler插件的方式,第二个方式可以把fiddler当做一个standalone的应用程序,即将fiddler嵌入到自己的程序,http://www.fiddlertool.com/Fiddler/dev/(最新地址在这里http://fiddler.wikidot.com/)。实际上,如果是http的方式截包,有了这种方式以后基本上可以解决所有问题了。
Fiddler.CONFIG.IgnoreServerCertErrors = false;
Fiddler.FiddlerApplication.AfterSessionComplete += delegate(Fiddler.Session oS)
{
......这里就是业务逻辑
};
2.开始截包,这里需要指定一个代理端口
Fiddler.FiddlerApplication.Startup(8877, FiddlerCoreStartupFlags.Default);
3.停止截包
Fiddler.FiddlerApplication.Shutdown();