android 代理服务器端通过url访问tornado服务器,那么tornado怎么返回消息给android 代理服务器客户端?

Java和Android Http连接程序:使用java.net.URL 下载服务器图片到客户端
本博客前面博文中利用org.apache.http包中API进行Android客户端HTTP连接的例子:
另一种常用的建立Http连接的常用方式是利用Java在JDK中提供的类,也即本文要演示的方法,本文的例子程序实现的功能是从服务器上下载图片到客户端。
关于两种建立Http连接方法(apache的包和JDK的包)的讨论可以看看后面的参考链接。
服务器端需要准备图片,因为是Demo程序,所以我就准备了一张图片,然后把它放在Web Project的WebRoot路径下:
然后只要启动Tomcat,ipconfig查出ip地址,放在之后要用的路径中就可以了。
Java程序:Http连接 获取并下载服务器端图片
写一个工具类:
其中第一个方法根据给出的服务器地址及资源路径得到输入流:
public static InputStream getInputStream(String path)
InputStream inputStream = null;
HttpURLConnection httpURLConnection = null;
URL url = new URL(path);
if (null != url)
httpURLConnection = (HttpURLConnection) url.openConnection();
// 设置连接网络的超时时间
httpURLConnection.setConnectTimeout(5000);
// 打开输入流
httpURLConnection.setDoInput(true);
// 设置本次Http请求使用的方法
httpURLConnection.setRequestMethod(&GET&);
if (200 == httpURLConnection.getResponseCode())
// 从服务器获得一个输入流
inputStream = httpURLConnection.getInputStream();
catch (MalformedURLException e)
e.printStackTrace();
catch (IOException e)
e.printStackTrace();
return inputS
第二个方法根据输入流将文件存储在本地一个路径:&
public static void saveInputStream(InputStream inputStream,
String saveToPath)
byte[] data = new byte[1024];
int len = 0;
FileOutputStream fileOutputStream = null;
fileOutputStream = new FileOutputStream(saveToPath);
while (-1 != (len = inputStream.read(data)))
fileOutputStream.write(data, 0, len);
catch (IOException e)
e.printStackTrace();
if (null != inputStream)
inputStream.close();
catch (IOException e)
e.printStackTrace();
if (null != fileOutputStream)
fileOutputStream.close();
catch (IOException e)
e.printStackTrace();
完整代码:&
package com.meng.
import java.io.FileOutputS
import java.io.IOE
import java.io.InputS
import java.net.HttpURLC
import java.net.MalformedURLE
import java.net.URL;
public class HttpUtils
public static InputStream getInputStream(String path)
InputStream inputStream = null;
HttpURLConnection httpURLConnection = null;
URL url = new URL(path);
if (null != url)
httpURLConnection = (HttpURLConnection) url.openConnection();
// 设置连接网络的超时时间
httpURLConnection.setConnectTimeout(5000);
// 打开输入流
httpURLConnection.setDoInput(true);
// 设置本次Http请求使用的方法
httpURLConnection.setRequestMethod(&GET&);
if (200 == httpURLConnection.getResponseCode())
// 从服务器获得一个输入流
inputStream = httpURLConnection.getInputStream();
catch (MalformedURLException e)
e.printStackTrace();
catch (IOException e)
e.printStackTrace();
return inputS
public static void saveInputStream(InputStream inputStream,
String saveToPath)
byte[] data = new byte[1024];
int len = 0;
FileOutputStream fileOutputStream = null;
fileOutputStream = new FileOutputStream(saveToPath);
while (-1 != (len = inputStream.read(data)))
fileOutputStream.write(data, 0, len);
catch (IOException e)
e.printStackTrace();
if (null != inputStream)
inputStream.close();
catch (IOException e)
e.printStackTrace();
if (null != fileOutputStream)
fileOutputStream.close();
catch (IOException e)
e.printStackTrace();
HttpUtils.java
测试程序:&
package com.meng.
import java.io.InputS
import com.meng.utils.HttpU
public class HttpTest
private static String URL_PATH = &http://192.168.11.6:8080/HelloWeb/android.jpg&;
public static void main(String[] args)
InputStream inputStream = HttpUtils.getInputStream(URL_PATH);
HttpUtils.saveInputStream(inputStream,&D:\\test1.jpg&);
程序运行成功之后可以在指定路径下发现多了服务器端的那个图片。
Android客户端 Http连接:下载服务器端的图片到SD卡
Android的程序还需要考虑的几点是:
1.对SD卡的访问权限及操作。
2.为了不阻塞UI线程,下载操作放在独立的线程中。
3.加入了网路访问的检查,确认网络连接后再进行下载。
需要添加的权限
&!-- 往SDCard写入数据权限 --&
&uses-permission android:name=&android.permission.WRITE_EXTERNAL_STORAGE& /&
&!-- 联网权限 --&
&uses-permission android:name=&android.permission.INTERNET& /&
&!-- 获取网络状态的权限 --&
&uses-permission android:name=&android.permission.ACCESS_NETWORK_STATE& /&
布局文件如下:
&LinearLayout xmlns:android=&/apk/res/android&
xmlns:tools=&/tools&
android:layout_width=&match_parent&
android:layout_height=&match_parent&
android:orientation=&vertical& &
android:padding=&10dp&
android:id=&@+id/info&
android:layout_width=&match_parent&
android:layout_height=&wrap_content&
android:textSize=&14sp& /&
android:id=&@+id/btn&
android:layout_width=&match_parent&
android:layout_height=&wrap_content&
android:text=&Download Image&
android:textSize=&14sp& /&
&ImageView
android:padding=&10dp&
android:id=&@+id/image&
android:layout_width=&match_parent&
android:layout_height=&match_parent& /&
&/LinearLayout&
activity_image_download.xml
Activity中所做的就是按下按钮之后,连接服务器,将图片取出显示在ImageView里,同时存往SD卡的指定路径:
package com.mengexample.
import java.io.InputS
import com.windexample.utils.FileU
import com.windexample.utils.HttpU
import android.app.A
import android.content.C
import android.graphics.B
import android.graphics.BitmapF
import android.net.ConnectivityM
import android.net.NetworkI
import android.os.AsyncT
import android.os.B
import android.view.V
import android.view.View.OnClickL
import android.widget.B
import android.widget.ImageV
import android.widget.TextV
import android.widget.T
public class ImageDownloadActivity extends Activity
private static String URL_PATH = &http://192.168.11.6:8080/HelloWeb/android.jpg&;
private TextView mTextView = null;
private Button mButton = null;
private ImageView mImageView = null;
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_image_download);
mTextView = (TextView) findViewById();
mTextView.setText(URL_PATH);
mButton = (Button) findViewById(R.id.btn);
mButton.setOnClickListener(mBtnClickListener);
mImageView = (ImageView) findViewById(R.id.image);
private OnClickListener mBtnClickListener = new OnClickListener()
public void onClick(View v)
// 首先确认网络连接
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager
.getActiveNetworkInfo();
if (null != networkInfo && networkInfo.isConnected())
new DownloadImageTask().execute(URL_PATH);
Toast.makeText(ImageDownloadActivity.this,
&No network connection available&, Toast.LENGTH_SHORT)
private class DownloadImageTask extends AsyncTask&String, Void, Bitmap&
protected Bitmap doInBackground(String... params)
String path = params[0];
InputStream inputStream = HttpUtils.getInputStream(path);
// 从输入流得到位图
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
// 将图像存储到SD卡
FileUtils.saveToSDCard(bitmap, &TestImage&, &android.jpg&);
protected void onPostExecute(Bitmap result)
// 将图像显示出来
mImageView.setImageBitmap(result);
其中用到的两个工具类:
建立连接并获取输入流的方法和Java代码中的一样:
package com.windexample.
import java.io.IOE
import java.io.InputS
import java.net.HttpURLC
import java.net.MalformedURLE
import java.net.URL;
public class HttpUtils
public static InputStream getInputStream(String path)
InputStream inputStream = null;
HttpURLConnection httpURLConnection = null;
URL url = new URL(path);
if (null != url)
httpURLConnection = (HttpURLConnection) url.openConnection();
// 设置连接网络的超时时间
httpURLConnection.setConnectTimeout(5000);
// 打开输入流
httpURLConnection.setDoInput(true);
// 设置本次Http请求使用的方法
httpURLConnection.setRequestMethod(&GET&);
if (200 == httpURLConnection.getResponseCode())
// 从服务器获得一个输入流
inputStream = httpURLConnection.getInputStream();
catch (MalformedURLException e)
e.printStackTrace();
catch (IOException e)
e.printStackTrace();
return inputS
另一个辅助类提供了方法,将位图存入SD卡的指定路径:
package com.windexample.
import java.io.F
import java.io.FileNotFoundE
import java.io.FileOutputS
import java.io.IOE
import android.graphics.B
import android.os.E
import android.util.L
public class FileUtils
private static String TAG = &File&;
public static String getSDCardRootPath()
// SD卡根目录
String sDCardRoot = Environment.getExternalStorageDirectory()
.getAbsolutePath();
return sDCardR
public static void saveToSDCard(Bitmap bitmap, String filePath,
String fileName)
// 将所给文件路径和文件名与SD卡路径连接起来
String sdcardRoot = getSDCardRootPath();
// 创建文件路径
File dir = new File(sdcardRoot + File.separator + filePath);
Log.i(TAG, &dir: & + dir);
if (!dir.exists())
dir.mkdirs();
File targetFile = new File(dir, fileName);
targetFile.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(targetFile);
pressFormat.JPEG, 100, fileOutputStream);
fileOutputStream.flush();
fileOutputStream.close();
catch (FileNotFoundException e)
e.printStackTrace();
catch (IOException e)
e.printStackTrace();
程序运行后并得到图片后,结果如下:
& 并且查看SD卡下的TestImage路径,发现其中有这个图片文件。
Android Training: Connecting to the Network:
Android Training: Processes and Threads
老罗Android开发视频教程。
Android之网络编程 系列博文:
本博客前面博文中利用org.apache.http包中API进行Android客户端HTTP连接的例子:
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:300531次
积分:3874
积分:3874
排名:第3775名
原创:31篇
转载:349篇
评论:52条
(4)(1)(34)(22)(71)(1)(9)(21)(1)(3)(8)(2)(26)(52)(51)(21)(8)(7)(5)(11)(5)(2)(3)(1)(1)(2)(2)(10)1758人阅读
上小节我们以简单的两个小实例讲解了在ANDROID系统上去操作网络数据的过程,下面我们继续以小案例来深入讲解关于在ANDROID系统上对网络的实际操作,在这里我就以一个通过获取服务端最新信息来实时的显示最新的数据在ANDROID手机客户端为列,类似于现在一些手机应用的资讯显示方式,如,通过服务端信息的更新来实时监测自己的应用信息的最新信息,对其匹配否来判断更新点,下面我们就来详细讲解要实现的全过程吧:
环境描述:首先有一个显示列表:XXX文件,大小XXXM,文件格式XXX,如果是视频的话,就会多添加一项,播放长度XXXX小时/分钟,在这里,我们采取判断,如果播放时间大于一个小时的话,就以小时计算,如果小于小时的话,就以分钟计算,这时当客户端关注服务器的数据有所更新时,ANDROID客户端就会发一个通知告诉浏览的用户是否更新,在处理这个更新时,我采用的是用户可自定义,如用户可以自己定制接受文件更新的方式有:默认为提示1分钟后自动更新,可设置不需要提示直接更新,与弹出对话框提示更新,或收到提示后按时5分钟提示一次希望你手动更新,并显示更新的记录
环境描述完了,下面我就来描述一下我们怎么去实现上面这个环境要求呢,这就需要我们去了解更多的关于网络知识与ANDROUD本地组件服务机制了,先我们假设列表已经正常显示,此时我们需要搭建服务端,相对来说服务端会做很复杂的工作,只是特别注意的是,在这里我讲解在服务端存储信息的方式分为XML与JSON格式,而在其文件头请一定要把&?XML VERSION="1.0" ENCODING="UTF-8"?&写在头放,不管你的头行有多少数据,你都不能把它写在其它行,不然会报错的,其次在服务端做的工作我就不多说了,如安全验证,等,当然我会简单对数据更新提示与编码格式做些简单的介绍,我们先从第一次去获取服务端的信息开始讲起吧,在这里我们就不在使用HttpRULConnection做请求头发送访问响应了,而是直接采用SOCKET编程来实现,因为我们固定了服务端主机名与端口来提供访问,然而在服务端,我们采用的是多线程并发访问机制来接收的,主要信息就是提供一个线程池:ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().avilableProcessors()*100)提供一百个线程并发的访问量,其次就是服务端特别提供的监听端口XXX.再次就是存放断点数据用来在传输信息时假如信息传输在某一个时刻失败的话就会记住当前断点并在再次开启传输时获取之前断点继续传输,只是这个功能是在这个讲解中多处来的,为什么是多出来的了,它用在服务器端,主要是实现上传文件时使用,如果我们在下载文件时只需要在客户端保存断点即可,OK,多点知识不是坏处嘛,其次就是单独开启一个方法来启动这些线程,在服务端,我们采用了数据采集的方式去获取网络上最新的信息,另外添加了人工处理数据的方式对其数据分别进行更新,当然这里数服务器端技术,主要使用的是J2EE方面的技术,由于我之前做J2EE比较长,所以在之前的项目中两方面都有兼任,在这里我就大体介绍一下我们其中的项目在J2EE上的架构吧,说到这里不说也不行了,只是我不会说出具体项目与详细细节,因为项目还在保密其,其架构我就说个基本的,相对来说这个架构比较简单,但是内部机制不是你想象的那么简单,呵呵,有点调胃口的感觉,废话不多说了,不然谁看到这样的文章都受不了的,甚至给我痛骂一顿,首先我们后台管理显示层页面使用JSP,其次是数据控制层,数据控制层实现了数据缓存与数据服务机制,再次就是业务逻辑层,使用MEMBERSHIP与RBAC提供对业务逻辑的处理,再次据是资源访问层,使用J2EE技术构建实体并对其实体进行管理控制,采用SubSonic机制来实现,这层主要是对资源层,即数据库资源做控制输入输出,在J2EE里的基本架构也就这样,如果是详细描述的话,那可能就是个糟糕的问题了,所以就简单描述在这里吧,还有就是在WEB最上层是提供了对手机客户端访问的验证需求,这是必要的,你可以采用SOAPHEADER+动态SESSIONID来对其操作,这是完全可以的,这就不知不觉好像谈起架构来了,前端时间做架构好像有点上瘾了,既然这样,我也就简单来介绍下我们手机客户端的大体架构模式吧,记住是大体,即以MVC模式的架构方式:在V层我们主要提供了数据显示与输入,所以需要一个VIEW来提供显示,还需要一个数据适配器来对数据进行配置显示,即Data Adapter,其次就是在C层,在这层主要是UI对其的控制,它主要的作用就是关注数据与对V层数据提供进行控制,其次就是M层,这层就是针对M层来讲的,它主要的作用就是获取数据来源并想C层提供数据,在获取数据来源的方式有几种,一种是内存缓存,一种是本地,另一种就是通过网络,即服务器端获取数据,对其也可以保存在本地,OK,基本架构就是这样了,可能有的人很关心我们在通过手机客户端与网络服务器进行数据交互时的过程,下面我们就来详细讲解了:在客户端服务线程,我们会发一个定时器,使用SOCKET方式去获得服务端的某地址XXXX,端口XXX,以Socket socket = new Socket(服务IP,端口);OutputStream outStream = Socket.getOutputStream()与服务端获得连接,然后把请求头信息组建成一个实体对象:String head = "Content-Type="+...."/r/n",然后通过outStream.write(head.getBytes())进行写入并请求,只是需要注意的是我们在这里面使用了PushbackInputStream,因为我们在数据读取过程中,在头或者内容的最后都会跟上回车换行,我是说假如,当你读取到/R后/N没有读取到的话,这时候根据文件读取长度,就会多读到下一个内容的部分,那样的话,下一个文件内容就会出现不完整,然而普通流只能向前读不能向后读,所以我们使用PushbackInputStream里的一个unread()方法可以对其已读取的数据进行回退,只要经过是的判断,如判断读到当前内容最后为/N时结束第一段,否则的话返回重新读取当前字节,这样就可以避免我们在使用SOCKET编程实现数据传输的过程误读不能后退的弊端,只是我们在这里要尽量小心才是,由于我们是去获得信息,所以我们第一次不会有默认ID的唯一标志,这是需要在服务端进行分配,也就是服务端获得了相应的的请求并判断你是不是带有一个系统服务赋予的动态ID,如果没有的话,就会动态新创建一个SESSIONID,并为当前进行保留,同时把相应的信息附加到要传递给客户端信息的后面,作为信息对象传递给客户端,当客户端获取到这样的信息后就直接进行解析,由于我服务端采用的是JSON数据存储信息格式,所以在客户端,我们首先要去对信息进行解析,以对象对应格式做相应的处理,如获得的是图片格式,我们采用的是直接以二进制接收内容的方式进行显示:Bitmap bitmap = BitmapFactory.encodeArray(datas,0,data.lengt);如是一般文件就直接使用new String(file,"编码格式")来进行包装并返回与接收即可,只是在服务端对信息响应时,它不是单纯的只会去响应,而是定时的更新服务端信息,在提示,服务端信息是通过网络或者人工输入来更新的,所以在对数据进行服务更新时,需要对历史数据进行更新处理,这个过程比较复杂,在这里我就不继续介绍了,OK服务端基本就是这样了,然后就是客户端获得信息后,根据流输出后并组建标志读取位置来实现判读读取的问价内容以至于踩用不同的格式进行读取,并使用PushbackInputStream流防止误读机制来写入,这点非常重要,请牢记,因为我们每种格式的内容都做了标记,只有我们通过这个标记的判断才能知道我需要写入的内容为格式以至于错读方式的倒退,OK,严格控制它就可以获得不同的对象格式进行很好的分类来储存取了,如获得的是以MP3格式的文件内容,我们就需要
通过ANDROUD自带的解码器来接受并解码,当然,我是说,如果你需要播放的话,当然首先,你得把它保存在本地,然后就可以直接使用ANDROID自带的播放接口来实现更多的比ANDROID自己实现的跟票亮与复杂的播放效果,ANDROID的自带的只是一个很简单的DEMO,但是它提供了免费的结构可以让去施展它的尽可能:File autioFile = new File(Environment.getExtenalStorageDirectory(),fileName.getText().toString());MediaPlayer().release();MediaPlayer().setDataSource(audioFile.getAbsolutePath());MediaPlayer().prepare(); MediaPlayer().start();在这里我不会讲关于在ANDROID播放可能会存在哪些问题,在后面,我会讲到在ANDROID中在播放视频与音频时遇到的问题与问题的解决方案,以至于我们该怎么去实现一个实时播放的流媒体效果.OK,在流的传输机制中,我可能讲得很简单,只是流写入与写出我想大家读很了解了,所以我只是在这个过程中我们怎么去取得实时数据与可能遇到的问题进行了分析,以后在于遇到这样的问题时应该用什么样的思路去解决,最后在ANDROID客户端的显示我就不在介绍了,那是关于UI显示处理问题,再下来我就想说一下,当我们第一次获得数据并成功显示在手机客户端后,我们怎么去知道服务器数据有所更新了呢,在这里我们是这样做的:在手机客户端开启一个服务线程,这个服务线程用来定时方法,也就是所谓的实现了定时刷新的效果,也就是定时带上本地第一次重服务端带过来的SESSIONID以头的方式包装起来向服务端发出请求,当服务端获得请求后,会根据客户端带过来的SESSIONID与本地之前保存的SESSIONID进行对比,当然,本地之前保存的SESSIONID收服务端数据更新服务来控制,如果有数据更新就会重新生成一个动态的SESSIONID对其之前的SESSIONID进行替换,此时,如果客户端传过来的SESSIONID与服务端更新后的SESSIONID不匹配的话,就说明数据已经更新,此时,就会打开服务端流进行写入,客户端也相应的对其索取与更新,在项目之后,我突发灵感,下面是个人技术探索,没真正去试验与应用过,如果感兴趣的读者或技术偏爱可以根据思路去测试一下:就是在客户端使用BroadcastReceive来实现对数据进行监听,但是需要在服务端可以指定一个端口供客户端来实现监听,也就是当服务端有数据更新时,服务端会发送一个消息给那个端口,是监听那个端口的接收者通过某种方式获得这样的信息后并通知客户端数据服务线程,让他自动打开与服务器端对流连接的请求来获取最新的信息并更新自己.......等,好了,在这里我简单介绍一下,为什么我采用直接使用Socket编程而不直接使用HTTP协议封装后的组建来做呢,比如说可以使用HttpCilent来操作,如果你想在网络上指定某个功能单纯实现某样工作的话,然而你使用开源框架,它们都是对一些通用机制的封装,虽然也包含了你所要的功能,只是你需要的只是它很小的一部分功能,缺惯用了一些很不需要的代码,这样到来的直接后果会导致你的应用程序性能严重下降,除非你要使用到了开源框架里的所有功能,为了方便即可以去使用,毕竟人家开源的优化效果还是很明显的,再次我们在做请求数据传输的时候为什么不直接使用HttpURLConnection呢,因为,我们已经知道主机的IP与端口,这是长时间固定的,然而在实现这个功能时,只是应用的很小的一部分,还有别的功能也需要引入类似的实现,如要实现FTP上传文件等功能,这时我们就需要使用SOCKET支持的断点续传上传功能,然而HTTP协议是不支持,即使支持,在服务端也是限制传输大小的,比如一般的服务器可能只能上传到达2M,有的可能有5M或者更少,但是使用SOCKET是不受限制的,如就以FTP上传服务器为例:如果我们使用HttpURLConnection的话,HtttpURLConnection是继承了URLConnection,然而它使用了一个默认缓存机制限制了你在网络上的传输速率,一般它最大只支持1M,它传入的方式是以写入内存的方式进行传入,也就是供文件一次性传入与写出,然而这样使其内存是严重受限的,维持当你数传比较大的文件,如音频或者视频上G的文件时,就会报内存溢出,然而我们使用SOCKET流机制写入是不过分的依赖与内存的限制,它使用了一个ByteArrayOutputStream来实现缓冲机制的边读边写的方式据会把读入内存的数据第一时间读出来写入本地数据库或本地文件进行保存,同样,我们提到断点,在这里就多说几句吧,说为实现断点续传就是在当我们用手机下载或上传某个文件时,突然没了网络,或者手机没电了,此时文件下载或者上传就会被断开,这时,服务端与客户端都分别会记载其文件上传或者下载的文件节点位置并写入本地文件或者数据库,当我们再次启动这个应用的时候去在上次丢失的工作就会首先去获取本地之前保存的节点来寻找对应的节点来进行匹配,如果匹配的话就继续从匹配点开始读取或写入信息.好了今天就说在这里,下次我会对其部分以更详细的案例进行解说.....
版权声明:本文为博主原创文章,未经博主允许不得转载。
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:221028次
积分:2987
积分:2987
排名:第5647名
原创:78篇
评论:92条
(1)(1)(1)(4)(5)(2)(9)(2)(5)(8)(3)(1)(3)(2)(4)(9)(2)(5)(7)(10)(3)当前访客身份:游客 [
物联网+云计算
:怎么写代码怎么读取啊? ...
:很有用,3Q 1.5.4少个start 少个t
:原先也考虑过maple,但发现已经没怎么更新了,倒...
:非常有用 万分感谢
:通篇tornado出现的不超过1%.博主确实是在讲torna...
:就这几步没感觉麻烦呢
今日访问:6
昨日访问:103
本周访问:6
本月访问:3356
所有访问:69577
48.深入理解Tornado——一个异步web服务器
发表于2年前( 01:02)&&
阅读(298)&|&评论()
0人收藏此文章,
原文地址:
这篇文章的目的在于对Tornado这个异步服务器软件的底层进行一番探索。我采用自底向上的方式进行介绍,从轮训开始,向上一直到应用层,指出我认为有趣的部分。
所以,如果你有打算要阅读Tornado这个web框架的源码,又或者是你对一个异步web服务器是如何工作的感兴趣,我可以在这成为你的指导。
通过阅读这篇文章,你将可以:
自己写一个架构程序的服务器端部分,即使你是从拷贝别人的代码开始。
如果你想在Tornado框架上做开发,通过这篇文章你将更好的理解Tornado web框架。
在上,你将更有见解。
假设你还不知道Tornado是什么也不知道为什么应该对它感兴趣,那我将用简短的话来介绍Tornado这个项目。如果你已经对它有了兴趣,你可以跳去看下一节内容。
是一个用Python编写的异步HTTP服务器,同时也是一个web开发框架。该框架服务于网站,最近Facebook也在使用它。FriendFeed网站有和应用实时性强的特点,所以性能和可扩展性是很受重视的。由于现在它是开源的了(这得归功于Facebook),我们可以彻底的对它是如何工作的一探究竟。
我觉得对非阻塞式IO (nonblocking IO) 和异步IO (asynchronous IO &AIO)很有必要谈一谈。如果你已经完全知道他们是什么了,可以跳去看下一节。我尽可能的使用一些例子来说明它们是什么。
让我们假设你正在写一个需要请求一些来自其他服务器上的数据(比如数据库服务,再比如新浪微博的open api)的应用程序,然后呢这些请求将花费一个比较长的时间,假设需要花费5秒钟。大多数的web开发框架中处理请求的代码大概长这样:
&handler_request(self,&request): &&&&answ&
&self.remote_server.query(request)&
&this&takes&5&seconds
&&&&request.write_response(answ)
如果这些代码运行在单个线程中,你的服务器只能每5秒接收一个客户端的请求。在这5秒钟的时间里,服务器不能干其他任何事情,所以,你的服务效率是每秒0.2个请求,哦,这太糟糕了。&
当然,没人那么天真,大部分服务器会使用多线程技术来让服务器一次接收多个客户端的请求,我们假设你有20个线程,你将在性能上获得20倍的提高, 所以现在你的服务器效率是每秒接受4个请求,但这还是太低了,当然,你可以通过不断地提高线程的数量来解决这个问题,但是,线程在内存和调度方面的开销是 昂贵的,我怀疑如果你使用这种提高线程数量的方式将永远不可能达到每秒100个请求的效率。
如果使用AIO,达到每秒上千个请求的效率是非常轻松的事情。服务器请求处理的代码将被改成这样:
&handler_request(self,&request): &&&&self.remote_server.query_async(request,&self.response_received)&&&&&
&response_received(self,&request,&answ):&&&&
&this&is&called&5&seconds&later
&&&&request.write(answ)
AIO的思想是当我们在等待结果的时候不阻塞,转而我们给框架一个回调函数作为参数,让框架在有结果的时候通过回调函数通知我们。这样,服务器就可以被解放去接受其他客户端的请求了。
然而这也是AIO不太好的地方:代码有点不直观了。还有,如果你使用像Tornado这样的单线程AIO服务器软件,你需要时刻小心不要去阻塞什么,因为所有本该在当前返回的请求都会像上述处理那样被延迟返回。
关于异步IO,比当前这篇过分简单的介绍更好的学习资料请看 。
该项目由github托管,你可以通过如下命令获得,虽然通过阅读这篇文章你也可以不需要它是吧。
git&clone&git:
tornado.git
在tornado的子目录中,每个模块都应该有一个.py文件,你可以通过检查他们来判断你是否从已经从代码仓库中完整的迁出了项目。在每个源代码 的文件中,你都可以发现至少一个大段落的用来解释该模块的doc string,doc string中给出了一到两个关于如何使用该模块的例子。
IOLoop模块
让我们通过查看文件直接进入服务器的核心。这个模块是异步机制的核心。它包含了一系列已经打开的文件描述符(译者:也就是文件指针)和每个描述符的处理器(handlers)。它的功能是选择那些已经准备好读写的文件描述符,然后调用它们各自的处理器(一种IO多路复用的实现,其实就是socket众多IO模型中的select模型,在Java中就是NIO,译者注)。
可以通过调用add_handler()方法将一个socket加入IO循环中:
&add_handler(self,&fd,&handler,&events): &&&&
Registers&the&given&handler&to&receive&the&given&events&for&fd.
&&&&self._handlers[fd]&
&handler &&&&self._impl.register(fd,&events&
&self.ERROR)
_handlers这个字典类型的变量保存着文件描述符(其实就是socket,译者注)到当该文件描述符准备好时需要调用的方法的映射(在Tornado中,该方法被称为处理器)。然后,文件描述符被注册到epoll(unix中的一种IO轮询机制,貌似,译者注)列表中。Tornado关心三种类型的事件(指发生在文件描述上的事件,译者注):READ,WRITE 和 ERROR。正如你所见,ERROR是默认为你自动添加的。
self._impl是和两者中的一个。我们稍后将看到Tornado是如何在它们之间进行选择的。
现在让我们来看看实际的主循环,不知何故,这段代码被放在了start()方法中:
def start(self):
"""Starts the I/O loop.
The loop will run until one of the I/O handlers calls stop(), which
will make the loop stop after the current event iteration completes.
self._running = True
while True:
if not self._running:
event_pairs = self._impl.poll(poll_timeout)
except Exception, e:
if e.args == (4, "Interrupted system call"):
logging.warning("Interrupted system call", exc_info=1)
# Pop one fd at a time from the set of pending fds and run
# its handler. Since that handler may perform actions on
# other file descriptors, there may be reentrant calls to
# this IOLoop that update self._events
self._events.update(event_pairs)
while self._events:
fd, events = self._events.popitem()
self._handlers[fd](fd, events)
except KeyboardInterrupt:
except OSError, e:
if e[0] == errno.EPIPE:
# Happens when the client closes the connection
logging.error("Exception in I/O handler for fd %d",
fd, exc_info=True)
logging.error("Exception in I/O handler for fd %d",
fd, exc_info=True)
poll()方法返回一个形如(fd: events)的键值对,并赋值给event_pairs变量。由于当一个信号在任何一个事件发生前到来时,C函数库中的poll()方法会返回 EINTR(实际是一个值为4的数值),所以"Interrupted system call"这个特殊的异常需要被捕获。更详细的请查看。
在内部的while循环中,event_pairs中的内容被一个一个的取出,然后相应的处理器会被调用。pipe 异常在这里默认不进行处理。为了让这个类适应更一般的情况,在http处理器中处理这个异常是一个更好的方案,但是选择现在这样处理或许是因为更容易一些。
注释中解释了为什么使用字典的popitem()方法,而不是使用更普遍一点的下面这种做法(指使用迭代,译者注):
&fd,&events&
&self._events.items():
原因很简单,在主循环期间,这个_events字典变量可能会被处理器所修改。比如remove_handler()处理器。这个方法把fd(即文件描述符,译者注)从_events字典中取出(extracts,意思是取出并从_events中删除,译者注),所以即使fd被选择到了,它的处理器也不会被调用(作 者的意思是,如果使用for迭代循环_events,那么在迭代期间_events就不能被修改,否则会产生不可预计的错误,比如,明明调用了 remove_handler()方法删除了某个&fd, handler&键值对,但是该handler还是被调用了,译者注)。
(意义不大的)循环结束技巧
怎么让这个主循环停止是很有技巧性的。self._running变量被用来在运行时从主循环中跳出,处理器可以通过调用stop()方法把它设置 为False。通常情况下,这就能让主循环停止了,但是stop()方法还能被一个信号处理器所调用,所以,如果1)主循环正阻塞在poll()方法 处,2)服务端没有接收到任何来自客户端的请求3)信号没有被OS投递到正确的线程中,你将不得不等待poll()方法出现超时情况后才会返回。考虑到这 些情况并不时常发生,还有poll()方法的默认超时时间只不过是0.2秒,所以这种让主循环停止的方式还算过得去。
但不管怎样,Tornado的开发者为了让主循环停止,还是额外的创建了一个没有名字的管道和对应的处理器,并把管道的一端放在了轮询文件描述符列表中。当需要停止时,在管道的另一端随便写点什么,这能高效率的(意思是马上,译者注)唤醒主循环在poll()方法处的阻塞(貌似Java NIO的Windows实现就用了这种方法,译者注)。这里节选了一些代码片段:
def __init__(self, impl=None):
# Create a pipe that we send bogus data to when we want to wake
# the I/O loop when it is idle
r, w = os.pipe()
self._set_nonblocking(r)
self._set_nonblocking(w)
self._waker_reader = os.fdopen(r, "r", 0)
self._waker_writer = os.fdopen(w, "w", 0)
self.add_handler(r, self._read_waker, self.WRITE)
def _wake(self):
self._waker_writer.write("x")
except IOError:
实际上,上述代码中存在一个bug:那个只读文件描述符r,虽然是用来读的,但在注册时却附加上了WRITE类型的事件,这将导致该注册实际不会被响应。正如我先前所说的,用不用专门找个方法其实没什么的,所以我对他们没有发现这个方法不起作用的事实并不感到惊讶。我在中报告了这个情况,但是尚未收到答复。
另外一个在IOLoop模块中很有特点的设计是对定时器的简单实现。一系列的定时器会被以是否过期的形式来维护和保存,这用到了python的模块:
def add_timeout(self, deadline, callback):
"""Calls the given callback at the time deadline from the I/O loop."""
timeout = _Timeout(deadline, callback)
bisect.insort(self._timeouts, timeout)
return timeout
在主循环中,所有过期了的定时器的回调会按照过期的顺序被触发。poll()方法中的超时时间会动态的进行调整,调整的结果就是如果没有新的客户端请求,那么下一个定时器就好像没有延迟一样的被触发(意思是如果没有新的客户端的请求,poll()方法将被阻塞直到超时,这个超时时间的设定会根据下一个定时器与当前时间之间的间隔进行调整,调整后,超时的时间会等同于距离下一个定时器被触发的时间,这样在poll()阻塞完后,下一个定时器刚好过期,译者注)。
选择select方案
让我们现在快速的看一下poll和select这两种select方案的实现代码。Python已经在版本2.6的标准库中支持了epoll,你可 以通过在select模块上使用hasattr()方法检测当前Python是否支持epoll。如果python版本小于2.6,Tornado将用它 自己的基于C的epoll模块。你可以在tornado/epoll.c文件中找到它源代码。如果最后这也不行(因为epoll不是每个Linux都有 的),它将回退到selec._Select并把_EPoll类包装成和select.epoll一样的api接口。在你做性能测试之前,请确定你能使用 epoll,因为select在有大量文件描述符情况下的效率非常低。
# Choose a poll implementation. Use epoll if it is available, fall back to
# select() for non-Linux platforms
if hasattr(select, "epoll"):
# Python 2.6+ on Linux
_poll = select.epoll
# Linux systems with our C module installed
import epoll
_poll = _EPoll
# All other systems
import sys
if "linux" in sys.platform:
logging.warning("ep using select()")
_poll = _Select
通过上述阅读,我们的介绍已经涵盖了大部分IOLoop模块。正如广告中介绍的那样,它是一段优雅而又简单的代码。
从sockets到流
让我们来看看模块。它的目的是提供一个对非阻塞式sockets的轻量级抽象,它提供了三个方法:
read_until(),从socket中读取直到遇到指定的字符串。这为在读取HTTP头时遇到空行分隔符自动停止提供了方便。
read_bytes(),从socket中读取指定数量的字节。这为读取HTTP消息的body部分提供了方便。
write(),将指定的buffer写入socket并持续监测直到这个buffer被发送。
所有上述的方法都可以通过异步方式在它们完成时触发回调函数。
write()方法提供了将调用者提供的数据加以缓冲直到IOLoop调用了它的(指write方法的,译者注)处理器的功能,因为到那时候就说明socket已经为写数据做好了准备:
def write(self, data, callback=None):
"""Write the given data to this stream.
If callback is given, we call it when all of the buffered write
data has been successfully written to the stream. If there was
previously buffered write data and an old write callback, that
callback is simply overwritten with this new callback.
self._check_closed()
self._write_buffer += data
self._add_io_state(self.io_loop.WRITE)
self._write_callback = callback
该方法只是用socket.send()来处理WRITE类型的事件,直到EWOULDBLOCK异常发生或者buffer被发送完毕。
读数据的方法和上述过程正好相反。读事件的处理器持续读取数据直到缓冲区被填满为止。这就意味着要么读取指定数量的字节(如果调用的是read_bytes()),要么读取的内容中包含了指定的分隔符(如果调用的是read_util()):
def _handle_read(self):
chunk = self.socket.recv(self.read_chunk_size)
except socket.error, e:
if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
logging.warning("Read error on %d: %s",
self.socket.fileno(), e)
self.close()
if not chunk:
self.close()
self._read_buffer += chunk
if len(self._read_buffer) &= self.max_buffer_size:
logging.error("Reached maximum read buffer size")
self.close()
if self._read_bytes:
if len(self._read_buffer) &= self._read_bytes:
num_bytes = self._read_bytes
callback = self._read_callback
self._read_callback = None
self._read_bytes = None
callback(self._consume(num_bytes))
elif self._read_delimiter:
loc = self._read_buffer.find(self._read_delimiter)
if loc != -1:
callback = self._read_callback
delimiter_len = len(self._read_delimiter)
self._read_callback = None
self._read_delimiter = None
callback(self._consume(loc + delimiter_len))
如下所示的_consume方法是为了确保在要求的返回值中不会包含多余的来自流的数据,并且保证后续的读操作会从当前字节的下一个字节开始(先将流中的数据读到self.read_buffer中,然后根据要求进行切割,返回切割掉的数据,保留切割后的数据供下一次的读取,译者注):
def _consume(self, loc):
result = self._read_buffer[:loc]
self._read_buffer = self._read_buffer[loc:]
return result
还值得注意的是在上述_handle_read()方法中read buffer的上限——self.max_buffer_size。默认值是100MB,这似乎对我来说是有点大了。举个例子,如果一个攻击者和服务端建 立了100个连接,并持续发送不带头结束分隔符的头信息,那么Tornado需要10GB的内存来处理这些请求。即使内存ok,这种数量级数据的复制操作 (比如像上述_consume()方法中的代码)很可能使服务器超负荷。我们还注意到在每次迭代中_handle_read()方法是如何在这个 buffer中搜索分隔符的,所以如果攻击者以小块形式发送大量的数据,服务端不得不做很多次搜索工作。归根结底,你应该想要将这个参数和谐掉,除非你真 的很希望那样(Bottom of line, you might want to tune this parameter unless you really expect requests that big 不大明白怎么翻译,译者注)并且你有足够的硬件条件。
HTTP 服务器
有了IOLoop模块和IOStream模块的帮助,写一个异步的HTTP服务器只差一步之遥,这一步就在中完成。
HTTPServer类它自己只负责处理将接收到的新连接的socket添加到IOLoop中。该监听型的socket自己也是IOLoop的一部分,正如在listen()方法中见到的那样:
def listen(self, port, address=""):
assert not self._socket
self._socket = socket.(socket.AF_INET, socket.SOCK_STREAM, 0)
flags = fcntl.fcntl(self._socket.fileno(), fcntl.F_GETFD)
flags |= fcntl.FD_CLOEXEC
fcntl.fcntl(self._socket.fileno(), fcntl.F_SETFD, flags)
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._socket.setblocking(0)
self._socket.bind((address, port))
self._socket.listen(128)
self.io_loop.add_handler(self._socket.fileno(), self._handle_events,
self.io_loop.READ)
除了绑定给定的地址和端口外,上述代码还设置了"close on exec"和"reuse address"这两个标志位。前者在应用程序创建子进程的时候特别有用。在这种情况下,我们不想让套接字保持打开的状态(任何设置了"close on exec"标志位的文件描述符,都不能被使用exec函数方式创建的子进程读写,因为该文件描述符在exec函数调用前就会被自动释放,译者注)。后者用来避免在服务器重启的时候发生“该地址以被使用”这种错误时很有用。
正如你所见到的,后备连接所允许的最大数目是128(注意,listen方法并不是你想象中的“开始在128端口上监听”的意思,译者注)。这意味着如果有128个连接正在等待被accept,那么直到服务器有时间将前面128个连接中的某几个accept了,新的连接都将被拒绝。我建议你在做性能测试的时候将该参数调高,因为当新的连接被抛弃的时候将直接影响你做测试的准确性。
在上述代码中注册的_handle_events()处理器用来accept新连接,并创建相关的IOStream对象和初始化一个HTTPConnection对象,HTTPConnection对象负责处理剩下的交互部分:
def _handle_events(self, fd, events):
while True:
connection, address = self._socket.accept()
except socket.error, e:
if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
stream = iostream.IOStream(connection, io_loop=self.io_loop)
HTTPConnection(stream, address, self.request_callback,
self.no_keep_alive, self.xheaders)
logging.error("Error in connection callback", exc_info=True)
可以看到这个方法在一次迭代中accept了所有正在等待处理的连接。也就是说直到EWOULDBLOCK异常发生while True循环才会退出,这也就意味着当前没有需要处理accept的连接了。
HTTP头的部分的解析工作开始于HTTPConnection类的构造函数__init()__():
def __init__(self, stream, address, request_callback, no_keep_alive=False,
xheaders=False):
self.stream = stream
self.address = address
self.request_callback = request_callback
self.no_keep_alive = no_keep_alive
self.xheaders = xheaders
self._request = None
self._request_finished = False
self.stream.read_until("rnrn", self._on_headers)
如果你很想知道xheaders参数的意义,请看这段注释:
如果xheaders为True,我们将支持把所有请求的HTTP头解析成X
Scheme格式,而原先我们将HTTP头解析成remote&IP和HTTP&scheme格式。这种格式的HTTP头在Tornado运行于反向代理或均衡负载服务器的后端时将非常有用。
_on_headers()回调函数实际用来解析HTTP头,并在有请求内容的情况下通过使用read_bytes()来读取请求的内容部分。_on_request_body()回调函数用来解析POST的参数并调用应用层提供的回调函数:
def _on_headers(self, data):
eol = data.find("rn")
start_line = data[:eol]
method, uri, version = start_line.split(" ")
if not version.startswith("HTTP/"):
raise Exception("Malformed HTTP version in HTTP Request-Line")
headers = HTTPHeaders.parse(data[eol:])
self._request = HTTPRequest(
connection=self, method=method, uri=uri, version=version,
headers=headers, remote_ip=self.address[0])
content_length = headers.get("Content-Length")
if content_length:
content_length = int(content_length)
if content_length & self.stream.max_buffer_size:
raise Exception("Content-Length too long")
if headers.get("Expect") == "100-continue":
self.stream.write("HTTP/1.1 100 (Continue)rnrn")
self.stream.read_bytes(content_length, self._on_request_body)
self.request_callback(self._request)
def _on_request_body(self, data):
self._request.body = data
content_type = self._request.headers.get("Content-Type", "")
if self._request.method == "POST":
if content_type.startswith("application/x-www-form-urlencoded"):
arguments = cgi.parse_qs(self._request.body)
for name, values in arguments.iteritems():
values = [v for v in values if v]
if values:
self._request.arguments.setdefault(name, []).extend(
elif content_type.startswith("multipart/form-data"):
boundary = content_type[30:]
if boundary: self._parse_mime_body(boundary, data)
self.request_callback(self._request)
将结果写回客户端的工作在HTTPRequest类中处理,你可以在上面的_on_headers()方法中看到具体的实现。HTTPRequest类仅仅将写回的工作代理给了stream对象。
def write(self, chunk):
assert self._request, "Request closed"
self.stream.write(chunk, self._on_write_complete)
未完待续?
通过这篇文章,我已经涵盖了从socket到应用层的所有方面。这应该能给你关于Tornado是如何工作的一个清晰的理解。总之,我认为Tornado的代码是非常友好的,我希望你也这样认为。
Tornado框架还有很大一部分我们没有探索,比如(应该是web.py,译者注)这个实际与你应用打交道的模块,又或者是模块。如果我有足够兴趣的话,我也会介绍这些部分。可以通过订阅我的来鼓励我。
1)">1)">1" ng-class="{current:{{currentPage==page}}}" ng-repeat="page in pages"><li class='page' ng-if="(endIndex<li class='page next' ng-if="(currentPage
相关文章阅读

我要回帖

更多关于 android客户端服务器 的文章

 

随机推荐