Socket
1) TCP/IP 통신
2) Http 통신
3) 양방향 통신으로 서버 ↔ 클라이언트 End Point를 연결
소켓으로 서버를 구성할 경우 우선 다음 순차에 맞춰 구현하면 됩니다.
1) ServerSocket(Port)로 서버를 생성한다.
2) 서버로부터 데이터 받을 준비를 하기 위해 연결 요청을 수락하는 accept()를 정의한다. ( = Socket)
3) 2의 경우의 객체를 socket으로 했을때 inputStream을 Buffer에 넣어둔다 (= 클라이언트 → 서버 로 데이터 전송)
4) 3을 한 다음에 socket의 outputStream을 PrintWriter에 넣어둔다. (= 서버 → 클라이언트로 데이터 전송)
5) 3번의 경우에 대해 Client → Server로 데이터 요청을 진행하는 것이기 때문에 요청 값이 비어있기 전까지 계속 받습니다.
6) 4의 경우로 만든 Printer로 응답을 어떻게 줄 지 정리합니다.
7) 서버 닫기 + 서버 리소스 flush() → 클라이언트 닫기 → 소켓 닫기 순으로 진행됩니다.
다음 내용은 .kt파일을 테스트 형태로 만들어 Socket 서버를 개인적으로 구현할 때 쓰입니다.
더불어 네트워크 관련된 내용은 UI Thread에서 구현해주지 않습니다. (단, OkHttpClient 나 Retrofit과 같은 네트워크 관련
업무 수행 라이브러리들은 내부적으로 스레드가 구현되어 돌아가기 때문에 별도로 스레드를 만들어 구현할 필요가 없습니다.
그렇지만, 네트워크 통신을 통한 데이터로 UI를 변경한다 할 경우 UI Thread에서 돌아간다고 개별적으로 구현이 필요합니다.)
[Socket.kt]
fun main(){
Thread {
val port = 8080
val server = ServerSocket(port)
while(true){
val socket = server.accept() // 서버로부터 데이터 받을 준비 완료
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
val printer = PrintWriter(socket.getOutputStream())
val input: String = "-1"
while(input.isNotEmpty()){
input = reader.readLine()
}
println("Client → Server request Data : $input")
printer.println("HTTP/1.1 200 OK")
printer.println("Content-Type: text/html\r\n")
printer.println("{\"message\" : \"Hello World~!\"}")
printer.println("\r\n")
// Server → Client 리소스 다 쏟아내기
printer.flush()
printer.close() // Server → Client 먼저 닫기
reader.close() // Client → Server 두 번째로 닫기
socker.close() // Socket 닫기
}
}.start()
}
다음에는 EditText에 IP를 입력해서 위에 서버 연결한 거에 데이터를 받는 역할을 할 것입니다.
우선 OkHttpClient를 먼저 import 합니다.
https://github.com/square/okhttp
[build.gradle(app)]
dependencies {
implementation(libs.gson)
// define a BOM and its version
implementation(libs.okhttp.loging)
implementation(libs.bundles.retrofit)
}
[libs.version.toml]
[versions]
agp = "8.3.0"
kotlin = "1.9.0"
retrofit = "2.9.0"
okhttp3 = "4.12.0"
gson = "2.11.0"
[libraries]
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp3" }
okhttp-loging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp3" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
[bundles]
retrofit = [
"retrofit",
"retrofit-converter-gson",
"okhttp",
"okhttp-loging"
]
우선 간단히 서버의 응답을 받는 것이기 때문에 GET 방식으로 진행할 것입니다.
POST와 그 외 방식은 OkHttp Github에 내용을 참고하면 좋을 것 같습니다.
1) OkHttpClient를 통한 Client를 선언합니다.
2) OkHttp3에 있는 Request를 가져와 Builder를 이용하여 연결할 url을 붙여 빌드합니다. (GET)
3) Callback Object를 만들어주는데, 여기에는 onFailure 함수와 onResponse 함수를 구현해야 합니다.
4) 1)에서 만든 Client에 새 Call로 2의 request를 넣어주고 그거에 대한 callback으로 3을 넣어서 구현합니다.
callback은 enqueue에 넣기 때문에 큐 안에서 차례가 돌아올 때 수행됩니다.
이는 UI Thread가 아니라 작업 스레드에서 수행되기 때문에 별도로 Thread를 구현할 필요가 없습니다.
단, 3에서 만들때 onFailure 함수와 onResponse 함수에서 결과가 UI Thread에 변경에 영향을 줄 경우
runOnUiThread와 같은 UI Thread에서 수행될 것임을 고지해줘야 네트워크 통신에서의 데이터와
UI Thread에서 UI 변경 로직이 구분되어 제대로 처리될 수 있습니다.
코드 내용은 아래와 같습니다.
[MainActivity.kt]
class MainActivity: AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
val client = OkHttpClient()
override fun onCreate(savedinstanceState: Bundle?){
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
initViews()
}
private fun initViews(){
var serverHost: String = ""
binding.serverHostEditText.addTextChangedListener {
serverHost = it.toString()
}
binding.confirmButton.setOnClickListener {
val request: Request = Request.Builder()
.url("http://$serverHost:8080")
.build()
val callback = object:Callback {
override fun onFailure(call: Call, e: IOException){
runOnUiThread {
Toast.makeText(this@MainActivity, "수신에 실패하였습니다." , Toast.LENGTH_SHORT).show()
}
}
override fun onResponse(call: Call, response: Response){
if(response.isSuccessful){
val responseBody = response.body?.string()
val message = Gson().fromJson(responseBody, Message::class.java)
runOnUiThread {
binding.confirmButton.isVisible = false
binding.serverHostEditText.isVisible = false
binding.informationTextView.isVisible = true
binding.informationTextView.visibility = View.VISIBLE
binding.informationTextView.text = message.a
}
}
else {
runOnUiThread {
Toast.makeText(this@MainActivity, "수신에 실패하였습니다.", Toast.LENGTH_SHORT)
.show()
}
}
}
}
}
client.newCall(request).enqueue(callback)
}
}
이렇게 OkHttpClient를 사용해서 client를 만들고 newCall에 OkHttp3에 Request로 가져온 것을 넣어줍니다.
또한 그거에 대한 결과 처리를 callback으로 진행하는데, 큐에 넣음으로서 이후에 비동기 처리로 진행하도록 구현됩니다.
또한 Gson을 이용해서 JSON → Java(Kotlin) / Java(Kotlin) → JSON 형태로 구현할 수 있는데 이번 예제에서는
JSON 형태를 Kotlin으로 변경하는 과정을 담았습니다.
val message = Gson().fromJson(responseBody, Message::class.java)
message.a
이렇게 구현되어 있는데, responseBody는 JSON 형태로 응답이 나오기 때문에 이를 Gson().fromJson 함수를 통해서
Kotlin/Java 형태로 반환하는 과정을 거칩니다.
이 때, 어떤 형태로 변경할 지에 대해 정의하기 위해서 Message.kt 파일을 만들어서 형태를 정의합니다.
Message.kt는 data class 형태로 담겨져 있으며, SerializedName을 사용해서 구현했습니다.
그 이유는 결과 파일을 디컴파일을 한다면 데이터가 어떤 키값으로 어떻게 표현되어 있는지 다 확인이 가능하기 때문에
보안상의 이유로 SerializedName을 사용해서 난독화 처리를 진행합니다. 코드는 아래와 같습니다.
package com.example.part2.todaynotification
import com.google.gson.annotations.SerializedName
data class Message (
@SerializedName("message") // a는 message로 관리할거야! (디컴파일해도 message로 안뜨고 a로 뜨기 때문에 안정성 확보)
val a: String // Key -> 난독화 가능 (decompile 했을때 확인 불가)
)
messasge라는 키값을 사용할 건데, 이는 a로 난독화해서 보이도록 하는 것입니다.
따라서 실질적으로 message.a는 message.message를 나타내는 것인데, SerializedName을 써서 난독화하였으므로
message는 a를 가리키고 이후 디컴파일을 하여 다시 확인하더라도 a로 보이기 때문에 키값을 확인할 수 없어 보안적으로
안전한 코드 구현이 될 것 입니다. 이로써 소켓으로 서버를 만들고 OkHttpClient, OkHttp3를 통해서 서버에 Call하는 것과
비동기적으로 Callback을 큐에서 관리하고 처리하는 방식, 그리고 UI Thread에서 보일땐 runOnUiThread를 통해
네트워크 통신의 결과 데이터를 UI Thread 상에서 보일 수 있도록 구현한 점을 확인하고 넘어가야 할 것입니다.
긴 글 읽어주셔서 감사드리며 다음 포스팅에서 뵙겠습니다!!
'Android' 카테고리의 다른 글
Firebase를 통한 로그인 구현 (2) | 2024.10.28 |
---|---|
Network / Service (feat. Retrofit) (11) | 2024.08.29 |
WebView & ViewPager2 / Fragment (0) | 2024.08.16 |
ViewPager2와 TabLayout (0) | 2024.08.14 |
Permission 처리와 ListAdapter 활용법 (0) | 2024.08.01 |