標籤:java4android socket server client
本節主要介紹Socket編程,發現Java裡面的socket編程和C語言的還是有一些不一樣,比如TCP socket ,在Java中區分了serverSocket。不過原理都一樣,在流程處理上也非常相似,所以,理解起來並不難。我們會先從基礎說起,從如何建立socket串連,到如何?一個合理的設計例如在android中,我們發送一條訊息,然後監聽一個回複,如何做到不卡死UI,本文將會由淺入深的為大家呈現一個相對完整的android socket編程。
在進入Java 的socket編程之前,我們從原理上切入,然後提及相應的代碼說明。
Socket可以說是一種針對網路的抽象,應用通過它可以來針對網路讀寫資料。就像通過一個檔案的file handler就可以都寫資料到存放裝置上一樣。根據TCP協議和UDP協議的不同,在網路編程方面就有面向兩個協議的不同socket,一個是面向位元組流的一個是面向報文的。
TCP
TCP主要是連線導向的協議,它包含有建立和拆除串連,保證資料流的順序和正確性等功能。每次對TCP中間的資料操作相當於對一個資料流進行訪問。它最典型的特徵就是那三向交握的建立串連過程。TCP的串連建立和撤銷過程如:
Server端Server端所要做的事情主要是建立一個通訊的端點,然後等待用戶端發送的請求。典型的處理步驟如下:
1. 構建一個ServerSocket執行個體,指定本地的連接埠。這個socket就是用來監聽指定連接埠的串連請求的。
2.重複如下幾個步驟:
a. 調用socket的accept()方法來獲得下面用戶端的串連請求。通過accept()方法返回的socket執行個體,建立了一個和用戶端的新串連。
b.通過這個返回的socket執行個體擷取InputStream和OutputStream,可以通過這兩個stream來分別讀和寫資料。
c.結束的時候調用socket執行個體的close()方法關閉socket串連。
單線程版本
這個流程的典型範例程式碼如下:
//1. 構造ServerSocket執行個體,指定服務連接埠。ServerSocket servSock = new ServerSocket(servPort);while(true){ // 2.調用accept方法,建立和用戶端的串連 Socket clntSock = servSock.accept(); SocketAddress clientAddress = clntSock.getRemoteSocketAddress(); System.out.println("Handling client at " + clientAddress); // 3. 擷取串連的InputStream,OutputStream來進行資料讀寫 InputStream in = clntSock.getInputStream(); OutputStream out = clntSock.getOutputStream(); while((recvMsgSize = in.read(receiveBuf)) != -1) { out.write(receiveBuf, 0, recvMsgSize); } // 4.操作結束,關閉socket. clntSock.close();}
多線程版本上述是單線程版本的伺服器流程,也可是是多線程的。大體代碼如下:
try { file://建立伺服器 ServerSocket server = new ServerSocket(9998); int i=1; for(;;) { <span style="white-space:pre"></span>Socket incoming = server.accept(); <span style="white-space:pre"></span>new ServerThread(incoming,i).start(); //開啟線程來處理用戶端的請求<span style="white-space:pre"></span>i++; } }catch (IOException ex){ ex.printStackTrace(); }
Client端用戶端的請求過程稍微有點不一樣:
1.構建Socket執行個體,通過指定的遠程伺服器位址和連接埠來建立串連。
2.通過Socket執行個體包含的InputStream和OutputStream來進行資料的讀寫。
3.操作結束後調用socket執行個體的close方法,關閉。
範例程式碼如下;
// 1.根據指定的server地址和連接埠,建立socket串連。Socket socket = new Socket(server, servPort);// 2. 根據socket執行個體擷取InputStream, OutputStream進行資料讀寫。InputStream in = socket.getInputStream();OutputStream out = socket.getOutputStream();out.write(data);//3.操作結束,關閉socket.socket.close();
上述代碼就是一個完整的用戶端/伺服器結構的socket通訊代碼。
不卡死UI的socket設計但是,在android中的socket通訊中,我們可以想象一下這樣的情境。我在EditText裡面輸入了一句“你好”,然後我按發送Button按鈕,就把訊息發送給了遠端的伺服器,然後伺服器給我回了一個“你好,歡迎您!”。我們發送完第一條資訊的時候,我們要阻塞在那裡等伺服器的話,會發生什嗎?我們 UI是不是會卡死?所以,很明顯,這樣的設計是不合理的。
用戶端
MyClientActivity.java檔案,主要是定義了一個繼承自Thread類的用於接收資料的類,覆寫了其中的run()方法,在這個函數裡面接收資料,接收到資料後就通過Handler發送訊息,收到訊息後在UI線程裡更新接收到的資料。完整的內容如下:
package com.nan.client;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.io.UnsupportedEncodingException;import java.net.Socket;import java.net.UnknownHostException;import android.app.Activity;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.TextView;import android.widget.Toast;public class MyClientActivity extends Activity { private EditText mEditText = null; private Button connectButton = null; private Button sendButton = null; private TextView mTextView = null; private Socket clientSocket = null; private OutputStream outStream = null; private Handler mHandler = null; private ReceiveThread mReceiveThread = null; private boolean stop = true; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mEditText = (EditText)this.findViewById(R.id.edittext); mTextView = (TextView)this.findViewById(R.id.retextview); connectButton = (Button)this.findViewById(R.id.connectbutton); sendButton = (Button)this.findViewById(R.id.sendbutton); sendButton.setEnabled(false); //連線按鍵監聽 connectButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub try { //執行個體化對象並串連到伺服器 clientSocket = new Socket("113.114.170.246",8888); } catch (UnknownHostException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } displayToast("串連成功!"); //連線按鍵使能 connectButton.setEnabled(false); //發送按鈕使能 sendButton.setEnabled(true); mReceiveThread = new ReceiveThread(clientSocket); stop = false; //開啟線程 mReceiveThread.start(); } }); //發送資料按鈕監聽 sendButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub byte[] msgBuffer = null; //獲得EditTex的內容 String text = mEditText.getText().toString(); try { //字元編碼轉換 msgBuffer = text.getBytes("GB2312"); } catch (UnsupportedEncodingException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } try { //獲得Socket的輸出資料流 outStream = clientSocket.getOutputStream(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { //發送資料 outStream.write(msgBuffer); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } //清空內容 mEditText.setText(""); displayToast("發送成功!"); } }); //訊息處理 mHandler = new Handler() { @Override public void handleMessage(Message msg) { //顯示接收到的內容 mTextView.setText((msg.obj).toString()); } }; } //顯示Toast函數 private void displayToast(String s) { Toast.makeText(this, s, Toast.LENGTH_SHORT).show(); } private class ReceiveThread extends Thread { private InputStream inStream = null; private byte[] buf; private String str = null; ReceiveThread(Socket s) { try { //獲得輸入資料流 this.inStream = s.getInputStream(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @Override public void run() { while(!stop) { this.buf = new byte[512]; try { //讀取輸入資料(阻塞) this.inStream.read(this.buf); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } //字元編碼轉換 try { this.str = new String(this.buf, "GB2312").trim(); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } Message msg = new Message(); msg.obj = this.str; //發送訊息 mHandler.sendMessage(msg); } } } @Override public void onDestroy() { super.onDestroy(); if(mReceiveThread != null) { stop = true; mReceiveThread.interrupt(); } } }
服務端MyServerActivity.java檔案,定義了兩個Thread子類,一個用於監聽用戶端的串連,一個用於接收資料,其他地方與MyClientActivity.java差不多。完整的內容如下:
package com.nan.server;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.io.UnsupportedEncodingException;import java.net.ServerSocket;import java.net.Socket;import android.app.Activity;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.TextView;import android.widget.Toast;public class MyServerActivity extends Activity { private TextView ipTextView = null; private EditText mEditText = null; private Button sendButton = null; private TextView mTextView = null; private OutputStream outStream = null; private Socket clientSocket = null; private ServerSocket mServerSocket = null; private Handler mHandler = null; private AcceptThread mAcceptThread = null; private ReceiveThread mReceiveThread = null; private boolean stop = true; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); ipTextView = (TextView)this.findViewById(R.id.iptextview); mEditText = (EditText)this.findViewById(R.id.sedittext); sendButton = (Button)this.findViewById(R.id.sendbutton); sendButton.setEnabled(false); mTextView = (TextView)this.findViewById(R.id.textview); //發送資料按鈕監聽 sendButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub byte[] msgBuffer = null; //獲得EditTex的內容 String text = mEditText.getText().toString(); try { //字元編碼轉換 msgBuffer = text.getBytes("GB2312"); } catch (UnsupportedEncodingException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } try { //獲得Socket的輸出資料流 outStream = clientSocket.getOutputStream(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { //發送資料 outStream.write(msgBuffer); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } //清空內容 mEditText.setText(""); displayToast("發送成功!"); } }); //訊息處理 mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch(msg.what) { case 0: { //顯示用戶端IP ipTextView.setText((msg.obj).toString()); //使能發送按鈕 sendButton.setEnabled(true); break; } case 1: { //顯示接收到的資料 mTextView.setText((msg.obj).toString()); break; } } } }; mAcceptThread = new AcceptThread(); //開啟監聽線程 mAcceptThread.start(); } //顯示Toast函數 private void displayToast(String s) { Toast.makeText(this, s, Toast.LENGTH_SHORT).show(); } private class AcceptThread extends Thread { @Override public void run() { try { //執行個體化ServerSocket對象並設定連接埠號碼為8888 mServerSocket = new ServerSocket(8888); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }
while(1)
{
try { //等待用戶端的串連(阻塞) clientSocket = mServerSocket.accept(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } mReceiveThread = new ReceiveThread(clientSocket); stop = false; //開啟接收線程 mReceiveThread.start(); Message msg = new Message(); msg.what = 0; //擷取用戶端IP msg.obj = clientSocket.getInetAddress().getHostAddress(); //發送訊息 mHandler.sendMessage(msg); } } } private class ReceiveThread extends Thread { private InputStream mInputStream = null; private byte[] buf ; private String str = null; ReceiveThread(Socket s) { try { //獲得輸入資料流 this.mInputStream = s.getInputStream(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @Override public void run() { while(!stop) { this.buf = new byte[512]; //讀取輸入的資料(阻塞讀) try { this.mInputStream.read(buf); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } //字元編碼轉換 try { this.str = new String(this.buf, "GB2312").trim(); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } Message msg = new Message(); msg.what = 1; msg.obj = this.str; //發送訊息 mHandler.sendMessage(msg); } } } @Override public void onDestroy() { super.onDestroy(); if(mReceiveThread != null) { stop = true; mReceiveThread.interrupt(); } } }上述代碼就是實現了真實環境下android的socket通訊。
UDP
UDP和TCP有兩個典型的區別,一個就是它不需要建立串連,另外就是它在每次收發的報文都保留了訊息的邊界。
server端因為UDP協議不需要建立串連,它的過程如下:
1. 構造DatagramSocket執行個體,指定本地連接埠。
2. 通過DatagramSocket執行個體的receive方法接收DatagramPacket.DatagramPacket中間就包含了通訊的內容。
3. 通過DatagramSocket的send和receive方法來收和發DatagramPacket.
典型的互動流程代碼如下:
// 1. 構建DatagramSocket執行個體,指定本地連接埠。DatagramSocket socket = new DatagramSocket(servPort);// 2. 構建需要收發的DatagramPacket報文DatagramPacket packet = new DatagramPacket(new byte[ECHOMAX], ECHOMAX);while(true){// 3. 收報文socket.receive(packet);System.out.println("Handling client at " + packet.getAddress().getHostAddress() + " on port " + packet.getPort());// 4. 發報文socket.send(packet);packet.setLength(ECHOMAX);}
client端UDP用戶端的步驟也比較簡單,主要包括下面3步:
1. 構造DatagramSocket執行個體。
2.通過DatagramSocket執行個體的send和receive方法發送DatagramPacket報文。
3.結束後,調用DatagramSocket的close方法關閉。
因為和TCP不同,UDP發送報文的時候可以在同一個本地連接埠隨意發送給不同的伺服器,一般不需要在UDP的DatagramSocket的建構函式中指定目的伺服器的地址。
另外,UDP用戶端還有一個重要的不同就是,TCP用戶端發送echo串連訊息之後會在調用read方法的時候進入阻塞狀態,而UDP這樣卻不行。因為UDP中間是可以允許報文丟失的。如果報文丟失了,進程一直在阻塞或者掛起的狀態,則進程會永遠沒法往下走了。所以會一般設定一個setSoTimeout方法,指定在多久的時間內沒有收到報文就放棄。也可以通過指定一個數字,迴圈指定的次數來讀取報文,讀到就返回,否則就放棄。
一個典型的UDP Client程式碼範例如下:
// 1. 構造UDP DatagramSocket對象DatagramSocket socket = new DatagramSocket();// 2。指定timeout時間,防止進入無限等待狀態socket.setSoTimeout(TIMEOUT);// 3. 構造收發的報文對象DatagramPacket sendPacket = new DatagramPacket(bytesToSend, bytesToSend.length, serverAddress, servPort);DatagramPacket receivePacket = new DatagramPacket(new byte[bytesToSend.length], bytesToSend.length);// 4.指定嘗試的次數int tries = 0;boolean receivedResponse = false; do{socket.send(sendPacket);try{socket.receive(receivePacket); if(!receivePacket.getAddress().equals(serverAddress)){throw new IOException("Received packet from an unknown source");}receivedResponse = true;}catch(InterruptedIOException e){tries += 1;System.out.println("Timed out, " + (MAXTRIES - tries) + "");}}while((!receivedResponse) && (tries < MAXTRIES));// 根據是否接收到報文進行反饋if(receivedResponse){System.out.println("Received: " + new String(receivePacket.getData()));}else{System.out.println("No response -- giving up.");}// 5. 關閉socketsocket.close();
這次的socket編程就說道這裡,裡面參考和引用轉載了不少別人的東西,mark一下。
Reference:
http://shmilyaw-hotmail-com.iteye.com/blog/1556187
http://www.cnblogs.com/lknlfy/archive/2012/03/04/2379628.html
Java4Android之socket網路通訊基礎