跨域?拒绝说概念(内含demo)
文章列出解决方案以及对应的demo,拒绝说概念,不在稀里糊涂。
什么情况出现跨域?
- 协议不同
- 域名不同
- 端口不同
跨域解决方案
1.同一个主域下不同子域之间的跨域请求 - document.domain+iframe
同一个 origin 下,父页面可以通过 iframe.contentWindow 直接访问 iframe 的全局变量、DOM 树等,iframe 可以也通过 parent/top 对父页面做同样的事情。
domain.html
<body><iframe id="ifr" src="http://b.tblog.com:3004/domain2.html"></iframe><script>document.domain = 'tblog.com';function aa(str) {console.log(str);}window.onload = function () {document.querySelector('#ifr').contentWindow.bb('aaa');}</script>
domain2.html
<body>2222222222<script>document.domain = 'tblog.com';function bb(str) {console.log(str);}parent.aa('bbb');</script></body>
完整demo
2. 完全不同源 - postMessage
html5新增API, 支持IE8+。
otherWindow.postMessage(message, targetOrigin, [transfer]);
- otherWindow 其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
- message 将要发送到其他 window的数据
- targetOrigin 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串""(表示无限制)或者一个URI。如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的targetOrigin,而不是。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。
- transfer 可选 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
传递过来的message的属性有:
- data 从其他 window 中传递过来的对象。
- origin 调用 postMessage 时消息发送方窗口的 origin . 这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成
- source 对发送消息的窗口对象的引用; 您可以使用此来在具有不同origin的两个窗口之间建立双向通信
下面index.html和index2.html通信
index.html
<body><input type="text" placeholder="http://b.tblog.com:3004/index2.html"><iframe src="http://192.168.101.5: 3004/index2.html" frameborder="0"></iframe><script>const input = document.querySelector('input');input.addEventListener('input', function () {window.frames[0].postMessage(this.value, '*');// window.frames[0].postMessage(this.value, 'http://192.168.101.5');// window.frames[0].postMessage(this.value, 'http://192.168.101.5:3004');});// 接收消息window.addEventListener('message', function (e) {input.value = e.data;console.log('父窗口', e.data);console.log('父窗口', e.source);console.log('父窗口', e.origin);});</script>
</body>
index2.html
<body>子窗口<input id="input" type="text" placeholder="http://a.tblog.com:3004/index.html"><script>const input = document.querySelector('#input');input.addEventListener('input', function () {window.parent.postMessage(this.value, '*');});// 接收消息window.addEventListener('message', function (e) {input.value = e.data;console.log('子窗口', e.data);console.log('子窗口', e.source);console.log('子窗口', e.origin);});</script>
</body>
完整demo
3. 完全不同源 - location.hash+iframe
原理是利用location.hash来进行传值。改变hash并不会导致页面刷新,所以可以利用hash值来进行数据传递,当然数据容量是有限的。 例如:假设a.tblog.com:3004 和 192.168.101.5:3004/index2.html
通信
原理:a.tblog.com:3004中index.html以iframe将192.168.101.5:3004/index2.html
页面引入,在192.168.101.5:3004/index2.html
中插入新的iframe, 此iframe引入的页面和a.tblog.com:3004同源,就可将192.168.101.5:3004/index2.html
的hash数据传入a.tblog.com:3004页面的hash值中。parent.parent.location.hash = self.location.hash.substring(1);
a.tblog.com:3004/index.html
<script>var ifr = document.createElement('iframe');ifr.style.display = 'none';ifr.src = 'http://192.168.101.5:3004/ index2.html#paramdo';document.body.appendChild(ifr);function checkHash() {try {var data = location.hash ? location.hash.substring(1) : '';if (console.log) {console.log('Now the data is ' + data);}} catch (e) { };}setInterval(checkHash, 2000); </script>
192.168.101.5:3004/ index2.html
<body><script>//模拟一个简单的参数处理操作 switch (location.hash) {case '#paramdo':callBack();break;case '#paramset'://do something…… break;}function callBack() {try {parent.location.hash = 'somedata';} catch (e) {var ifrproxy = document.createElement('iframe');ifrproxy.style.display = 'none';ifrproxy.src = 'http://a.tblog.com:3004/index3.html#somedata'; // 注意该文件在"a.com"域下 document.body.appendChild(ifrproxy);}} </script>
</body>
a.tblog.com:3004/index3.html
<body><script>//因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值 parent.parent.location.hash = self.location.hash.substring(1); </script>
</body>
完整demo
4. window.name + iframe 跨域
window.name 获取/设置窗口的名称。 窗口的名字主要用于为超链接和表单设置目标(targets)。窗口不需要有名称。 window.name属性可设置或者返回存放窗口名称的一个字符串, name值在不同页面或者不同域下加载后依旧存在,没有修改就不会发生变化,并且可以存储非常长的name(2MB)。
场景1 - 同源
a.html
<body><script type="text/javascript">const iframe = document.createElement('iframe');iframe.src = 'http://a.tblog.com:3004/b.html';iframe.style.display = 'none';document.body.appendChild(iframe);iframe.onload = function () {console.log(iframe.contentWindow.name)};</script></body>
b.html
<body><script>window.name = '子页面的数据';</script>
</body>
场景2 - 不同源
利用iframe中window.name在不同页面或者不同域下加载后依旧存在的特性。 a.tblog.com:3004/a.html
中通过iframe添加192.168.0.103:3004/b.html
(数据页面, 指定window.name 的值),监听iframe的load, 改变iframe的src与a.tblog.com:3004/a.html
同源代理页面a.tblog.com:3004/c.html
(空页面)。
a.tblog.com:3004/a.html
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
let state = 0;iframe.onload = function () {console.log('iframe.onload', state, iframe.contentWindow);if (state === 1) {const data = JSON.parse(iframe.contentWindow.name);console.log(data, state);iframe.contentWindow.document.write('');iframe.contentWindow.close();document.body.removeChild(iframe);} else if (state === 0) {state = 1;console.log('数据', window.name)iframe.contentWindow.location = 'http://a.tblog.com:3004/c.html';}
};iframe.src = 'http://192.168.0.103:3004/b.html';
document.body.appendChild(iframe);
完整demo
5. 跨域jsonp
jsonp原理:
- 首先是利用script标签的src属性来实现跨域。
- 客户端注册callback方法名,携带在URL上, 如'http://127.0.0.1:8080/getNews?callback=getData'
- 服务器响应后生成json, 将json放在刚才接收到的callback的函数中,就生成一段getData(json)
- 客户端浏览器将script 标签插入 DOM,解析script标签后,会执行getData(json)。 由于使用script标签的src属性,因此只支持get方法 客户端代码
<body><button class="get">get data</button><script>const btn = document.querySelector('.get');btn.addEventListener('click', function () {const script = document.createElement('script');script.setAttribute('src', 'http://127.0.0.1:8080/getNews?callback=getData');document.head.appendChild(script);document.head.removeChild(script);})function getData(news) {console.log(news)}</script>
</body>
服务端代码
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');http.createServer(function(req, res){const pathObj = url.parse(req.url, true);switch(pathObj.pathname){ case '/getNews': const news = [{id: 678}];res.setHeader('Content-type', 'text/json; charset=utf-8'); if(pathObj.query.callback){res.end(pathObj.query.callback + '(' + JSON.stringify(news) + ')');}else {res.end(JSON.stringify(news));} break; default:res.writeHead(404, 'not found');}
}).listen(8080);
完整demo
6. CORS跨域
原理
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。跨域资源共享( CORS )机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。
什么情况下需要CORS
- 前文提到的由 XMLHttpRequest 或 Fetch 发起的跨域 HTTP 请求。
- Web 字体 (CSS 中通过 @font-face 使用跨域字体资源), 因此,网站就可以发布 TrueType 字体资源,并只允许已授权网站进行跨站调用。
- WebGL 贴图
- 使用 drawImage 将 Images/video 画面绘制到 canvas
- 样式表(使用 CSSOM)
功能概述
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。允许服务器声明哪些源站通过浏览器有权限访问哪些资源。对于get以外的请求,浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。 真个过程浏览器自动完成,服务器会添加一些附加的头信息, 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
简单请求
某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”,请注意,该术语并不属于 Fetch (其中定义了 CORS)规范。只要同时满足以下两大条件,就属于简单请求:
(1) 请求方法是以下三种方法之一:HEADGETPOST(2)HTTP的头信息不超出以下几种字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
请求响应结果多出的字段:
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
- Access-Control-Allow-Origin
Access-Control-Allow-Origin: <origin> | *
; 该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求, 有次响应头字段就可以跨域 - Access-Control-Allow-Credentials
Access-Control-Allow-Credentials: true
; 当浏览器的credentials设置为true时, 此响应头表示是否允许浏览器读取response的内容,返回true则可以,其他值均不可以,Credentials可以是 cookies, authorization headers 或 TLS client certificates。
Access-Control-Allow-Credentials 头 工作中与XMLHttpRequest.withCredentials 或Fetch API中的Request() 构造器中的credentials 选项结合使用。Credentials必须在前后端都被配置(即the Access-Control-Allow-Credentials header 和 XHR 或Fetch request中都要配置)才能使带credentials的CORS请求成功。 如果withCredentials 为false,服务器同意发送Cookie,浏览器也不会发送,或者,服务器要求设置Cookie,浏览器也不会处理。
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
// 允许credentials:
Access-Control-Allow-Credentials: true// 使用带credentials的 XHR :
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);// 使用带credentials的 Fetch :
fetch(url, {credentials: 'include'
})
- Access-Control-Expose-Headers
在跨域访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma, 如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
代码如下:
<!-- 服务端 -->
http.createServer(function(req, res){const pathObj = url.parse(req.url, true);switch(pathObj.pathname){ case '/user': const news = [{id: 678}];res.setHeader('Content-type', 'text/json; charset=utf-8'); res.setHeader('Access-Control-Allow-Origin', req.headers.origin);// res.setHeader('Access-Control-Allow-Origin', '*');// 需要cookie等凭证是必须res.setHeader('Access-Control-Allow-Credentials', true);res.end(JSON.stringify(news)); break; default:res.writeHead(404, 'not found');}
}).listen(8080, (err) => {if (!err) {console.log('8080已启动');}
});
<!-- 客户端 -->
<body><script>const xhr = new XMLHttpRequest();xhr.open('GET', 'http://localhost:8080/user', true);// 需要cookie等凭证是必须xhr.withCredentials = true;xhr.onreadystatechange = (e) => {console.log('onreadystatechange', e)}xhr.send();</script>
</body>
完整demo
非简单请求
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。 以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
当请求满足下述任一条件时,即应首先发送预检请求:
- 使用了下面任一 HTTP 方法:
- put
- delete
- connect
- OPTIONS
- trace
- patch
- 人为设置了对cors安全首部字段集合外的其他首部字段, 该集合为:
- Accept
- Accept-Language
- Content-Language
- Content-Type
- DPR
- Downlink
- Save-data
- Viewport-Width
- Width
- Content-Type的值不属于下列之一:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 请求中的XMLHttpRequestUpload 对象注册了任意多个事件监听器。
- 请求中使用了ReadableStream对象。
如下是一个需要执行预检请求的 HTTP 请求:
<body><script>const invocation = new XMLHttpRequest();const url = 'http://localhost:8080/user';const body = JSON.stringify({ name: 'toringo' });function callOtherDomain() {if (invocation) {invocation.open('POST', url, true);invocation.setRequestHeader('X-PINGOTHER', 'pingpong');invocation.setRequestHeader('Content-Type', 'application/json');invocation.onreadystatechange = (e) => {console.log('onreadystatechange', e)};invocation.send(body);}}callOtherDomain();</script>
</body><!-- 服务端 -->
http.createServer(function(req, res){const pathObj = url.parse(req.url, true);switch(pathObj.pathname){ case '/user': const news = {id: 678};res.setHeader('Content-type', 'text/json; charset=utf-8'); res.setHeader('Access-Control-Allow-Origin', req.headers.origin);// res.setHeader('Access-Control-Allow-Origin', '*');// 需要cookie等凭证是必须res.setHeader('Access-Control-Allow-Credentials', true);res.end(JSON.stringify(news)); break; default:res.writeHead(404, 'not found');}
}).listen(8080, (err) => {if (!err) {console.log('8080已启动');}
});
浏览器请求结果:
cors2.html:1 Access to XMLHttpRequest at 'http://localhost:8080/user' from origin 'http://127.0.0.1:3004' has been blocked by CORS policy: Request header field x-pingother is not allowed by Access-Control-Allow-Headers in preflight response.
如图所示发起了预检请求,请求头部多了两个字段:
Access-Control-Request-Method: POST; // 该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法.
Access-Control-Request-Headers: Content-Type, X-PINGOTHER; 告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。
上例需要成功响应数据,服务端需要同意:
http.createServer(function(req, res){const pathObj = url.parse(req.url, true);switch(pathObj.pathname){ case '/user': const news = {id: 678};res.setHeader('Content-type', 'text/json; charset=utf-8'); res.setHeader('Access-Control-Allow-Origin', req.headers.origin);// 新增的res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');res.setHeader('Access-Control-Allow-Headers', 'X-PINGOTHER, Content-Type');res.setHeader('Access-Control-Max-Age', 86400);res.end(JSON.stringify(news)); break; default:res.writeHead(404, 'not found');}
}).listen(8080, (err) => {if (!err) {console.log('8080已启动');}
});
服务段新增的字段:
Access-Control-Allow-Origin: req.headers.origin
Access-Control-Allow-Methods: POST, GET, OPTIONS // 表明服务器允许客户端使用 POST, GET 和 OPTIONS 方法发起请求。该字段与 HTTP/1.1 Allow: response header 类似,但仅限于在需要访问控制的场景中使用。这是为了避免多次"预检"请求。
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type // 如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
Access-Control-Max-Age: 86400 // 表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
7. nodejs代理跨域
node中间件实现跨域代理,是通过一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登陆认证。
原理:服务器之间数据请求不存在跨域限制(同源策略是浏览器行为), 所以先将请求代理到代理服务器, 代理服务器在内部请求真实的服务器得到结果后end连接。
<!-- 服务 -->
http.createServer(function(req, res){const pathObj = url.parse(req.url, true);console.log('server', pathObj.pathname) switch(pathObj.pathname){ case '/user': const news = {id: 678};res.end(JSON.stringify(news)); break; default:res.setHeader('Content-type', 'text/json; charset=utf-8'); res.end('未知错误'); }
}).listen(4000, (err) => {if (!err) {console.log('4000已启动');}
});
<!-- 代理 -->
http.createServer(function(req, res){const pathObj = url.parse(req.url, true);switch(pathObj.pathname){ case '/user': res.setHeader('Content-type', 'text/json; charset=utf-8'); res.writeHead(200, {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Methods': 'POST, GET, OPTIONS','Access-Control-Allow-Headers': 'X-PINGOTHER, Content-Type',});console.log('proxy', req.method, pathObj.pathname);// 请求真实服务器const proxyRequest = http.request({host: '127.0.0.1',port: 4000,url: '/',path: pathObj.pathname,method: req.method,headers: req.headers}, (proxyRes) => {let body = '';proxyRes.on('data', (chunk) => {body += chunk;});proxyRes.on('end', () => {console.log('响应的数据 ' + body );res.end(body);})}).end();break; default:res.writeHead(404, 'not found');res.end(body);break; }
}).listen(8080, (err) => {if (!err) {console.log('8080已启动');}
});
<!-- 客户端 index.html -->
<body><script>const invocation = new XMLHttpRequest();const url = 'http://localhost:8080/user';const body = JSON.stringify({ name: 'toringo' });function callOtherDomain() {if (invocation) {invocation.open('POST', url, true);// invocation.setRequestHeader('X-PINGOTHER', 'pingpong');invocation.setRequestHeader('Content-Type', 'application/json');invocation.onreadystatechange = (e) => {console.log('onreadystatechange', e)};invocation.send(body);}}callOtherDomain();</script>
</body>
注意: 服务器和浏览器数据交互也需要遵循同源策略
-- 持续更新 --
Tips:
博客代码地址。~ github
参考文章
developer.mozilla.org/zh-CN/docs/…
vinc.top/2017/02/09/…
www.ruanyifeng.com/blog/2016/0…
segmentfault.com/a/119000000…
developer.mozilla.org/zh-CN/docs/…
developer.mozilla.org/zh-CN/docs/…
www.ruanyifeng.com/blog/2016/0…
转载于:https://juejin.im/post/5cc3e1b5f265da036207b12e
跨域?拒绝说概念(内含demo)相关推荐
- 跨域问题的一次深入研究
前言 最近在业务代码中深受跨域问题困扰,因此特别写一篇博客来记录一下自己对跨域的理解以及使用到的参考资料.本文的项目背景基于vue+vuex+axios+springboot.涉及以下内容: 何为跨域 ...
- 点击率预估模型汇总_CIKM20MiNet:阿里|跨域点击率预估混合兴趣模型
" 本文介绍了阿里提出的一种利用跨域信息的CTR预估模型,基于UC头条的应用场景,将新闻feed流作为源域,广告作为目标域.跨域点击率预估的最大优势在于通过使用跨域数据,目标域中的数据稀疏和 ...
- java 视频切片_关于视频播放、视频切片、跨域访问视频
关于视频播放.视频切片.跨域访问视频 前言 最近在着手部署上线做的一个视频网站,当我们部署到云服务器上后并开始测试视频观看并发量,发现了一个很严重的问题:带宽不足.9 或 10 个人同时观看视频的时候 ...
- Ajax请求,跨域小坑
今天在上班的时候,被坐在旁边项目经理叫过去问了一个Ajax请求跨域的问题,一开始没理解清楚也还有对这个没有理解的透,后面被打击的要死. 当时的需求是需要测试一个已发布的api接口,需要在本地写测试程序 ...
- 【论文解读】CIKM20-MiNet:阿里|跨域点击率预估混合兴趣模型
" 本文介绍了阿里提出的一种利用跨域信息的CTR预估模型,基于UC头条的应用场景,将新闻feed流作为源域,广告作为目标域.跨域点击率预估的最大优势在于通过使用跨域数据,目标域中的数据稀疏和 ...
- ASP.NET Core微服务(三)——【跨域配置】
ASP.NET Core微服务(三)--[跨域配置] 对应练习demo(跨域)下载路径(1积分):[https://download.csdn.net/download/feng8403000/151 ...
- javascript实现jsonp跨域问题+原理
在工作中往往存在跨域的问题 ,跨域是什么概念就不在这里了,搜这类问题的肯定已经知道了.下面直接探讨jsonp跨域原理 jspon跨域原理: 1.动态创建一个script标签 var script = ...
- 网络-HTTP请求跨域访问控制
本文根据MDN整理 我不说什么是跨域,网上很多,但是要说一下,跨域访问控制这个概念,是浏览器的行为,不是服务器的行为(虽然不少工作都是后端的工程师去配置),产生跨域访问控制的原因有两个: 1.前端发出 ...
- (转)HTML5开发中Access-Control-Allow-Origin跨域问题
今天准备通过JavaScript的方式调用问说问答的内容,由于使用的不同的二级域名,遇到了一个跨域问题,虽然可以使用JSON或者XML来解决这个问题,但是我们可以通过Access-Control-Al ...
最新文章
- scala报错20/08/31 23:48:40 WARN TaskSetManager: Lost task 1.0 in stage 0.0 (TID 1, 192.168.28.94, exec
- LNMT部署详细步骤并实现动静分离和负载均衡
- c语言随机产生arp报文,c语言构造arp报文
- html距离已过去多久,用javascript写的倒计时,从某年某月距离到今天还有多少时间...
- C#中使用MD5对用户密码加密与解密
- 记一道面试题:STL两个栈实现一个队列。
- 硬核!如何全面系统地自学 Java ?
- Layui 表格渲染
- 功能1 -- 顶部导航栏和返回顶部效果
- 精密星历卫星钟差插值程序
- get_digits
- ESP8266控制SG90舵机
- 约分最简分式 (15 分)
- 查询数据库空间(mysql和oracle)
- 新课发布-SpringBoot2.0缓存中间件Redis技术入门与实战(抢红包系统设计与实战)
- 【7.0】 数学建模 | 相关系数详解 | Person相关系数、Spearman相关系数
- SK海力士将以90亿美元收购英特尔的NAND闪存及存储业务
- rgb三色直方图的绘制
- 初学mininet之Mininet的基本命令
- 纪念日页面变灰是怎么实现的( filter: grayscale(100%);)
热门文章
- 用Genymotion来调试android应用
- Java 并发/多线程教程(四)-并发模型
- JAVA基础 (二)反射 深入解析反射机制
- iOS 之 UICollectionView
- 转:Tkinter教程之Text(2)篇
- ipv6改为ipv4
- sar分辨率公式_初识合成孔径雷达SAR
- elasticsearch 客户端工具_elasticsearch初使用
- android流量控制的实现,Android系统中P2P应用数据包捕获及流量控制研究
- python androidhelper kivy_学习qpython相关