Android 에서  socket을 이용한 2인용 게임 개발중이다

 

소켓을 이용해서 메시지를 두번 보낼 때 빠진 메시지가 있어 Log를 검토해 보았다

 

game code

if (tempPoint.y < 0 || tempPoint.y > 500) {
    isPaused = true
    gameData.resetData()
    sendMessageToClientViaSocket("SERVER_STOPPED_GAME")
    messageCallback.onGameStateMessageFromThread(GameState.STOPPED)
    if(tempPoint.y <0) {
        sendMessageToClientViaSocket("SERVER_WIN")
        messageCallback.onGameWinnerFromThread(true)
    } else {
        sendMessageToClientViaSocket("CLIENT_WIN")
        messageCallback.onGameWinnerFromThread(false)
    }
    return
}

 

sendMesssage

fun sendMessageToClientViaSocket(message: String): Unit {
    try{
        if (connectedSocket != null && connectedSocket?.isConnected == true) {
            CoroutineScope(Dispatchers.IO).launch {
                outputStream?.write(message.toByteArray())
            }
        }
    } catch(e:Exception) {
        Log.e(">>>>","sendMessageToClientViaSocket@serverSocketThread exception : ${e.message}")
    }
}

 

Client의 SocketThread에서 Log를 보면 간간히 한번씩 두개의 Message가 합해져 전달 되고 이것 때문에 정확한 상태 전달이 이루어 지지 않았다

 

concatenated Log

2024-04-25 13:03:07.409 32518-395   >>>>                    com.example.lamb0693.p2papp          I  ClientThread ReceivedMessage : SERVER_STOPPED_GAMESERVER_WIN
2024-04-25 13:03:07.410 32518-395   >>>>                    com.example.lamb0693.p2papp          I  onOtherMessageReceivedFromServerViaSocket : SERVER_STOPPED_GAMESERVER_WIN

 

원래 이렇게 되는 경우가 TCP전달에 있을 수 있다고 하는 것 같다

 

코드를 수정했다

message를 보낼때 끝에 "\n"을 delimeter 용으로 넣고

 

fun sendMessageToClientViaSocket(message: String): Unit {
    try{
        if (connectedSocket != null && connectedSocket?.isConnected == true) {
            CoroutineScope(Dispatchers.IO).launch {
                val messageWithLF = message + "\n"
                outputStream?.write(messageWithLF.toByteArray())
            }
        }
    } catch(e:Exception) {
        Log.e(">>>>","sendMessageToClientViaSocket@serverSocketThread exception : ${e.message}")
    }
}

 

client 의 Socket Thread에서는 

합쳐져서 올 가능성에 대비해서

message를 delimeter를 기준으로 split하고

split된 message 하나 하나를 처리해 주었다

 

try{
    val bytesRead = inputStream?.read(buffer)
    if (bytesRead != null && bytesRead > 0) {
        val receivedMessage = String(buffer, 0, bytesRead)

        val messages = receivedMessage.split("\n")
        for (msg in messages){
            if(msg.isNotBlank()){
                if(msg.startsWith("GAME_DATA")){
                    messageCallback.onGameDataReceivedFromServerViaSocket(msg)
                } else {
                    Log.i(">>>>",  "ClientThread ReceivedMessage : $msg")
                    when(msg) {
                        "SERVER_STARTED_GAME" -> messageCallback.onGameStateFromServerViaSocket(GameState.STARTED)
                        "SERVER_PAUSED_GAME" -> messageCallback.onGameStateFromServerViaSocket(GameState.PAUSED)
                        "SERVER_RESTARTED_GAME" -> messageCallback.onGameStateFromServerViaSocket(GameState.STARTED)
                        "SERVER_STOPPED_GAME" -> messageCallback.onGameStateFromServerViaSocket(GameState.STOPPED)
                        "SERVER_WIN" -> messageCallback.onGameWinnerFromServerViaSocket(true)
                        "CLIENT_WIN" -> messageCallback.onGameWinnerFromServerViaSocket(false)
                        else -> messageCallback.onOtherMessageReceivedFromServerViaSocket(msg)
                    }
                }
            }
        }

    } else {
        isRunning = false
    }

 

이렇게 바꾸니 게임이 오류가 발생하거나 빠지는 정보 없이 진행되었다

 

전 회 까지 소켓을 이용한 연결을 만들었으니

이제 UI를 이용해서 메시지를 보내기 위한 작업을 해 보자

 

1. Socket button 활성화

    override fun onCapabilitiesChanged(
        network: Network,
        networkCapabilities: NetworkCapabilities
    ) {
        ..................................
        if(clientSocketThread == null){
            try{
                clientSocketThread = ClientSocketThread(this@MainActivity,
                    InetSocketAddress(peerIpv6, 8888), this@MainActivity)
                clientSocketThread?.also{
                    it.start()
                    runOnUiThread{
                        bindMain.btnSendViaSocket.isEnabled = true
                    }
                }
            } catch(e : Exception){
                Log.e(">>>>", "clientSocket : ${e.message}")
            }
        }
    }

 

    severSocketThread에서도 같은 처리를 해 주었다

 

2.  ThreadMessageCallback interface 

Socket Thread에서 받은 내용을 main Thread에서 chantting message를 update 하기 위해 전달용  interface를 만들었다

 

interface ThreadMessageCallback {
    fun onMessageReceivedFromThread(message : String)
}

 

 

3. SocketThread 수정

1) main Thread에서 message를 보내기 위한 method를 추가하고, 

 

2) ThreadMessageCallback을 이용해 받은 message를 MainActivity에 전달하였다

 

3) ClientThread도 같은 code를 구현

...................................

class ServerSocketThread(private val context: Context, private val messageCallback: ThreadMessageCallback) : Thread(){
    private var serverSocket: ServerSocket? = null
    private var clientSocket : Socket? = null

    private var outputStream: OutputStream? = null
    private var inputStream : InputStream? = null
    private var isRunning = true
    override fun run() {
        Log.i(">>>>", "ServerSocketTrhead Thread Started")

        try {
            serverSocket = ServerSocket(8888)

            serverSocket?.also { serverSocket1 ->
                clientSocket = serverSocket1.accept()
                Log.i(">>>>" , "server socket ; Accepted  clientSocket = $clientSocket")
                clientSocket?.also {
                    inputStream = it.getInputStream()
                    outputStream = it.getOutputStream()

                    sendMessage("hello from server through socket")

                    while(isRunning){
                        val buffer = ByteArray(1024)
                        val bytesRead = inputStream?.read(buffer)
                        if (bytesRead != null && bytesRead > 0) {
                            val receivedMessage = String(buffer, 0, bytesRead)
                            Log.i(">>>>",  "ReceivedMessage : $receivedMessage")
                            messageCallback.onMessageReceivedFromThread(receivedMessage)
                            if(receivedMessage == "quit") isRunning = false
                        }
                    }
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            outputStream?.close()
            inputStream?.close()
            clientSocket?.close()
            serverSocket?.close()
        }
        Log.i(">>>>", "Server Thread terminating...")
    }

    private fun sendMessage(message: String): Unit {
        try{
            val strMessage = "server : $message << via socket"
            outputStream?.write(strMessage.toByteArray())
        } catch(e:Exception) {
            Log.e(">>>>","sendMessage in socket thread : ${e.message}")
        }

    }

    fun sendMessageFromMainThread(message : String) {
        CoroutineScope(Dispatchers.IO).launch {
            sendMessage(message)
        }
    }
}

 

3. MainActivity 와 Trehad간의 소통을 위해 code 수정

 

1) btnSendViaSocket을 활성화 하고

2) ThreadMessageCallback 을 이용해서 MainActivity에 들어오는 chatting message를 보여 주었다

 

class MainActivity : AppCompatActivity(), ThreadMessageCallback {
   .............................  
    @RequiresApi(Build.VERSION_CODES.S)
    fun initUIListener() {
        .....................
        bindMain.btnSendViaSocket.setOnClickListener{
            if(bindMain.editMessage.text.isEmpty()) return@setOnClickListener
            sendMessageViaSocket(bindMain.editMessage.text.toString())
            bindMain.editMessage.text.clear()
        }
    }

    @RequiresApi(Build.VERSION_CODES.S)
    override fun onCreate(savedInstanceState: Bundle?) {
       ..............................
        connectivityManager= getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        initViewModel()
        initUIListener()
        initWifiAwareManager()
        checkPermission()
        bindMain.btnSendViaSocket.isEnabled = false
    }

    .........................

    // 이 button은 뽑아내도 되지만 비교를 위해 남겨 두었다
    fun sendMessageViaSession(message : String){
        if(asServer!!) {
            val strToSend = "server : $message"
            viewModel.publishDiscoverySession.value?.sendMessage(
                viewModel.peerHandle.value!!,101, strToSend.toByteArray(Charsets.UTF_8))
        } else {
            val strToSend = "client : $message"
            viewModel.subscribeDiscoverySession.value?.sendMessage(
                viewModel.peerHandle.value!!,101, strToSend.toByteArray(Charsets.UTF_8))
        }
        val strDisplay = bindMain.tvChattingArea.text.toString() + "\n" + message
        bindMain.tvChattingArea.text = strDisplay
    }

    private fun sendMessageViaSocket(message : String){
        if(asServer!!) {
            val strToSend = "server : $message"
            serverSocketThread?.sendMessageFromMainThread(message)
        } else {
            val strToSend = "client : $message"
            clientSocketThread?.sendMessageFromMainThread(message)
        }
        val strDisplay = bindMain.tvChattingArea.text.toString() + "\n" + message
        bindMain.tvChattingArea.text = strDisplay
    }

    override fun onMessageReceivedFromThread(message: String) {
        Log.i(">>>>", "from thread : $message")
        val strDisplay = bindMain.tvChattingArea.text.toString() + "\n" + message
        bindMain.tvChattingArea.text = strDisplay
    }

    override fun onBackPressed() {
         ...........................
    }
}

 

4. App Test

소켓을 통해 전달 된 message가 chatting 창에 들어 왔다

 

이제 Session을 이용한 message 전달을 위한 code는 제거해도 될 듯하다

 

하고 보니 내용은 WifiAware Session을 만들어 서로 연결하고

연결에서 IPV6주소를 얻고

WifiAwareSession을 바탕으로 Socket 연결을 한다 는 내용인 것 같다.

 

아직 연결이 끊기거나, 범위를 벗어나든지, 다시 연결 되든지 이런 상황에 대해 test 해보고  수정할 것이  남은 것 같다.

 

 

전 회에 이어

 

Wifi Aware를 이용한 DiscoverySession으로 연결된 network를 바탕으로 Socket 연결을 만들어 두 Anrdroid 디바이스 간의 고속의 안정적인 연결을 만들어 내는 과정이다

 

Developer Page에 나와 있는 내용을 따라 가보자

 

1. Server side OnMessageReceived 수정

 이전회 까지의 연결에서 Subscriber로 부터의 message가 오면 onMessageReceived() 가 호출 되고 

override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
	// peerHandled을 set
    viewModel.setPeerHandle(peerHandle)
    
    // 이제 이것은 필요 없긴 하다
    val receivedMessage = String(message, Charsets.UTF_8)
    Log.i(">>>>", "onMessageReceived...$peerHandle, $receivedMessage")
    val strDispay = bindMain.tvChattingArea.text.toString() + "\n" + receivedMessage
    bindMain.tvChattingArea.text = strDispay

    // serverSocketThread가 없으면 server socket 활성화를 시작한다
    if(viewModel.serverSocketThread.value != null) initServerSocket()
    
    // client에 message를 보내고 client는 아래 message를 받으면 ClientSocket을 만든다
    sendMessageViaSession("hello from server")
}

 

   peerHandle 을 Global 변수에 set 한 후 ServerSocket을 준비 한다

   한편으로는 client(subscriber) 에 session을 이용해 message를 보내고

   이 메시지를 받은 client는 onMessageReceived()에서 Socket을 만들고 sever socket에 connect  할 수 있게 된다.

 

2. ServerSocketThread 준비

일반적인 socket 연결 에서와 비슷한 Thread를 준비 했다

port 번호는 8888 로 임의로 설정했다

class ServerSocketThread(private val context: Context) : Thread(){
private var serverSocket: ServerSocket? = null

    private var outputStream: OutputStream? = null
    private var inputStream : InputStream? = null
    private var isRunning = true
    override fun run() {
        Log.i(">>>>", "ServerSocketTrhead Thread Started")

        try {
            // 포트는 임의로 8888로 했다
            serverSocket = ServerSocket(8888)
            serverSocket?.also { serverSocket1 ->
                val clientSocket : Socket? = serverSocket1.accept()
                Log.i(">>>>" , "server socket ; Accepted  clientSocket = $clientSocket")
                clientSocket?.also {
                    inputStream = it.getInputStream()
                    outputStream = it.getOutputStream()

                    sendMessage("hello from server through socket")

                    while(isRunning){
                        val buffer = ByteArray(1024)
                        val bytesRead = inputStream?.read(buffer)
                        if (bytesRead != null && bytesRead > 0) {
                            val receivedMessage = String(buffer, 0, bytesRead)
                            Log.d(">>>>",  "ReceivedMessage : $receivedMessage")
                            // 종료 메시지
                            if(receivedMessage == "quit") isRunning = false
                        }
                    }

                    outputStream?.close()
                    inputStream?.close()
                    clientSocket.close()

                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            serverSocket?.close()
        }
        Log.i(">>>>", "Server Thread terminating...")
    }

    fun sendMessage(message: String): Unit {
        outputStream?.write(message.toByteArray())
    }
}

 

3.  Network 생성 및 Callback 등록

ConnectivityManager.requestNetworkCallback()을 이용해 Network를 생성하고 Callback 을 등록

 

onAvailable() callback 에서 ServerSocketThread를 실행시킴

 

class MainActivity : AppCompatActivity() {
    
    ..........

    private lateinit var connectivityManager : ConnectivityManager
    private var serverSocketThread : ServerSocketThread? =  null
    private var clientSocketThread : ClientSocketThread? =  null

    ............
    
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onCreate(savedInstanceState: Bundle?) {
        ..............

        connectivityManager= getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

        initViewModel()
        initUIListener()
        initWifiAwareManager()
        checkPermission()
    }

    @SuppressLint("MissingPermission")
    fun setWifiAwareSession(wifiAwareSession: WifiAwareSession?){

        .........

        if(asServer!!) {
            ............
            viewModel.wifiAwareSession.value?.publish(config, object : DiscoverySessionCallback() {
                ...............
                @RequiresApi(Build.VERSION_CODES.Q)
                override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
                    viewModel.setPeerHandle(peerHandle)
                    
                    //이부분은 필요 없긴 하다 이제
                    val receivedMessage = String(message, Charsets.UTF_8)
                    Log.i(">>>>", "onMessageReceived...$peerHandle, $receivedMessage")
                    val strDispay = bindMain.tvChattingArea.text.toString() + "\n" + receivedMessage
                    bindMain.tvChattingArea.text = strDispay

                    // serverSocketThread가 null 이면 serverSocket을 만들고 실행
                    if(serverSocketThread == null) initServerSocket()
                    // client는 아래 message를 받으면 ClientSocket을 만든다
                    sendMessageViaSession("hello from server")
                }
                ..............
            }, null)
        } else {
            .........
        }
    }

    
    @RequiresApi(Build.VERSION_CODES.Q)
    fun initServerSocket(){
        if(asServer == null || asServer==false) return
        Log.i(">>>>", "init serversocket")

        if(viewModel.publishDiscoverySession.value == null
            || viewModel.peerHandle.value == null) return

        // WifiAwareNetworkSpecifier 생성
        val networkSpecifier = WifiAwareNetworkSpecifier.Builder(
            viewModel.publishDiscoverySession.value!!,
            viewModel.peerHandle.value!!)
            .setPskPassphrase("12340987")
            .build()
        Log.i(">>>>", "init serversocket $networkSpecifier")

        // WifiAware 를 이용 하는 NetworkRequest 생성
        val myNetworkRequest = NetworkRequest.Builder()
            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
            .setNetworkSpecifier(networkSpecifier)
            .build()
        Log.i(">>>>", "init serversocket $myNetworkRequest")

        // 콜백 만들고 등록
        val networkCallback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                super.onAvailable(network)
                Log.i(">>>>", "NetworkCallback onAvailable")
                Toast.makeText(this@MainActivity, "Socket network availabe", Toast.LENGTH_LONG).show()

                // ServerSocketThread가 만들어 져 있지 않으면
                // ServerSocketThread를 만들고 시작시킴
                try{
                    if(serverSocketThread == null) {
                        serverSocketThread = ServerSocketThread(this@MainActivity)
                        serverSocketThread?.start()
                    }
                } catch ( e : Exception){
                    Log.e(">>>>", "starting socket thread exception : ${e.message}")
                }
            }
            override fun onCapabilitiesChanged(
                network: Network,
                networkCapabilities: NetworkCapabilities
            ) {
                super.onCapabilitiesChanged(network, networkCapabilities)
                Log.i(">>>>", "NetworkCallback onCapabilitiesChanged network : $network")
                Log.i(">>>>", "NetworkCapabilities : $networkCapabilities")
            }
            override fun onLost(network: Network) {
                super.onLost(network)
                Log.i(">>>>", "NetworkCallback onLost")
            }
        }

        connectivityManager.requestNetwork(myNetworkRequest, networkCallback)
    }

    .............
}

 

 

4. Client Side의 onMessageReceived() 수정

위의 과정중 server에서 ServerSocket 시작 후 Session을 이용해 메시지를 보내게 되고

onMessageReceived()에서 clientSocket을 준비한다

@SuppressLint("MissingPermission")
fun setWifiAwareSession(wifiAwareSession: WifiAwareSession?){
    ...........

    if(asServer!!) {
        .........
    } else {
        .........
        viewModel.wifiAwareSession.value?.subscribe(config, object : DiscoverySessionCallback() {
            .............
            @RequiresApi(Build.VERSION_CODES.Q)
            override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
                viewModel.setPeerHandle(peerHandle)
                
                //마찬가지 이제 필요 없다
                val receivedMessage = String(message, Charsets.UTF_8)
                Log.i(">>>>", "onMessageReceived...$peerHandle, $receivedMessage")
                val strDisplay = bindMain.tvChattingArea.text.toString() + "\n" + receivedMessage
                bindMain.tvChattingArea.text = strDisplay
                
                // clientSocketThread가 없으면 실행 시킨다
                if(clientSocketThread ==null) connectToServerSocket()
            }
            ............
        }, null)
    }
}

 

5. ClientSocketThread 준비

일반적인 client 용 socket thread 와 비슷하다

 

class ClientSocketThread(val context: Context, private val host : InetSocketAddress) : Thread() {
    private lateinit var socket : Socket
    private var outputStream: OutputStream? = null
    private var inputStream : InputStream? = null
    private var isRunning = true
    override fun run() {
        Log.i(">>>>", "Client Thread Started")

        try {
            socket = Socket()
            socket.connect(host, 10000)
            Log.i(">>>>" , "client socket ; connected to server = $socket")

            outputStream = socket.getOutputStream()
            inputStream = socket.getInputStream()
            // Send message to the server (group owner)
            sendMessage("hello from client through socket")

            while(isRunning){
                val buffer = ByteArray(1024)
                val bytesRead = inputStream?.read(buffer)
                if (bytesRead != null && bytesRead > 0) {
                    val receivedMessage = String(buffer, 0, bytesRead)
                    // Handle the received message
                    Log.d(">>>>",  "ReceivedMessage : $receivedMessage")
                    
                    //탈출구
                    if(receivedMessage == "quit") isRunning = false
                }
            }

            outputStream?.close()
            inputStream?.close()
            socket.close()
        } catch (e: SocketTimeoutException) {
            // Handle timeout exception
            e.printStackTrace()
        } catch (e: Exception) {
            // Handle other exceptions
            e.printStackTrace()
        }

        Log.i(">>>>", "Client Thread terminating...")
    }

    fun sendMessage(message: String): Unit {
        val strMessage : String  = "" + message
        outputStream?.write(strMessage.toByteArray())
    }
}

 

6. connectToServerSocket() 구현

 

서버의 경우와 내용이 거의 비슷하긴 하지만 이번에는

override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities
        )  Callback 이 실행 되어야 비로소 Network 정보(server의 IPV6 Address 포함) 를 알게 되고

 

그것을 이용해 InetSocketAddress를 만들어 ClientSocketThread를 만들고 실행시킨다, 포트는 서버와 같은 포트를 설정

 

@RequiresApi(Build.VERSION_CODES.Q)
fun connectToServerSocket(){
    if(asServer == null || asServer==true) return
    Log.i(">>>>", "init client socket")

    if(viewModel.subscribeDiscoverySession.value == null
        || viewModel.peerHandle.value == null) return

    // NetworkSpectifer를 만들고
    val networkSpecifier = WifiAwareNetworkSpecifier.Builder(
        viewModel.subscribeDiscoverySession.value!!,
        viewModel.peerHandle.value!!
    )
    .setPskPassphrase("12340987")
    .build()

    Log.i(">>>>", "connecting to server socket $networkSpecifier")

	// NetworkRequest를 만든 후
    val myNetworkRequest = NetworkRequest.Builder()
        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
        .setNetworkSpecifier(networkSpecifier)
        .build()
    Log.i(">>>>", "connecting to server socket $myNetworkRequest")
    
    // 해당 Callback을 만들고
    val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            super.onAvailable(network)
            Log.i(">>>>", "NetworkCallback onAvailable")
            Toast.makeText(this@MainActivity, "Socket network availabe", Toast.LENGTH_LONG)
                .show()
        }

        override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities
        ) {
            super.onCapabilitiesChanged(network, networkCapabilities)
            Log.i(">>>>", "NetworkCallback onCapabilitiesChanged network : $network")
            Log.i(">>>>", "NetworkCapabilities : $networkCapabilities")

            val peerAwareInfo = networkCapabilities.transportInfo as WifiAwareNetworkInfo
            val peerIpv6 = peerAwareInfo.peerIpv6Addr

            if(clientSocketThread == null){
                try{
                    clientSocketThread = ClientSocketThread(this@MainActivity, InetSocketAddress(peerIpv6, 8888))
                    clientSocketThread?.start()
                } catch(e : Exception){
                    Log.e(">>>>", "clientSocket : ${e.message}")
                }
            }
        } 
        override fun onLost(network: Network) {
            super.onLost(network)
            Log.i(">>>>", "NetworkCallback onLost")
        }
    }
    
    // connectivityManager를 이용해 등록하고 실행
    connectivityManager.requestNetwork(myNetworkRequest,
        networkCallback as ConnectivityManager.NetworkCallback
    )
}

 

7.    종료시에는

      위의 Socket connection이 종료 되어도 WifiAware Network는 살아 있어서,  현 상태에서 다시 Socket Connection을 연결 할 수도 있다.  Socket network connection이 종료되면  connectivityManager.unregisterNetworkCallback(netwokrCallback) 을 이용해 현재 등록된 networkCallback을 해제 해 주어야 한다.   해제를 안하고 다시 netwokrkCallback을 다시 만들어서 등록을 하니 등록이 안 되는 것 같았다.

8 . 실행 결과

Log를 보면 Socket이 잘 연결되고

양 측 Thread가 정상적으로 시작 되었다. 현재는 Disconnect로 끝 내는 수 밖에 없고,  다음에 메시지 보내는 기능을 추가 하면 "quit" message로 종료 시킬 수 있다

onCapabilitiesChanged, 나  onAvailable이 여러번 호출 되기도 해서  현재 Thread가 생성 되어 있는지 확인 하는 것은 꼭 추가 되어 있어야 할 듯 하다.

 

1) server side

2024-03-25 00:35:34.512 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCallback onAvailable

2024-03-25 00:35:34.548 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCallback onCapabilitiesChanged network : 718
2024-03-25 00:35:34.548 21097-22418 >>>>                    com.example.wifi_aware_test          I  ServerSocketTrhead Thread Started
2024-03-25 00:35:34.549 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCapabilities : [ Transports: WIFI_AWARE Capabilities: NOT_METERED&NOT_RESTRICTED&TRUSTED&NOT_VPN&NOT_ROAMING&FOREGROUND&NOT_CONGESTED&NOT_SUSPENDED LinkUpBandwidth>=1Kbps LinkDnBandwidth>=1Kbps TransportInfo: <AwareNetworkInfo: IPv6=/fe80::87:56ff:fec7:c02a%aware_data0, port=0, transportProtocol=-1> SignalStrength: 1]
2024-03-25 00:35:34.552 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCallback onCapabilitiesChanged network : 718
2024-03-25 00:35:34.552 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCapabilities : [ Transports: WIFI_AWARE Capabilities: NOT_METERED&NOT_RESTRICTED&TRUSTED&NOT_VPN&VALIDATED&NOT

2024-03-25 00:35:35.561 21097-22418 >>>>                    com.example.wifi_aware_test          I  server socket ; Accepted  clientSocket = Socket[address=/fe80::87:56ff:fec7:c02a%aware_data0,port=49654,localPort=8888]
2024-03-25 00:35:35.563 21097-22418 >>>>                    com.example.wifi_aware_test          D  ReceivedMessage : hello from client through socket

 

2) client side

2024-03-25 00:31:15.208 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCallback onAvailable

2024-03-25 00:31:15.255 21097-22358 >>>>                    com.example.wifi_aware_test          I  Client Thread Started
2024-03-25 00:31:15.257 21097-22358 TcpOptimizer            com.example.wifi_aware_test          D  TcpOptimizer-ON
2024-03-25 00:31:15.261 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCallback onCapabilitiesChanged network : 717
2024-03-25 00:31:15.261 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCapabilities : [ Transports: WIFI_AWARE Capabilities: NOT_METERED&NOT_RESTRICTED&TRUSTED&NOT_VPN&VALIDAT

2024-03-25 00:31:16.279 21097-22358 >>>>                    com.example.wifi_aware_test          I  client socket ; connected to server = Socket[address=/fe80::c3:4dff:febf:b76c%aware_data0,port=8888,localPort=38700]
2024-03-25 00:31:16.283 21097-22358 >>>>                    com.example.wifi_aware_test          D  ReceivedMessage : hello from server through socket

 

8 다음에는

전송 button을 활성화 시켜서 서로 메시지를 보내고,  UI에 message를 나타내도록 추가해 보자

이전 회에 알아본 내용을 실제로 코드로 구현 해 보자

 

1. ViewModel에 observer 추가

    1) ViewModel의 observer에 publishDiscoverSession, subscribeSession에 대한 observer 를 추가하고

    2) btnSendViaSession 을 disable 및 enable 시키는 부분 구현

private fun initViewModel(){
    viewModel = ViewModelProvider(this)[MainViewModel::class.java]

    .................

        viewModel.publishDiscoverySession.observe(this){
            var strDisplay = "publishDiscoverySession : "
            strDisplay += it?.toString() ?: "null"
            bindMain.tvPublishDiscoverySession.text = strDisplay
            bindMain.btnConnect.isEnabled = (
                viewModel.publishDiscoverySession.value == null
                    && viewModel.subscribeDiscoverySession.value == null)
            bindMain.btnSendViaSession.isEnabled = (
                    viewModel.publishDiscoverySession.value != null
                            || viewModel.subscribeDiscoverySession.value != null)
        }

        viewModel.subscribeDiscoverySession.observe(this){
            var strDisplay = "subscribeDiscoverySession : "
            strDisplay += it?.toString() ?: "null"
            bindMain.tvSubscribeDiscoverySession.text = strDisplay
            bindMain.btnConnect.isEnabled = (
                    viewModel.publishDiscoverySession.value == null
                            && viewModel.subscribeDiscoverySession.value == null)
            bindMain.btnSendViaSession.isEnabled = (
                    viewModel.publishDiscoverySession.value != null
                            || viewModel.subscribeDiscoverySession.value != null)
        }
}

 

2. setWifiAwareSession 구현

이전 회의 내용 그대로 순서대로 구현 하였다

fun setWifiAwareSession(wifiAwareSession: WifiAwareSession?){

    Log.i(">>>>", "setting wifiAwareSession")
    if(wifiAwareSession == null) Log.i(">>>>", "wifiAwareSession null")
    removeCurrentWifiAwareSession()
    viewModel.setWifiAwareSession(wifiAwareSession)

    if(asServer == null || bindMain.editServiceName.text.isEmpty()) return

    if(asServer!!) {
        val config: PublishConfig = PublishConfig.Builder()
            .setServiceName(bindMain.editServiceName.text!!.toString())
            .setTtlSec(0)
            .build()
        viewModel.wifiAwareSession.value?.publish(config, object : DiscoverySessionCallback() {
            override fun onPublishStarted(session: PublishDiscoverySession) {
                Log.i(">>>>", "onPublishStarted... $session")
                viewModel.setPublishDiscoverySession(session)
            }
            @RequiresApi(Build.VERSION_CODES.Q)
            override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
                viewModel.setPeerHandle(peerHandle)
                val receivedMessage = String(message, Charsets.UTF_8)
                Log.i(">>>>", "onMessageReceived...$peerHandle, $receivedMessage")
                val strDispay = bindMain.tvChattingArea.text.toString() + "\n" + receivedMessage
                bindMain.tvChattingArea.text = strDispay
                //initServerSocket()
            }
            override fun onSessionTerminated() {
                Log.i(">>>>", "onSessionTerminated")
                removeCurrentWifiAwareSession()
                Toast.makeText(this@MainActivity, "fail to connect to server", Toast.LENGTH_SHORT).show()
                super.onSessionTerminated()
            }
            override fun onServiceLost(peerHandle: PeerHandle, reason: Int) {
                super.onServiceLost(peerHandle, reason)
                Log.i(">>>>", "onServiceLost $peerHandle, $reason")
            }
        }, null)
    } else {
        val config: SubscribeConfig = SubscribeConfig.Builder()
            .setServiceName(bindMain.editServiceName.text!!.toString())
            .setTtlSec(0)
            .build()
        viewModel.wifiAwareSession.value?.subscribe(config, object : DiscoverySessionCallback() {
            override fun onSubscribeStarted(session: SubscribeDiscoverySession) {
                Log.i(">>>>", "onSubscribeStarted... $session")
                viewModel.setSubscribeDiscoverySession(session)
            }
            override fun onServiceDiscovered(
                peerHandle: PeerHandle,
                serviceSpecificInfo: ByteArray,
                matchFilter: List<ByteArray>
            ) {
                Log.i(">>>>", "onServiceDiscovered... $peerHandle, $serviceSpecificInfo")
                val messageToSend = "hello...connected"
                viewModel.setPeerHandle(peerHandle)
                sendMessageViaSession(messageToSend)
                Toast.makeText(this@MainActivity, "Connected to server", Toast.LENGTH_SHORT).show()
            }
            @RequiresApi(Build.VERSION_CODES.Q)
            override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
                viewModel.setPeerHandle(peerHandle)
                val receivedMessage = String(message, Charsets.UTF_8)
                Log.i(">>>>", "onMessageReceived...$peerHandle, $receivedMessage")
                val strDisplay = bindMain.tvChattingArea.text.toString() + "\n" + receivedMessage
                bindMain.tvChattingArea.text = strDisplay
                //connectToServerSocket()
            }
            override fun onSessionTerminated() {
                removeCurrentWifiAwareSession()
                Toast.makeText(this@MainActivity, "fail to connect to server", Toast.LENGTH_SHORT).show()
                super.onSessionTerminated()
            }
            override fun onServiceLost(peerHandle: PeerHandle, reason: Int) {
                super.onServiceLost(peerHandle, reason)
                Log.i(">>>>", "onServiceLost $peerHandle, $reason")
            }
        }, null)
    }   
    
    fun sendMessageViaSession(message : String){
        if(asServer!!) {
            val strToSend = "server : $message"
            viewModel.publishDiscoverySession.value?.sendMessage(
                viewModel.peerHandle.value!!,101, strToSend.toByteArray(Charsets.UTF_8))
        } else {
            val strToSend = "client : $message"
            viewModel.subscribeDiscoverySession.value?.sendMessage(
                viewModel.peerHandle.value!!,101, strToSend.toByteArray(Charsets.UTF_8))
        }
        val strDisplay = bindMain.tvChattingArea.text.toString() + "\n" + message
        bindMain.tvChattingArea.text = strDisplay
    }

 

3. 실행 결과

두개의 device로 실행 시켜 보았다.

하나는 Galaxy S9으로 이전 글에서 살펴 본 최소한의 사양은 만족하지만, 권장 사양은 만족하지 못하지만 일단 정상적인 작동은 하는 것으로 확인 되었다

 

4.  전송 버튼 작동 시키기

전송 버튼에 리스너를 달고 message를 서로 보내 보았다

@RequiresApi(Build.VERSION_CODES.S)
fun initUIListener() {
    ..........
    bindMain.btnSendViaSession.setOnClickListener{
        if(bindMain.editMessage.text.isEmpty()) return@setOnClickListener
        sendMessageViaSession(bindMain.editMessage.text.toString())
        bindMain.editMessage.text.clear()
    }
}

 

이제 서로 서로 메시지를 주고 받을 수 있게 되었다

 

정상 작동 한다

 

5. Socket을 통한 연결

앞에 살펴 보았던 개발자 페이지 내용을 보면 이 방법은 안정적이지 못하고(데이터의 전달이 안 되는 경우도 있다라고 했다), 256 byte까지의 데이터만 전달 가능하다

 

따라서 만들어진 Session을 이용한 Socket 연결 방법이 있다고 하여 다음 회에는 그 것을 구현 해 보겠다

 

서비스 게시(Publish) 및 서비스 구독(Subscribe) 에 대한 내용이다

 

Developer page에 설명되어 있는 과정은 다음과 같다

 

1) 한 쪽에서 WifiAwareSession.publish()를 이용해서 서비스를 게시

     게시에 성공하면 onPublishStarted() 콜백 메서드가 호출됩니다.

     여기서 PublishDiscoverySession을 얻을 수 있다

 

2) 구독자는 WifiAwareSession.subscribe() 를 이용해서 구독 시도  연결되면

    onSubscribeStarted() 콜백이 실행되고 SubscribeDiscoverySession을 얻을 수 있다

    게시자와 연결되면 시스템은 onServiceDiscovered() 콜백 메서드를 실행합니다.

    이 콜백에서 상대방과 연결된  PeerHandle을 얻음

 

3) 구독자는 SubscribeDiscoverySession.sendMessage(peerHandel, .. )을 이용해 Publisher에 Message를 보내고

 

4) 게시자는 onMessageReceived(peerHandle: PeerHandle, message: ByteArray) 을 이용해 상대방의 peerHandle을 얻는다

 

5) 게시자도 PublishDiscoverySession.sendMessage(peerHandle, ....) 을 통해 상대방에 메시지를 보낼 수 있다

 

 

연결이 되면 간단한 메시지를 주고 받는 것은 가능 해 진다

 

그러나....

 

참고: 메시지는 전송되지 않을 수 있고 (또는 순서에 맞지 않거나 두 번 이상 전송될 수 있음) 길이가 약 255바이트로 제한되므로 일반적으로 간단한 메시징에 사용됩니다. 정확한 길이 제한을 확인하려면 getMaxServiceSpecificInfoLength()를 호출합니다. 고속 양방향 통신을 위해서는 앱에서 대신 연결을 생성해야 합니다.

 

상당히 황당한 내용이다.

 

일단 다음회에 PublishDiscoverySession 과 SubscribeDiscoverSession,  PeerHandle을 얻는 과정을 구현해서 메시지를 주고 받아 보고

 

그 다음 마지막으로 Session연결을 바탕으로 한 Socket을 이용한 두 디바이스간의 연결을 생성해 보겠다

 

 

 

이전 회에 이어 WifiAwareSession을 얻는 과정이다

 

1.  ViewModel 초기화 및 observer 추가

WifiAwareSession의 instance 값을 화면에서 볼 수 있도록 변수에 대한 observer를 설정했다

더불어 connect 와 disconnect 버튼도 활성화 및 비활성화 하도록  했다

class MainActivity : AppCompatActivity() {

    lateinit var bindMain : ActivityMainBinding

    ...........

    lateinit var viewModel : MainViewModel

    private fun initViewModel(){
        viewModel = ViewModelProvider(this)[MainViewModel::class.java]

        viewModel.wifiAwareSession.observe(this){
            var strDisplay = "wifiAwareSession : "
            strDisplay += it?.toString() ?: "null"
                bindMain.tvWifiAwareSession.text = strDisplay
            // session이 있으면 연결 버튼 disable
            bindMain.btnConnect.isEnabled = (it==null)
            bindMain.btnDisconnect.isEnabled = (it!=null)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        .............

        initViewModel()

        initWifiAwareManager()

        checkPermission()

    }
}

 

 

2. Session 가져 오기

Developer page의 내용은 다음과 같다

 

Wi-Fi Aware 사용을 시작하려면 앱에서 attach()를 호출하여 WifiAwareSession를 가져와야 합니다. 이 메서드는 다음을 실행합니다.

  • Wi-Fi Aware 하드웨어를 켭니다.
  • Wi-Fi Aware 클러스터에 참여하거나 클러스터를 형성합니다.
  • 생성된 모든 검색 세션의 컨테이너 역할을 하는 고유한 네임스페이스를 사용하여 Wi-Fi Aware 세션을 만듭니다.

앱이 성공적으로 연결되면 시스템은 onAttached() 콜백을 실행합니다. 이 콜백은 앱이 모든 추가 세션 작업에 사용해야 하는 WifiAwareSession 객체를 제공합니다. 앱은 이 세션을 사용하여 서비스를 게시하거나 서비스를 구독할 수 있습니다.

앱은 attach()를 한 번만 호출해야 합니다. 앱에서 attach()를 여러 번 호출하는 경우 앱은 호출마다 고유한 네임스페이스를 가진 다른 세션을 수신합니다. 이는 복잡한 시나리오에서 유용할 수 있지만 일반적으로는 피해야 합니다.

 

3. Callback class 작성

WifiAwareManager.attach() 를 이용하여  WifiAwareSession을 가져오기 위해 Callback 함수를 작성

 

onAttached() method가 실행될 때 parameter 로 WifiAwareSession을 얻을 수 있다

새로운 session이 생기면 먼저 removeCurrentWifiAwareSession을 이용해 모든 연결을 닫고 변수를 초기화 해 주고 난뒤

seWifiAwareSession method를 시행 시킴

class CustomAttachCallback(private val activity: MainActivity) : AttachCallback() {
    override fun onAttachFailed() {
        super.onAttachFailed()
        Log.i(">>>>", "onAttachFailed")
    }

    override fun onAttached(session: WifiAwareSession?) {
        super.onAttached(session)
        Log.i(">>>>", "onAttached")
        session?.let{
            Log.i(">>>>", "onAttached session : $it")
            activity.removeCurrentWifiAwareSession()
            activity.setWifiAwareSession(it)
        }
    }

    override fun onAwareSessionTerminated() {
        super.onAwareSessionTerminated()
        Log.i(">>>>", "onAwareSessionTerminated")
    }
}

 

4. Button들에 Listener를 달아 주었다

attach() 기능을 실행 전 server or client 인지,  만들거나 접속할 서비스의 이름이 설정되었는지 확인 해 주었다.

class MainActivity : AppCompatActivity() {

    lateinit var viewModel : MainViewModel

    private var asServer : Boolean? = null

    @RequiresApi(Build.VERSION_CODES.S)
    fun initUIListener() {
        bindMain.btnConnect.setOnClickListener{
            if(asServer == null) {
                Toast.makeText(this, "server or client를 선택하세요", Toast.LENGTH_LONG).show()
                return@setOnClickListener
            }
            if(bindMain.editServiceName.text.isEmpty()) {
                Toast.makeText(this, "service 이름을 입력하세요", Toast.LENGTH_LONG).show()
                return@setOnClickListener
            }
            attach()
        }
        bindMain.btnDisconnect.setOnClickListener{
            removeCurrentWifiAwareSession()
        }
        bindMain.rbServer.setOnClickListener{
            asServer = true
        }
        bindMain.rbClient.setOnClickListener{
            asServer = false
        }
    }

    @RequiresApi(Build.VERSION_CODES.S)
    override fun onCreate(savedInstanceState: Bundle?) {

        ...............
        initViewModel()
        initUIListener()
        initWifiAwareManager()
        checkPermission()

    }

}

 

5.  WifiAwareSession을 구하기 - attach()

attach button을 누르면 AttachCallback을 등록하고

CustomAttachCallback의 onAttach()에서 WifiAwareSession의 instance를 구해

일단 ViewModel내의 wifiAwareSesssion 변수에 등록 해 주었다

 


class MainActivity : AppCompatActivity() {

    ..............

    private val customAttachCallback=  CustomAttachCallback(this)
    private var asServer : Boolean? = null

    ...............
    
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onCreate(savedInstanceState: Bundle?) {
        ..........

        initViewModel()
        initUIListener()
        initWifiAwareManager()
        checkPermission()
    }

    fun setWifiAwareSession(wifiAwareSession: WifiAwareSession?){
        Log.i(">>>>", "setting wifiAwareSession")
        if(wifiAwareSession == null) Log.i(">>>>", "wifiAwareSession null")
        removeCurrentWifiAwareSession()
        // wifiAwareSession에 값 등록
        viewModel.setWifiAwareSession(wifiAwareSession)
        //......  다음에 계속
    }

    // AttachCallback을 등록하고 attach 기능을 시도
    @RequiresApi(Build.VERSION_CODES.S)
    fun attach(){
        wifiAwareManager.attach(customAttachCallback, null)
    }

    // 새로운 session을 얻으면 이전 session을 모두 없앰
    fun removeCurrentWifiAwareSession(){
        try{
            viewModel.publishDiscoverySession.value?.close()
            viewModel.setPublishDiscoverySession(null)
            viewModel.subscribeDiscoverySession.value?.close()
            viewModel.setSubscribeDiscoverySession(null)
            viewModel.wifiAwareSession.value?.close()
            viewModel.setWifiAwareSession(null)
            viewModel.setPeerHandle(null)
        } catch (e: Exception) {
            Log.e(">>>>", "removeWifiAwareSession : ${e.message}")
        }
    }

    .............
}

 

5.  wifi 상태 변화에 따른 대비 추가

wifAwareBroadastReceiver에 내용을 추가 했다

내용을 추가하면 wifi가 끊어 졌다 연결되면 자동으로 재 연결 된다.

class WifiAwareBroadcastReceiver(
    private val activity: MainActivity,
    private val wifiAwareManager: WifiAwareManager,
    private val wifiAwareSession: WifiAwareSession?,
) : BroadcastReceiver() {
    @SuppressLint("MissingPermission")
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onReceive(context: Context?, intent: Intent?) {
        if(wifiAwareManager.isAvailable){
            Log.i(">>>>", "wifiAwareManager is available")
            activity.attach()
        } else {
            wifiAwareSession.let {
                activity.removeCurrentWifiAwareSession()
            }
            Log.e(">>>>", "wifiAwareManager is not available")
        }
    }
}

 

6. 결과

Connect button을 누르면 

2024-03-24 15:51:16.105 14239-14239 >>>>                    com.example.wifi_aware_test          I  onAttached
2024-03-24 15:51:16.105 14239-14239 >>>>                    com.example.wifi_aware_test          I  onAttached session : android.net.wifi.aware.WifiAwareSession@6c37598
2024-03-24 15:51:16.106 14239-14239 >>>>                    com.example.wifi_aware_test          I  setting wifiAwareSession

 

Disconnect Button을 누르면 

2024-03-24 15:52:06.835 14239-14239 WifiAwareManager        com.example.wifi_aware_test          V  disconnect()

 

예상과 다르게 onAwareSessionTerminated()는 불리지 않았다.  이 건 다음에 쓸 일이 있다

 

Device에도 정상적으로 wifiAwareSession이 표시되어 있다

1. Device가 Wi-fi Aware 를 지원하는 지 확인  및 해당 BroadcastReceiver 작성

(1) WifiAwareManager를 구하는 방법

context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE)

 

(2) WifiAware 가 사용 가능한지에 대한 Receiver 작성

- ACESS_WIFI_STATE 에 대한 permission체크가 필요하다고 되어있지만 Suppress 시켰다. check 하면 missing permission으로 작동되지 않는 것 같았다.

 

물론 이전 회에 필요한 Permission을 추가하고 앱 시작시에 permisson 체크는 해 두어야 한다.

class WifiAwareBroadcastReceiver(
    private val activity: MainActivity,
    private val wifiAwareManager: WifiAwareManager,
    private val wifiAwareSession: WifiAwareSession?,
) : BroadcastReceiver() {
    @SuppressLint("MissingPermission")
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onReceive(context: Context?, intent: Intent?) {
        if(wifiAwareManager.isAvailable){
            Log.i(">>>>", "wifiAwareManager is available")
            //activity.attach()  나중에 추가할 내용
        } else {
        	//나중에 추가할 내용
//            wifiAwareSession.let {
//                activity.removeCurrentWifiAwareSession()
//            }
            Log.e(">>>>", "wifiAwareManager is not available")
        }
    }
}

 

(3) BroadcastReceiver를 등록

class MainActivity : AppCompatActivity() {

    ..........

    private var wifiAwareReceiver: WifiAwareBroadcastReceiver? = null
    private lateinit var intentFilter : IntentFilter
    private lateinit var wifiAwareManager : WifiAwareManager

    ...........

    override fun onCreate(savedInstanceState: Bundle?) {
        ..........
        initWifiAwareManager()
        
        checkPermssion()
    }

    private fun initWifiAwareManager(){
        wifiAwareManager = getSystemService(Context.WIFI_AWARE_SERVICE) as WifiAwareManager
        Log.i(">>>>", "wifiAwareManager : $wifiAwareManager")
        intentFilter = IntentFilter(WifiAwareManager.ACTION_WIFI_AWARE_STATE_CHANGED)
        wifiAwareReceiver = WifiAwareBroadcastReceiver(this, wifiAwareManager, viewModel.wifiAwareSession.value )
    }

    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    override fun onResume() {
        registerReceiver(wifiAwareReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
        super.onResume()
    }
}

 

 

(4) 결과

 

이제 앱을 작동 시키고 Wifi 를 껐다 켜면 다음과 같이 Receiver의 정상 작동을 확인 할 수 있다

 

그렇지만 앱 시작시에 자동으로 isAvaialble() 이 수신되지는 않아서, 여기에 초기화 과정을 넣어서는 안될 듯 하다

 

2024-03-24 11:50:58.574 29281-29281 >>>>                    com.example.wifi_aware_test          E  wifiAwareManager is not available
2024-03-24 11:50:58.677  1016-2349  SemIWCMonitor.File      system_server                        E  Foreground package,:  com.example.wifi_aware_test(10539)
2024-03-24 11:51:04.620 29281-29281 >>>>                    com.example.wifi_aware_test          I  wifiAwareManager is available

 

다음의 구글의 개발자 페이지를 참고해서 제작해 보았다.

 

 

 

Wi-Fi Aware 개요  |  Connectivity  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Wi-Fi Aware 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Wi-Fi Aware 기능을 사용하면 Android 8.0 (API

developer.android.com

 

 

1. Project 만들기

개발자 페이지에 "Wi-Fi Aware 기능을 사용하면 Android 8.0 (API 수준 26) 이상을 실행하는 기기 간에 다른 유형의 연결 없이 서로를 검색하고 직접 연결할 수 있습니다" 라고 되어있다

 

SDK 31이상의 Device에만 적용되는 API도 있다고 되어있다

 

Android 12 (API 수준 31)에서는 Wi-Fi Aware가 다음과 같이 개선되었습니다.

  • Android 12 (API 수준 31) 이상을 실행하는 기기에서는 onServiceLost() 콜백을 사용하여 서비스가 중지되거나 범위를 벗어나서 앱이 검색된 서비스를 손실했을 때 알림을 받을 수 있습니다.
  • Wi-Fi Aware 데이터 경로 설정이 간소화되었습니다. 이전 버전에서는 L2 메시징을 사용하여 시작자의 MAC 주소를 제공하므로 지연 시간이 발생했습니다. Android 12 이상을 실행하는 기기에서는 응답자(서버)가 모든 피어를 수락하도록 구성할 수 있습니다. 즉, 초기화자의 MAC 주소를 미리 알 필요가 없습니다. 이렇게 하면 데이터 경로 가져오기가 빨라지고 단 한 번의 네트워크 요청으로 여러 지점 간 링크를 사용할 수 있습니다.
  • Android 12 이상에서 실행되는 앱은 WifiAwareManager.getAvailableAwareResources() 메서드를 사용하여 현재 사용 가능한 데이터 경로 수를 가져오고 세션을 게시하고 구독 세션을 구독할 수 있습니다. 이렇게 하면 앱에서 원하는 기능을 실행하는 데 사용 가능한 리소스가 충분한지 확인할 수 있습니다.

내가 가진 테스트용 안드로이드 기계중 하나가 갤럭시 S9라 SDK 31은 지원이 되지는 않는다.  그렇지만 테스트 용으로 일단  Minimum SDK  26(Oreo, Android 8.0)으로 프로젝트를 생성 했다

 

2. Permission 추가 및 체크

(1) Permisson 추가

개발자 페이지에 적혀 있는 것 말고 추가로  더 필요한 내용이 있어 추가 했다

 

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <uses-feature
        android:name="android.hardware.wifi.direct"
        android:required="true" />

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

 

(2) Permission check 및 획득

실제 앱 속에서 Permission check는 두가지만 하면 정상 작동했다

   나머지는 추가하면 앱 시작시 확인도 되지 않고, 오히려 중간 중간 Permission check 상황에서 permission Denied로 체크 되어 앱이 정상 작동하지 않는 결과를 보여 주었다

 

private val permissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) {
            permissions -> val granted = permissions.entries.all {
        it.value
    }
        if(granted) Log.i("permission Launcher>>>>", "all permission granted in permission Launcher")
        else {
            Log.e("permission Launcher >>>>", "not all of permission granted in permission Launcher ")
        }
    }

    private fun checkPermission(){
        val statusCoarseLocation = ContextCompat.checkSelfPermission(this,
            "android.permission.ACCESS_COARSE_LOCATION")
        val statusFineLocation = ContextCompat.checkSelfPermission(this,
            "android.permission.ACCESS_FINE_LOCATION")

        val shouldRequestPermission = statusCoarseLocation != PackageManager.PERMISSION_GRANTED
                || statusFineLocation != PackageManager.PERMISSION_GRANTED

        if (shouldRequestPermission) {
            Log.d(">>>>", "One or more Permission Denied, Starting permission Launcher")
            permissionLauncher.launch(
                arrayOf(
                    "android.permission.ACCESS_COARSE_LOCATION",
                    "android.permission.ACCESS_FINE_LOCATION",
                )
            )
        } else {
            Log.d(">>>>", "All Permission Permitted, No need to start permission Launcher")
        }
    }

 

3. 상태를 표시할 변수 만들기

Test 용이라 네트워크 연결 중에 필요한 몇개의 변수가 있는데, 현재 변수의 연결 상태를 쉽게 알고, 디버깅에 참고 하기 위해 변수의 상태를 앱에 표시해 주려고 한다

그래서 ViewModel 과 Observer를 등록해 사용하였다.  실제 프로그램에서는 필요가 없을 것으로 생각된다.  Sample 용이라. 

 

(1) View Model

 


class MainViewModel : ViewModel() {

    private val _wifiAwareSession = MutableLiveData<WifiAwareSession>()
    val wifiAwareSession : LiveData<WifiAwareSession> get() = _wifiAwareSession

    private val _peerHandle = MutableLiveData<PeerHandle>()
    val peerHandle : LiveData<PeerHandle> get() = _peerHandle

    private val _publishDiscoverySession = MutableLiveData<PublishDiscoverySession>()
    val publishDiscoverySession : LiveData<PublishDiscoverySession> get()= _publishDiscoverySession

    private val _subscribeDiscoverySession = MutableLiveData<SubscribeDiscoverySession>()
    val subscribeDiscoverySession : LiveData<SubscribeDiscoverySession> get()= _subscribeDiscoverySession

    init {
        _wifiAwareSession.value = null
        _peerHandle.value = null
        _publishDiscoverySession.value = null
        _subscribeDiscoverySession.value = null
    }

    fun setWifiAwareSession(session: WifiAwareSession?){
        _wifiAwareSession.value = session
    }

    fun setPeerHandle(handle: PeerHandle?){
        _peerHandle.value = handle
    }

    fun setPublishDiscoverySession(session: PublishDiscoverySession?){
        _publishDiscoverySession.value = session
    }

    fun setSubscribeDiscoverySession(session: SubscribeDiscoverySession?){
        _subscribeDiscoverySession.value = session
    }

}

4. User Interface를 생성

위의 변수 상태를 포함한 User Interface 를 생성했다

원래 ServerSocketThread, ClientSocketThred도 ViewModel에 추가 시키려고 했으나, Thread를 상속한 class는 ViewModel 의 observer를 쓰기가 까다로워서 뺐다., UI에는 남아 있다

 

 

다음회 부터 본격적인 시작을 해 보자

Wi-Fi 를 이용한 P to P 연결을 이용해 두 개의 안드로이드 디바이스를 Socket으로 연결해 보고자 한다

 

사실 Internet을 통한 디바이스 간의 연결이 거의 대중화 되어 있기도 하고, 블루투스를 통한 연결이 널리 알려져 있어 Wi-Fi를 이용한 두 디바이스의 연결이 큰 의미가 있을지는 의문이긴 하다.  게다가 안드로이드 디바이스 사이의 연결만 지원 가능하니 더욱 사용 용도가 적을 것 같기도 하다

 

목표는 Wi-Fi 를 이용한  P2P 연결을 이용한 2인용 App을 만들어 보고자 한다

 

일단 두 디바이스 간의 소켓 연결을 만들어 내고, 간단하게 메시지를 주고받는 과정까지 기록을 남겨 보기로 한다

메시지를 주고 받을 수 있으면 파일 전송및 수신, 게임 개발등 다른 방향으로 쉽게 확장 가능하리라 생각된다.

 

Android 개발자 페이지를 보면 다음과 같은 2가지 형식이 있다

 

1) Wi-Fi Direct를 이용한 P2P 연결을 이용

 

2) Wi-Fi Aware 를 이용한 Socket 연결

 

Wi-Fi Direct를 이용한 연결은 개발자 페이지를 보면 간단한 예제가 있어서, 참고로 해서 어느 정도 구현이 가능했고,

Wifi Aware를 이용한 연결은 개발자 페이지의 설명도 자세하지 않고, 참고할 만한 예제가 개발자 페이지 및 웹 상에도 검색이 불가능 하여 상당한 시행 착오 후 연결에 성공하였다.

 

개인적인 느낌으로는 Wi-Fi Aware를 이용한 연결이 관리가 조금 더 쉬운 것 같았다.

 

다음 회에 먼저 Wi-Fi Aware를 이용한 연결을 구현해 보려고 한다

 

 

 

 

 

1.  필요 Pakage 설치 및 migration

ldw@ldw-bmax:~/laravel/restapi$ composer require laravel/sanctum
............
ldw@ldw-bmax:~/laravel/restapi$ php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
................................
ldw@ldw-bmax:~/laravel/restapi$ php artisan migrate

 

2. app/Http/Kernel.php 수정

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
    protected $middleware = [
        ..........
        'api' => [
            // 아래 줄을 추가하면 더 이상은 CSRF token이 없으면 419 CSRF Token mismatch error가 발생한다
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
        ...................
    ];
    ......................
}

 

4. register 기능을 위해 /users/api route는 CSRF token 을 검증하지 않게 해 준다

app/Http/Middleware/VerifyCsrfToken.php 에 $except 배열에 추가해 준다

이제 /api/users  route는 csrf token이 없어도 Post method도 가능하다

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
    * The URIs that should be excluded from CSRF verification.
    *
    * @var array<int, string>
    */
    protected $except = [
        //
        '/api/users'
    ];
}

 

3. api 에 대한 route를  middleware('auth:sanctum') 로 보호

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BookController;
use App\Http\Controllers\ApiUserController;
 

Route::middleware('auth:sanctum')->get('/books', [BookController::class, 'index']);

Route::middleware('auth:sanctum')->get('/books/{book}', [BookController::class, 'show']);

Route::middleware('auth:sanctum')->post('/books', [BookController::class, 'store']);

Route::middleware('auth:sanctum')->put('/books/{book}', [BookController::class, 'update']);

Route::middleware('auth:sanctum')->delete('/books/{book}', [BookController::class, 'destroy']);

Route::get('/users', [UserController::class, 'index']);

Route::post('/users', [UserController::class, 'store']);

 

 

4. 이제 그냥 접속하면 401 Unauthorized response를 받는다

 

 

1. Sanctum vs Passport

https://laravel.com/docs/10.x/passport#passport-or-sanctum

 

Laravel - The PHP Framework For Web Artisans

Laravel is a PHP web application framework with expressive, elegant syntax. We’ve already laid the foundation — freeing you to create without sweating the small things.

laravel.com

 

Api 인증을 필요할 때 꼭 OAuth token 이 필요하면 Laravel Passport를 이용하고

아니면 Laravel Sanctum을 사용해 SPA 나 mobile Application 에 대한 인증을 사용하는 것이 더 좋다고 되어 있다

 

2.  Laravel Sanctum을 이용한 인증 종류

https://laravel.com/docs/10.x/sanctum#introduction

 

Laravel - The PHP Framework For Web Artisans

Laravel is a PHP web application framework with expressive, elegant syntax. We’ve already laid the foundation — freeing you to create without sweating the small things.

laravel.com

 

(1) Api token을 이용한 인증 

     -- 로그인시 API token을 발행, SPA application이나 mobile App에서 그 token을 이용해 Laravel Rest API Server 에 인증

     -- 주로 mobile application을 위해 사용

 

(2) built-in cookie based session authentication services

     -- Web browser를 이용하는 경우에는 이 system을 이용할 수 있다,  Session과 비슷한 것 같다.

     -- 즉 SPA application에는 이 것을 사용하는 것이 바람직하다 라고 한다.

 

여기서는 지난번 회차 까지 React를 이용한 SPA로 Laravel api server를 이용중이니, 다음 글에서 먼저 (2) 방법을 적용해서 API에 대한 인증을 적용해 보았다

 

이후 flutter를 이용 간단한 mobile App을 만들고 API token 발행 및 인증을 위한  사용 방법을 시도해 볼 생각이다

 

 

 

Let's Encrypt certificate expiration notice for domain "www.xxxxx.com"

라는 e-mail 이 왔다

 

처음 무료 ssl받은 것이 3개월이 다 된 모양이다

이렇게 연장해 주면 된다 한다

 

내 server는 nginx reverse proxy 에 두개의 server가 연결 되어 있다.

 

1. server 중단

ubuntu@ip-172-31-7-173:~$ sudo service nginx stop

 

2. 인증서 다시 받기

ubuntu@ip-172-31-7-173:~$ sudo certbot certonly --standalone -d xxxxxxx.com -d abc.xxxxxxx.com
Saving debug log to /var/log/letsencrypt/letsencrypt.log
.........................................
Do you want to expand and replace this existing certificate with the new
certificate?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(E)xpand/(C)ancel: e
Renewing an existing certificate for xxxxxx.com and king.xxxxxx.com

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/king.xxxxx.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/king.xxxxxxx.com/privkey.pem
This certificate expires on 2024-05-30.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

.........................

 

3. Server 재시작

ubuntu@ip-172-31-7-173:~$ sudo service nginx start

 

정상 작동한다.

'우분투' 카테고리의 다른 글

sudo 권한 복구  (0) 2024.02.17

로그인 및 인증을 위하여 기본적으로 만들어 져 있는 users table 과 User Model을 이용하여 사용자를 추가해 보았다

API를 이용한 register기능을 위해 Book Model과 같은 과정을 거쳤다

 

1. 기본적으로 만들어져 있는 user table의 구조이다

mysql> desc users;
+-------------------+-----------------+------+-----+---------+----------------+
| Field             | Type            | Null | Key | Default | Extra          |
+-------------------+-----------------+------+-----+---------+----------------+
| id                | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| name              | varchar(255)    | NO   |     | NULL    |                |
| email             | varchar(255)    | NO   | UNI | NULL    |                |
| email_verified_at | timestamp       | YES  |     | NULL    |                |
| password          | varchar(255)    | NO   |     | NULL    |                |
| remember_token    | varchar(100)    | YES  |     | NULL    |                |
| created_at        | timestamp       | YES  |     | NULL    |                |
| updated_at        | timestamp       | YES  |     | NULL    |                |
+-------------------+-----------------+------+-----+---------+----------------+

 

2. controller 생성

ldw@ldw-bmax:~/laravel/restapi php artisan make:controller --resource

 ┌ What should the controller be named? ───────────────────────┐
 │ UserController                                               │
 └────────────────────────────────────────────────┘

   INFO  Controller [app/Http/Controllers/UserController.php] created successfully.  

3. resource 생성

ldw@ldw-bmax:~/laravel/restapi$ php artisan make:resource UserResource

   INFO  Resource [app/Http/Resources/UserResource.php] created successfully.  

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
    * Transform the resource into an array.
    *
    * @return array<string, mixed>
    */
    public function toArray(Request $request): array
    {
        //return parent::toArray($request);
        return [
            'name' => $thisi->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'id' => $this->id,
        ];
    }
}

 

4. router 수정

routers/api.php

 

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BookController;
use App\Http\Controllers\UserController;

.................

Route::get('/users', [UserController::class, 'index']);

Route::post('/users', [UserController::class, 'store']);
 

 

6. controller 수정

store()와 결과를 보여주기 위해 index() 함수를 만들었고,

password는 Hash::make() 함수를 이용 hash해 주었다

 

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;
use App\Http\Controllers\UserController;
use App\Http\Resources\UserResource;
use Illuminate\Support\Facades\Hash;


class UserController extends Controller
{
    /**
    * Display a listing of the resource.
    */
    public function index()
    {
        $users = User::all();
        return UserResource::collection($users);
     }

 

/**
* Store a newly created resource in storage.
*/
    public function store(Request $request)
    {
        $validatedData = $request->validate([
            'name' => ['required', 'string', 'unique:users'],
            'email' => ['required', 'email', 'unique:users,email'],
            'password' => ['required', 'string', 'min:8'],
        ]);

        $validatedData['password'] = Hash::make($validatedData['password']);

        $user = User::create($validatedData);

        return new UserResource($user);
     }

}

 

7. React 에 register page 작성

Regster.js

 
import {Button, Container, Table, Form, InputGroup} from 'react-bootstrap'
import axios from "axios"
import { useEffect, useState} from 'react'
 
export const Register = () => {
 
    const [users, setUsers] = useState([])
    const [email, setEmail] = useState('')
    const [name, setName] = useState('')
    const [password, setPassword] = useState('')
    const [createResult, setCreateResult] = useState([])
    const [message, setMessage] = useState('')

    const getUser = async () => {
        try {
             const result = await axios.get( "http://localhost:8000/api/users" )
             console.log(result.data)
             setUsers(result.data.data)
        } catch ( err){
             console.log( err )
        }
     }

     const createUser = async () => {
         try {
             const data = {
                  "name" : name,
                  "email" : email,
                  "password" : password,
             }

             console.log("data to upload", data)

             const result = await axios.post("http://localhost:8000/api/users", data)

             console.log(result.data)
             window.location.reload();
 
       } catch ( err){
            if (axios.isAxiosError(err)) {
                 console.log("Axisos Error ===> " , err)
                 setCreateResult(err.response.data)
                 setMessage(err.response.data.message)
            } else {
                  console.log("Other Error", err)
            }
       }
    }

    const handleNameChange = (event) => {
        setName(event.currentTarget.value)
    }

    const handleEmailChange = (event) => {
        setEmail(event.currentTarget.value)
    }

    const handlePasswordChange = (event) => {
        setPassword(event.currentTarget.value)
    }

    useEffect( () => {
        getUser()
    }, [])

    return (
        <Container>
            <Container className='fs-2'>
                User
            </Container>
            <Container>
                 <Table striped bordered>
                 <thead>
                       <tr>
                            <th>id</th>
                            <th>name</th>
                            <th>Email</th>
                       </tr>
                 </thead>
                 <tbody>
                 {users?.map((user, index) => (
                        <tr key={index}>
                             <td>{user.id}</td>
                             <td>{user.name}</td>
                             <td>{user.email}</td>
                        </tr>
                  ))}
                  </tbody>
                  </Table>
             </Container>

            {message && (
            <Container>
                 {message}
            </Container>
            )}
 
           {createUser && createUser.errors && (
           <Container>
                 {createUser.message}
                 {Object.keys(createUser.errors).map( (key, index) => {
                      return <div> {createUser.errors[key][0]} </div>
                 })}
           </Container>
       )}

           <Container>
                <Form className="mb-3">
                <InputGroup>
                      <Form.Control type="text" aria-label="Recipient's username with two button addons"
                            placeholder="name" onChange={handleNameChange}/>
                      <Form.Control type="text" aria-label="Recipient's username with two button addons"
                            placeholder="email" onChange={handleEmailChange}/>
                      <Form.Control type="text" aria-label="Recipient's username with two button addons"
                            placeholder="password" onChange={handlePasswordChange}/>
                      <Button variant="outline-secondary" onClick={createUser}>전송</Button>
               </InputGroup>
               </Form>
          </Container>
     </Container>
   )
}

8. 결과

 

 

    정상적으로 users table에 등록 된다

 

9. mysql 로 조회

 

mysql> select name, email, password from users;
+------+-------------+--------------------------------------------------------------+
| name | email       | password                                                     |
+------+-------------+--------------------------------------------------------------+
| ldw  | aaa@bbb.ccc | $2y$12$AN9OXwtexAyluoS1HTQpTuTut4YiiULd4HiGdIEs2yM5r.2RnqN4y |
+------+-------------+--------------------------------------------------------------+
1 row in set (0.00 sec)

 

password 가 hash 되어 저장되어 있다

 

 

이제 다음에는 API 에 대한 인증 시스템을 추가해 보겠다

 

 

1.  store update destroy를 위한 route를 추가

routes/api.php 수정

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BookController;

...............................

Route::get('/books', [BookController::class, 'index']);

Route::get('/books/{book}', [BookController::class, 'show']);

Route::post('/books', [BookController::class, 'store']);

Route::put('/books/{book}', [BookController::class, 'update']);

Route::delete('/books/{book}', [BookController::class, 'destroy']);

 

2. controller에 store, update, destroy를 위한 function 추가

app/Http/Controllers/BookController.php

<?php

namespace App\Http\Controllers;

use App\Models\Book;
use Illuminate\Http\Request;
use App\Http\Resources\BookResource;

class BookController extends Controller
{
.....................

    /**
    * Store a newly created resource in storage.
    */
    public function store(Request $request)
    {
        $validatedData = $request->validate([
            'title' => 'required|string|max:10',
            'author' => 'required|string|max:10',
            'content' => 'required|string|max:20',
            'pages' => 'required|integer',
        ]);

        $validatedData['pages'] = intval($validatedData['pages']);

        $book = Book::create($validatedData);

        return new BookResource($book);
    }


    /**
    * Update the specified resource in storage.
    */
    public function update(Request $request, Book $book)
    {
        $validatedData = $request->validate([
            'title' => 'required|string|max:10',
            'author' => 'required|string|max:10',
            'content' => 'required|string|max:20',
            'pages' => 'required|integer',
        ]);

        $validatedData['pages'] = intval($validatedData['pages']);

        $book->update($validatedData);

        return new BookResource($book);
    }

    /**
    * Remove the specified resource from storage.
    */
    public function destroy(Book $book)
    {
         $book->delete();

         return response()->json(null, 204);
    }
}

 

 

3.  React project 에서  Laravel Rest api를 이용 

 

Create.js 

const createBook = async () => {
    try {
        const data = {
            "title" : name,
            "author" : author,
            "content" : content,
            "pages" : pages,
        }
 
        const result = await axios.post("http://localhost:8000/api/books", data)
 
        navTo("/")
    } catch ( err){
       if (axios.isAxiosError(err)) {
           console.log("Axisos Error ===> " , err)
           setCreateResult(err.response.data)
           setMessage(err.response.data.message)
       } else {
           console.log("Other Error", err)
       }
   }
}

 

Update.js

const updateBook = async () => {
    try {
        const data = {
             "title" : name,
             "author" : author,
             "content" : content,
             "pages" : pages,
        }
 
        const result = await axios.put(`http://localhost:8000/api/books/${id}`, data)
        navTo("/")
    } catch ( err){
        if (axios.isAxiosError(err)) {
             console.log("Axisos Error ===> " , err)
             setUpdateResult(err.response.data)
             setMessage(err.response.data.message)
        } else {
             console.log("Other Error", err)
        }
   }
}

 

delete 기능

const deleteBook = async (id) => {
    try {
        const result = await axios.delete(`http://localhost:8000/api/books/${id}`)
        console.log(result.data)
        window.location.reload();
    } catch ( err){
        if (axios.isAxiosError(err)) {
            console.log("Axisos Error ===> " , err)
        } else {
            console.log("Other Error", err)
        }
    }
}

 

4. 결과 시험

(1) create page

 

(2) create result

(3) update

 

 

 

(4) update result

 

 

(5) delete result

 

 

모두 정상 작동한다

 

 

다음은 Token을 이용한 인증 시스템을 추가 해 보려고 한다

 

Spring boot로 JWT token authentication을 이용한 API server를 만들어 보기는 했지만 귀찮은 점이 많은데, 이전에 Laravel로 간단하게 만들었던 기억이 나서 다시 Laravel로 Token Authentication을 추가한 RESTful API Server를 만들어 보고자 한다.

1. Laravel project 만들기

composer create-project --prefer-dist laravel/laravel restapi

 

2. mysql db 생성

3. migration, model, controller 만들기

ldw@ldw-bmax:~/laravel/restapi  php artisan make:model -m -c --resource Book

 

<?php
............

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('books', function (Blueprint $table) {
            $table->id();
            $table->string('title', 10);
            $table->string('author', 10);
            $table->string('content', 20);
            $table->integer('pages');
            $table->timestamps();
        });
    }
    ................
};

 

4. migration

 ldw@ldw-bmax:~/laravel/restapi$ php artisan migrate

 

5. table 구조 확인

 

mysql> desc books;


+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| title      | varchar(10)         | NO   |     | NULL    |                |
| author     | varchar(10)         | NO   |     | NULL    |                |
| content    | varchar(20)         | NO   |     | NULL    |                |
| pages      | int(11)             | NO   |     | NULL    |                |
| created_at | timestamp           | YES  |     | NULL    |                |
| updated_at | timestamp           | YES  |     | NULL    |                |
+------------+---------------------+------+-----+---------+----------------+
7 rows in set (0.01 sec)

 

6. 몇 개의 record를 직접 넣어 주었습니다.

mysql> select * from books;
+----+---------+--------+--------------------------+-------+------------+------------+
| id | title   | author | content                  | pages | created_at | updated_at |
+----+---------+--------+--------------------------+-------+------------+------------+
|  1 | laravel | lee    | laravel에 대한 기초      |   342 | NULL       | NULL       |
|  2 | spring  | kim    | spring boot 활용         |   425 | NULL       | NULL       |
|  3 | react   | park   | laravel api 와 react     |   212 | NULL       | NULL       |
+----+---------+--------+--------------------------+-------+------------+------------+
3 rows in set (0.01 sec)

 

7. Rescource 생성

ldw@ldw-bmax:~/laravel/restapi$ php artisan make:resource BookResource

 

만들어진 BookResource 파일 수정

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class BookResource extends JsonResource
{
    /**
    * Transform the resource into an array.
    *
    * @return array<string, mixed>
    */
    public function toArray(Request $request): array
    {
        //별 변화가 없으면 defalult로
        //return parent::toArray($request);

        //필요한 attribute만 json으로 만들려면 다음과 같이
        return [
            'id' => $this->id,
            'name' => $this->title,
            'author' => $this->author,
            'content' => $this->content,
            'pages' => $this->pages,
        ];
    }

    // response를 다시 customize해주는 withResponse method를 overide해서
    // charset 과 encoding을 바꾸어 주어 한글이 제대로 보이게 함
    public function withResponse($request, $response)
    {
        $response->header('Charset', 'utf-8');
        $response->setEncodingOptions(JSON_UNESCAPED_UNICODE);
    }
}

 

8. Controller에 function 추가

BookResource를 이용해 Json 을 return 가능

 

app/Http/Controllers/BookController 

<?php

namespace App\Http\Controllers;

use App\Models\Book;
use Illuminate\Http\Request;
use App\Http\Resources\BookResource;

class BookController extends Controller{
    /**
    * Display a listing of the resource.
    */
    public function index()
    {
        $books = Book::all();
        return BookResource::collection($books);
    }

    /**
    * Display the specified resource.
    */
    public function show(Book $book)
    {
        return new BookResource(Book::findOrFail($book->id));
    }
    ................
}
 

 

9 Route 생성

routers/api.php

 

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BookController;

...............................

Route::get('/books', [BookController::class, 'index']);

Route::get('/books/{book}', [BookController::class, 'show']);

 

10. Web page에서 다음과 같은 api로 조회 가능

localhost:8000/api/books

{
    "data": [
        {
            "name": "laravel",
            "author": "lee",
            "content": "laravel에 대한 기초",
            "pages": 342
        },
        {
            "name": "spring",
            "author": "kim",
            "content": "spring boot 활용",
            "pages": 425
        },
        {
            "name": "react",
            "author": "park",
            "content": "laravel api 와 react",
            "pages": 212
        }
    ]
}

 

11. React project를 만들고 결과 조회

 

Home.js

import {Button, Container, Table} from 'react-bootstrap'
import axios from "axios"
import { useEffect, useState} from 'react'

export const Home = () => {

    const [books, setBooks] = useState([])

    const getBook = async () => {
    try {
        const result = await axios.get( "http://localhost:8000/api/books" )
        console.log(result.data)
        setBooks(result.data.data)
    } catch ( err){
        console.log( err )
    }
}

useEffect( () => {
    getBook()
}, [])
 
return (
    <Container>
        Books
        
        <Table striped bordered>
        <thead>
            <tr>
                <th>Title</th>
                <th>Author</th>
                <th>Content</th>
                <th>Pages</th>
            </tr>
        </thead>
        <tbody>
            {books?.map((book, index) => (
                <tr key={index}>
                    <td>{book.name}</td>
                    <td>{book.author}</td>
                    <td>{book.content}</td>
                    <td>{book.pages}</td>
                </tr>
            ))}
        </tbody>
        </Table>
    </Container>
)
 
}

 

 

12. 결과 test

localhost:3000으로 접속

 

 

다음은 REST 기능에 맞게 create, update, delete에 대한 기능을 추가해 본다

1. laravel Rest Api server 에 react app에서 new record를 생성하려고 한다

 

 

2.  Fillable

Add [title] to fillable property to allow mass assignment onf [App\Model\Book] 이라는 오류가 보인다

 

laravel에서는 Model을 정의할 때 외부에서 data를 넣을 항목은 미리 정해 주어야 한다

 

 

3 app/Models/Book.php 수정

추가할 내용들을 $fillable에 추가해 준다

 

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
use HasFactory;

protected $fillable = [
'title',
'author',
'content',
'pages',
];
}

 

 

4. 결과

 

다시 시행하니 정상 작동하고 새로운 record가 생성 되었다

평소 console에서 기본적인 상태는 확인 가능하지만

 

parameter의 전달  여부, header의 내용, method등에 대한 조금 더 자세한 log를 보고 싶었다

 

다음과 같이 하니 확인 가능 하다

 

1. app/Http/Middleware/LogRequests.php file이 있나 확인하고 없으면 만들어 준다

 

$ php artisan make:middleware LogRequests

 

2. file을 customize 한다

    app/Http/Middleware/LogRequests.php >>

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Log;

class LogRequests
{
    /**
    * Handle an incoming request.
    *
    * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
    */
    public function handle(Request $request, Closure $next): Response
    {
        Log::info('Request:', [
'           method' => $request->method(),
'           url' => $request->fullUrl(),
'           headers' => $request->headers->all(),
'           parameters' => $request->all(),
         ]);

         return $next($request);
    }
}

 

 

3. App\Http\Kernel 의 middleware에 추가한다

 

App\Http\Kernel.php >>

 

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
    * The application's global HTTP middleware stack.
    *
    * These middleware are run during every request to your application.
    *
    * @var array<int, class-string|string>
    */
    protected $middleware = [
        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        ........
        \App\Http\Middleware\LogRequests::class,
    ];

    ..................
}

 

 

4. storage/logs/laravel.log 파일 내용 확인

    추가한 내용들이 로그에 잘 나타나 있다.

[2024-02-25 03:13:13] local.INFO: Request: {"method":"GET","url":"http://localhost:8000/api/books","headers":{"host":["localhost:8000"],"connection":["keep-alive"],"pragma":["no-cache"],"cache-control":["no-cache"],"sec-ch-ua":["\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Google Chrome\";v=\"122\""],"accept":["application/json, text/plain, */*"],"sec-ch-ua-mobile":["?0"],"user-agent":["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"],"sec-ch-ua-platform":["\"Linux\""],"origin":["http://localhost:3000"],"sec-fetch-site":["same-site"],"sec-fetch-mode":["cors"],"sec-fetch-dest":["empty"],"referer":["http://localhost:3000/"],"accept-encoding":["gzip, deflate, br, zstd"],"accept-language":["ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7"]},"parameters":[]}
[2024-02-25 03:18:03] local.INFO: Request: {"method":"GET","url":"http://localhost:8000/api/books","headers":{"host":["localhost:8000"],"connection":["keep-alive"],"pragma":["no-cache"],"cache-control":["no-cache"],"sec-ch-ua":["\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Google Chrome\";v=\"122\""],"accept":["application/json, text/plain, */*"],"sec-ch-ua-mobile":["?0"],"user-agent":["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"],"sec-ch-ua-platform":["\"Linux\""],"origin":["http://localhost:3000"],"sec-fetch-site":["same-site"],"sec-fetch-mode":["cors"],"sec-fetch-dest":["empty"],"referer":["http://localhost:3000/"],"accept-encoding":["gzip, deflate, br, zstd"],"accept-language":["ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7"]},"parameters":[]}
[2024-02-25 03:18:04] local.INFO: Request: {"method":"GET","url":"http://localhost:8000/api/books","headers":{"host":["localhost:8000"],"connection":["keep-alive"],"pragma":["no-cache"],"cache-control":["no-cache"],"sec-ch-ua":["\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Google Chrome\";v=\"122\""],"accept":["application/json, text/plain, */*"],"sec-ch-ua-mobile":["?0"],"user-agent":["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"],"sec-ch-ua-platform":["\"Linux\""],"origin":["http://localhost:3000"],"sec-fetch-site":["same-site"],"sec-fetch-mode":["cors"],"sec-fetch-dest":["empty"],"referer":["http://localhost:3000/"],"accept-encoding":["gzip, deflate, br, zstd"],"accept-language":["ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7"]},"parameters":[]}
[2024-02-25 03:29:23] local.INFO: Request: {"method":"GET","url":"http://localhost:8000/api/books","headers":{"host":["localhost:8000"],"connection":["keep-alive"],"pragma":["no-cache"],"cache-control":["no-cache"],"sec-ch-ua":["\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Google Chrome\";v=\"122\""],"accept":["application/json, text/plain, */*"],"sec-ch-ua-mobile":["?0"],"user-agent":["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"],"sec-ch-ua-platform":["\"Linux\""],"origin":["http://localhost:3000"],"sec-fetch-site":["same-site"],"sec-fetch-mode":["cors"],"sec-fetch-dest":["empty"],"referer":["http://localhost:3000/"],"accept-encoding":["gzip, deflate, br, zstd"],"accept-language":["ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7"]},"parameters":[]}

 

Mixed Content 문제를 해결하기 위해 project를 수정해 주었다

 

설정에 대한 이해는 심화학습이 필요 할 듯 하고 다음 site를 참고하였다

 

스프링에 비해 참고 자료가 부족해 해결도 쉽지 않았다.

 

https://stackoverflow.com/questions/57815100/proxypass-mixed-content-in-laravel-https

 

ProxyPass: Mixed Content in Laravel HTTPS

I'm using Laravel 5.8 Currently I have this site: sample name https://www.myssldomain.com/ewallet and I have this site: sample name http://my-aws-public-ip/login Using Proxy Pass included in...

stackoverflow.com

 

 

1. config/app.php 수정

// 'url' => env('APP_URL', 'http://localhost'),
// 'asset_url' => env('ASSET_URL'),
'url' => env('APP_URL', 'https://king.****.com'),
'asset_url' => env('ASSET_URL', 'https://king.w****.com'),

 

2. .env 파일 수정

APP_NAME=bukdu10
APP_ENV=production #local
APP_KEY=base64:CnE0DZ9xssyJ0f/2ufbq1cKexJPMDzZC8Zk+iuNZdHo=
APP_DEBUG=true

 

 

3. app/Providers/AppServiceProvider.php 수정

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
 
   public function register(): void
   {
   //
   }

/**
* Bootstrap any application services.
*/
   public function boot(): void
   {
      if (env('APP_ENV') !== 'local') {
          $this->app['request']->server->set('HTTPS', true);
      }
   }
}

 

4. routes/web.php 수정

 

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\IbchulController;

// added
use Illuminate\Support\Facades\URL;
URL::forceRootUrl('https://king.w****.com');
.........

 

5. 다시 build

ldw@ldw-bmax:~/laravel/bukdu10$ npm run build

> build
> vite build

 

6. 이제 다시 Image를 빌드 업로드 

ldw@ldw-bmax:~/laravel/bukdu10$ docker build -t ldw365/ldwdockerimage365:bukdu10 .

 

ldw@ldw-bmax:~/laravel/bukdu10$  docker push ldw365/ldwdockerimage365:bukdu10

 

 

7. 이제 AWS에서 서버 중지, 기존 콘테이너 삭제, 기존 image 삭제 

 

ubuntu@ip-172-31-7-173:/etc/nginx/sites-available$ docker ps -a
CONTAINER ID   IMAGE                              COMMAND          CREATED             STATUS             PORTS                                   NAMES
0d0b7e450331   ldw365/ldwdockerimage365:bukdu10   "sh /start.sh"   About an hour ago   Up About an hour   0.0.0.0:8020->80/tcp, :::8020->80/tcp   amazing_liskov
ubuntu@ip-172-31-7-173:/etc/nginx/sites-available$ docker stop 0d0b7e450331
0d0b7e450331
ubuntu@ip-172-31-7-173:/etc/nginx/sites-available$ docker rm 0d0b7e450331
0d0b7e450331
[1]+  Exit 137                docker run -p 8020:80 ldw365/ldwdockerimage365:bukdu10  (wd: ~)
(wd now: /etc/nginx/sites-available)
ubuntu@ip-172-31-7-173:/etc/nginx/sites-available$ docker images
REPOSITORY                 TAG       IMAGE ID       CREATED             SIZE
ldw365/ldwdockerimage365   bukdu10   e9ec64a014a4   About an hour ago   822MB
ubuntu@ip-172-31-7-173:/etc/nginx/sites-available$ docker rmi e9ec64a014a4
Untagged: ldw365/ldwdockerimage365:bukdu10

 

8. Docker hub 에서 Image 다운 로드 및 실행

 

ubuntu@ip-172-31-7-173:/etc/nginx/sites-available$ docker pull ldw365/ldwdockerimage365:bukdu10
bukdu10: Pulling from ldw365/ldwdockerimage365
.......................................
Status: Downloaded newer image for ldw365/ldwdockerimage365:bukdu10
docker.io/ldw365/ldwdockerimage365:bukdu10
ubuntu@ip-172-31-7-173:/etc/nginx/sites-available$ docker run -p 8020:80 ldw365/ldwdockerimage365:bukdu10 &
[1] 54208
ubuntu@ip-172-31-7-173:/etc/nginx/sites-available$ Starting services...
Ready.
==> /var/log/nginx/access.log <==
==> /var/log/nginx/error.log <==

9. 결과

드디어 https://king.w*****.com 으로 정상 접속 되고 정상적으로 작동한다

 

앞의 과정의 반복이지만 이제는 local 용이 아닌 AWS에서 serving 할 Image를 만들고 AWS에 upload 해서 실행 시켜 보았다

 

이번에는 local에서 실행 할 것은 아니라 home 폴더에서 작업을 하였다

 

앞 부분은 이전회의 반복이다

 

1. github 에서 clone및 복구

 

1) ldw@ldw-max:~/laravel$ git clone https://lamb0693:*****************@github.com/lamb0693/bukdu10.git

 

2) 앞에서 만들어 놓은 Dockerfile을 복사해옴

ldw@ldw-bmax:~/laravel$ cd bukdu10
ldw@ldw-bmax:~/laravel/bukdu10$ cp /var/www/html/bukdu10/Dockerfile ./

 

3) 미리 복사해 놓은 .env 파일 복사

ldw@ldw-bmax:~/laravel/bukdu10$ cp /home/ldw/다운로드/a.env ./.env

 

4) VSCode로 .env 파일 수정 

     AWS EC2 에 있는 DB에 접속할 수 있게 환경 수정

     서버 이름 수정등

 

APP_NAME=bukdu10
APP_ENV=production
APP_KEY=base64:CnE0DZ9xssyJ0f/2ufbq1cKexJPMDzZC8Zk+iuNZdHo=
APP_DEBUG=true

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=w*******.com
DB_PORT=3306
DB_DATABASE=bukdu10
DB_USERNAME=***
DB_PASSWORD=******

BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120

MEMCACHED_HOST=127.0.0.1

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1

VITE_APP_NAME="${APP_NAME}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

 

5) ldw@ldw-bmax:~/laravel/bukdu10$ composer install

 

6) ldw@ldw-bmax:~/laravel/bukdu10$ npm install

 

7) ldw@ldw-bmax:~/laravel/bukdu10$ npm run build

 

 

2. Image를  /home/ldw/laravel/bukdu10 폴더에서  빌드하고 docker hub 로 upload 함

 

1)  ldw@ldw-bmax:~/laravel/bukdu10$ docker build -t ldw365/ldwdockerimage365:bukdu10 .

 

9. ldw@ldw-bmax:~/laravel/bukdu10$ docker push ldw365/ldwdockerimage365:bukdu10
The push refers to repository [docker.io/ldw365/ldwdockerimage365]

 

 

3. AWS에서 작업

이제  terminal 에서 AWS 로 접속 image를 다운 로드 받고 실행 해 보았다

 

1)  AWS EC2 의 mysql server에 shema  생성하고 이전 server에서 backup 받아 놓은 schema 를 복원함

 

2)  AWS에는 미리 docker를 설치해 놓았다

 

3)  AWS에는 기존 Nginx로 reverse proxy를 설정해 주었다

https://king.w****.com  -> http://localhost:8020

 

ubuntu@ip-172-31-7-173:/etc/nginx/sites-available$ cat wxxxx.com
server {
    server_name wxxxx.com ;
    l..............................
}


server {
    server_name king.w****.com;
    location / {
        proxy_pass http://127.0.0.1:8020;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;  # Set the X-Forwarded-Proto header
        proxy_redirect off;
    }
    listen [::]:443 ssl; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/king.w****.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/king.w****.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}


server {
    if ($host = w****.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot
    listen 80;
    listen [::]:80;

    server_name w****.com;
        return 404; # managed by Certbot
}

 

 

4)  docker hub에서 Image pull

 

ubuntu@ip-172-31-7-173:~$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES


ubuntu@ip-172-31-7-173:~$ docker images
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE


ubuntu@ip-172-31-7-173:~$ docker pull ldw365/ldwdockerimage365:bukdu10
bukdu10: Pulling from ldw365/ldwdockerimage365
.......................................
Status: Downloaded newer image for ldw365/ldwdockerimage365:bukdu10
docker.io/ldw365/ldwdockerimage365:bukdu10


ubuntu@ip-172-31-7-173:~$ docker images
REPOSITORY                 TAG       IMAGE ID       CREATED         SIZE
ldw365/ldwdockerimage365   bukdu10   e9ec64a014a4   7 minutes ago   822MB

 

5)  8020번 포트로 실행 시켜 주었다


ubuntu@ip-172-31-7-173:~$ docker run -p 8020:80 ldw365/ldwdockerimage365:bukdu10 &


[1] 53885
ubuntu@ip-172-31-7-173:~$ Starting services...
Ready.
==> /var/log/nginx/access.log <==

==> /var/log/nginx/error.log <==

 

4. 결과 확인 - 오류 발생

 

1) 접속하니 접속은 되지만 링크들이 css, route, js 등이 http로 설정이 되어있어 https 접속에서 문제를 일으킨다

 

문제는 다음에 해결해 보겠다

 

 

2) https://king.w*****.com으로 접속 - 접속은 된다

 

3) 링크 클릭 웹페이지 이동하면 https 와 http Mixed Content를 문제로 잘 작동되지 않는다

다음 회에서 해결 예정이다

 

 

이제 만들어진 project및 환경을 docker image로 제작해 보기로 했다

1. document root 에 Docker file 만들기

기본 틀을 복사해 와서 수정할 예정이다 먼저 복사해 왔다.

ldw@ldw-bmax:/var/www/html/bukdu10$ ls -l Dockerfile 
-rw-r--r-- 1 root root 2402  2월 19 21:18 Dockerfile

 

혹시나 해서 777로 주었다.  docker를 sudo가 아니고 일반 계정으로 실행할거라
ldw@ldw-bmax:/var/www/html/bukdu10$ sudo chmod 777 Dockerfile 

 

2. /var/www/html/bukdu10/Dockerfile 수정

 

Docker file은 아래 두군데 페이지를 참고로 만들었다

 

처음에 개발환경도 아래쪽 image만드는 과정과 거의 같게 만들었으니 환경에 대한 오류는 없을 거라 기대해 봄

 

https://laravel.com/docs/10.x/deployment

 

Laravel - The PHP Framework For Web Artisans

Laravel is a PHP web application framework with expressive, elegant syntax. We’ve already laid the foundation — freeing you to create without sweating the small things.

laravel.com

 

 

https://dev.to/adnanbabakan/dockerizing-laravel-10-ubuntu-image-php-82-fpm-nginx-318p

 

Dockerizing Laravel 10 [Ubuntu image + PHP 8.2 FPM + NGINX] 🛳️🛳️

Hi there DEV.to community! I've been trying to dockerize my Laravel app which acts as an API and run...

dev.to

 

ldw@ldw-bmax:/var/www/html/bukdu10$ vim Dockerfile 

 

ROM ubuntu:latest AS base

ENV DEBIAN_FRONTEND noninteractive

# Install dependencies
RUN apt update
RUN apt install -y software-properties-common
RUN add-apt-repository -y ppa:ondrej/php
RUN apt update
RUN apt install -y php8.2\
    php8.2-cli\
    php8.2-common\
    php8.2-fpm\
    php8.2-mysql\
    php8.2-zip\
    php8.2-gd\
    php8.2-mbstring\
    php8.2-curl\
    php8.2-xml\
    php8.2-bcmath\
    php8.2-pdo

# Install php-fpm
RUN apt install -y php8.2-fpm php8.2-cli

# Install composer
RUN apt install -y curl
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

 

# Install nodejs
RUN apt install -y ca-certificates gnupg
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
ENV NODE_MAJOR 20
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt update
RUN apt install -y nodejs

# Install nginx
RUN apt install -y nginx

 

RUN echo "\
server {\n\
    listen 80;\n\
    listen [::]:80;\n\
    server_name localhost;\n\
    root /var/www/html/bukdu10/public;\n\
    add_header X-Frame-Options \"SAMEORIGIN\";\n\
    add_header X-Content-Type-Options \"nosniff\";\n\
    index index.php;\n\
    charset utf-8;\n\
    location / {\n\
        try_files \$uri \$uri/ /index.php?\$query_string;\n\
    }\n\
    location = /favicon.ico { access_log off; log_not_found off; }\n\
    location = /robots.txt  { access_log off; log_not_found off; }\n\
    error_page 404 /index.php;\n\
    location ~ \.php$ {\n\
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;\n\
        fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;\n\
        include fastcgi_params;\n\
    }\n\
    location ~ /\.(?!well-known).* {\n\
        deny all;\n\
    }\n\
}\n" > /etc/nginx/sites-available/default

 

RUN echo "\
    #!/bin/sh\n\
    echo \"Starting services...\"\n\
    service php8.2-fpm start\n\
    nginx -g \"daemon off;\" &\n\
    echo \"Ready.\"\n\
    tail -s 1 /var/log/nginx/*.log -f\n\
    " > /start.sh

COPY  . /var/www/html/bukdu10
WORKDIR /var/www/html/bukdu10

RUN chown -R www-data:www-data /var/www/html/bukdu10
RUN chmod -R 755 /var/www/html/bukdu10

RUN composer install

EXPOSE 80

CMD ["sh", "/start.sh"]

 

3. Image 생성 및 확인

 

ldw@ldw-bmax:/var/www/html/bukdu10$ sudo service docker start
ldw@ldw-bmax:/var/www/html/bukdu10$ docker build -t bukdu10image .
[+] Building 152.3s (28/28) FINISHED                                                                                               
.................................................. 

 

ldw@ldw-bmax:/var/www/html/bukdu10$ docker images
REPOSITORY     TAG       IMAGE ID       CREATED          SIZE
bukdu10image   latest    97f1c5ca995d   39 seconds ago   834MB

 

4. docker 에서 image 실행

 

8000번 포트로 실행 시켜 주었다 -

나중에 AWS EC2 에서 nginx reverse proxy로  https://xxx.xxx.com => http://localhost:8000으로 redirect 시켜 사용할 예정

 

ldw@ldw-bmax:/var/www/html/bukdu10$ docker run -p 8000:80 bukdu10image
Starting services...
Ready.
==> /var/log/nginx/access.log <==

==> /var/log/nginx/error.log <==

 

5. localhost:8000 확인

정상 작동한다

 

6. 다음에는 마지막으로 AWS에 docker를 설치후 만들어진 image를 올려 보기로 했다

+ Recent posts