首页 > 代码库 > 【一步一个脚印】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 请求的方式一般有两种:HttpURLConnectionHttpClient,我们都来试用一下:


(一)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方式