하나의 디바이스에서 간단한 서버 앱, 클라이언트 앱을 구성하여 소켓 통신을 수행할 수 있는 예제이다.
자세한 개념설명은 생략하였으며, 개념이해 전 간단한 예제 먼저 작성해보고싶을 경우 해당 포스팅을 읽는 것을 추천한다.
같이 알아두면 좋은 내용
- Socket 의 개념
- Android Permission
- Service 생명주기, ForegroundService
- Android 에서 Thread 를 다루는 방법
- Notification
구현 순서 요약
- 서버 앱 구성
- 서버를 실행할 Service 클래스 작성
- 서버 역할을 하는 ServerThread 클래스 작성
- 메인 액티비티에서 구현한 Service 실행(Foreground)
- 클라이언트 앱 구성
- 화면 구성
- 서버에 연결할 ClientThread 클래스 작성
서버 앱 구성하기
0. Manifest.xml 파일에 아래 권한을 추가
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
1. ServerService 클래스 작성 -> Manifest.xml 의 application 안에 여기서 만든 Service 컴포넌트를 명시해야한다
class ServerService : Service() {
private var serverThread: Thread? = null
override fun onBind(intent: Intent?): IBinder? {
throw UnsupportedOperationException("not implement")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
serverThread = ServerThread()
serverThread?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = "com.delay.test.serverservice"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val mChannel = NotificationChannel(channelId, "Test", importance)
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(mChannel)
val notification = Notification.Builder(this, channelId)
.setContentTitle("테스트 서버")
.setContentText("테스트 서버 진행중")
.setSmallIcon(R.mipmap.ic_launcher_round)
.build()
startForeground(1111, notification)
}
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
serverThread?.interrupt()
super.onDestroy()
}
class ServerThread : Thread() {
companion object {
private const val TAG = "ServerThread"
}
override fun run() {
Log.d(TAG, "서버가 실행됨")
val port = 10001 // 리눅스에서 이미 사용하는 포트를 쓰면 익셉션이 발생함
val server = ServerSocket(port)
try {
while (true) {
val socket = server.accept() // 서버 대기상태. 클라이언트 접속 시 소켓 객체 리턴
val inputStream = ObjectInputStream(socket.getInputStream())
val input = inputStream.readObject() // 클라이언트에서 보낸 데이터
Log.d(TAG, "$input -- from client")
val outputStream = ObjectOutputStream(socket.getOutputStream())
outputStream.writeObject("200 OK from server -- client data: $input")
outputStream.flush()
Log.d(TAG, "클라이언트로 결과 보내기 완료")
socket.close() // 더이상 연결을 유지할 필요가 없다면 끊어줌
}
} catch (e: SocketException) {
e.printStackTrace() // fixme 올바른 에러 처리 방법은 아님
}
}
}
}
2. MainActivity 클래스 작성
class MainActivity : AppCompatActivity() {
private val serviceIntent by lazy { Intent(this, ServerService::class.java) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
}
override fun onDestroy() {
stopService(serviceIntent)
super.onDestroy()
}
}
3. 서버 앱 실행(위에 노티 떠있는 것 확인)
** 주의 : 소켓 객체를 생성할 때 리눅스에서 이미 사용중인 포트를 사용하면 아래와 같은 에러가 발생하는 것을 알아두자!
java.net.ConnectException: failed to connect to localhost/127.0.0.1 (port 1001) from /:: (port 45672): connect failed: ECONNREFUSED (Connection refused)
클라이언트 앱 구성하기
0. Manifest.xml 파일에 아래 권한 추가하기
<uses-permission android:name="android.permission.INTERNET"/>
1. 간단한 화면 구성
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="10dp"
android:lineSpacingExtra="10dp"
android:text="서버 연결 대기중...\n"
android:textSize="19sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button"
app:layout_constraintVertical_bias="0.0" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="서버 전송"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="서버로 보낼 데이터 입력"
app:layout_constraintBottom_toBottomOf="@+id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/button"
app:layout_constraintTop_toTopOf="@+id/button" />
</androidx.constraintlayout.widget.ConstraintLayout>
2. 메인 액티비티 내에 inner 클래스로 서버에 연결할 ClientThread 작성
inner class 로 해야하는 이유는, 메인 액티비티의 멤버변수(handler 나 View들)에 접근하기 위함이다. 이렇게 짜는건 여러가지 이유로 좋지 않은 코드지만, 이 예제에서 중요한 부분이 아니기 때문에 짚지 않을 것이다. (절대 이렇게 짜지 말고 어떻게 해결하면 좋을지 스스로 알아서 공부해보자!!)
class ClientThread : Thread() {
override fun run() {
val hostName = "localhost"
val port = 10001
try {
val socket = Socket(hostName, port)
val outputStream = ObjectOutputStream(socket.getOutputStream())
outputStream.writeObject(editText.text.toString())
outputStream.flush()
val inputStream = ObjectInputStream(socket.getInputStream())
val input = inputStream.readObject() as String
handler.post {
textView.append("$input\n")
}
} catch (e: SocketException) {
handler.post {
Toast.makeText(applicationContext, "소켓 연결에 실패했습니다.", Toast.LENGTH_SHORT).show()
}
}
}
}
3. 메인액티비티에서 서버로 연결하는 코드 추가하기
class MainActivity : AppCompatActivity() {
private val handler = Handler()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
ClientThread().start()
}
}
// ClientThread inner class 정의
}
이제 먼저 구현한 서버 앱을 실행시킨 후 클라이언트 앱을 실행하여 내용을 입력한 후 버튼을 누르면 아래와 같이 데이터를 받을 수 있다. 서버 앱에서 서비스가 구동중인지 알 수 있는 방법은 알림채널에 선언한 노티(테스트 서버)가 떠있으면 된다. 클라이언트에서 서버로 제대로된 데이터가 전송됐는지 여부는 로그캣에 찍게 해뒀으리 서버프로젝트 로그캣을 확인해보자.
해당 예제 작성 시 문제가 있다면 댓글 부탁드립니다!
'Android > Basic' 카테고리의 다른 글
[Android] CoordinatorLayout 활용 (0) | 2018.10.25 |
---|---|
[Android] 스플래시 화면 (0) | 2018.09.12 |
[Android] 페이지 슬라이딩 (0) | 2018.09.07 |
[Android] 트윈 애니메이션 (0) | 2018.09.07 |
[Android] 스레드 애니메이션 (0) | 2018.09.07 |