好久没更新了,罪过罪过。最对不起的人莫过于那些支持和等待在下拙文的诸位,在此道一声抱歉。管窥之见,侥幸博得各位认同,给了我莫大的鼓励。
话休絮烦,文接前章。
到【一步一个脚印】Tomcat+MySQL为自己的APP打造服务器(2-3)Servlet连接MySQL数据库为止,我们已经将服务端的部分走通了:通过 Servlet 连接 MySQL ,分析业务需求进行响应的增删改查操作返回对应的处理结果。(上一篇结尾是说接下来该说POST请求了,但是在准备这篇文章时发现POST再推后一篇,等我们把 Android 通过 GET 方式和 Servlet 服务器交互全部走完了,回过头来对比着说 POST 会更加明了,所以决定修正一下之前的思路,本章我们继续完成 GET 的剩下内容)
很明显,想要 Android 和服务器进行交互,必然要使用到网络,为了解决后顾之忧,我们先下手为强,在 Manifest 文件中声明网络访问权限:
这个权限可不是平白无故就去申请的,因为我们要通过网络和服务器交互,要完成这一交互过程,就要用到 Android 网络技术。Android 网络技术包含目前所有主流网络技术,比如你听过的TCP/IP(Socket、ServiceSocket)、UDP... ...(妈的,不写了。讲真,网络这块其实我已经不懂了,曾经真的懂过,反正大学时候网络基础学的挺嗨,几年不接触已经恍如隔世了。以免误人子弟,或者是遇到真正的大神被拆穿就尴尬了)。我们最常用的应该算是 HTTP 和 WebView 了,这里就以最常用的 HTTP 通信为例来说明:
在 Android 上发送 HTTP 请求的方式一般有两种:HttpURLConnection和 HttpClient,我们都来试用一下:
先是 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 是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,和前一个图一样,节省篇幅图就不贴了。
年轻人,是不是要到问题了(没遇到问题的请自觉忽略,如果你用的 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来处理吧——
来吧,来到了今天的主题。首先,请确保你的 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的界面
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 {
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方式的交互。
佶屈聱牙,作抛砖引玉之用,水平有限,如有不足欢迎留言指正!