카테고리 보관물: Java

Java

안드로이드 TCP Socket + Webview bridge 샘플

목적 : 안드로이드 webview 와 native 간의 bridge 를 이용하여 연결하는 방법을 알아보도록 하겠습니다.

또한 native 는 TCP Socket 을 이용하여 데이터 전송을 할 예정입니다.

다소 복잡할것 같지만 소스 보면서 확인하시죠

안드로이드

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:usesCleartextTraffic="true"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Worker_bridge"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

MainActivity.java

package com.example.worker_bridge;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.StrictMode;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

public class MainActivity extends AppCompatActivity {

    private WebView webView;
    TextView mTextView;

    public TextView Toptext;
    public TextView datatext;
    public TextView byText;
    public Button StartButton;
    public Button StopButton;
    public Button ConnButton;
    public Button DiconButton;
    public Button IsconButton;
    public Button Lilly;
    private Socket socket;
    // fixme: TAG
    String TAG = "socketTest";

    private final Handler handler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = findViewById(R.id.mTextView);

        ConnButton = findViewById(R.id.button1);
        StartButton = findViewById(R.id.button2);
        StopButton = findViewById(R.id.button3);
        DiconButton = findViewById(R.id.button4);
        IsconButton = findViewById(R.id.button5);
        Lilly = findViewById(R.id.button6);
        final EditText ipNumber = findViewById(R.id.ipText);

        webView = (WebView)findViewById(R.id.webview);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.loadUrl("http://devtest.coforward.com/andrioWebSocket.html");

        webView.addJavascriptInterface(new AndroidBridge(), "HybridApp");
        webView.setWebChromeClient(new WebChromeClient());      // Javascript alert 기능


        Log.i(TAG, "Application createad");

        int SDK_INT = android.os.Build.VERSION.SDK_INT;
        if (SDK_INT > 8) {
            StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
            StrictMode.setThreadPolicy(policy);
        }


        ConnButton.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(getApplicationContext(), "Connect 시도", Toast.LENGTH_SHORT).show();
                String addr = ipNumber.getText().toString().trim();
                ConnectThread thread = new ConnectThread(addr);

                //키보드 자동 내리기
                InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                imm.hideSoftInputFromWindow(ipNumber.getWindowToken(), 0);

                thread.start();


            }
        });

        // fixme: 버튼 ClickListener
        StartButton.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                StartThread sthread = new StartThread();
                StartButton.setEnabled(false);
                StopButton.setEnabled(true);

                sthread.start();

            }
        });
        StopButton.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                StopThread spthread = new StopThread();
                StartButton.setEnabled(true);
                StopButton.setEnabled(false);
                spthread.start();
            }
        });
        DiconButton.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    socket.close();
                    Toast.makeText(getApplicationContext(), "DisConnect", Toast.LENGTH_SHORT).show();
                    DiconButton.setEnabled(false);
                    ConnButton.setEnabled(true);
                    StartButton.setEnabled(false);
                    StopButton.setEnabled(false);
                } catch (IOException e) {
                    e.printStackTrace();
                    Toast.makeText(getApplicationContext(), "DisConnect 실패", Toast.LENGTH_SHORT).show();
                }
            }
        });
        IsconButton.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                boolean iscon = socket.isClosed();
                InetAddress addr = socket.getInetAddress();
                String tmp = addr.getHostAddress();
                if(!iscon){
                    Toast.makeText(getApplicationContext(), tmp + " 연결 중", Toast.LENGTH_SHORT).show();
                }
                else{
                    Toast.makeText(getApplicationContext(), "연결이 안 되어 있습니다.", Toast.LENGTH_SHORT).show();
                }
            }
        });
        Lilly.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(getApplicationContext(), " Lilly is Cute.\n Lilly is working hard.", Toast.LENGTH_SHORT).show();
            }
        });

    }


    private class AndroidBridge {
        @JavascriptInterface
        public void sendMessage(final String arg) {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    mTextView.setText(arg);
                }
            });
        }
    }


    // fixme: Start 버튼 클릭 시 데이터 송/수신.
    class StartThread extends Thread{

        int bytes;
        String Dtmp;
        int dlen;

        public StartThread(){

            datatext = findViewById(R.id.recvByte);
            byText = findViewById(R.id.ByteText);
        }


        public String byteArrayToHex(byte[] a) {
            StringBuilder sb = new StringBuilder();
            for(final byte b: a)
                sb.append(String.format("%02x ", b&0xff));
            return sb.toString();
        }

        public void run(){

            // 데이터 송신
            try {

                String OutData = "AT+START\n";
                byte[] data = OutData.getBytes();
                OutputStream output = socket.getOutputStream();
                output.write(data);
                Log.d(TAG, "AT+START\\n COMMAND 송신");

            } catch (IOException e) {
                e.printStackTrace();
                Log.d(TAG,"데이터 송신 오류");
            }

            // 데이터 수신
            try {
                Log.d(TAG, "데이터 수신 준비");

                //TODO:수신 데이터(프로토콜) 처리

                while (true) {
                    byte[] buffer = new byte[1024];

                    InputStream input = socket.getInputStream();

                    bytes = input.read(buffer);
                    Log.d(TAG, "byte = " + bytes);

                    //바이트 헥사(String)로 바꿔서 Dtmp String에 저장.
                    Dtmp = byteArrayToHex(buffer);
                    Dtmp = Dtmp.substring(0,bytes*3);
                    Log.d(TAG, Dtmp);

                    String obj = (String) Dtmp;
                    webDataSend(obj);


                    //프로토콜 나누기
                    String[] DSplit = Dtmp.split("a5 5a"); // sync(2byte) 0xA5, 0x5A
                    Dtmp = "";
                    for(int i=1;i<DSplit.length-1;i++){ // 제일 처음과 끝은 잘림. 데이터 버린다.
                        Dtmp = Dtmp + DSplit[i] + "\n";
                    }
                    dlen =  DSplit.length- 2;


                    runOnUiThread(new Runnable() {
                        public void run() {
                            datatext.setText(Dtmp);
                            byText.setText("데이터 " + dlen + "개");
                        }
                    });

                }
            }catch(IOException e){
                e.printStackTrace();
                Log.e(TAG,"수신 에러");
            }


        }

    }

    public void webDataSend(String obj) {
        System.out.println("웹으로 데이터 전송........................................!!!!!!!!!!!!!!!!!!");
        webView.post(new Runnable() {
            @Override
            public void run() {
                webView.loadUrl("javascript:AndroidToSend('안드로이드에서 받은 메시지 = "+obj+"')");
            }
        });
    }

    // fixme: Stop 버튼 클릭 시 데이터 송신.
    class StopThread extends Thread{


        public StopThread(){
        }

        public void run(){

            // 데이터 송신
            try {

                String OutData = "AT+STOP\n";
                byte[] data = OutData.getBytes();
                OutputStream output = socket.getOutputStream();
                output.write(data);
                Log.d(TAG, "AT+STOP\\n COMMAND 송신");

            } catch (IOException e) {
                e.printStackTrace();
            }



        }

    }
    // fixme: Socket Connect.
    class ConnectThread extends Thread {
        String hostname;

        public ConnectThread(String addr) {
            hostname = addr;
        }

        public void run() {
            try { //클라이언트 소켓 생성

                int port = 25003;
                socket = new Socket(hostname, port);
                Log.d(TAG, "Socket 생성, 연결.");

                Toptext = findViewById(R.id.text1);

                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        InetAddress addr = socket.getInetAddress();
                        String tmp = addr.getHostAddress();
                        Toptext.setText(tmp + " 연결 완료");
                        Toast.makeText(getApplicationContext(), "Connected", Toast.LENGTH_LONG).show();

                        DiconButton.setEnabled(true);
                        ConnButton.setEnabled(false);
                        StartButton.setEnabled(true);
                    }
                });




            } catch (UnknownHostException uhe) { // 소켓 생성 시 전달되는 호스트(www.unknown-host.com)의 IP를 식별할 수 없음.

                Log.e(TAG, " 생성 Error : 호스트의 IP 주소를 식별할 수 없음.(잘못된 주소 값 또는 호스트 이름 사용)");
                runOnUiThread(new Runnable() {
                    public void run() {
                        Toast.makeText(getApplicationContext(), "Error : 호스트의 IP 주소를 식별할 수 없음.(잘못된 주소 값 또는 호스트 이름 사용)", Toast.LENGTH_SHORT).show();
                        Toptext.setText("Error : 호스트의 IP 주소를 식별할 수 없음.(잘못된 주소 값 또는 호스트 이름 사용)");
                    }
                });

            } catch (IOException ioe) { // 소켓 생성 과정에서 I/O 에러 발생.

                Log.e(TAG, " 생성 Error : 네트워크 응답 없음");
                runOnUiThread(new Runnable() {
                    public void run() {
                        Toast.makeText(getApplicationContext(), "Error : 네트워크 응답 없음", Toast.LENGTH_SHORT).show();
                        Toptext.setText("네트워크 연결 오류");
                    }
                });


            } catch (SecurityException se) { // security manager에서 허용되지 않은 기능 수행.

                Log.e(TAG, " 생성 Error : 보안(Security) 위반에 대해 보안 관리자(Security Manager)에 의해 발생. (프록시(proxy) 접속 거부, 허용되지 않은 함수 호출)");
                runOnUiThread(new Runnable() {
                    public void run() {
                        Toast.makeText(getApplicationContext(), "Error : 보안(Security) 위반에 대해 보안 관리자(Security Manager)에 의해 발생. (프록시(proxy) 접속 거부, 허용되지 않은 함수 호출)", Toast.LENGTH_SHORT).show();
                        Toptext.setText("Error : 보안(Security) 위반에 대해 보안 관리자(Security Manager)에 의해 발생. (프록시(proxy) 접속 거부, 허용되지 않은 함수 호출)");
                    }
                });


            } catch (IllegalArgumentException le) { // 소켓 생성 시 전달되는 포트 번호(65536)이 허용 범위(0~65535)를 벗어남.

                Log.e(TAG, " 생성 Error : 메서드에 잘못된 파라미터가 전달되는 경우 발생.(0~65535 범위 밖의 포트 번호 사용, null 프록시(proxy) 전달)");
                runOnUiThread(new Runnable() {
                    public void run() {
                        Toast.makeText(getApplicationContext(), " Error : 메서드에 잘못된 파라미터가 전달되는 경우 발생.(0~65535 범위 밖의 포트 번호 사용, null 프록시(proxy) 전달)", Toast.LENGTH_SHORT).show();
                        Toptext.setText("Error : 메서드에 잘못된 파라미터가 전달되는 경우 발생.(0~65535 범위 밖의 포트 번호 사용, null 프록시(proxy) 전달)");
                    }
                });


            }




        }
    }




    @Override
    protected void onStop() {  //앱 종료시
        super.onStop();
        try {
            socket.close(); //소켓을 닫는다.
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <WebView
        android:id="@+id/webview"
        android:layout_width="407dp"
        android:layout_height="381dp"
        android:layout_marginTop="300dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/ByteText"
        tools:ignore="MissingConstraints"
        tools:layout_editor_absoluteX="1dp" />

    <TextView
        android:id="@+id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="60dp"
        android:text="IP 주소 입력 후 Connect 버튼을 눌러 연결을 시도하세요."
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="MissingConstraints" />

    <EditText
        android:id="@+id/ipText"
        android:layout_width="250dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="80dp"
        android:text="220.72.212.247"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="MissingConstraints" />


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="120dp"
        android:orientation="horizontal"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="MissingConstraints">

        <Button
            android:id="@+id/button1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:enabled="true"
            android:text="Connect" />

        <Button
            android:id="@+id/button2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:enabled="false"
            android:text="START" />

        <Button
            android:id="@+id/button3"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:enabled="false"
            android:text="STOP" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/linearLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="160dp"
        android:orientation="horizontal"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="MissingConstraints">

        <Button
            android:id="@+id/button4"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:enabled="false"
            android:text="Disconnect" />

        <Button
            android:id="@+id/button5"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:text="Connect확인" />

        <Button
            android:id="@+id/button6"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:text="LIILY" />
    </LinearLayout>
    <TextView
        android:paddingTop="20dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:id="@+id/ByteText"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="MissingConstraints" />
    <TextView
        android:paddingTop="30dp"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:id="@+id/recvByte"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="MissingConstraints" />

    <TextView
        android:id="@+id/mTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:text="TextView"
        app:layout_constraintTop_toBottomOf="@+id/linearLayout"
        tools:layout_editor_absoluteX="176dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

Socket 서버 (php)

<?php

$host = "0.0.0.0";
$port = 25003;

set_time_limit(0); // no timeout

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("Could not create socket\n");

$result = socket_bind($socket, $host, $port) or die("Could not bind to socket\n");

$result = socket_listen($socket, 3) or die("Could not set up socket listener\n");


do {
    $spawn = socket_accept($socket) or die("Could not accept incoming connection\n");

    do {
        $input = socket_read($spawn, 1024) or die("Could not read input\n");

        $input = trim($input);
        echo "Client Message : " . $input . "\n";

        //$response = 'received' . "\n";
        $response = $input ."\n";

        socket_write($spawn, $response, strlen($response)) or die("Could not write output\n");

        if ($input == 'quit') {
            break;
        }
        if ($input == 'shutdown') {
            socket_close($spawn);
            break 2;
        }

    } while (true);
} while (true);
socket_close($socket);

소켓서버 시작

socket_server]# php server2_php.php

https://ddangeun.tistory.com/31

https://github.com/DDANGEUN/TCP_ClientSocket/tree/master/app/src/main/java/com/lilly/tcp_clientsocket