基于TCP协议的网络通信
使用URL访问网络资源
使用HTTP访问网络
使用WebView视图显示网页
基于TCP协议的网络通信
TCP/IP通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket,通信的两端之间形成网络虚拟链路。Java对基于TCP协议的网络通信提供了良好的封装,Java使用Socket对象来代表两端的通信接口,并通过Socket产生IO流来进行网络通信。
1.1 使用ServerSocket创建TCP服务器端
Java中能接收其他通信实体连接请求的类是ServerSocket, ServerSocket对象用于监听来自客户端的Socket连接,如果没有连接,它将一直处于等待状态。 ServerSocket包含一个监听来自客户端连接请求的方法。
Socket accept():如果接收到一个客户端Socket的连接请求,该方法将返回一个与客户端Socket对应的Socket;否则该方法将一直处于等待状态,线程也被阻塞。
为了创建ServerSocket对象,ServerSocket类提供了如下几个构造器:
ServerSocket(int port):用指定的端口port来创建一个ServerSocket。该端口应该是有一个有效的端口整数值:0~65535。
ServerSocket(int port,int backlog):增加一个用来改变连接队列长度的参数backlog。
ServerSocket(int port,int backlog,InetAddress localAddr):在机器存在多个 IP地址的情况下,允许通过localAddr这个参数来指定将ServerSocket绑定到指定的IP地址。
当ServerSocket使用完毕,应使用ServerSocket的close()方法来关闭该ServerSocket。
通常情况下,服务器不应该只接受一个客户端请求,而应该不断地接受来自客户端的所有请求,所以Java程序通常会通过循环,不断地调用ServerSocket的accept()方法。如下代码片段所示:
1.2 使用Socket进行通信
客户端通常可使用Socket的构造器来连接到指定服务器,Socket通常可使用如下两个构造器:
Socket(InetAddress/String remoteAddress, int port):创建连接到指定远程主机、远程端口的Socket。
Socket(InetAddress/String remoteAddress, int port, InetAddress localAddr, int localPort):创建连接到指定远程主机、远程端口的Socket,并指定本地IP地址和本地端口号。
上面两个构造器中指定远程主机时既可使用InetAddress来指定,也可直接使用String对象来指定,但程序通常使用String对象(如127.0.0.1)来指定远程IP。
以上代码将会连接到指定服务器,让服务器端的ServerSocket的accept()方法向下执行。
当客户端、服务器端产生了对应的Socket之后,程序无须再区分服务器、客户端,而是通过各自的Socket进行通信,Socket提供如下两个方法来获取输入流和输出流:
InputStream getInputStream():返回该Socket对象对应的输入流,让程序通过该输入流从Socket中取出数据。
OutputStream getOutputStream():返回该Socket对象对应的输出流,让程序通过该输出流向Socket中输出数据。
例:简单网络通信:
服务器端程序代码:
SimpleServer.java
public class SimpleServer { public static void main(String[] args) throws IOException { //创建一个ServerSocket,用于监听客户端Socket的连接请求 ServerSocket ss = new ServerSocket(30000); //采用循环不断接受来自客户端的请求 while (true) { //每当接受到客户端Socket的请求,服务器端也对应产生一个Socket Socket s = ss.accept(); OutputStream os = s.getOutputStream(); os.write("您好,您收到了服务器的新年祝福!\n" .getBytes("utf-8")); //关闭输出流,关闭Socket os.close(); s.close(); } } }
客户端程序:
SimpleClient.java
public class SimpleClient extends Activity { EditText show; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); show = (EditText) findViewById(R.id.show); //关闭输入流、socket try { Socket socket = new Socket("127.0.0.1" , 30000); //将Socket对应的输入流包装成BufferedReader BufferedReader br = new BufferedReader( new InputStreamReader(socket.getInputStream())); //进行普通IO操作 String line = br.readLine(); show.setText("来自服务器的数据:" + line); br.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } }
<!-- 授权访问互联网--> <uses-permission android:name="android.permission.INTERNET"/>
总结Socket通信,创建服务器步骤:
指定端口实例化一个ServerSocket
调用ServerSocket的accept()等待连接
获取位于该底层Socket的流以进行读写操作
对数据封装成流
对Socket进行读写
关闭打开的流
总结Socket通信,创建客户端的步骤:
通过IP地址和端口实例化Socket,请求连接服务器
获取Socket上的流以进行读写
把流包装进BufferedReader/PrintWriter的实例
对Socket进行读写
关闭打开的流
1.3 多线程
实际应用中的客户端则可能需要和服务器端保持长时间通信,即服务器需要不断地读取客户端数据,并向客户端写入数据;客户端也需要不断地读取服务器数据,并向服务器写入数据。
服务器应该为每个Socket单独启动一条线程,每条线程负责与一个客户端进行通信。
客户端读取服务器数据的线程同样会被阻塞,所以系统应该单独启动一条线程,该线程专门负责读取服务器数据。
例:C/S聊天室程序:
服务端程序:
MyServer.java
public class MyServer { //定义保存所有Socket的ArrayList public static ArrayList<Socket> socketList = new ArrayList<Socket>(); public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(30000); while(true) { //此行代码会阻塞,将一直等待别人的连接 Socket s = ss.accept(); socketList.add(s); //每当客户端连接后启动一条ServerThread线程为该客户端服务 new Thread(new ServerThread(s)).start(); } } }
ServerThread.java
//负责处理每个线程通信的线程类 public class ServerThread implements Runnable { //定义当前线程所处理的Socket Socket s = null; //该线程所处理的Socket所对应的输入流 BufferedReader br = null; public ServerThread(Socket s) throws IOException { this.s = s; //初始化该Socket对应的输入流 br = new BufferedReader(new InputStreamReader( s.getInputStream() , "utf-8")); //② } public void run() { try { String content = null; //采用循环不断从Socket中读取客户端发送过来的数据 while ((content = readFromClient()) != null) { //遍历socketList中的每个Socket, //将读到的内容向每个Socket发送一次 for (Socket s : MyServer.socketList) { OutputStream os = s.getOutputStream(); os.write((content + "\n").getBytes("utf-8")); } } } catch (IOException e) { e.printStackTrace(); } } //定义读取客户端数据的方法 private String readFromClient() { try { return br.readLine(); } //如果捕捉到异常,表明该Socket对应的客户端已经关闭 catch (IOException e) { //删除该Socket。 MyServer.socketList.remove(s); //① } return null; } }
客户端程序:
MultiThreadClient.java
public class MultiThreadClient extends Activity { // 定义界面上的两个文本框 EditText input, show; // 定义界面上的一个按钮 Button send; OutputStream os; Handler handler; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); input = (EditText) findViewById(R.id.input); send = (Button) findViewById(R.id.send); show = (EditText) findViewById(R.id.show); Socket s; handler = new Handler() { @Override public void handleMessage(Message msg) { // 如果消息来自于子线程 if (msg.what == 0x123) { // 将读取的内容追加显示在文本框中 show.append("\n" + msg.obj.toString()); } } }; try { s = new Socket("127.0.0.1", 30000); // 客户端启动ClientThread线程不断读取来自服务器的数据 new Thread(new ClientThread(s, handler)).start(); // ① os = s.getOutputStream(); } catch (Exception e) { e.printStackTrace(); } send.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { try { // 将用户在文本框内输入的内容写入网络 os.write((input.getText().toString() + "\r\n") .getBytes("utf-8")); // 清空input文本框 input.setText(""); } catch (Exception e) { e.printStackTrace(); } } }); } }
客户端线程:
ClientThread.java
public class ClientThread implements Runnable { //该线程负责处理的Socket private Socket s; private Handler handler; //该线程所处理的Socket所对应的输入流 BufferedReader br = null; public ClientThread(Socket s , Handler handler) throws IOException { this.s = s; this.handler = handler; br = new BufferedReader( new InputStreamReader(s.getInputStream())); } public void run() { try { String content = null; //不断读取Socket输入流中的内容。 while ((content = br.readLine()) != null) { // 每当读到来自服务器的数据之后,发送消息通知程序界面显示该数据 Message msg = new Message(); msg.what = 0x123; msg.obj = content; handler.sendMessage(msg); } } catch (Exception e) { e.printStackTrace(); } } }
使用URL访问网络资源
URL对象代表统一资源定位器,它是指向互联网”资源”的指针,资源可以是简单的文件或目录,也可以是对更复杂的对象的引用,URL可由协议名、主机、端口和资源组成 。
URL类提供了多个构造器用于创建URL对象,一旦获得了URL对象之后,可以调用如下常用方法来访问该URL对应的资源。
String getFile():获取此URL的资源名
String getHost():获取此URL的主机名
String getPath():获取些URL的路径部分
int getPort():获取此URL的端口号
String getProtocol():获取此URL的协议名称
String getQuery():获取此URL的查询字符串部分
URLConnection openConnection():URL所引用远程对象连接
InputStream openStream():打开与些URL的连接,并返回一个用于读取该URL资源的InputStream。
例:使用URL读取网络资源:
URLTest.java
public class URLTest extends Activity { ImageView show; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); show = (ImageView) findViewById(R.id.show); // 定义一个URL对象 try { URL url = new URL("http://www.xxx.com/photo.png"); // 打开该URL对应的资源的输入流 InputStream is = url.openStream(); // 从InputStream中解析出图片 Bitmap bitmap = BitmapFactory.decodeStream(is); // 使用ImageView显示该图片 show.setImageBitmap(bitmap); is.close(); // 再次打开URL对应的资源的输入流 is = url.openStream(); // 打开手机文件对应的输出流 OutputStream os = openFileOutput("crazyit.png" , MODE_WORLD_READABLE); byte[] buff = new byte[1024]; int hasRead = 0; // 将URL对应的资源下载到本地 while((hasRead = is.read(buff)) > 0) { os.write(buff, 0 , hasRead); } is.close(); os.close(); } catch (Exception e) { e.printStackTrace(); } } }
2.1 使用URLConnection提交请求
通常创建一个和URL的连接,并发送请求。读取此URL引用的资源需要如下几个步骤 :
通过调用Url对象openConnection()方法创建URLConnection对象。
设置URLConnection的参数和普通请求属性。
如果只是发送get方式请求,使用Connect方法建立和远程资源之间的实际连接即可;如果需要发送post方式的请求需要获取URlConnection实例对应的输出流来发送请求参数。
远程资源变为可用,程序可以访问远程资源的头字段或通过输入流读取远程资源的数据。
在建立和远程资源的实际连接之前,可以通过如下方法来设置请求头字段。
setAllowUserInteraction:设置该URLConnection的allowUserInteraction请求头字段的值。
setDoInput:设置该URLConnection的doInput请求头字段的值。
setDoOutput:设置该URLConnection的doOutput请求头字段的值。
setIfModifiedSince:设置该URLConnection的ifModifiedSince请求头字段的值。
setUseCaches:设置该URLConnection的useCaches请求头字段的值
还可以使用如下方法来设置或增加通用头字段。
setRequestProperty(String key,String value):设置该URLConnection的key请求头字段的值为value。
addRequestProperty(String key,String value):为该URLConnection的key请求头字段增加value值。
当远程资源可用时,程序可以使用以下方法用于访问头字段和内容。
Object getContent():获取该URLConnection的内容
String getHeaderField(String name):获取指定响应头字段的值
getInputStream():返回该URLConnection对应的输入流,用于获取URLConnection响应的内容。
getOutputStream():返回该URLConnection对应的输出流,用于向URLConnection发送请求参数。
例:向Web站点发送GET、POST请求:
GetPostMain.java
public class GetPostMain extends Activity { Button get , post; EditText show; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); get = (Button) findViewById(R.id.get); post = (Button) findViewById(R.id.post); show = (EditText)findViewById(R.id.show); get.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String response = GetPostUtil .sendGet("http://127.0.0.1:8080/abc/a.jsp" , null); show.setText(response); } }); post.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String response = GetPostUtil .sendPost("http://127.0.0.1:8080/abc/login.jsp" , "name=xxx&pass=123"); show.setText(response); } }); } }
GetPostUtil.java
public class GetPostUtil { /** * 向指定URL发送GET方法的请求 * * @param url * 发送请求的URL * @param params * 请求参数,请求参数应该是name1=value1&name2=value2的形式。 * @return URL所代表远程资源的响应 */ public static String sendGet(String url, String params) { String result = ""; BufferedReader in = null; try { String urlName = url + "?" + params; URL realUrl = new URL(urlName); // 打开和URL之间的连接 URLConnection conn = realUrl.openConnection(); // 设置通用的请求属性 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"); // 建立实际的连接 conn.connect(); // 获取所有响应头字段 Map<String, List<String>> map = conn.getHeaderFields(); // 遍历所有的响应头字段 for (String key : map.keySet()) { System.out.println(key + "--->" + map.get(key)); } // 定义BufferedReader输入流来读取URL的响应 in = new BufferedReader( new InputStreamReader(conn.getInputStream())); String line; while ((line = in.readLine()) != null) { result += "\n" + line; } } catch (Exception e) { System.out.println("发送GET请求出现异常!" + e); e.printStackTrace(); } // 使用finally块来关闭输入流 finally { try { if (in != null) { in.close(); } } catch (IOException ex) { ex.printStackTrace(); } } return result; } /** * 向指定URL发送POST方法的请求 * * @param url * 发送请求的URL * @param params * 请求参数,请求参数应该是name1=value1&name2=value2的形式。 * @return URL所代表远程资源的响应 */ public static String sendPost(String url, String params) { PrintWriter out = null; BufferedReader in = null; String result = ""; try { URL realUrl = new URL(url); // 打开和URL之间的连接 URLConnection conn = realUrl.openConnection(); // 设置通用的请求属性 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"); // 发送POST请求必须设置如下两行 conn.setDoOutput(true); conn.setDoInput(true); // 获取URLConnection对象对应的输出流 out = new PrintWriter(conn.getOutputStream()); // 发送请求参数 out.print(params); // flush输出流的缓冲 out.flush(); // 定义BufferedReader输入流来读取URL的响应 in = new BufferedReader( new InputStreamReader(conn.getInputStream())); String line; while ((line = in.readLine()) != null) { result += "\n" + line; } } catch (Exception e) { System.out.println("发送POST请求出现异常!" + e); e.printStackTrace(); } // 使用finally块来关闭输出流、输入流 finally { try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (IOException ex) { ex.printStackTrace(); } } return result; } }
使用HTTP访问网络
3.1 使用HttpURLConnection
URLConnection还有一个子类:HttpURLConnection,可以用于向指定网站发送GET请求、POST请求。它在URLConnection的基础上提供了如下方法:
int getResponseCode():获取服务器的响应代码
String getResponseMessage():获取服务器的响应信息
String getRequestMethod():获取发送请求的方法
Void setRequestMethod(String method):设置发送请求的方法
例:多线程下载:
为了实现多线程,程序可按如下步骤进行:
创建URL对象
获取指定URL对象所指象的资源大小(由getContentLength()方法实现),此处用到了HttpURLConnection类。
在本地磁盘上创建一个与网络资源相同大小的空文件
计算每条线程应该下载网络资源的哪个部分
依次创建、启动多条线程来下载网络资源的指定部分。
3.2 使用Apache HttpClient
HttpClient是一个增强版的HttpURLConnection,它是一个简单的客户端(并不是浏览器),可以发送HTTP请求,接收HTTP响应,以及管理HTTP连接。但不会缓存服务器的响应,不能执行HTML页面中嵌入的JavaScript代码,也不会对页面内容进行任何解析、处理。
Android已经成功地集成了HttpClient,可以直接在Android应用中使用HttpClient来访问提交请求、接收响应。使用HttpClient的步骤如下:
创建HttpClient对象
如果需要发送GET请求,创建HttpGet对象,如果需要发送POST请求,创建HttpPost对象。
如果需要发送请求参数,可调用HttpGet、HttpPost共同的setParams(HttpParams params)方法来添加请求参数。
调用HttpClient对象的execute(HttpUriRequest request)发送请求,该方法返回一个HttpResponse。
调用HttpResponse的getAllHeader()、getHeaders(String name)等方法可获取服务器的响应头;调用HttpResponse的getEntity()方法可获取HttpEntity对象,该对象包含了服务器的响应内容。程序可通过该对象获取服务器的响应内容。
例:HttpClient访问被保护的资源:
HttpClientTest.java
public class HttpClientTest extends Activity { Button get; Button login; EditText response; HttpClient httpClient; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // 创建DefaultHttpClient对象 httpClient = new DefaultHttpClient(); get = (Button) findViewById(R.id.get); login = (Button) findViewById(R.id.login); response = (EditText) findViewById(R.id.response); get.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // 创建一个HttpGet对象 HttpGet get = new HttpGet( "http://127.0.0.1:8080/foo/secret.jsp"); try { // 发送GET请求 HttpResponse httpResponse = httpClient.execute(get); HttpEntity entity = httpResponse.getEntity(); if (entity != null) { // 读取服务器响应 BufferedReader br = new BufferedReader( new InputStreamReader(entity.getContent())); String line = null; response.setText(""); while ((line = br.readLine()) != null) { // 使用response文本框显示服务器响应 response.append(line + "\n"); } } } catch (Exception e) { e.printStackTrace(); } } }); login.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { final View loginDialog = getLayoutInflater().inflate( R.layout.login, null); new AlertDialog.Builder(HttpClientTest.this) .setTitle("登录系统") .setView(loginDialog) .setPositiveButton("登录", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String name = ((EditText) loginDialog .findViewById(R.id.name)).getText() .toString(); String pass = ((EditText) loginDialog .findViewById(R.id.pass)).getText() .toString(); HttpPost post = new HttpPost( "http://127.0.0.1:8080/foo/login.jsp"); // 如果传递参数个数比较多的话可以对传递的参数进行封装 List<NameValuePair> params = new ArrayList<NameValuePair>(); params .add(new BasicNameValuePair("name", name)); params .add(new BasicNameValuePair("pass", pass)); try { // 设置请求参数 post.setEntity(new UrlEncodedFormEntity( params, HTTP.UTF_8)); // 发送POST请求 HttpResponse response = httpClient .execute(post); // 如果服务器成功地返回响应 if (response.getStatusLine() .getStatusCode() == 200) { String msg = EntityUtils .toString(response.getEntity()); // 提示登录成功 Toast.makeText(HttpClientTest.this, msg, 5000).show(); } } catch (Exception e) { e.printStackTrace(); } } }).setNegativeButton("取消", null).show(); } }); } }
使用WebView视图显示网页
4.1 使用WebView浏览网页
WebView的用法与普通的ImageView组件的用法基本相似,它提供了大量方法来执行浏览器操作,例如如下常用方法。
void goBack():后退
void goForward():前进
void loadUrl(String url):加载指定的URL对应的网页
boolean zoomIn():放大网页
boolean zoomOut():缩小网页
例:迷你浏览器:
MiniBrowser.java
public class MiniBrowser extends Activity { EditText url; WebView show; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // 获取页面中文本框、WebView组件 url = (EditText) findViewById(R.id.url); show = (WebView) findViewById(R.id.show); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_SEARCH) { String urlStr = url.getText().toString(); // 加载、并显示urlStr对应的网页 show.loadUrl(urlStr); return true; } return false; } }
Main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <EditText android:id="@+id/url" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <!-- 显示页面的WebView组件 --> <WebView android:id="@+id/show" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
4.2 使用WebView加载HTML代码
利用WebView可以对HTML字符串进行解析、当成HTML页面来显示。 WebView提供了一个loadDataWithBaseURL(String baseUrl,String data,String mimeType,String encoding,String historyUrl)方法,该方法是对loadData(String data, data,String mimeType,String encoding)方法的增强,它不会产生乱码。
例:使用WebView加载HTML:
在配置文件中加上访问网络的权限
ViewHtml.java
public class ViewHtml extends Activity { WebView show; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // 获取程序中的WebView组件 show = (WebView) findViewById(R.id.show); StringBuilder sb = new StringBuilder(); // 拼接一段HTML代码 sb.append("<html>"); sb.append("<head>"); sb.append("<title> 欢迎您 </title>"); sb.append("</head>"); sb.append("<body>"); sb.append("<h2> 欢迎您访问<a href=\"http://http://blog.csdn.net\">" + "CSDN</a></h2>"); sb.append("</body>"); sb.append("</html>"); // 使用简单的loadData方法会导致乱码,可能是Android API的Bug //show.loadData(sb.toString() , "text/html" , "utf-8"); // 加载、并显示HTML代码 show.loadDataWithBaseURL(null, sb.toString() , "text/html" , "utf-8", null); } }