NetAnalyzer笔记 之 九 使用C#对HTTP数据还原
[创建时间:2016-05-12 00:19:00]
在NetAnalyzer2016中加入了一个HTTP分析功能,很过用户对此都很感兴趣,那么今天写一下具体的实现方式,对我自己也算是一个总结吧,好了,废话就不多少了,直接开始吧。
本文是专注于HTTP数据的分析,所以前期数据包采集相关的内容并不会本文不会涉及,相关内容可以见 NetAnalyzer 笔记 四
在这边默认你已经获取到了http数据报文。
一,提取TCP会话数据
通过TCP/IP协议知道,我们可以通过网络层的IP地址可以唯一的确定一台主机,通过传输层的端口确定一个主机应用,而主机应用与另外一个主机应用的一次数据通信(报文交换)我们称之为一次会话,而建立的传输层协议为TCP上面的一次交互数据我们就称之为TCP会话数据。
一次常规的TCP会话
在这里我们可以看到本地主机应用标志 IP地址: 192.168.1.102 端口 55298 和远端主机应用标志 IP地址:124.238.254.191 端口 80 (80端口所对应的应用协议就是HTTP),通过这组标志,而下面每行记录都代表一TCP数据包,
每个TCP包包含了TCP状态标志,载荷数据(TCP数据包封装的数据)量,序列号等信息,大家可以留意一下每两条记录之间的序列号和载荷数据量之间的关系。
我们这里要提取的数据正是这组TCP报文中的载荷数据
这段代码用于定义并生成一个TCP标志
/// <summary>
/// 根据IP与端口构建ID
/// </summary>
public class ConnIDbyIPPort : IConnectionID
{
/// <summary>
/// Client IP
/// </summary>
IPAddress _srcIPaddress;//Client IP
/// <summary>
/// Server IP
/// </summary>
IPAddress _dstIPaddress;//Server IP
/// <summary>
/// Client port
/// </summary>
int _srcPort;//Client port
/// <summary>
/// Server port
/// </summary>
int _dstPort;//Server port
/// <summary>
/// 源IP地址属性
/// </summary>
public IPAddress SrcIPAddress
{
get { return _srcIPaddress; }
}
/// <summary>
/// 目标IP地址属性
/// </summary>
public IPAddress DstIPAddress
{
get { return _dstIPaddress; }
}
/// <summary>
/// 源端口属性
/// </summary>
public int SrcPort
{
get { return _srcPort; }
}
/// <summary>
/// 目的端口属性
/// </summary>
public int DstPort
{
get { return _dstPort; }
} /// <summary>
/// 判断当前的数据包的方向
/// 注意:
/// 此处所说的方向具有相对性
/// </summary>
/// <param name="srcPort"></param>
/// <returns></returns>
public bool isSameDirection(int srcPort)
{
if (srcPort == _srcPort)
return true;
else if (srcPort == _dstPort)
return false;
else
throw new ArgumentException("无效的参数,不是该链接之中的数据!");
}
/// <summary>
/// 构造方法
/// </summary>
/// <param name="ClientIPaddress">源IP地址</param>
/// <param name="ClientPort">源端口</param>
/// <param name="ServerIPaddress">目标IP地址</param>
/// <param name="ServerPort">目标端口</param>
public ConnIDbyIPPort(IPAddress ClientIPaddress, int ClientPort, IPAddress ServerIPaddress, int ServerPort)
{
_srcIPaddress = ClientIPaddress;
_srcPort = ClientPort;
_dstIPaddress = ServerIPaddress;
_dstPort = ServerPort;
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="rawpacket"></param>
public ConnIDbyIPPort(RawCapture rawpacket)
{
SetInfo(rawpacket);
}
/// <summary>
/// 创建ID
/// </summary>
/// <param name="rawpacket"></param>
private void SetInfo(RawCapture rawpacket)
{
try
{
Packet packet = Packet.ParsePacket(rawpacket.LinkLayerType, rawpacket.Data); TcpPacket tcp = TcpPacket.GetEncapsulated(packet);
if (tcp != null)
{
IpPacket ip = (IpPacket)tcp.ParentPacket;
_srcIPaddress = ip.SourceAddress;
_srcPort = tcp.SourcePort;
_dstIPaddress = ip.DestinationAddress;
_dstPort = tcp.DestinationPort;
return;
}
UdpPacket udp = UdpPacket.GetEncapsulated(packet);
if (udp != null)
{
IpPacket ip = (IpPacket)udp.ParentPacket;
_srcIPaddress = ip.SourceAddress;
_srcPort = udp.SourcePort;
_dstIPaddress = ip.DestinationAddress;
_dstPort = udp.DestinationPort;
return;
}
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// 创建ConnectionID;
/// </summary>
/// <param name="rawpacket">原始数据报文</param>
/// <returns></returns>
public IConnectionID CreatID(RawCapture rawpacket)
{
try
{
ConnIDbyIPPort id = new ConnIDbyIPPort(rawpacket);
return id; }
catch (Exception ex)
{
return null;
}
}
/// <summary>
/// 重写Equals()方法
/// </summary>
/// <param name="obj">所要比较的ConnectionID</param>
/// <returns>比较结果</returns>
public override bool Equals(object obj)//重写的Equals,用于比较两个连接是否相同
{
if (obj == null)
return false;
ConnIDbyIPPort tempID = (ConnIDbyIPPort)obj;
if (this._srcIPaddress.Equals(tempID._srcIPaddress) && this._srcPort == tempID._srcPort && this._dstIPaddress.Equals(tempID._dstIPaddress) && this._dstPort.Equals(tempID._dstPort))//正向比较
{
return true;
}
else if (this._srcIPaddress.Equals(tempID._dstIPaddress) && this._srcPort == tempID._dstPort && this._dstIPaddress.Equals(tempID._srcIPaddress) &&this._dstPort==tempID._srcPort)//逆向比较
{
return true;
}
else
{
return false;
}
}
/// <summary>
/// 使标识用Equals进行比较
/// </summary>
/// <returns></returns>
public override int GetHashCode()//系统默认对象通过HashCode比较,故在此屏蔽此方法
{
return ;
}
/// <summary>
/// 重写ToString()方法
/// </summary>
/// <returns>ConnectionID</returns>
public override string ToString()//获取ID字符串
{
return _srcIPaddress.ToString() + ":" + _srcPort.ToString() + "--" +
_dstIPaddress.ToString() + ":" + _dstPort.ToString();
}
#region 操作符重载
//public static bool operator==(ConnectionID id1,ConnectionID id2)
//{
// return id1.Equals(id2);
//}
//public static bool operator !=(ConnectionID id1, ConnectionID id2)
//{
// return !id1.Equals(id2);
//}
#endregion }
对于这段代码的使用方法如下:
// 选择一个RawCapture
RawCapture rawPacket = PacketList[i];
// 通过RawCapture 获取到一个标志
var tmpId = new ConnIDbyIPPort(rawPacket);
通过标志位挑选会话数据
/// <summary>
/// 通过ID和连接类型获取原始数据列表
/// </summary>
/// <param name="id"></param>
/// <param name="connType">之定义了三种,以后会扩展</param>
/// <returns></returns>
private RawCapture[] getConnectionList(IConnectionID id, ConnectionType connType)
{
if (id == null)
return null;
try
{
//var values = from v in PacketList
// where id.Equals(id.CreatID(v))
// select v;
//if (values == null)
// return null; List<RawCapture> values = new List<RawCapture>();
foreach (var i in PacketList)
{
if (id.Equals(id.CreatID(i)))
{
values.Add(i);
}
} return values.ToArray();
}
catch (Exception ex)
{
MessageBox.Show("正在获取网络获取相关数据,请稍后再试!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return null;
通过这段代码,我们最终获取到了这组TCP数据,接下来就是载荷数据的提取
二,TCP载荷数据的提取与组合
我们现在获取到的数据包都是TCP,接下来我们要面对两个重要的问题:
1、对于HTTP协议中传输的文本,图片,文件等数据有可能需要多个TCP数据包才能发送或接受完整;
2、存在一些载荷数据为0的TCP数据包,这些数据包对于TCP的会话特别重要(在TCP中完成确认或重传机制,保证数据可靠性),但是对于我们分析数据并没有太多的意义。
根据以上两点,我们需要对得到的这组会话数据进行重新的组合,这里的基本思路是,建立一个单向链表结构,以第一个载荷数据不为空的数据包作进行拆包提取特征量和载荷数据,然后移到下一个载荷数据不为空的数据包,提取数据包特征量,与当前节点特征量比较,如果方向一致(端口号相同)则将载荷数据缓存到当前节点,如果不一致(端口号不相同)则生成新的节点,生成特征,提取载荷数据缓存,并把上个节点指向新节点,
新结构示意图
对于每个节点的定义如下:
/// <summary>
/// TCP单向载荷数据节点
/// </summary>
class DataNode
{
/// <summary>
/// 构造
/// </summary>
/// <param name="port"> 端口</param>
public DataNode(ushort port)
{
this.Port = port;
this.Buffer = new List<byte>();
} public DataNode NextNode { get; set; } public ushort Port { get; private set; }
public List<byte> Buffer { get; private set; } public void InsertData(byte[] data)
{
this.Buffer.AddRange(data);
}
}
构建单向列表
/// <summary>
/// 头部节点
/// </summary>
private DataNode HeadNode; //当前比对的节点
private DataNode flagNode; public void InsertData(byte[] data, ushort port)
{
// 载荷数据判空
if (data == null || data.Length == )
return;
// 生成头部节点
if (HeadNode == null)
{
flagNode = new DataNode(port);
HeadNode = flagNode;
}
// 插入数据 和 生成后续节点
if (port == flagNode.Port)
{
flagNode.InsertData(data);
}
else
{
var tmpNode = new DataNode(port);
flagNode.NextNode = tmpNode;
tmpNode.InsertData(data);
flagNode = tmpNode;
}
}
TCP会话的转换
hp = new HttpHelper.HttpHelper(); foreach (var rawPacket in PacketList)
{
Packet packet = Packet.ParsePacket(rawPacket.LinkLayerType, rawPacket.Data);
TcpPacket tcp = TcpPacket.GetEncapsulated(packet);
if (tcp != null)//TCP
{
hp.InsertData(tcp.PayloadData, tcp.SourcePort);
}
}
自此,我们已经获取到了,期望的数据结构模型,那么下一节就要着手开始提取HTTP信息了。
三,http特征量提取
我们知道常规的http协议包含消息头和数据两部分:消息头是由ASCII编码的多条命令行数据组成,每行之间使用CRLF(回车和换行)进行分割,消息头结束后,需要多增加一个CRLF,
一个典型的http请求与回复会话(根据上一节内容,这段数据,红色部分为头部节点,蓝色部分在第二个节点中)
有图可知,客户端发起一个 get 的请求(红色部分),该请求并没有携带数据;服务端回复 http/1.1 200 OK 表示已经接受请求,并返回数据(蓝色部分),服务请返回的数据除了http消息头 还有自己所带的数据。
我们可以很轻松的看到http协议具体的内容,但是对于数据部分是一对乱码,那么接下来任务就是要根据http消息头获取这些乱码的信息
因为本篇以实践为主,并不想讨论具体的http命令,所以此处只对部分命令做一些详细分析,其他命令,网上内容很多请自行搜索。
通过http协议对回复数据(第二个节点)查找到以下两个字段:
1. Content-Type: application/zip 通过查询MIME 我们知道这是一个*.zip文件,那也就是消息头后面的乱码事实上是一个zip压缩文件,
2. Content-Length: 7261 由此我们判断出这个zip文件的大小为 7261个字节
对于一些其他的命令对我们后续还原数据不大,直接跳过。
至此我们就找到了http协议特征量,但是因为http传输数据格式的多样性,传输过程的复杂性,有些服务器的http协议特征更为复杂
HTTP/1.1 200 OK
Date: Wed, 11 May 2016 14:47:47 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Cache-Control: public, max-age=137
Expires: Wed, 11 May 2016 14:50:05 GMT
Last-Modified: Wed, 11 May 2016 14:45:05 GMT
Vary: *
X-UA-Compatible: IE=10
Content-Encoding: gzip
这段http回复中可以看到数据类型变为 Content-Type: text/html外,没有了content-Length 字段,但是增加几条新的字段,分别是:
1. charset=utf-8 这部分对于数据类型为字符串的还原非常重要,尤其是对于非英语文本的还原,
2. Transfer-Encoding: chunked 这个字段,用于标出动态加载的数据 详细信息请参见:http://www.cnblogs.com/zhaozhan/archive/2010/08/24/1807639.html
3. Content-Encoding: gzip 为了缩小数据传输量,使用该字段进行数据压缩,所以我们在还原数据的时候遇到该字段就需要解压缩
好了,扯了一大堆,那我们开始准备提取这些特征字段吧
先定义一个http数据结构
public class HttpEntity
{
/// <summary>
/// 消息头
/// </summary>
public string HeadStr { get; set; }
/// <summary>
/// 数据 文本,图片或二进制数据
/// </summary>
public object Data { get; set; }
/// <summary>
/// 数据类型
/// </summary>
public Type DataType { get; set; }
/// <summary>
/// 扩展名
/// </summary>
public string ExtName { get; set; } }
接下来对上一节提到的数据链表以此进行消息头提取
/// <summary>
/// 获取http实体列表
/// </summary>
/// <param name="encode">预指定的文本编码方式</param>
/// <returns></returns>
public List<HttpEntity> GetHttpEntityList(Encoding encode)
{
List<HttpEntity> DataList = new List<HttpEntity>(); for (DataNode tmp = HeadNode; tmp != null; tmp = tmp.NextNode)
{
for (int i = ; i <= tmp.Buffer.Count - ; i++)
{
if (tmp.Buffer[i] == 0x0D && tmp.Buffer[i + ] == 0x0A && tmp.Buffer[i + ] == 0x0D && tmp.Buffer[i + ] == 0x0A)
{
string headStr = Encoding.ASCII.GetString(tmp.Buffer.Take(i + ).ToArray());
DataList.Add(getInnerStr(tmp.Buffer.Skip(i + ).ToArray(), headStr, encode));
break;
}
}
} return DataList;
}
这里使用for循环,从链表表头开始以此查找两组CRLF(0x0D 0x0A 0x0D 0x0A)直到直到之后,截取前面的数据并使用ASCII进行解码为headStr ,而后面的数据就可以根据前面获取到的消息头进行进一步还原了,在 getInnerStr() 方法中完成
接下来我们继续看getInnerStr()方法的前半部分
private HttpEntity getInnerStr(byte[] data, string headFlag, Encoding defaultEncode)
{ //最终数据呈现
HttpEntity result = new HttpEntity();
result.HeadStr = headFlag; if (data == null || data.Length == )
return result; StringBuilder sbr = new StringBuilder(); //是否使用chunked
bool isChunked = headFlag.ToLower().Contains("transfer-encoding: chunked");
bool isGzip = headFlag.ToLower().Contains("content-encoding: gzip");
string contentType = "";
string charSet = "";
int ContentLength = ;
Encoding enCode = defaultEncode;
var mtype = Regex.Match(headFlag, @"Content-Type:\s*(\w+\/[\w\.\-\+]+)", RegexOptions.IgnoreCase);
if (mtype.Success && mtype.Groups.Count >= )
{
contentType = mtype.Groups[].Value.Trim().ToLower();
}
var Mchar = Regex.Match(headFlag, @"charset=\s*([-\w]+)", RegexOptions.IgnoreCase);
if (Mchar.Success && Mchar.Groups.Count >= )
{
charSet = Mchar.Groups[].Value.Trim();
if (charSet.ToLower() == "utf8")
{
charSet = "utf-8";
}
try
{
enCode = Encoding.GetEncoding(charSet);
}
catch (Exception)
{
enCode = defaultEncode;
}
} var MLength = Regex.Match(headFlag, @"Content-Length:\s*(\d+)", RegexOptions.IgnoreCase);
if (MLength.Success && MLength.Groups.Count >= )
{
ContentLength = int.Parse(MLength.Groups[].Value.Trim());
}
…………
这里使用正则表达式和字符串查找功能,在消息头中分别查找和去取对应的字段(对于字符编码,有些服务器使用的utf8 在解码是需要改为utf-8 否则会引起异常)
代码相对比较简短,这里就不做过多介绍了。
四,http数据解析(包括chunked 和 gzip)
通过上一节,我们已经拿到了http消息头的特征信息和数据,这部分就开始还原数据,具体有三个步骤:
1. 数据提取,数据提取基本就是找开始位置和数据长度,根据页面加载方式的不同,数据头提取分为 通过Content-Length 提取和 通过 Chunked 提取两种方法,
2. 数据解压缩,有时候我们获得的数据可能经过gzip压缩的,所以这个时候要对数据进行解压缩处理。
3. 数据还原,对于不同的数据有着不同的还原方式,转为字符串、保存为图片,生成文件等等
那接下来就开始看代码吧,首先我们补上上一节方法的后半部分
/// 数据整合
byte[] rawData = null;
if (isChunked)// 通过Chunked获取数据
{
GetchunkedData(data);
rawData = ChunkBuffer.ToArray();
}
else if (ContentLength > && ContentLength <= data.Length)//通过ContentLength 获取数
{
rawData = data.Take(ContentLength).ToArray();
}
else
{
rawData = data;
}
if (rawData == null && rawData.Length == )
return result; /// 数据解压缩
if (isGzip)
{
rawData = Tools.GzipDecompress(rawData);
}
if (rawData == null && rawData.Length == )
return result; //获取扩展名
string extName = "";
if (EimeData.EimeDic.Keys.Contains(contentType))
{
extName = EimeData.EimeDic[contentType];
}
result.ExtName = extName;
//获取数据或类型
switch (Tools.GetMimeType(contentType))
{
case MimeType.Text:
result.Data = Tools.ShowPopFormStr(enCode.GetString(rawData));
result.DataType = typeof(string);
break;
case MimeType.Image:
result.Data = rawData;
result.DataType = typeof(System.Drawing.Image);
break;
default:
result.Data = rawData;
result.DataType = typeof(byte[]);
break;
}
return result;
首先是整合数据,我们通过chunked、content-length等特征字段对获取的数据进行重新整合,这里涉及到Chunked数据整合,那让我们一起来看看代码吧
(具体chunked数据形式自行搜索)
/// <summary>
/// Chunked数据缓存列表
/// </summary>
List<byte> ChunkBuffer = new List<byte>();
private void GetchunkedData(byte[] data)
{
if (data.Length == )
return;
for (int i = ; i < data.Length; i++)
{
//查找CRCL标志
if (data[i] == 0x0D && data[i + ] == 0x0A)
{
int count = data.Length - ;
try
{
//获取下一段数据的长度 bytes -> ASCII字符 ->加上0x前缀 (如:0x34) -> 转为数值(下一块的数量长度)
count = Convert.ToInt32("0x" + Encoding.ASCII.GetString(data.Take(i).ToArray()), );
}
catch (Exception ex)
{
//产生异常的直接返回
ChunkBuffer.AddRange(data.Skip(i - ).ToArray());
break;
}
//如果为0表示完成Chunked数据转化跳出循环返回
if (count == )
break; if (i + + count <= data.Length)
{
//加入已经计算好的数据
ChunkBuffer.AddRange(data.Skip(i + ).Take(count));
//递归 进行下一轮匹配
GetchunkedData(data.Skip(i + + count).ToArray());
}
else
{
//对于存在数据不完整的 直接返回
ChunkBuffer.AddRange(data.Skip(i - ).ToArray());
}
break;
}
}
}
在这里预先定义一个缓存列表 ChunkBuffer, 然后通过GetchunkedData()方法递归调用最后获取到需要的数据。
此时我们已经获取到了数据,那么就可以考虑是否解压缩的问题了,解压缩比较简单,这里依然直接贴代码了
/// <summary>
/// 通过Gzip方式解压缩数据
/// </summary>
/// <param name="data">字节数据</param>
/// <returns></returns>
public static byte[] GzipDecompress(byte[] data)
{
//解压
using (MemoryStream dms = new MemoryStream())
{
using (MemoryStream cms = new MemoryStream(data))
{
using (System.IO.Compression.GZipStream gzip = new System.IO.Compression.GZipStream(cms, System.IO.Compression.CompressionMode.Decompress))
{
byte[] bytes = new byte[];
int len = ;
int totalLen = ;
//读取压缩流,同时会被解压
try
{
while ((len = gzip.Read(bytes, , bytes.Length)) > )
{
dms.Write(bytes, , len);
totalLen += len;
}
}
catch (InvalidDataException ex)
{
//dms.Write(data.Skip(totalLen).ToArray(),0,data.Length-totalLen);
}
}
}
return dms.ToArray();
}
}
这样我们就拿到了真正需要的数据了
最后我们就可以通过 Content-Type 来判断将数据还原为那种类型,就这样我们就获得了需要的数据
接下来我们看看解析完的数据
首先是zip的数据:
导出为文件后
最后是哪个带有Chunked 和gzip 标志的数据还原
五,更加可靠的数据还原
通过上面的数据还原方法,已经基本满足我们的数据还原需求,但是因为网络、计算机、以及我们程序本身的问题,我们拿到的数据包并不会严格的按照发包的先后顺序获取到数据包,而且因为TCP协议确认重传,有可能发生在某个数据包中,及发送端发送了一个1440的数据包,接收端有可能只取700个字节,剩余的需要重现传输,对于监控端,如果没有处理这种情形的机制,就会造成数据差生误差而不能进行正确还原,更别说还有丢包的情况。
而这种机制就是TCP的数据重组,通过建立合理的模型对tcp状态迁移、序列号、数量等一系列变量进行统一的规划提取还原,最终生成完成而且正确的数据。因为本篇只是简单讨论http数据的还原,所以此处不打算深入的展开, 如果有时间,再写一篇关于TCP重做的文章吧。
感谢你的阅读,欢迎使用NetAnalyzer,欢迎关注NetAnalyzer公众平台。
NetAnalyzer笔记 之 九 使用C#对HTTP数据还原的更多相关文章
- NetAnalyzer笔记 目录
目录 NetAnalyzer笔记 之 一 开篇语 NetAnalyzer笔记 之 二 简单的协议分析 NetAnalyzer笔记 之 三 用C++做一个抓包程序 NetAnalyzer笔记 之 四 C ...
- 简单的玩玩etimer <contiki学习笔记之九 补充>
这幅图片是对前面 <<contiki学习笔记之九>> 的一个补充说明. 简单的玩玩etimer <contiki学习笔记之九> 或许,自己正在掀开contiki ...
- NetAnalyzer笔记 之 五 一些抓包技巧分享(不定期更新)
[创建时间:2016-03-12 10:00:00] [更新时间:2016-05-21 10:00:00] NetAnalyzer下载地址 前一段时间应为工作关系,NetAnalyzer笔记系列已经很 ...
- NetAnalyzer笔记 之 八 NetAnalyzer2016使用方法(2)
[创建时间:2016-05-06 22:07:00] NetAnalyzer下载地址 在写本篇的时候,NetAnalyzer 3.1版本已经发布,所以本篇就以最新版本的为例继续使用,并且顺带说明一下, ...
- VSTO学习笔记(九)浅谈Excel内容比较
原文:VSTO学习笔记(九)浅谈Excel内容比较 说起文件内容比较,或许我们首先想到的是UltraCompare这类专业比较的软件,其功能非常强大,能够对基于文本的文件内容作出快速.准确的比较,有详 ...
- Python学习笔记(九)
Python学习笔记(九): 装饰器(函数) 内置函数 1. 装饰器 1. 作用域 2. 高阶函数 3. 闭包 如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就 ...
- 《C#从现象到本质》读书笔记(九)第11章C#的数据结构
<C#从现象到本质>读书笔记(九)第11章C#的数据结构 C#中的数据结构可以分为两类:非泛型数据结构和泛型数据结构. 通常迭代器接口需要实现的方法有:1)hasNext,是否还有下一个元 ...
- 深度学习课程笔记(九)VAE 相关推导和应用
深度学习课程笔记(九)VAE 相关推导和应用 2018-07-10 22:18:03 Reference: 1. TensorFlow code: https://jmetzen.github.io/ ...
- python3.4学习笔记(十九) 同一台机器同时安装 python2.7 和 python3.4的解决方法
python3.4学习笔记(十九) 同一台机器同时安装 python2.7 和 python3.4的解决方法 同一台机器同时安装 python2.7 和 python3.4不会冲突.安装在不同目录,然 ...
随机推荐
- 遍历Jenkins全部项目的配置
随着任务的增多.须要一个脚本能够检查全部的jenkins project的配置.比方提取任务计划配置,开发人员信息等. 首先要能够得到全部的project名称. 能够通过REST API实现: htt ...
- jquery——zTree, 完美好用的树插件
Demo 这绝对是我见过最完美的tree了,尽管是国产货,但一点不输国外产品,国外的还没有见过这么强的. _______________________________________________ ...
- 常用语言api语法Cheat Sheet
http://overapi.com/jquery/ OverAPI.com Python jQuery NodeJS PHP Java Ruby Javascript ActionScript CS ...
- Android之来历
Android一词的本义指“机器人”,同时也是谷歌于2007年11月5日宣布的基于Linux平台的开源手机操作系统的名称,该平台由操作系统.中间件.用户界面和应用 软件组成,号称是首个为移动终端打造的 ...
- Intent 能传递的数据类型
1. Serializable,将对象序列化成二进制数据传递 2. charsequence: 主要用来传递String,char等 3. parcelable: 这个android提供的一种新的类型 ...
- NSUserDefaults的使用方法
NSUserDefaults对象是用来保存,恢复应用程序相关的偏好设置,配置数据等等,用户再次打开程序或开机后这些数据仍然存在.默认系统允许应用程序自定义它的行为去迎合用户的喜好.你可以在程序运行的时 ...
- MRC和ARC混编
iOS5.0以后就开始可以使用ARC( Automatic Reference Counting:自动引用计数)来代替之前的MRC(Manual Reference Counting:人工引用计数). ...
- debian系(Ubuntu)安装jenkins(持续集成)
wget -q -O - http://pkg.jenkins-ci.org/debian/jenkins-ci.org.key | sudo apt-key add - sudo sh -c 'ec ...
- c++中vector等容器的实现机制
stl容器区别: vector list deque set map-底层实现 stl容器区别: vector list deque set map (转) 在STL中基本容器有: vector.li ...
- hdu 5188
it's a dp difficult problem 试想如果我们遇见这样一道题,: 有n道题目,每道题有一个得分v和用时t: 我们要得够w分:用时最少 怎么做?? 这是一个裸奔的01背包 如 ...