Android Socket範例



Socket介紹

有爬過一些文,了解當前的網路傳輸除了上一章的HTTP還有Socket的方式,而Socket可以分兩種

一種是連接的TCP應用服務,另一種是無連接的UDP應用服務

比較容易理解的說法是:TCP為打電話的方式(連接性)、UDP為發簡訊的方式(無連接性)
上一章的HTTP使用的是請求回應方式,表示當APP發出請求時建立連結通道,當客戶端向伺服器發送完請求,伺服器端才能向客戶端返回資料

而Socket的TCP/IP連線方式是雙方建立連結後可以直接進行資料的雙向傳輸,就不需要每次由客戶端向伺服器發送請求

而UDP連線方式提供無連接的資料傳輸,UDP在發送資料前不需建立連結,不對資料傳輸檢查,即可發送數據包


TCP協議的Socket連接:

伺服器端:

1.實作一個ServerSocket物件並在Server端開個端口
2.調用ServerSocket的accept()連接客戶端(在沒有客戶端連進來時,該方法會讓執行緒阻塞)
3.當客戶端連進來時可以透過InputStream、outputStream做輸出入

客戶端:
1.實作一個Socket物件並指定Server端IP與端口
2.當連上伺服器後可以透過InputStream、outputStream做輸出入


UDP協議的資料傳輸:

伺服器端:

1.實作一個DatagramSocket物件並在Server端開個端口
2.創建一個空的DatagramPacket數據包物件並指定大小,用來接收數據
3.使用DatagramSocket的receive()接收客戶端發送的數據(與TCP的調用ServerSocket的accept()類似,在沒有客戶端連進來時,該方法會讓執行緒阻塞)

客戶端:

1.客戶端也創建一個DatagramSocket物件並且開個端口
2.創建一個InetAddress物件設定要傳送到的Server端的IP地址
3.定義好要發送的資料,創建一個DatagramPacke數據包,並設定該數據包發送到哪個網路地址以及端口
4.最後使用DatagramSocket的send()發送數據包


Socket範例:
以下範例為app作為Client端,而主機用java作為Server端,傳輸的資料就此省略,依使用者需求再自行修改

使用的是TCP協議的Socket連接,本人只有在小作品上用過TCP,而UDP協議的資料傳輸等以後有使用再回來補上

上一章有提到過Android 4.0 之後,有明文規定所有的網路行為都不能在主執行緒(Main Thread)執行,主執行緒又稱UI執行緒(UI Thread),如果要在主執行緒做網路的事,就需要使用Thread-執行緒或是AsyncTask-異步任務

任何有關UI的東西都在主執行緒中執行,若是你的程式佔據主執行緒很久,使用者體驗會非常的差
Log.d("text","thread id="+String.valueOf(Thread.currentThread().getId()).toString);    //使用該LOG可以知道當下是使用哪個執行緒,1為主執行緒


app-Client端:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.Socket;
import org.json.JSONObject;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
public class Client_Socket extends Activity {
    private Thread thread;                //執行緒
    private Socket clientSocket;        //客戶端的socket
    private BufferedWriter bw;            //取得網路輸出串流
    private BufferedReader br;            //取得網路輸入串流
    private String tmp;                    //做為接收時的緩存
    private JSONObject json_write,json_read;        //從java伺服器傳遞與接收資料的json
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        thread=new Thread(Connection);                //賦予執行緒工作
        thread.start();                    //讓執行緒開始執行
    }
    //連結socket伺服器做傳送與接收
    private Runnable Connection=new Runnable(){
        @Override
        public void run() {
            // TODO Auto-generated method stub
            try{
                // IP為Server端
                InetAddress serverIp = InetAddress.getByName("192.168.0.1");
                int serverPort = 5050;
                clientSocket = new Socket(serverIp, serverPort);
                //取得網路輸出串流
                bw = new BufferedWriter( new OutputStreamWriter(clientSocket.getOutputStream()));
                // 取得網路輸入串流
                br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                // 當連線後
                while (clientSocket.isConnected()) {
                    // 取得網路訊息
                    tmp = br.readLine();    //宣告一個緩衝,從br串流讀取值
                    // 如果不是空訊息
                    if(tmp!=null){
                        //將取到的String抓取{}範圍資料
                        tmp=tmp.substring(tmp.indexOf("{"), tmp.lastIndexOf("}") + 1);
                        json_read=new JSONObject(tmp);
                        //從java伺服器取得值後做拆解,可使用switch做不同動作的處理
                    }
                }
            }catch(Exception e){
                //當斷線時會跳到catch,可以在這裡寫上斷開連線後的處理
                e.printStackTrace();
                Log.e("text","Socket連線="+e.toString());
                finish();    //當斷線時自動關閉房間
            }
        }
    };
    @Override
    protected void onDestroy() {            //當銷毀該app時
        super.onDestroy();
        try {
            json_write=new JSONObject();
            json_write.put("action","離線");    //傳送離線動作給伺服器
            Log.i("text","onDestroy()="+json_write+"\n");
            //寫入後送出
            bw.write(json_write+"\n");        
            bw.flush();
            //關閉輸出入串流後,關閉Socket
            //最近在小作品有發現close()這3個時,導致while (clientSocket.isConnected())這個迴圈內的區域錯誤
            //會跳出java.net.SocketException:Socket is closed錯誤,讓catch內的處理再重複執行,如有同樣問題的可以將下面這3行註解掉
            bw.close();
            br.close();
            clientSocket.close();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            Log.e("text","onDestroy()="+e.toString());
        }
    }
}
以上範例只在onDestroy()做輸出,可自行在控制項做監聽器,輸出資料至java伺服器一樣使用JSON格式
而上面的IP只是範例,到時候更改為Server端的IP



java-Server端:

package Server_Socket;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import org.json.JSONObject;
public class Server_Socket {
    private static Thread th_close;                //執行緒
    private static int serverport = 5050;
    private static ServerSocket serverSocket;    //伺服端的Socket
    private static ArrayList<Socket> socketlist=new ArrayList<Socket>();
    // 程式進入點
    public static void main(String[] args){
        try {
            serverSocket = new ServerSocket(serverport);    //啟動Server開啟Port接口
            System.out.println("Server開始執行");
            th_close=new Thread(Judge_Close);                //賦予執行緒工作(判斷socketlist內有沒有客戶端網路斷線)
            th_close.start();                                //讓執行緒開始執行
            // 當Server運作中時
            while (!serverSocket.isClosed()) {
                // 呼叫等待接受客戶端連接
                waitNewPlayer();
            }
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    private static Runnable Judge_Close=new Runnable(){    //讓執行緒每兩秒判斷一次SocketList內是否有客戶端強制斷線
        @Override
        public void run() {                                //在此抓取的是關閉wifi等斷線動作 
            // TODO Auto-generated method stub
            try {
                while(true){
                    Thread.sleep(2000);                    //每兩秒執行一輪
                    for(Socket close:socketlist){
                        if(isServerClose(close))        //當該客戶端網路斷線時,從SocketList剔除
                            socketlist.remove(close);
                    }
                }
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    };
    private static Boolean isServerClose(Socket socket){    //判斷連線是否中斷
        try{  
            socket.sendUrgentData(0);        //發送一個字節的緊急數據,默認情況下是沒有開啟緊急數據處理,不影響正常連線
            return false;                    //如正常則回傳false
        }catch(Exception e){
            return true;                      //如連線中斷則回傳true
        }  
    }  
    // 等待接受客戶端連接
    public static void waitNewSocket() {
        try {
            Socket socket = serverSocket.accept();
            // 呼叫創造新的使用者
            createNewThread(socket);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    // 創造新的使用者
    public static void createNewThread(final Socket socket) {
        // 以新的執行緒來執行
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 增加新的使用者
                    socketlist.add(socket);
                    //取得網路輸出串流
                    BufferedWriter bw = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream()));
                    // 取得網路輸入串流
                    BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    String tmp;
                    JSONObject json_read,json_write;
                    // 當Socket已連接時連續執行
                    while (socket.isConnected()) {
                        tmp = br.readLine();        //宣告一個緩衝,從br串流讀取值
                        // 如果不是空訊息
                        if(tmp!=null){
                            //將取到的String抓取{}範圍資料
                            tmp=tmp.substring(tmp.indexOf("{"), tmp.lastIndexOf("}") + 1);
                            json_read=new JSONObject(tmp);
                            //從客戶端取得值後做拆解,可使用switch做不同動作的處理與回應
                        }else{    //在此抓取的是使用使用強制關閉app的客戶端(會不斷傳null給server)
                            //當socket強制關閉app時移除客戶端
                            socketlist.remove(socket);
                            break;    //跳出迴圈結束該執行緒    
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }  
            }
        });
        // 啟動執行緒
        t.start();
    }
}
	
在serverSocket連線後,用serverSocket.isClosed()判斷ServerSocket是否關閉,沒關閉則等待客戶端連進來
連線進來的每個Socket都存進ArrayList,也可寫個User物件存進Socket而ArrayList存進User
在Judge_Close是判斷客戶端有無使用直接關閉網路或強制斷線的手段,抓取到有客戶端斷線時從列表移除
waitNewSocket()是在接收到客戶端時呼叫createNewThread()創建一個新的執行緒各別處理Socket
在 while (socket.isConnected()) 內可以使用switch將讀取到的資料依動作各別讀取與回傳資料
在Server端開port可能會被防火牆擋下,如發現無法連上可能需要關閉防火牆或防毒
最後記住在AndroidManifest.xml文件添加網路權限
<uses-permission android:name="android.permission.INTERNET" />


=============================================================================================

補充範例中的isConnected()、.isClosed()這兩種方法其實都不太準確

isConnected()只是判斷第一次連接時是否有連上,連上了以後就永遠是true值,就算socket斷開他還是true

isClosed()是判斷使用者是否有對socket呼叫close()方法而已,並不是真的判斷socket是否斷開

這兩個方法取得的值都只是設定好的狀態,其實都不能真正的判斷socket是否斷線,在server端可以向範例這樣設置sendUrgentData(0)來判斷client的網路是否有關閉,下面還有判斷client是否有傳null值代表該client的app被強制關閉,也可以手動寫個按下上一頁關閉client app時傳送指定的動作通知server端可以關閉對client的連結,而client端可以將while (clientSocket.isConnected())寫在try-catch區域內,當處理socket輸出入發生錯誤斷開時就能跳出while執行catch處理



arrow
arrow

    SIN 發表在 痞客邦 留言(3) 人氣()