27 ตุลาคม 2555

[Android Code] การค้นหา IP ในวงแลนด้วย ICMP


บทความนี้ก็ยังคงวนเวียนอยู่ในเรื่อง WLAN เหมือนเดิมนี่แหละ
แต่คราวนี้เจ้าของบล็อกจะขอพูดถึงเรื่อง IP ในวงแลนหน่อย
ในตัวอย่างคราวนี้จะเป็นการเช็คในวงแลนว่ามีเครื่องไหนเชื่อมต่อบ้าง
ซึ่งตัวอย่างนี้เจ้าของบล็อกจะใช้วิธีเช็คจาก ICMP
หรือชื่อเต็มๆว่า Internet Control Massage Protocol
ถ้าผู้ที่หลงเข้ามาอ่านอยากรู้มันคืออะไรก็หาข้อมูลเพิ่มเติมเอาเองนะ

ในตัวอย่างนี้จะต้องต่อ WiFi ด้วยนะ เพราะต้องอยู่ในวงแลน
โดยจะให้กดปุ่มเพื่อสแกน IP ในวงแลน แล้วแสดงใน List View
และถ้ารู้ชื่อของเครื่องนั้นๆได้ ก็จะดึงชื่อมาแสดงด้วย
เหมาะสำหรับคนที่ทำแอพเชื่อมต่อกันในวงแลนด้วยกัน



สำหรับคำสั่งเจ้าของบล็อกทำเป็นคลาสไว้ให้แล้วเหมือนเดิม
เวลาเรียกใช้ก็สร้างแค่ List View สำหรับแสดงรายชื่อ
Button สำหรับกดเพื่อสแกน และ Progress Bar แบบวงกลม
เพื่อใช้สำหรับเวลาที่กำลังสแกนอยู่ เพื่อให้รู้ว่ากำลังค้นหา


IPDiscover,java
package app.akexorcist.ipviewericmp;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;

import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class IPDiscover {
    private Context mContext;
    public boolean getIPState = false;
    public String DEVICE_IP_ADDRESS;
    private String ipTrim;
    private ArrayList<String> arr_ip, arr_name;
    private ListView mListIP;
    private View mNormal, mLoad;
    
    public IPDiscover(Context context, ListView listIP, View normal
            , View load) {
        mContext = context;
        getDeviceIP();
        mListIP = listIP;
        mNormal = normal;
        mLoad = load;
    }
    
    public boolean isDiscovered() {
        return getIPState;
    }
    
    public String get(int index) {
        return arr_ip.get(index);
    }
    
    public ArrayList<String> getIPList() {
        return arr_ip;
    }
    
    private void getDeviceIP() {
        WifiManager wifiManager = 
               (WifiManager) mContext.getSystemService
               (Context.WIFI_SERVICE);
        WifiInfo wifiInfo = wifiManager.getConnectionInfo();
        int ipAddress = wifiInfo.getIpAddress();
        ipTrim = (ipAddress & 0xFF) + "."
                + ((ipAddress >> 8 ) & 0xFF) + "."
                + ((ipAddress >> 16 ) & 0xFF);
        DEVICE_IP_ADDRESS = (ipAddress & 0xFF) + "."
                + ((ipAddress >> 8 ) & 0xFF) + "."
                + ((ipAddress >> 16 ) & 0xFF) + "."
                + ((ipAddress >> 24 ) & 0xFF);
        Log.i("IP Discoverage",  "Device IP : " + DEVICE_IP_ADDRESS);
    }
    
    public void getConnectedDevices() {
        getIPState = false;

        Handler refresh = new Handler(Looper.getMainLooper());
        refresh.post(new Runnable() {
            public void run() {
                mNormal.setVisibility(View.INVISIBLE);
                mLoad.setVisibility(View.VISIBLE);
            }
        });

        arr_ip = new ArrayList<String>();
        arr_name = new ArrayList<String>();
        setListView();

        for (int i = 0; i <= 255; i++) {
            final int j = i;
            Runnable runnable = new Runnable() {
                public void run() {
                    try {
                        InetAddress addr = InetAddress.getByName(ipTrim 
                                + "." + String.valueOf(j));
                        Boolean get = addr.isReachable(500);
                        if (get && !DEVICE_IP_ADDRESS.equals
                                    (addr.getHostAddress())) {
                            Log.e("InetAddress", String.valueOf(addr));
                            arr_ip.add(addr.getHostAddress());
                            if(!addr.getHostName().equals
                                    (addr.getHostAddress())) {
                                arr_name.add(addr.getHostName().replace
                                        (".mshome.net", ""));
                            } else {
                                arr_name.add("");
                            }
                        }

                        if(j >= 255) {
                            try {
                                Thread.sleep(500);
                            } catch (InterruptedException e) { }
                            
                            Handler refresh = 
                                    new Handler(Looper.getMainLooper());
                            refresh.post(new Runnable() {
                                public void run() {
                                    setListView();
                                    mNormal.setVisibility(View.VISIBLE);
                                    mLoad.setVisibility(View.INVISIBLE);
                                }
                            });
                            
                            getIPState = true;    
                        }
                    } catch (UnknownHostException ex) {
                    } catch (IOException ex) { }
                }
            };
            new Thread(runnable).start();
        }
    }
    
    private void setListView() {
        ArrayList<String> arr_list = new ArrayList<String>();
        for(int i = 0 ; i < arr_ip.size() ; i++) {
            arr_list.add(arr_ip.get(i) + "\n" + arr_name.get(i));
        }
        mListIP.setAdapter(new ArrayAdapter<String>(mContext
                , android.R.layout.simple_list_item_1, arr_list));        
    }
}


สำหรับคลาส IPDiscover ก็จะรับค่าเริ่มต้นด้วยกัน 3 ค่าคือ
Context, ListView, View และ View โดยที่ View ตัวแรก
คือ Widget อะไรก็ได้อย่าง TextView หรือ Button ก็ได้
สำหรับกดแล้วเริ่มสแกน ที่เจ้าของบล็อกใช้เป็น View
แทนที่จะเจาะจงว่าเป็น Button โดยตรงก็เผื่อผู้ที่หลงเข้ามาอ่าน
จะเอาไปใช้กับอย่างอื่นที่ไม่ใช่ Button นั่นเอง
ส่วน View ตัวที่สองสำหรับเป็นตัวแสดงการโหลด
หรือก็คือที่เจ้าของบล็อกใช้เป็น Progress Bar นั่นเอง

ฟังก์ชัน isDiscovered เอาไว้เช็คว่ากำลังสแกนอยู่หรือป่าว
ถ้าเป็น False คือไม่ได้สแกน ถ้าเป็น True คือกำลังสแกนอยู่

ฟังก์ชัน get เอาไว้ดึงค่า IP Address ที่เก็บไว้ใน arr_ip
มี Exception เป็น ArrayOutOfBoundsException
ถ้าเผลอไประบุช่องอาร์เรย์ที่จะอ่านค่าเกินที่มันมีอยู่

ฟังก์ชัน getIPList เอาไว้ดึงค่า IP Address ที่ค้นหาเจอ
โดยจะส่งค่าออกมาเป็น ArrayList<String>

ฟังก์ชัน getDeviceIP เอาไว้ดึง IP Address ของเครื่อง
โดยจะดึง IP เต็มๆเก็บไว้ใน DEVICE_IP_ADDRESS
และเก็บอีกชุดไว้ใน ipTrim โดยที่ ipTrim จะมีการตัด
ตัวเลขชุดสุดท้ายของ IP Address ออก เพื่อใช้สแกน

ฟังก์ชัน getConnectedDevice เอาไว้ค้นหา IP Address
ของเครื่องอื่นๆที่อยู่ในวงแลนด้วยกันนั่นเอง
เจ้าของบล็อกจะให้เปลี่ยนสถานะเป็นกำลังสแกนอยู่
โดยจะให้ตัวแปร getIPState เป็น false นั่นเอง
แล้วทำการซ่อนปุ่มสแกนก่อน พร้อมกับแสดงตัวโหลดขึ้นมา
เพื่อให้รู้ว่ากำลังสแกนหา IP Address ในวงแลนอยู่
จะเห็นว่ามีการสร้าง Handler ขึ้นมา เพื่อใช้คำสั่ง setVisibility
เพราะคำสั่งนี้จะทำงานบน Thread หลักเท่านั้น
แล้วสร้าง arr_ip กับ arr_name ขึ้นมา
สำหรับเก็บ IP Address กับชื่ออุปกรณ์นั้นๆ
แล้วเรียกฟังก์ชัน setListView (เดี๋ยวอธิบายทีหลัง)
จากนั้นก็เข้าลูป For ให้วนตั้งแต่ 0 ถึง 255
ซึ่งในที่นี้ก็คือตัวเลขชุดท้ายของ IP ในวงแลนนั่นเอง
ส่วน IP Address สามชุดแรกก็อยู่ใน ipTrim แล้วนั่นไง
ก็คือจะดึงสามชุดแรกมาแล้วก็เติมชุดสุดท้ายลงไป
ตั้งแต่ 0 ถึง 255 โดยการในวนแต่ละครั้งก็คือ
จะให้สแกนว่าเลขนั้นๆมีเครื่องใดต่ออยู่หรือป่าว
หรือพูดง่ายๆก็คือ ไล่เช็คตั้งแต่ 0 ถึง 255 นั่นแหละ

โดยการวนลูปแต่ละรอบก็จะเอา IP Address ไปอ้างอิง
ในคำสั่ง InetAddress แล้วเช็คด้วยคำสั่ง isReachable
โดยมี Timeout ที่ 500ms ซึ่งเป็นการ ECHO นั่นเอง
ถ้าไม่มีการ ECHO กลับมาใน 500ms ก็จะถือว่า
IP นั้นๆไม่มีเครื่องใดต่ออยู่ แต่ถ้ามี ECHO กลับมา
ก็ให้จะทำการเก็บ IP Address ไว้ใน arr_ip
แต่ถ้าหมายเลขนั้นๆตรงกับเครื่องตัวเองก็จะไม่บันทึก
และจะทำการอ่านชื่อเครื่องด้วย getHostName
ในกรณีที่อ่านชื่อเครื่องได้ก็จะเก็บไว้ใน arr_name
แต่ถ้าอ่านชื่อเครื่องไม่ได้ จะได้เป็นชื่อ IP Address แทน
จะทำให้ arr_ip กับ arr_name ซ้ำกัน จึงให้เช็คว่าซ้ำกันหรือไม่
ถ้าซ้ำก็ให้เก็บเป็นช่องว่างลงใน arr_name แทน

เมื่อวนครบ 255 แล้วก็จะหน่วงเวลา 300ms
เพราะว่า XXX.XXX.XXX.255 อาจจะยังสแกนอยู่
อย่าลืมว่า Timeout เจ้าของบล็อกได้กำหนดไว้ 300ms
จึงหน่วงเวลา 300ms ก่อน เพื่อให้มั่นใจว่า Timeout แล้ว
แล้วจึงกำหนดให้ List View แสดง IP Address ที่สแกนเจอ
จากนั้นซ่อนตัวแสดงการโหลด (เจ้าของบล้อกใช้ ProgressBar)
แล้วแสดงปุ่มสแกนขึ้นมา เพื่อให้สามารถสแกนใหม่อีกครั้งได้
แล้วก็ให้ตัวแปร getIPState เป็น true เพื่อให้รู้ว่าสแกนเสร็จแล้ว

และฟังก์ชันสุดท้ายก็คือ setListView (พิมซะยาวกว่าจะมาถึง)
เอาไว้ดึง arr_ip และ arr_name มาแสดงบน List View นั่นเอง
โดยจะรวม arr_ip และ arr_name ในแต่ละช่อง เก็บไว้ใน arr_list
แล้วจึงทำการแสดงบน List View ต่อไป
ก็จะได้แถวหนึ่งมีสองบรรทัด บรรทัดแรกเป็น IP Address
และบรรทัดที่สองเป็นชื่อของเครื่องที่สแกนเจอ
และถ้าอ่านชื่อเครื่องไม่ได้ก็จะเป็นช่องว่างนั่นเอง


Main.java
package app.akexorcist.ipviewericmp;

import android.os.Bundle;
import android.app.Activity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ProgressBar;

public class Main extends Activity {
    Button btnScan;
    ListView listIP;
    ProgressBar progressScan;
    IPDiscover ipd;
    
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        listIP = (ListView) findViewById(R.id.listIP);
        progressScan = (ProgressBar) findViewById(R.id.progressScan);
        progressScan.setVisibility(View.INVISIBLE);
        btnScan = (Button) findViewById(R.id.btnScan);
        btnScan.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                btnScan.setVisibility(View.INVISIBLE);
                ipd.getConnectedDevices();    
            }
        });
        
        ipd = new IPDiscover(this, listIP, btnScan, progressScan);
    }
}

สำหรับการใช้งานคลาส IPDiscover ก็จะโคตรสั้นเลย
เพราะเจ้าของบล็อกมะรุมมะตุ้มโค๊ดไว้ในคลาสหมดแล้ว
เวลาใช้ก็แค่ประกาศ List View, Button และ Progress Bar
สำหรับ Progress Bar จะให้ซ่อนก่อน จะแสดงแค่ตอนสแกน
แล้วก็เรียกใช้คลาส IPDiscover โดยส่งค่าเริ่มต้นตามที่บอก
เวลาจะสแกนหา IP Address เครื่องอื่นในวงแลน
ก็แค่ใช้คำสั่ง getConnectedDevice ก็เท่านั้นเอง
โดยในตัวอย่างเจ้าของบล็อกจะให้สแกน เมื่อกดปุ่มสแกน
และก่อนจะเรียกคำสั่งสแกนจะให้ปุ่มสแกนซ่อนก่อนด้วย
เพื่อป้องกันปุ่มถูกซ่อนไม่ทันแล้วผู้ใช้กดปุ่มนี้ซ้ำ
เพราะจะทำให้สแกนซ้ำแล้วขึ้น IP Address ซ้ำ
(จริงๆถ้าไม่ซีเรียสจุดนี้ก็ไม่ต้องใส่ก็ได้นะ)


main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <Button
        android:id="@+id/btnScan"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:text="SCAN" />
    <ProgressBar
        android:id="@+id/progressScan"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true" />
    <ListView
        android:id="@+id/listIP"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:layout_margin="20dp" />
</RelativeLayout>

สำหรับ Layout ก็ไม่มีอะไร แค่สร้าง List View, Button และ Progress Bar 
โดยใช้ RelativeLayout เพื่อให้ Button กับ Progress Bar ทับซ้อนกันได้
เวลากดปุ่มสแกนก็จะซ่อนปุ่มแล้วแสดง Progress Bat ขึ้นมาแทนที่
ให้ดูเหมือนว่าโปรแกรมกำลังทำการสแกนอยู่ก็เท่านั้นเอง


AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="app.akexorcist.ipviewericmp"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="15" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".Main"
            android:label="@string/title_activity_main" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

สำหรับ AndroidManifest ต้องประกาศ User-Permission เพิ่มสองตัว
คือ INTERNET กับ ACCESS_WIFI_STATE 

สำหรับผู้ที่หลงเข้ามาอ่านคนใดต้องการดาวน์โหลดไฟล์ตัวอย่าง
สามารถดาวน์โหลดได้จากที่นี่เลย IPViewer-ICMP [Google Drive]

ถึงแม้ว่าการใช้ ICMP จะช่วยให้การหาเครื่องอื่นในวงแลนทำได้ง่าย
แต่มันก็มีก็ข้อเสียอยู่เช่นกัน ถ้าผู้ที่หลงเข้ามาอ่านคนไหน
ได้ลองแอพฯของเจ้าของบล็อกแล้ว จะพบว่าสแกนไม่เจอบางเครื่อง
(แนะนำให้ใช้ IP Scan บน PC เพื่อใช้เปรียบเทียบดู)
ทั้งนี้เนื่องจากว่า ICMP เป็น Service ตัวหนึ่งเท่านั้น
ซึ่งสามารถจะเปิดหรือปิดการใช้งาน Service นี้ได้
ดังนั้นเครื่องที่สแกนหาไม่เจอก็จะเป็นเครื่องที่ปิด Service ตัวนี้
จึงทำให้ไม่สามารถค้นหาทุกเครื่องในวงแลนได้

แต่ปัญหานี้ก็แก้แล้วด้วยการใช้อีกวิธีหนึ่ง ตามไปอ่านกันเองนะ


เอาไปประยุต์ใช้กันเองนะ




เหล่าพันธมิตรแอนดรอยด์

Devahoy Layer Net NuuNeoI The Cheese Factory Somkiat CC Mart Routine Artit-K Arnondora Kamonway Try to be android developer Oatrice Benz Nest Studios Kotchaphan@Medium Jirawatee@Medium Travispea@Medium