io stream 과 socket 을 이용한 tcp 통신은 java 를 처음 공부할 때, 간단하게 멀티 스레드 채팅 정도의 예제로 이해하며 넘어갔다..
웬만한 클라이언트 - 서버 간의 통신은 http 로 이루어지므로 이를 더 깊게 파며 공부했었다.
이번에 이직한 회사에서 본격적으로 맡게 된 첫 과제는 TCP 전문 통신을 구축하는 것이다.
소켓 통신을 구현하는데, java socket api 를 이용하여 순수 구현할 수 있지만, kotlin 에서는 okio 라는 통신 전용 라이브러리가 있다고 해서 이를 한번 사용해서 전문 통신을 위한 소켓 클라이언트 초석을 구현해봤다.
잘 해보지 않은 socket 통신을 이해하고 구현하며 이틀간 많이 고통 받았고, 그 과정에서 좀 더 편리한 kotlin okio 라이브러리를 사용하여 간단하게 소켓 통신을 할 수 있는 예시 코드를 보자.
혹시나 okio 라이브러리를 사용하여 byte 배열 socket 통신을 하시려는 분들에게 도움이 되길 바란다.
참고 [Okio 라이브러리]
https://github.com/square/okio
https://square.github.io/okio/
Server 코드
import okio.buffer
import okio.sink
import okio.source
import java.net.ServerSocket
import java.util.concurrent.TimeUnit
fun main() {
Server().server()
}
class Server {
fun server() {
val serverSocket = ServerSocket(9999)
while (true) {
val client = serverSocket.accept()
val bufferedSource = client.source().buffer() // 내부적으로 getInputStream() 을 호출한다.
bufferedSource.timeout().timeout(READ_TIME_OUT, TimeUnit.MILLISECONDS)
val readByteArray = bufferedSource.readByteArray() // byte 배열로 읽기
println(String(readByteArray))
val bufferedSink = client.sink().buffer() // 내부적으로 getOutputStream() 을 호출한다.
bufferedSink.timeout().timeout(WRITE_TIME_OUT, TimeUnit.MILLISECONDS)
bufferedSink.write(readByteArray) // 에코 서버 마냥 받은 byte 배열을 클라이언트로 보내기
bufferedSink.flush() // flush 를 호출해서 buffer 를 비우자.
client.shutdownOutput() // eof 마냥, write 이 끝났다는 것을 socket 의 shutdownOutput() 을 호출해야 한다.
}
}
}
- 자원의 close() 함수를 명시하진 않았다.
- 실제 코드에서는 호출해야.
- okio 에서는 input, output stream 같은 것을 한번 래핑하여 추상화했다.
- source, sink 라는 객체로 추상화
Client 코드
import okio.buffer
import okio.sink
import okio.source
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Socket
import java.util.concurrent.TimeUnit
const val SOCKET_CONNECT_TIME_OUT = 5000
const val WRITE_TIME_OUT: Long = 12000
const val READ_TIME_OUT: Long = 12000
fun main() {
print(
String(
requestToTrsServer(
niceVanIp = "127.0.0.1",
niceVanPort = 9999,
buffer = "Hello Server!".toByteArray()
)
)
)
}
fun requestToTrsServer(niceVanIp: String, niceVanPort: Int, buffer: ByteArray): ByteArray {
Socket().use { socket ->
try {
socket.connect(InetSocketAddress(niceVanIp, niceVanPort), SOCKET_CONNECT_TIME_OUT)
val bufferedSink = socket.sink().buffer()
with(bufferedSink) {
timeout().timeout(WRITE_TIME_OUT, TimeUnit.MILLISECONDS)
write(buffer)
flush()
socket.shutdownOutput()
}
// 서버로 bytearray 보내기
val bufferedSource = socket.source().buffer()
val responseBytes =
with(bufferedSource) {
timeout().timeout(READ_TIME_OUT, TimeUnit.MILLISECONDS)
readByteArray()
}
// 서버에서 bytearray 받기
bufferedSink.close()
bufferedSource.close()
return responseBytes
} catch (e: IOException) {
println("소켓 서버와 TCP 통신 중 IOException 발생")
return ByteArray(1)
}
}
}
- 클라이언트도 마찬가지
- bytearray 를 보내고, 받는 코드이다.
- 위의 개념과 동일하게, source, sink 를 Socket 객체에서 얻을 수 있다.
이렇게, java 코드보다 훨씬 간결하게 작성할 수 있다.
소켓 통신을 제대로 처음 구현해봤는데, 역시 공부할 건 끝이 없고, 필요 없는 것은 어디에도 없다.
"이것만 하면 되지~"
라는 마인드는 개발자로서는 부적합한 마인드 같다.
필요한 모든 것은 상황에 따라 학습하고 적용할 수 있어야 한다.
반응형
'📚 Kotlin' 카테고리의 다른 글
[Effective Kotlin] 아이템 49. 하나 이상의 처리 단계를 가진 경우에는 시퀀스를 사용하라 (0) | 2023.02.04 |
---|---|
[Effective Kotlin] 아이템 47. 인라인 클래스의 사용을 고려하라 (0) | 2023.02.03 |
[Effective Kotlin] 아이템 36. 상속보다는 컴포지션을 사용하라 (0) | 2023.01.30 |
[Effective Kotlin] 아이템 32. 생성자 대신 팩토리 함수를 사용하라 (2) | 2023.01.29 |
[Effective kotlin] 아이템 27. 변화로부터 코드를 보호하려면 추상화를 사용하라 (0) | 2023.01.29 |