25 กันยายน 2555

[Android Code] การส่งข้อมูลผ่าน WLAN ด้วย TCP Socket


        ลาก่อนบทความเก่า ตอนนี้เจ้าของบล็อกได้ทำไลบรารี TCP เป็นที่เรียบร้อยแล้ว ดังนั้นแนะนำให้อ่านบทความ [Android Code] Simple TCP Library - ลากันที TCP ที่ยุ่งยาก จะได้ใช้งานสะดวกไม่ยุ่งยากอีกต่อไป และทำงานได้ดีกว่าของเดิมในบทความนี้อีกด้วย

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

        บทความคราวนี้ขอเป็นเรื่องของ WLAN กันซักหน่อย เจ้าของบล็อกจะสร้างแอพสำหรับสื่อสารผ่าน WLAN โดยใช้ TCP Socket (ไปหาอ่านเพิ่มเติมกันเอาเองนะ)

        สำหรับตัวแอพจะใช้กับวงแลนไร้สายเท่านั้น ไม่สามารถสื่อสารผ่านอินเตอร์เน็ตได้


        ก็จะมีให้เลือกว่าจะเป็นฝ่ายส่งหรือเป็นฝ่ายรับข้อมูล ซึ่งจริงๆเจ้าของบล็อกทำให้สามารถรับส่งพร้อมกันไว้แล้ว แต่ในบทความนี้จะขอแยกเป็นสองส่วนออกจากกันชัดๆก่อนจะได้ง่ายต่อการเข้าใจในตัวคำสั่งของแต่ละส่วน
หน้าแรกสุด (Main)
Main.java
package app.akexorcist.tcpcommunication;

import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.app.Activity;
import android.content.Intent;

public class Main extends Activity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Button btnSender = (Button)findViewById(R.id.btnSender);
        btnSender.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                Intent intent = new Intent(getApplicationContext()
                        , Sender.class);
                startActivity(intent);
            }
        });
        
        Button btnReceiver = (Button)findViewById(R.id.btnReceiver);
        btnReceiver.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                Intent intent = new Intent(getApplicationContext()
                        , Receiver.class);
                startActivity(intent);
            }
        });
    }
}

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"
    android:background="#454545"
    android:gravity="center" >
    <Button
        android:id="@+id/btnReceiver"
        android:layout_width="100dp"
        android:layout_height="70dp"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="80dp"
        android:text="Receiver" />
    <Button
        android:id="@+id/btnSender"
        android:layout_width="100dp"
        android:layout_height="70dp"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="80dp"
        android:text="Sender" />
</RelativeLayout>


สำหรับ Main.java ก็ไม่มีอะไรมาก แค่ข้ามไปยัง Activity นั้นๆ
ไม่ขออธิบายอะไรละกันเนอะ เพราะเป็นแค่การ Intent ทั่วๆไป


ส่งข้อความ (Sender)

Sender.java
package app.akexorcist.tcpcommunication;

import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.UnknownHostException;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class Sender extends Activity {
    private static final int TCP_SERVER_PORT = 21111;
    
    TextView txtStatus;
    EditText etxtIP, etxtMessage;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.sender);
        txtStatus = (TextView)findViewById(R.id.txtStatus);
        etxtIP = (EditText)findViewById(R.id.etxtIP);
        etxtMessage = (EditText)findViewById(R.id.etxtMessage);
        Button btnSend = (Button)findViewById(R.id.btnSend);
        btnSend.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                txtStatus.setText("Sending...");
                sendMessage();
            }
        });
    }
    
    public void sendMessage() {
        Runnable runSend = new Runnable() {
            public void run() {
                try {
                    Socket s = new Socket(etxtIP.getText().toString()
                            , TCP_SERVER_PORT);
                    BufferedWriter out = new BufferedWriter
                            (new OutputStreamWriter(s.getOutputStream()));
                    String outgoingMsg = etxtMessage.getText().toString(); 
                    out.write(outgoingMsg);
                    out.flush();
                    Handler refresh = new Handler(Looper.getMainLooper());
                    refresh.post(new Runnable() {
                        public void run()
                        {
                            txtStatus.setText("Message has been sent.");
                            etxtMessage.setText("");
                        }
                    });
                    Log.i("Sender", outgoingMsg);
                    s.close();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                    setText("No device on this IP address.");
                } catch (Exception e) {
                    e.printStackTrace();
                    setText("Connection failed. Please try again.");
                }
            }

            public void setText(String str) {
                final String string = str;
                Handler refresh = new Handler(Looper.getMainLooper());
                refresh.post(new Runnable() {
                    public void run()
                    {
                        txtStatus.setText(string);
                    }
                });    
            }
        };
        new Thread(runSend).start();
    }
}

sender.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#454545"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:orientation="vertical" >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp" >
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="10dp"
            android:text="IP"
            android:textColor="#FFFFFF"
            android:textSize="20dp" />
        <EditText
            android:id="@+id/etxtIP"
            android:layout_width="160dp"
            android:layout_height="wrap_content"
            android:ems="10"
            android:gravity="center"
            android:text="192.168.1.1" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp" >
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="10dp"
            android:text="Say"
            android:textColor="#FFFFFF"
            android:textSize="20dp" />
        <EditText
            android:id="@+id/etxtMessage"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginRight="30dp"
            android:ems="10"
            android:gravity="center"
            android:hint="Say something here"
            android:lines="1"
            android:maxLength="15"
            android:maxLines="1"
            android:singleLine="true"  />
    </LinearLayout>
    <TextView
        android:id="@+id/txtStatus"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="status"
        android:textColor="#FFFFFF" />
    <Button
        android:id="@+id/btnSend"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:text="Send" />
</LinearLayout>


        สำหรับ Sender หรือฝั่งส่งข้อมูลจะมี Edit Text ให้ใส่ IP และข้อความ และปุ่ม Send สำหรับส่งข้อความ แล้วก็ Text View บอกสถานะ เมื่อปุ่มSend ถูกกด ก็จะเรียกฟังก์ชัน sendMessage ทันที

        โดยจะให้สร้าง Runnable ขึ้นมาชื่อว่า runSend เพราะว่าในการส่งข้อมูลอาจจะมีการส่งข้อมูลผิดพลาดทำให้ข้อมูลไม่สามารถถูกส่งได้ ซึ่งจะทำให้แอพค้างได้เพราะคำสั่งส่งข้อมูลจะรอการตอบกลับจากอีกฝั่ง Runnable จะทำการสร้าง Thread แยกขึ้นมาเพื่อทำงานในส่วนการส่งข้อมูล และใช้วิธีแสดงสถานะผ่าน Text View ชื่อ txtStatus เพื่อให้รู้ว่าส่งแล้วหรือยังจากนั้นก็ทำการสร้าง Socket ขึ้นมาด้วยคำสั่ง

Socket s = new Socket(etxtIP.getText().toString(), TCP_SERVER_PORT);

        สำหรับคำสั่งนี้จะต้องกำหนด IP Address และ Port ที่จะสื่อสารด้วย IP Adress ก็จะดึงจากจาก etxtIP และ Port ก็จะดึงจาก TCP_SERVER_PORT โดยบทความนี้จะขอใช้ Port เป็น 21111

        จากนั้นก็ทำการสร้าง Object ของคลาส BufferedWriter เพื่อใช้ส่งข้อมูลและทำการสร้างตัวแปร outgoingMsg เพื่อเก็บข้อความใน etxtMessage แล้วจึงทำการส่งข้อมูลโดยใช้คำสั่งจากคลาส BufferedWriter ด้วยคำสั่ง

out.write(outgoingMsg);

        ในการรับส่งข้อมูลทุกครั้งควรทำการเคลียร์ค่าในบัฟเฟอร์ทิ้งด้วยเพื่อป้องกันข้อมูลตกค้างอยู่ในบัฟเฟอร์ ด้วยคำสั่ง

out.flush();

    จากนั้นทำการสร้าง Handler ขึ้นมา เพราะว่าจะทำการกำหนดค่าใน txtStatus และ etxtMessage เพราะทั้งสองนี้จะต้องทำใน Thread หลักเท่านั้น (เพราะเจ้าของบล็อกได้สร้าง Runnable เพื่อทำเป็น Thread แยกออกมาอีกทีหนึ่ง) โดยใช้คำสั่ง

Handler refresh = new Handler(Looper.getMainLooper());

        จากนั้นก็ทำการกำหนดค่าให้กับ txtStatus แสดงว่าส่งข้อความแล้วและเคลียร์ข้อความใน etxtMessage ทิ้ง เพื่อให้ผู้ใช้พิมข้อความใหม่ได้ทันที

        เมื่อใช้ Socket เสร็จแล้วก็ควรปิดด้วยคำสั่ง

s.close();

        (ในกรณีที่ยังมีการใช้งานอยู่ก็ยังไม่ต้องปิดก็ได้)

        สำหรับฟังก์ชัน setText เจ้าของบล็อกเขียนไว้สำหรับตอนเกิด Exception จะให้แสดงข้อผิดพลาดบน txtStatus แต่กลัวว่ามันจะดูยากและยาว ก็เลยสร้างฟังก์ชันขึ้นมาให้ดูได้ง่ายขึ้นน่ะ 555

        แล้วก็เริ่มทำการสร้าง Thread ได้เลย โดยใช้คำสั่ง

new Thread(runSend).start();

        เพียงเท่านี้ข้อความก็จะถูกส่งไปยัง IP ปลายทาง


รับข้อความ (Receiver)

Receiver.java
package app.akexorcist.tcpcommunication;

import java.util.ArrayList;

import android.app.Activity;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class Receiver extends Activity {
    private static final int TCP_SERVER_PORT = 21111;
    
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.receiever);
        TextView txtIP = (TextView)findViewById(R.id.txtIP);
        txtIP.setText(getIP());
        ArrayList<String> arr_list = new ArrayList<String>();
        ListView listView = (ListView)findViewById(R.id.listView1);
        listView.setAdapter(new ArrayAdapter<String>
                   (getApplicationContext()
                   , android.R.layout.simple_list_item_1, arr_list));
        InService inTask = new InService(getApplicationContext()
                    , TCP_SERVER_PORT, listView, arr_list);
        inTask.execute();
    }
    
    public String getIP() {
        WifiManager wifiManager = 
                (WifiManager)getSystemService(WIFI_SERVICE);
        WifiInfo wifiInfo = wifiManager.getConnectionInfo();
        int ipAddress = wifiInfo.getIpAddress();
        String ip = (ipAddress & 0xFF) + "." +
                ((ipAddress >> 8 ) & 0xFF) + "." +
                ((ipAddress >> 16 ) & 0xFF) + "." +
                ((ipAddress >> 24 ) & 0xFF ) ;
        return ip;
    }

    public void onPause() {
        System.exit(0);
    }
}

        ที่ Receiver.java จะสร้าง Text View สำหรับแสดง IP Addressและสร้าง List View สำหรับแสดงข้อความที่ส่งเข้ามา โดย txtIP จะแสดง IP Address ด้วยฟังก์ชัน getIP ซึ่งฟังก์ชันนี้จะเป็นคำสั่งสำหรับรับค่า IP Address มา ซึ่งโดยปกติแล้วเมื่ออ่านค่าที่ได้จากคำสั่ง getIpAddress() จะได้ค่าออกมาเป็น Integer ซึ่งต้องแปลงข้อมูลอีกที จึงจะได้ค่าเป็น IP Address ที่แท้จริง

        จากนั้นจะเรียกสร้างคลาส InService ซึ่งเป็นคลาสที่เจ้าของบล็อกเขียนไว้ มีการส่งค่า Port, ArrayList<String> และ ListView เข้าไปด้วย ซึ่งเป็นคลาส AsyncTask เอาไว้สำหรับคอยรับข้อความที่ส่งเข้ามา เพราะว่าการรอรับค่าจะต้องทำใน Thread เบื้องหลัง หรือ Background Thread อันนี้ต่างกับ Runnable นะ โดย Runnable จะเป็น Foreground Thread เมื่อแอพหยุดทำงานชั่วคราวหรือถูกปิดลง Runnable ก็จะหยุดทำงานทันที

        แต่สำหรับ AsyncTask จะทำงานอยู่ตลอดไม่ว่าจะหยุดหรือปิดแอพก็ตามการปิดแอพที่ว่านี้คือคำสั่ง finish() หรือกดปุ่ม Back บนอุปกรณ์แอนดรอยด์ ซึ่งปกติแล้วก็เป็นการปิดแอพปกติทั่วไปน่ะแหละ แต่การปิดแอพแบบปกตินี้จะยังมี Thread เบื้อหลังทำงานอยู่ได้ การปิดแอพจริงๆจะใช้คำสั่ง

System.exit(0);

        ซึ่งจะทำให้ไม่มี Thread หรือ Activity ของแอพหลงเหลืออยู่

        หมายเหตุ - เลิกใช้แล้วเนื่องจากมีการทำงานที่ไม่ค่อยเหมาะซักเท่าไร จึงแนะนำให้ดูบทความใหม่ตามที่บอกไว้ข้างบนสุด


InService.java
package app.akexorcist.tcpcommunication;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

import android.content.Context;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class InService extends AsyncTask<Void, Void, Void> {

    ServerSocket ss;
    ListView LISTVIEW;
    ArrayList<String> ARR_LIST;
    int TCP_SERVER_PORT;
    Context CONTEXT;
    public InService(Context context, int port, ListView lv
            , ArrayList<String> arr_list) {
        CONTEXT = context;
        TCP_SERVER_PORT = port;
        LISTVIEW = lv;
        ARR_LIST = arr_list;
    }
    
    protected Void doInBackground(Void... params) {  
        try {
            ss = new ServerSocket(TCP_SERVER_PORT);
            while(true) {
                Socket s = ss.accept();
                BufferedReader in = new BufferedReader
                        (new InputStreamReader(s.getInputStream()));
                final String incomingMsg = in.readLine();
                Log.i("Receiver", incomingMsg);
                Handler refresh = new Handler(Looper.getMainLooper());
                refresh.post(new Runnable() {
                    public void run()
                    {
                        ARR_LIST.add(incomingMsg);         
                        LISTVIEW.setAdapter(new ArrayAdapter<String>
                                   (CONTEXT
                                   , android.R.layout.simple_list_item_1
                                   , ARR_LIST));
                        LISTVIEW.setSelection(LISTVIEW.getCount());
                    }
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}


        สำหรับคลาสนี้ก็จะเป็นคลาส AsyncTask ดังที่กล่าวไว้ในข้างบน ก็จะทำการสร้าง ServerSocket โดยอิงค่าจาก TCP_SERVER_PORT ที่ส่งเข้ามาและทำการสร้าง BufferedReader เพื่อใช้สำหรับรับข้อมูลที่ส่งเข้ามา โดยให้รับข้อมูลแล้วเก็บ้ขอมูลที่ได้ไว้ในตัวแปร String จากนั้นก็ทำการสร้าง Handler ให้เป็น Thread หลักขึ้นมา เพื่อกำหนดค่าให้กับ ArrayList<String> และ ListView ซึ่งการทำงานทั้งหมดนี้จะอยู่ในเงื่อนไข while(true) หรือก็คือวนการทำงานเช่นนี้ตลอดไปเรื่อยๆ เพื่อให้แอพรอรับข้อความที่ส่งเข้ามาได้ตลอด


receiver.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#454545"
    android:orientation="vertical" >
    <TextView
        android:id="@+id/txtIP"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:text="IP Address : 0.0.0.0"
        android:textColor="#FFFFFF"
        android:textSize="20dp" />
    <ListView
        android:id="@+id/listView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="40dp" >
    </ListView>
</LinearLayout>        

        และเนื่องจากในตัวอย่างนี้จะมีการเข้าถึงการใช้งาน Network และ WiFi ซึ่งทั้งสองนี้จะต้องประกาศการขออนุญาตเข้าถึงการทำงานดังกล่าว โดยประกาศเป็น Uses Permission ไว้ใน Android Manifest ดังนี้


AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="app.akexorcist.tcpcommunication"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="15" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <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" 
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="Receiver" 
                  android:screenOrientation="portrait"></activity>
        <activity android:name="Sender" 
                  android:screenOrientation="portrait"></activity>
    </application>
</manifest>

        อ้อลืมบอกไปเลย  พอดีเจ้าของบล็อกมีแอนดรอยด์สองเครื่องเลยไม่ได้ใช้ AVD สำหรับผู้ที่หลงเข้ามาอ่านที่รันผ่าน AVD ตัว AVD จะทำได้แค่ส่งข้อมูล (Sender) เท่านั้นนะครับ ไม่สามารถรับข้อมูล (Receiver) ได้ ลองใส่ไอพีของคอมก็แล้ว

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

        สำหรับตัวอย่างการต่อยอดตัวอย่างนี้ ตามไปดูได้ที่นี่เลย [Android Code] การ Chat ผ่าน WLAN ด้วย TCP Socket


        เสร็จแล้ว เฮ~ ลองพิมข้อความส่งดูละกันนะ



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

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