大多数的涉及到网络交互的Android App都是采用HTTP协议发送和接收数据。Android引入了多种HTTP协议编程方式,如Apache HTTP Client,HttpURLConnection,开源框架xUtils,Google官方的Volley等等。日前,笔者开发中的项目Frutiger v1.0进展到构建网络交互的阶段,不想再做伸手党套用xUtils框架,于是抽点时间来研究HttpURLConnection,Basic-HTTP-Client,Volley三种方式。
笔者选择了登录问题作为研究客户端与服务器交互的场景。首先客户端向服务器提交用户名(userName)和密码(password)两个参数,然后由服务器对提交的数据进行检查,产生“用户不存在!”,“密码错误!”,“登录成功!”三种可能的结果返回给客户端。
下面给出服务器端的Servlet示例代码:
1 public class SignInServlet extends HttpServlet { 2 3 /** 4 * 响应客户端的GET请求; 5 */ 6 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 7 PrintWriter writer = response.getWriter(); 8 writer.write("登录页面!"); 9 } 10 11 /** 12 * 响应客户端的POST请求; 13 */ 14 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 15 String responseResult = ""; 16 /** 17 * 获取客户端提交的数据; 18 */ 19 System.out.println(request.getRemoteAddr()); 20 String userName = request.getParameter("userName"); 21 String password = request.getParameter("password"); 22 System.out.println("userName:" + userName + " password:" + password + "!"); 23 responseResult = handleSignInData(userName, password); 24 25 response.setCharacterEncoding("UTF-8"); 26 PrintWriter writer = response.getWriter(); 27 writer.write(responseResult); 28 } 29 30 private String handleSignInData(String userName, String password){ 31 String responseResult = ""; 32 if(!userName.equals("test")){ 33 responseResult = "用户不存在!"; 34 }else if(!password.equals("test")){ 35 responseResult = "密码错误!"; 36 }else{ 37 responseResult = "登录成功!"; 38 } 39 return responseResult; 40 } 41 42 }
接下来将分别对HttpURLConnection,Basic-HTTP-Client,Volley三种方式的具体实现进行详细的讲解。
(一)HttpURLConnection
HttpURLConnection被用来通过网络发送和接收数据,也可以用来发送和接收长度未知的流数据。这段概念指出了HttpURLConnection的两种常见用途,一是向服务器提交表单数据,二是向服务器上传、下载文本等流数据。
对HttpURLConnection的使用可以按照下面的步骤:
(1)调用URL.openConnection()获得URLConnection实例对象,然后将其转换为HttpURLConnection;
(2)设置Request的相关内容,包括请求的URL,请求头,请求体。若Request包含请求体,则setDoOutPut(true),同时通过getOutPutStream()获得输出流来向服务器提交数据;
(3)读取来自服务器的响应,通过getInputStream()获得来自服务器的响应的内容(Content);
(4)通过disconnect()关闭网络连接,释放连接资源。
套用上面的步骤,解决登录问题的代码如下:
1 public class SignInActivity extends Activity { 2 private TextView mTextView; 3 private EditText userNameText; 4 private EditText passwordText; 5 private Button mButton; 6 private Handler handler; 7 8 @Override 9 protected void onCreate(Bundle savedInstanceState) { 10 super.onCreate(savedInstanceState); 11 setContentView(R.layout.activity_sign_in); 12 initView(); 13 mButton.setOnClickListener(new OnClickListener() { 14 15 @Override 16 public void onClick(View v) { 17 signIn(); 18 } 19 }); 20 21 handler = new Handler() { 22 @Override 23 public void handleMessage(Message msg) { 24 switch (msg.what) { 25 case 1: 26 Bundle data = msg.getData(); 27 String result = data.getString("result"); 28 mTextView.setVisibility(View.VISIBLE); 29 mTextView.setText(result); 30 break; 31 } 32 } 33 }; 34 } 35 36 private void initView() { 37 mTextView = (TextView) this.findViewById(R.id.signin_textview_result); 38 userNameText = (EditText) this 39 .findViewById(R.id.signin_edittext_username); 40 passwordText = (EditText) this 41 .findViewById(R.id.signin_edittext_password); 42 mButton = (Button) this.findViewById(R.id.signin_button_login); 43 } 44 45 private void signIn() { 46 new Thread(new Runnable() { 47 48 @Override 49 public void run() { 50 HttpURLConnection urlConnection = null; 51 try { 52 URL url = new URL("http://192.168.31.221:8080/Frutiger_v1.0/SignIn.do"); 53 urlConnection = (HttpURLConnection)url.openConnection(); 54 urlConnection.setDoOutput(true); 55 urlConnection.setDoInput(true); 56 57 OutputStream out = new BufferedOutputStream(urlConnection 58 .getOutputStream()); 59 writeStream(out); 60 61 String result = "网络异常,请检查网络连接..."; 62 int responseCode = urlConnection.getResponseCode(); 63 Log.i(TAG, "responseCode:" + responseCode); 64 if(responseCode == 200){ 65 InputStream in = new BufferedInputStream(urlConnection.getInputStream()); 66 result = readStream(in); 67 } 68 69 /** 70 * 更新UI主线程控件; 71 */ 72 Message msg = new Message(); 73 msg.what = 1; 74 Bundle data = new Bundle(); 75 data.putString("result", result); 76 msg.setData(data); 77 handler.sendMessage(msg); 78 } catch (IOException e) { 79 e.printStackTrace(); 80 } 81 finally{ 82 urlConnection.disconnect(); 83 } 84 } 85 }).start(); 86 } 87 88 private void writeStream(OutputStream out) { 89 Map<String, String > params = getRequestParams(); 90 byte[] buffer = wrapRequestParams(params); 91 try { 92 out.write(buffer); 93 } catch (IOException e) { 94 e.printStackTrace(); 95 } 96 finally{ 97 try { 98 out.close(); 99 } catch (IOException e) { 100 e.printStackTrace(); 101 } 102 } 103 } 104 105 private Map<String, String > getRequestParams(){ 106 Map<String, String > params = new HashMap<String, String>(); 107 params.put("userName", userNameText.getText().toString()); 108 params.put("password", passwordText.getText().toString()); 109 return params; 110 } 111 112 private byte[] wrapRequestParams(Map<String, String> params){ 113 StringBuffer stringBuffer = new StringBuffer(); 114 for(Map.Entry<String, String> entry : params.entrySet()){ 115 stringBuffer.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); 116 } 117 stringBuffer.deleteCharAt(stringBuffer.length() - 1); 118 return stringBuffer.toString().getBytes(); 119 } 120 121 private String readStream(InputStream in) { 122 String result = ""; 123 byte[] buffer = new byte[256]; 124 int len; 125 try { 126 while((len = in.read(buffer)) != -1){ 127 result += new String(buffer, 0, len, "UTF-8"); 128 } 129 } catch (IOException e) { 130 e.printStackTrace(); 131 } 132 finally{ 133 try { 134 in.close(); 135 } catch (IOException e) { 136 e.printStackTrace(); 137 } 138 } 139 return result; 140 } 141 142 }
根据上述代码可知,点击Sign In按钮后触发相应的signIn()方法,在signIn()中打开了新的线程来向服务器提交数据,接收到服务器端的响应后,将响应结果转换为字符串,通过Handler来更新主线程的TextView。当然,要想成功执行上述代码,还需要加上<uses-permission android:name="android.permission.INTERNET" />网络权限。要注意的是使用HttpURLConnection连接服务器,必须要在打开流读取、写入数据后及时关闭相应的输入、输出流,否则可能得不到想要的结果。
笔者把上面代码中涉及到网络的操作剥离了出来,封装成了工具类,需要的读者可以参考下:
1 package org.warnier.zhang.support.v1.net; 2 3 import java.io.BufferedInputStream; 4 import java.io.BufferedOutputStream; 5 import java.io.InputStream; 6 import java.io.OutputStream; 7 import java.net.HttpURLConnection; 8 import java.net.URL; 9 import java.util.Map; 10 11 /** 12 * 处理POST请求的HTTP协议工具类,调用HttpPost的类必须实现 13 * HttpPost.CallBacks回调接口,来处理HTTP响应。 14 * 调用HttpPost类的步骤: 15 * (1)实现HttpPost.CallBacks回调接口; 16 * (2)传递spec参数; 17 * (3)设置请求体params参数; 18 * (4)调用send()方法; 19 * @author Warnier-zhang 20 * 21 */ 22 public class HttpPost { 23 24 private String spec; 25 private Map<String, String> params; 26 private CallBacks callBacks; 27 28 public HttpPost(CallBacks callBacks){ 29 this.callBacks = callBacks; 30 } 31 32 public HttpPost(CallBacks callBacks, String spec) { 33 this(callBacks); 34 this.spec = spec; 35 } 36 37 /** 38 * 设置URL字符串; 39 */ 40 public void setSpec(String spec) { 41 this.spec = spec; 42 } 43 44 /** 45 * 设置POST请求的请求体; 46 */ 47 public void setParameters(Map<String, String> params) { 48 this.params = params; 49 } 50 51 /** 52 * 格式化POST请求的请求体; 53 */ 54 private byte[] formatParameters(Map<String, String> params) { 55 StringBuffer stringBuffer = new StringBuffer(); 56 for (Map.Entry<String, String> entry : params.entrySet()) { 57 stringBuffer.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); 58 } 59 stringBuffer.deleteCharAt(stringBuffer.length() - 1); 60 return stringBuffer.toString().getBytes(); 61 } 62 63 /** 64 * 发送POST请求; 65 */ 66 public void send(){ 67 send(callBacks); 68 } 69 70 /** 71 * 处理发送POST请求细节; 72 */ 73 private void send(CallBacks callBacks){ 74 HttpURLConnection httpURLConnection = null; 75 try { 76 URL url = new URL(spec); 77 httpURLConnection = (HttpURLConnection)url.openConnection(); 78 httpURLConnection.setDoOutput(true); 79 httpURLConnection.setDoInput(true); 80 81 OutputStream outputStream = new BufferedOutputStream(httpURLConnection.getOutputStream()); 82 byte[] requestBody = formatParameters(params); 83 outputStream.write(requestBody); 84 outputStream.close(); 85 86 int responseCode = httpURLConnection.getResponseCode(); 87 if(responseCode == 200){ 88 InputStream inputStream = new BufferedInputStream(httpURLConnection.getInputStream()); 89 callBacks.handleResponse(inputStream); 90 inputStream.close(); 91 } 92 } catch (Exception e) { 93 e.printStackTrace(); 94 }finally{ 95 httpURLConnection.disconnect(); 96 } 97 } 98 99 /** 100 * 调用类HttpPost的父类必须实现该接口,以处理HTTP请求的响应; 101 */ 102 public static interface CallBacks{ 103 void handleResponse(InputStream inputStream); 104 } 105 }
HttpPost.Callbacks接口定义为泛型的效果可能更好。
其实提到Android HTTP Client时,始终存在一个问题,即Apache HTTP Client和HttpURLConnection选择哪个?由于在Android的早期版本中(Android 2.2之前),HttpURLConnection存在一些“令人厌恶”,搞不定的bug,大而全的Apache HTTP Client成为进行网络操作的不二选择。但是随着Android 2.3(Gingerbread )的发布,HttpURLConnection得到了很大的改善,能够对服务器端的响应进行透明压缩、缓存,加快了交互的速度,而Apache HTTP Client反而因其大而全在完善api过程中很难保证前后版本的一致性,导致Android官方停止了对其更新。因此,考虑现下的知名ROM都是基于Android 4.4及更高的版本定制的,是时候抛弃Apache HTTP Client了。
(二)Basic-HTTP-Client
既然HttpURLConnection已经很好了,为什么还需要Basic-HTTP-Client?
HttpURLConnection是不错,但是HttpURLConnection是一个低级别的api,在使用的过程中,每次都需要对URL编码,设置内容类型,处理输入、输出流,以及捕获各种异常。如果不对代码进行适当的封装,很容易会造成重复的代码,产生耦合。所以为了消除每次HTTP请求都要重复上述操作的问题,有必要将涉及到网络的操作封装成一个单独的模块,从系统中独立出来。Android大神David M.Chandler就替我们做了这样一件事,官方链接:https://github.com/turbomanage/basic-http-client。
Basic-HTTP-Client具有如下的特点:
(1)能够发送GET,POST,PUT,和DELETE请求;
(2)能够发送异步的请求;
(3)能够用RequestHandler来定制请求;
(4)能够将请求自动包裹在Android异步任务中。
采用Basic-HTTP-Client解决登录问题的部分示例代码如下:
1 BasicHttpClient httpClient = new BasicHttpClient(); 2 ParameterMap params = httpClient.newParams() 3 .add("userName", ameText.getText().toString()) 4 .add("password", wordText.getText().toString()); 5 httpClient.setReadTimeout(3000); 6 HttpResponse httpResponse = httpClient.post(Config.SIGN_IN_URL, params);
(三)Volley
Volley是Google在Google I/O 2013大会上推出的全新、高效的Android HTTP协议通信框架。Volley能够简单地发送HTTP请求,也能轻松加载网络上的图片。Volley的设计目标是“Great for RPC-style network operations that populate UI; Fine for background RPCs; Terrible for larger payloads。”,也就是说Volley是专门为数据量不大,但是网络操作频繁设计的,对于像下载大文件这样的大数据量操作,Volley不适用。
(1)下载Volley
可以借助于Git来获得Volley库:git clone https://android.googlesource.com/platform/frameworks/volley。这种方式获得的Volley没有打包成.jar文档,可以自己打包或者到Google I/O 2013的开源Android app中找到.jar包。
(2)Volley教程
Volley的用法很简单,下面简单介绍发送一个HTTP请求,同时接收一个HTTP响应。在Volley中,网络请求是有一个RequestQueue(请求队列)来管理的,最好的使用RequestQueue的方式是将RequestQueue初始化为一个Singleton(单件实例),如下:
RequestQueue mRequestQueue = Volley.newRequestQueue(this);
接下来,通过向RequestQueue中添加,移除Request来发送或取消HTTP请求。为了发送请求,我们需要创建一个StringRequest对象。
1 StringRequest stringRequest = new StringRequest("http://www.cnblogs.com/Warnier-zhang/", new Response.Listener<String>() { 2 3 @Override 4 public void onResponse(String response) { 5 Log.i(TAG, response); 6 } 7 }, new Response.ErrorListener() { 8 9 @Override 10 public void onErrorResponse(VolleyError error) { 11 Log.i(TAG, error.getMessage()); 12 } 13 }); 14 mRequestQueue.add(request);
可见上面的代码创建了一个StringRequest实例,构造方法需要三个参数,第一个是请求的URL,第二个是对请求的响应进行监听的监听器,第三个是对请求错误进行监听的监听器。
当然,上面是发送了一个GET请求,如果要发送一个POST请求应该怎么做?其实,StringRequest还提供了一个构造方法可以设置HTTP协议的请求方法,例如:
1 StringRequest stringRequest = new StringRequest(method, url, listener, errorListener);
通过设置method为Method.Post来指定POST请求方式。那么如何向服务器提交参数呢?遗憾的是,StringRequest本身并没有提供设置POST请求参数的方法,但是当StringRequest请求被发送时会调用其父类Request的getParams()方法获取提交给服务器的POST参数。因此,我们可以通过重写Request类中的getParams()方法来设置POST请求的参数,例如:
1 StringRequest stringRequest = new StringRequest(Method.POST, url, listener, errorListener){ 2 3 @Override 4 protected Map<String, String> getParams() throws AuthFailureError { 5 Map<String, String> params = new HashMap<>(); 6 params.put("userName", userName); 7 params.put("password", password); 8 return params; 9 } 10 11 };
采用Volley解决登录问题的完整代码如下:
public class SignInActivity3 extends Activity { private TextView mTextView; private EditText userNameText; private EditText passwordText; private Button mButton; private Handler handler; private RequestQueue mRequestQueue; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sign_in); initView(); mButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { signIn(); } }); handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 1: Bundle data = msg.getData(); String result = data.getString("result"); mTextView.setVisibility(View.VISIBLE); mTextView.setText(result); break; } } }; mRequestQueue = Volley.newRequestQueue(this); } private void initView() { mTextView = (TextView) this.findViewById(R.id.signin_textview_result); userNameText = (EditText) this .findViewById(R.id.signin_edittext_username); passwordText = (EditText) this .findViewById(R.id.signin_edittext_password); mButton = (Button) this.findViewById(R.id.signin_button_login); } private void signIn() { new Thread(new Runnable() { @Override public void run() { StringRequest request = new StringRequest(Method.POST, Config.SIGN_IN_URL, new Response.Listener<String>() { @Override public void onResponse(String response) { Message msg = new Message(); msg.what = 1; Bundle data = new Bundle(); Log.i("Response >>", response); String result = "网络异常,请检查网络连接..."; try { result = new String(response.getBytes("ISO-8859-1"), "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } data.putString("result", result); msg.setData(data); handler.sendMessage(msg); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.i("Response Error >>", error.getMessage()); } }) { /** * 重写父类的Request方法,添加POST请求参数; */ @Override protected Map<String, String> getParams() throws AuthFailureError { Map<String, String> params = new HashMap<>(); params.put("userName", userNameText.getText() .toString()); params.put("password", passwordText.getText() .toString()); return params; } }; } }).start(); } }
到此,本片博客就结束了。接下来的Android HTTP协议编程的内容将会重点研究Volley的使用方法。本篇博客中涉及到的源码包下载链接:http://pan.baidu.com/s/10i1u2 密码: p2jy。
由于笔者水平有限,可能有些错误,欢迎读者留言交流。