首页 > 代码库 > 【一步一个脚印】Tomcat+MySQL为自己的APP打造服务器(3-1)Android 和 Service 的交互之GET方式
【一步一个脚印】Tomcat+MySQL为自己的APP打造服务器(3-1)Android 和 Service 的交互之GET方式
好久没更新了,罪过罪过。最对不起的人莫过于那些支持和等待在下拙文的诸位,在此道一声抱歉。管窥之见,侥幸博得各位认同,给了我莫大的鼓励。
话休絮烦,文接前章。
到【一步一个脚印】Tomcat+MySQL为自己的APP打造服务器(2-3)Servlet连接MySQL数据库为止,我们已经将服务端的部分走通了:通过 Servlet 连接 MySQL ,分析业务需求进行响应的增删改查操作返回对应的处理结果。(上一篇结尾是说接下来该说POST请求了,但是在准备这篇文章时发现POST再推后一篇,等我们把 Android 通过 GET 方式和 Servlet 服务器交互全部走完了,回过头来对比着说 POST 会更加明了,所以决定修正一下之前的思路,本章我们继续完成 GET 的剩下内容)
很明显,想要 Android 和服务器进行交互,必然要使用到网络,为了解决后顾之忧,我们先下手为强,在 Manifest 文件中声明网络访问权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
这个权限可不是平白无故就去申请的,因为我们要通过网络和服务器交互,要完成这一交互过程,就要用到 Android 网络技术。Android 网络技术包含目前所有主流网络技术,比如你听过的TCP/IP(Socket、ServiceSocket)、UDP... ...(妈的,不写了。讲真,网络这块其实我已经不懂了,曾经真的懂过,反正大学时候网络基础学的挺嗨,几年不接触已经恍如隔世了。以免误人子弟,或者是遇到真正的大神被拆穿就尴尬了)。我们最常用的应该算是 HTTP 和 WebView 了,这里就以最常用的 HTTP 通信为例来说明:
在 Android 上发送 HTTP 请求的方式一般有两种:HttpURLConnection和HttpClient,我们都来试用一下:
(一)HttpURLConnection 进行 HTTP 请求
先是 HttpURLConnection,直接上代码吧,用法有注释:
public class HttpURLConActivity extends AppCompatActivity { private TextView tvContent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_http_urlcon); tvContent = (TextView) findViewById(R.id.tv_content); // 这里页面上就一个简单的TextView,用于展示获取到报文内容 requestUsingHttpURLConnection(); } private void requestUsingHttpURLConnection() { // 网络通信属于典型的耗时操作,开启新线程进行网络请求 new Thread(new Runnable() { @Override public void run() { HttpURLConnection connection = null; try { URL url = new URL("https://www.baidu.com"); // 声明一个URL,注意——如果用百度首页实验,请使用https connection = (HttpURLConnection) url.openConnection(); // 打开该URL连接 connection.setRequestMethod("GET"); // 设置请求方法,“POST或GET”,我们这里用GET,在说到POST的时候再用POST connection.setConnectTimeout(8000); // 设置连接建立的超时时间 connection.setReadTimeout(8000); // 设置网络报文收发超时时间 InputStream in = connection.getInputStream(); // 通过连接的输入流获取下发报文,然后就是Java的流处理 BufferedReader reader = new BufferedReader(new InputStreamReader(in)); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null){ response.append(line); } tvContent.setText(response.toString()); // 地雷 } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } }Run,Fuck!报错——
典型的子线程试图操作 UI 元素报错,为啥,因为网络请求是在新开的子线程中运行,当然不能直接拿到结果就给 TextView 赋值了!怎么做?Android 的Handler消息机制这不就用上了嘛!
/** * 消息处理 */ private Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { if(msg.what == 1){ tvContent.setText(msg.obj.toString()); } } }; private void requestUsingHttpURLConnection() { ...... /* 获取返回报文部分省略,将原来 * tvContent.setText(response.toString())替换为 * 给handler发送消息 */ Message msg = new Message(); msg.what = 1; msg.obj = response.toString(); Log.e("WangJ", response.toString()); handler.sendMessage(msg); ...... }重新 Run,结果
什么?看不懂,什么鬼!其实服务器返回的百度首页就是这样的 HTML 代码,只是平时我们使用浏览器打开的时候,浏览器引擎帮我们把这些代码解析和展示成了花花绿绿的页面,仅此而已。
(二)HttpClient 进行 HTTP 请求
HttpClient 是Apache 提供的 HTTP 网络访问接口,但是原生 Android 系统内置了这套借口,所以不用引入第三方 jar 就可以直接用。他可以和 HttpURLConnection 完成几乎一模一样的效果,但是两者的使用方法还是有一些区别的。下面我们用代码来说明:
public class HttpClientActivity extends AppCompatActivity { private TextView tvContent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_http_client); tvContent = (TextView) findViewById(R.id.tv_content); requestUsingHttpClient(); } // 同样的消息机制 private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 1) { tvContent.setText(msg.obj.toString()); } } }; private void requestUsingHttpClient() { new Thread(new Runnable() { @Override public void run() { HttpClient client = new DefaultHttpClient(); // HttpClient 是一个接口,无法实例化,所以我们通常会创建一个DefaultHttpClient实例 HttpGet get = new HttpGet("https://www.baidu.com"); // 发起GET请求就使用HttpGet,发起POST请求则使用HttpPost,这里我们先使用HttpGet try { HttpResponse httpResponse = client.execute(get); // 调用HttpClient对象的execute()方法 // 状态码200说明响应成功 if (httpResponse.getStatusLine().getStatusCode() == 200) { HttpEntity entity = httpResponse.getEntity(); // 取出报文的具体内容 String response = EntityUtils.toString(entity, "utf-8"); // 报文编码 // 发送消息 Message msg = new Message(); msg.what = 1; msg.obj = response; handler.sendMessage(msg); } } catch (IOException e) { e.printStackTrace(); } } }).start(); } }
怎么样,还是挺简洁的吧!Run,和前一个图一样,节省篇幅图就不贴了。
(三)HttpClient 的窘境
年轻人,是不是要到问题了(没遇到问题的请自觉忽略,如果你用的 Android Studio比较新,compileSdkVersion >= 23,相信你会遇到的)?是不是在写代码中找不到 HttpClient 类?那就对了!因为从 Android 6.0(API 23) 往后 Google 又把 HttpClient 给干掉了,为什么?说是因为它接口、方法太多,API太过复杂,升级维护难以在当前版本API上进行,这就会导致 Android 版本兼容上出现难以解决的问题,所以就把它干掉了,因为使用 HttpURLConnection 也能达到同样的效果,并且易于维护。啰嗦个屁呀,问题咋解决呢?改 SDK 版本呗,让他SDK <= 22 就可以了。什么!业务不允许?要求最新版 SDK?别怕,到 Apache 下载最新 jar 包导入工程,还像以前一样用,不过方法名可能不一样了。
就这样,Android 发送 HTTP 请求就完成了。什么?消息处理机制太麻烦了?是的!我也举得麻烦,其实 Android 官方也觉得麻烦,所以 Android 为了降低这个开发难度,提供了AsyncTask。AsyncTask就是一个封装过的后台任务类,顾名思义就是异步任务,其实现原理也是基于异步消息处理机制,只是 Android给我们做了很好的封装而已,相对于 Handler 更轻量,适用于简单的异步处理,但是在面对多个异步任务更新同一个或同一组 UI 时的同步就比较困难,不了解不要紧,以后用用就知道问题在哪了,一口也吃不成个大胖子。下面我们在 http 请求时就用AsyncTask来处理吧——
(四)Android 和 Servlet 服务器通过 HTTP GET 模式进行交互
来吧,来到了今天的主题。首先,请确保你的 Tomcat 上部署的 Servlet 已经启动,确保数据库服务正常启动并且数据库连接正常,有问题请参考之前的 【一步一个脚印】Tomcat+MySQL为自己的APP打造服务器。
(双12换了电脑,环境都是新装的,如与之前的数据不符,请以自己的为准)
数据库表就是这么一个简单的表
下边是服务器Servlet的代码,其实和之前文章中的代码原理上一模一样,但是为了写一个完整的交互,我这里重写了一个:
处理“注册”逻辑的Servlet:
@WebServlet(description = "注册使用的Servlet", urlPatterns = { "/RegisterServlet" }) public class RegisterServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * Default constructor. */ public RegisterServlet() { LogUtil.log("RegisterServlet construct..."); } @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String method = request.getMethod(); if ("GET".equals(method)) { LogUtil.log("请求方法:GET"); doGet(request, response); } else if ("POST".equals(method)) { LogUtil.log("请求方法:POST"); doPost(request, response); } else { LogUtil.log("请求方法分辨失败!"); } } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse * response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String code = ""; String message = ""; String account = request.getParameter("account"); String password = request.getParameter("password"); LogUtil.log(account + ";" + password); Connection connect = DatabaseUtil.getConnection(); try { Statement statement = connect.createStatement(); String sql = "select account from " + DatabaseUtil.Table_Account + " where account=‘" + account + "‘"; LogUtil.log(sql); ResultSet result = statement.executeQuery(sql); if (result.next()) { // 能查到该账号,说明已经注册过了 code = "100"; message = "该账号已存在"; } else { String sqlInsert = "insert into " + DatabaseUtil.Table_Account + "(account, password) values(‘" + account + "‘, ‘" + password + "‘)"; LogUtil.log(sqlInsert); if (statement.executeUpdate(sqlInsert) > 0) { // 否则进行注册逻辑,插入新账号密码到数据库 code = "200"; message = "注册成功"; } else { code = "300"; message = "注册失败"; } } } catch (SQLException e) { e.printStackTrace(); } response.getWriter().append("code:").append(code).append(";message:").append(message); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse * response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } @Override public void destroy() { LogUtil.log("RegisterServlet destory."); super.destroy(); } }处理“登录”逻辑的Servlet:
@WebServlet(description = "登录", urlPatterns = { "/LoginServlet" }) public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public LoginServlet() { super(); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse * response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String code = ""; String message = ""; String account = request.getParameter("account"); String password = request.getParameter("password"); LogUtil.log(account + ";" + password); Connection connect = DatabaseUtil.getConnection(); try { Statement statement = connect.createStatement(); String sql = "select account from " + DatabaseUtil.Table_Account + " where account=‘" + account + "‘ and password=‘" + password + "‘"; LogUtil.log(sql); ResultSet result = statement.executeQuery(sql); if (result.next()) { // 能查到该账号,说明已经注册过了 code = "200"; message = "登陆成功"; } else { code = "100"; message = "登录失败,密码不匹配或账号未注册"; } } catch (SQLException e) { e.printStackTrace(); } response.getWriter().append("code:").append(code).append(";message:").append(message); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse * response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { LogUtil.log("不支持POST方法"); } }/*请求和响应编码格式设置、数据库连接代码和之前的一样,这里就不重复贴了 */
接下来是Android客户端的代码:
常量类
public class Constant { public static String URL = "http://192.168.1.109:8080/FirstServletService/"; // IP地址请改为你自己的IP public static String URL_Register = URL + "RegisterServlet"; public static String URL_Login = URL + "LoginServlet"; }Activity的界面
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" android:padding="@dimen/activity_vertical_margin"> <EditText android:id="@+id/et_account" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入账号" /> <EditText android:id="@+id/et_password" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入登录密码" android:inputType="textPassword" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:orientation="horizontal"> <Button android:id="@+id/btn_register" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Register" /> <Button android:id="@+id/btn_login" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Login" /> </LinearLayout> <!-- 用来显示报文返回结果 --> <TextView android:id="@+id/tv_result" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout>Activity的代码
public class MainActivity extends Activity { private EditText etAccount; private EditText etPassword; private TextView tvResult; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); etAccount = (EditText) findViewById(R.id.et_account); etPassword = (EditText) findViewById(R.id.et_password); tvResult = (TextView) findViewById(R.id.tv_result); Button btnRegister = (Button) findViewById(R.id.btn_register); btnRegister.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (!StringUtil.isEmpty(etAccount.getText().toString()) && !StringUtil.isEmpty(etPassword.getText().toString())) { Log.e("WangJ", "都不空"); register(etAccount.getText().toString(), etPassword.getText().toString()); } else { Toast.makeText(MainActivity.this, "账号、密码都不能为空!", Toast.LENGTH_SHORT).show(); } } }); Button btnLogin = (Button) findViewById(R.id.btn_login); btnLogin.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (!StringUtil.isEmpty(etAccount.getText().toString()) && !StringUtil.isEmpty(etPassword.getText().toString())) { Log.e("WangJ", "都不空"); login(etAccount.getText().toString(), etPassword.getText().toString()); } else { Toast.makeText(MainActivity.this, "账号、密码都不能为空!", Toast.LENGTH_SHORT).show(); } } }); } private void register(String account, String password) { String registerUrlStr = Constant.URL_Register + "?account=" + account + "&password=" + password; new MyAsyncTask(tvResult).execute(registerUrlStr); } private void login(String account, String password) { String registerUrlStr = Constant.URL_Login + "?account=" + account + "&password=" + password; new MyAsyncTask(tvResult).execute(registerUrlStr); } /** * AsyncTask类的三个泛型参数: * (1)Param 在执行AsyncTask是需要传入的参数,可用于后台任务中使用 * (2)后台任务执行过程中,如果需要在UI上先是当前任务进度,则使用这里指定的泛型作为进度单位 * (3)任务执行完毕后,如果需要对结果进行返回,则这里指定返回的数据类型 */ public static class MyAsyncTask extends AsyncTask<String, Integer, String> { private TextView tv; // 举例一个UI元素,后边会用到 public MyAsyncTask(TextView v) { tv = v; } @Override protected void onPreExecute() { Log.w("WangJ", "task onPreExecute()"); } /** * @param params 这里的params是一个数组,即AsyncTask在激活运行是调用execute()方法传入的参数 */ @Override protected String doInBackground(String... params) { Log.w("WangJ", "task doInBackground()"); HttpURLConnection connection = null; StringBuilder response = new StringBuilder(); try { URL url = new URL(params[0]); // 声明一个URL,注意如果用百度首页实验,请使用https开头,否则获取不到返回报文 connection = (HttpURLConnection) url.openConnection(); // 打开该URL连接 connection.setRequestMethod("GET"); // 设置请求方法,“POST或GET”,我们这里用GET,在说到POST的时候再用POST connection.setConnectTimeout(80000); // 设置连接建立的超时时间 connection.setReadTimeout(80000); // 设置网络报文收发超时时间 InputStream in = connection.getInputStream(); // 通过连接的输入流获取下发报文,然后就是Java的流处理 BufferedReader reader = new BufferedReader(new InputStreamReader(in)); String line; while ((line = reader.readLine()) != null) { response.append(line); } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return response.toString(); // 这里返回的结果就作为onPostExecute方法的入参 } @Override protected void onProgressUpdate(Integer... values) { // 如果在doInBackground方法,那么就会立刻执行本方法 // 本方法在UI线程中执行,可以更新UI元素,典型的就是更新进度条进度,一般是在下载时候使用 } /** * 运行在UI线程中,所以可以直接操作UI元素 * @param s */ @Override protected void onPostExecute(String s) { Log.w("WangJ", "task onPostExecute()"); tv.setText(s); } } }代码很简单,没什么解释的。本文的主要内容是将数据库、服务器、Android串联起来这一过程。
*注意* 这里我们不可能通过移动网络来连接我们的服务器,因为我们的服务器部署在本机本地,没有公网IP,所以这里我们用自己的电脑开启一个共享热点,然后用手机连上这个热点来进行访问本机服务器,IP地址通过ipconfig命令查看。开启网络热点自己百度吧,命令行不行直接找360免费WIFI等等,这篇已经够啰嗦了,就不再加入其他干扰内容了,请关注本文的重点。
运行前再检查一遍:(1)数据库连接正常;(2)服务器运行正常;(3)Android端访问的本机服务器地址可连通。然后Run,看结果
就这样就完事了,其实篇幅不短,内容却不多。到此GET方式的交互就完成了,同理,POST交互也是依葫芦画瓢,下一篇我们就来说说POST方式的交互。
佶屈聱牙,作抛砖引玉之用,水平有限,如有不足欢迎留言指正!
【一步一个脚印】Tomcat+MySQL为自己的APP打造服务器(3-1)Android 和 Service 的交互之GET方式