안드로이드 기기 간에 Wifi Socket 채팅방 만들기(Kotlin)

 

 안드로이드에 Kotlin이 사용되면서 기존에 Java로 작성하였던 프로젝트를 진행하였던 적이 있었습니다. 그 당시 낮선 언어였던 Kotlin을 처음 공부하면서 소스코드를 다시 만들어봤었는데 그게 벌써 3년전의 일이 되었군요. 그 사이 안드로이드도 12버전(API 32, Snow cake)이 나온 상황입니다. 그런데 신기하게도 통신방식인 TCP는 프로그래밍 언어의 정의 방법의 차이일 뿐 동작 원리는 똑같기에 Kotlin으로 소스코드를 고쳐씀에 다행히 큰 불편함은 없었습니다.

 

 이번 포스팅에서는 기존에 작성하였던 서버-클라이언트 1:1 통신 환경을 확장하여 여러 대의 기기가 서버에 동시 접속하여 대화하는 채팅 시스템을 안드로이드 Kotlin으로 구현해 보았습니다.

 혹시 이전에 제가 작업하였던 서버-클라이언트 1:1 통신 방법에 대한 내용이 궁금하신 분은 아래의 링크를 참조하시기 바랍니다.

 

안드로이드 기기 간에 Wifi Socket 통신하기(Kotlin)

 오랜만에 안드로이드를 다루어보는 기회가 생겼는데 생각보다 많은 변화가 있었습니다. 특히 올해부터는 구글에서 새로 만든 프로그래밍 언어인 Kotlin이 도입되면서 Java 위주로 설계된 안드로

elecs.tistory.com

 

동작원리

 이 프로젝트에서는 1개의 서버에 다수의 클라이언트가 접속할 수 있는 환경으로 아래와 같이 구성하였습니다.

먼저 Server 역할을 하는 기기에서 서버 Port를 엽니다.

포트가 열린 서버에 클라이언트가 접속을 시도하면 서버는 ServerSocket가 클라이언트의 접속을 받습니다.

 클라이언트의 접속을 받아들인 ServerSocket는 해당 클라이언트와 통신을 수행할 별개의 Socket을 생성해 이를 SocketList에 추가합니다.

 이후 다른 클라이언트가 서버에 접속하면 위와 같은 방식으로 해당 클라이언트와 통신을 수행하는 Socket을 생성하여 이를 SocketList에 추가합니다.

 다수의 클라이언트가 연결된 환경에서 클라이언트 한 곳에서 메시지를 서버에 보내면

 서버에서는 SocketList에 있는 모든 Socket에 메시지를 Broadcast합니다.

만약 클라이언트에서 접속을 종료하게 될 경우 서버는 접속이 종료된 소켓을 SocketList에서 삭제함으로서 채팅을 종료합니다.

 

※ 이 프로젝트는 Lolipop 이상의 버전에서 테스트 하였습니다.

 

AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.androidchatroom">
 
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidChatRoom">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>
cs

 

 

strings.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<resources>
    <string name="app_name">AndroidChatRoom</string>
    <string name="hello_world">Hello world!</string>
    <string name="action_settings">Settings</string>
    <string name="ip">SERVER IP</string>
    <string name="port">PORT</string>
    <string name="name">NAME</string>
    <string name="msg">MESSAGE</string>
    <string name="hint_port">PORT NUMBER</string>
    <string name="hint_ip">192.168.0.1</string>
    <string name="hint_name">USER</string>
    <string name="hint_msg">Hello, world!</string>
    <string name="button1">Connect</string>
    <string name="button2">Disconnect</string>
    <string name="button3">Set Server</string>
    <string name="button4">Close Server</string>
    <string name="button5">Clear All Text</string>
    <string name="button6">Send Message</string>
</resources>
cs

 

Activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
 
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
 
            <TextView
                android:id="@+id/textView"
                android:layout_width="70dp"
                android:layout_height="wrap_content"
                android:text="@string/ip" />
 
            <EditText
                android:id="@+id/et_ip"
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:ems="10"
                android:hint="@string/hint_ip"
                android:importantForAccessibility="no"
                tools:ignore="Autofill"
                android:inputType="textUri"
                android:singleLine="true" />
        </LinearLayout>
 
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
 
            <TextView
                android:id="@+id/textView2"
                android:layout_width="70dp"
                android:layout_height="wrap_content"
                android:text="@string/port" />
 
            <EditText
                android:id="@+id/et_port"
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:ems="10"
                android:hint="@string/hint_port"
                android:importantForAccessibility="no"
                tools:ignore="Autofill"
                android:inputType="number"
                android:singleLine="true" />
        </LinearLayout>
 
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
 
            <TextView
                android:id="@+id/textView3"
                android:layout_width="70dp"
                android:layout_height="wrap_content"
                android:text="@string/name" />
 
            <EditText
                android:id="@+id/et_name"
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:ems="10"
                android:hint="@string/hint_name"
                android:importantForAccessibility="no"
                tools:ignore="Autofill"
                android:singleLine="true" />
        </LinearLayout>
 
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
 
            <TextView
                android:id="@+id/textView4"
                android:layout_width="70dp"
                android:layout_height="wrap_content"
                android:text="@string/msg" />
 
            <EditText
                android:id="@+id/et_msg"
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:ems="10"
                android:hint="@string/hello_world"
                android:importantForAccessibility="no"
                tools:ignore="Autofill"
                android:inputType="textPersonName" />
        </LinearLayout>
 
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
 
            <Button
                android:id="@+id/button_connect"
                style="@style/Widget.AppCompat.Button.Small"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:minWidth="0dip"
                android:text="@string/button1"
                android:textSize="12sp" />
 
            <Button
                android:id="@+id/button_disconnect"
                style="@style/Widget.AppCompat.Button.Small"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:minWidth="0dip"
                android:text="@string/button2"
                android:textSize="12sp" />
 
            <Button
                android:id="@+id/button_msg"
                style="@style/Widget.AppCompat.Button.Small"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:minWidth="0dip"
                android:text="@string/button6"
                android:textSize="12sp" />
        </LinearLayout>
 
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
 
            <Button
                android:id="@+id/button_setserver"
                style="@style/Widget.AppCompat.Button.Small"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/button3"
                android:textSize="12sp" />
 
            <Button
                android:id="@+id/button_closeserver"
                style="@style/Widget.AppCompat.Button.Small"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/button4"
                android:textSize="12sp" />
 
            <Button
                android:id="@+id/button_clear"
                style="@style/Widget.AppCompat.Button.Small"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/button5"
                android:textSize="12sp" />
 
        </LinearLayout>
 
        <ScrollView
            android:id="@+id/sv"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
 
            <TextView
                android:id="@+id/text_status"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="18sp"
                />
        </ScrollView>
 
    </LinearLayout>
 
</androidx.constraintlayout.widget.ConstraintLayout>
cs

Activity_main.xml을 MainActivity.kt에서 바로 불러오기 위해서는 "import kotlinx.android.synthetic.main.activity_main.*"

을 통해 불러올 수 있습니다. 이 기능을 수행하기 위해 Gradle에 아래와 같이 'id kotlin-android-extentions'를 추가하여야 합니다.

 

 먼저 Gradle Scripts→build.gradle (Module: *.app)을 선택합니다.

 

플러그인에 'id kotlin-android-extension' 추가 후 'Sync Now'를 클릭하세요.

 

위 과정을 수행하면 MainActivity.kt에서 id로 설정한 view를 바로 불러올 수 있습니다.

 

MainActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
package com.example.androidchatroom
 
import androidx.appcompat.app.AppCompatActivity
 
import android.content.Context
import android.net.ConnectivityManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.widget.Toast
 
import kotlinx.android.synthetic.main.activity_main.*
 
import java.io.DataInputStream
import java.io.DataOutputStream
import java.net.*
import kotlin.properties.Delegates
 
class MainActivity : AppCompatActivity() {
 
    companion object{
        var socket = Socket()
        var server = ServerSocket()
        lateinit var writeSocket: DataOutputStream
        lateinit var readSocket: DataInputStream
        lateinit var cManager: ConnectivityManager
        lateinit var myIp: String
 
        var ip = "192.168.0.1"
        var port = 2222
        //var mHandler = Handler()      -> API30부터 Deprecated됨. Looper를 직접 명시해야함
        var mHandler = Handler(Looper.getMainLooper())
        var serverClosed = true
 
        var cList = mutableListOf<Client>()
    }
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        cManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        server.close()
        socket.close()
 
        button_connect.setOnClickListener {    //클라이언트 -> 서버 접속
            if(et_ip.text.isNotEmpty()) {
                ip = et_ip.text.toString()
                myIp = et_name.text.toString()
                if(et_port.text.isNotEmpty()) {
                    port = et_port.text.toString().toInt()
                    if(port<0 || port>65535){
                        Toast.makeText(this@MainActivity, "PORT 번호는 0부터 65535까지만 가능합니다.", Toast.LENGTH_SHORT).show()
                    }else{
                        if(!socket.isClosed){
                            Toast.makeText(this@MainActivity, ip + "에 이미 연결되어 있습니다.", Toast.LENGTH_SHORT).show()
                        }else {
                            Connect().start()
                        }
                    }
 
                }else{
                    Toast.makeText(this@MainActivity, "PORT 번호를 입력해주세요.", Toast.LENGTH_SHORT).show()
                }
            }else{
                Toast.makeText(this@MainActivity, "IP 주소를 입력해주세요.", Toast.LENGTH_SHORT).show()
            }
        }
 
        button_disconnect.setOnClickListener {    //클라이언트 -> 서버 접속 끊기
            if(!socket.isClosed){
                Disconnect().start()
            }else{
                Toast.makeText(this@MainActivity, "서버와 연결이 되어있지 않습니다.", Toast.LENGTH_SHORT).show()
            }
        }
 
        button_setserver.setOnClickListener{    //서버 포트 열기
            if(et_port.text.isNotEmpty()) {
                val cport = et_port.text.toString().toInt()
                if(cport<0 || cport>65535){
                    Toast.makeText(this@MainActivity, "PORT 번호는 0부터 65535까지만 가능합니다.", Toast.LENGTH_SHORT).show()
                }else{
                    if(server.isClosed) {
                        port = cport
                        SetServer().start()
                    }else{
                        val tstr = port.toString() + "번 포트가 열려있습니다."
                        Toast.makeText(this@MainActivity, tstr, Toast.LENGTH_SHORT).show()
                    }
                }
            }else{
                Toast.makeText(this@MainActivity, "PORT 번호를 입력해주세요.", Toast.LENGTH_SHORT).show()
            }
        }
 
        button_closeserver.setOnClickListener {    //서버 포트 닫기
            if(!server.isClosed){
                CloseServer().start()
            }else{
                mHandler.obtainMessage(17).apply {
                    sendToTarget()
                }
            }
        }
 
        button_clear.setOnClickListener {    //채팅방 내용 지우기
            text_status.text = ""
        }
 
        button_msg.setOnClickListener {    //상대에게 메시지 전송
            if(socket.isClosed){
                Toast.makeText(this@MainActivity, "연결이 되어있지 않습니다.", Toast.LENGTH_SHORT).show()
            }else {
                val mThread = SendMessage()
                mThread.setMsg(2, et_name.text.toString(), et_msg.text.toString())
                mThread.start()
            }
        }
 
        mHandler = object : Handler(Looper.getMainLooper()){  //Thread들로부터 Handler를 통해 메시지를 수신
            override fun handleMessage(msg: Message) {
                super.handleMessage(msg)
                when(msg.what){
                    1->Toast.makeText(this@MainActivity, "IP 주소가 잘못되었거나 서버의 포트가 개방되지 않았습니다.", Toast.LENGTH_SHORT).show()
                    2->Toast.makeText(this@MainActivity, "서버 포트 "+port +"가 준비되었습니다.", Toast.LENGTH_SHORT).show()
                    3->Toast.makeText(this@MainActivity, msg.obj.toString(), Toast.LENGTH_SHORT).show()
                    4->Toast.makeText(this@MainActivity, "연결이 종료되었습니다.", Toast.LENGTH_SHORT).show()
                    5->Toast.makeText(this@MainActivity, "이미 사용중인 포트입니다.", Toast.LENGTH_SHORT).show()
                    6->Toast.makeText(this@MainActivity, "서버 준비에 실패하였습니다.", Toast.LENGTH_SHORT).show()
                    7->Toast.makeText(this@MainActivity, "서버가 종료되었습니다.", Toast.LENGTH_SHORT).show()
                    8->Toast.makeText(this@MainActivity, "서버가 정상적으로 닫히는데 실패하였습니다.", Toast.LENGTH_SHORT).show()
                    9-> ((text_status.text as String+ (msg.obj as String+ "\n").also { text_status.text = it }
                    11->Toast.makeText(this@MainActivity, "서버에 접속하였습니다.", Toast.LENGTH_SHORT).show()
                    12->Toast.makeText(this@MainActivity, "메시지 전송에 실패하였습니다.", Toast.LENGTH_SHORT).show()
                    13->Toast.makeText(this@MainActivity, (msg.obj as String+ " 클라이언트와 연결되었습니다.",Toast.LENGTH_SHORT).show()
                    14->Toast.makeText(this@MainActivity,"서버에서 응답이 없습니다.", Toast.LENGTH_SHORT).show()
                    15->Toast.makeText(this@MainActivity, "서버와의 연결을 종료합니다.", Toast.LENGTH_SHORT).show()
                    16->Toast.makeText(this@MainActivity, (msg.obj as String)+" 클라이이언트와의 연결을 종료합니다.", Toast.LENGTH_SHORT).show()
                    17->Toast.makeText(this@MainActivity, "포트가 이미 닫혀있습니다.", Toast.LENGTH_SHORT).show()
                    18->Toast.makeText(this@MainActivity, "서버와의 연결이 끊어졌습니다.", Toast.LENGTH_SHORT).show()
                    19->Toast.makeText(this@MainActivity, "인터넷이 연결되지 않았습니다. 연결 후 다시 시도하세요.", Toast.LENGTH_LONG).show()
                    20->{
                        et_name.setText(msg.obj as String)
                        myIp = msg.obj as String
                    }
                }
            }
        }
 
        ShowInfo().start()    //자신의 IP주소 확인
    }
 
    //클라이언트-서버 접속 시도
    class Connect:Thread(){
 
        override fun run() = try{
            socket = Socket(ip, port)
            writeSocket = DataOutputStream(socket.getOutputStream())
            readSocket = DataInputStream(socket.getInputStream())
            val b = readSocket.readInt()
            if(b==1){    //서버로부터 접속이 확인되었을 때
                mHandler.obtainMessage(11).apply {
                    sendToTarget()
                }
                ClientSocket(myIp).start()
            }else{    //서버 접속에 성공하였으나 서버가 응답을 하지 않았을 때
                mHandler.obtainMessage(14).apply {
                    sendToTarget()
                }
                socket.close()
            }
        }catch(e:Exception){    //연결 실패
            val state = 1
            mHandler.obtainMessage(state).apply {
                sendToTarget()
            }
            socket.close()
        }
    }
 
    //클라이언트-서버 통신 개시
    class ClientSocket(private val addr: String):Thread(){
 
        override fun run() {
            try{
                while (true) {
                    val ac = readSocket.readInt()
                    val cname = readSocket.readUTF()
 
                    if( ac == 3){
                        readSocket.readUTF()
                        if(addr != cname){
                            mHandler.obtainMessage(9"$cname 님이 입장하였습니다.").apply {
                                sendToTarget()
                            }
                        }else{
                            mHandler.obtainMessage(9"채팅방에 입장하였습니다.").apply {
                                sendToTarget()
                            }
                        }
                    }else if(ac == 2) {    //서버로부터 메시지 수신 명령을 받았을 때
                        val bac = readSocket.readUTF()
                        val input = bac.toString()
                        val recvInput = input.trim()
 
                        val clientName = cname.toString().trim()
 
                        val msg = mHandler.obtainMessage()
                        msg.what = 9
                        msg.obj = "$clientName> $recvInput"
                        mHandler.sendMessage(msg)
                    }else if(ac == 4){
                        readSocket.readUTF()
                        if(addr != cname) {
                            mHandler.obtainMessage(9"$cname 님이 퇴장하였습니다.").apply {
                                sendToTarget()
                            }
                        }
 
                    }else if(ac == 10){    //서버로부터 접속 종료 명령을 받았을 때
                        mHandler.obtainMessage(18).apply {
                            sendToTarget()
                        }
                        mHandler.obtainMessage(9,"서버에서 연결을 끊었습니다.").apply {
                            sendToTarget()
                        }
                        socket.close()
                        break
                    }
                }
            }catch(e:SocketException){    //소켓이 닫혔을 때
                mHandler.obtainMessage(15).apply {
                    sendToTarget()
                }
                mHandler.obtainMessage(9"채팅방을 나갔습니다.").apply {
                    sendToTarget()
                }
            }
        }
    }
 
    //클라이언트 접속 종료
    class Disconnect:Thread(){
 
        override fun run() {
 
            try{
                writeSocket.write(10)    //서버에게 접속 종료 명령 전송
                writeSocket.writeUTF(myIp)  //종료 요청 클라이언트 주소
                socket.close()
            }catch(e:Exception){
 
            }
        }
    }
 
    //서버 통신 개시
    class SetServer:Thread(){
 
        override fun run(){
            try{
                server = ServerSocket(port)    //포트 개방
                mHandler.obtainMessage(2"").apply {
                    sendToTarget()
                }
                mHandler.obtainMessage(9"서버가 열렸습니다.").apply {
                    sendToTarget()
                }
 
                while(true) {
                    socket = server.accept()    //클라이언트가 접속할 때 까지 대기
                    val client = Client(socket)    //접속한 Client의 socket을 저장
                    cList.add(client)    //접속 client socket 리스트 추가
                    client.start()    //접속한 클라이언트 전용 socket thread 실행
                }
 
            }catch(e:BindException) {    //이미 개방된 포트를 개방하려 시도하였을때
                mHandler.obtainMessage(5).apply {
                    sendToTarget()
                }
            }catch(e:SocketException){    //소켓이 닫혔을 때
                mHandler.obtainMessage(7).apply {
                    sendToTarget()
                }
            }
            catch(e:Exception){
                if(!serverClosed) {
                    mHandler.obtainMessage(6).apply {
                        sendToTarget()
                    }
                }else{
                    serverClosed = false
                }
            }
        }
    }
 
    //서버 소켓 닫기
    class CloseServer:Thread(){
        override fun run(){
            try{
                if(!socket.isClosed){
                    writeSocket.write(10)    //클라이언트에게 서버가 종료되었음을 알림
                    writeSocket.close()
                    socket.close()
                }
                server.close()
                serverClosed = true
                mHandler.obtainMessage(9"서버가 닫혔습니다.").apply {
                    sendToTarget()
                }
            }catch(e:Exception){
                e.printStackTrace()
                mHandler.obtainMessage(8).apply {
                    sendToTarget()
                }
            }
        }
    }
 
    //메시지 전송
    class SendMessage:Thread(){
        private var state by Delegates.notNull<Int>()
        private lateinit var msg:String
        private lateinit var cname:String
 
        fun setMsg(s: Int, n:String, m:String){
            state = s
            msg = m
            cname = n
        }
 
        override fun run() {
 
            if(cList.size>0){    //메시지를 전송하는 주체가 서버일 경우
                val cIter = cList.iterator()
                while(cIter.hasNext()){
                    val client = cIter.next()
                    if (!client.isClosed()) client.sendMessage(state, cname, msg)
                    else cIter.remove()
                    mHandler.obtainMessage(9"$cname> $msg").apply {
                        sendToTarget()
                    }
                }
            }else {
                try {
                    writeSocket.writeInt(state)    //메시지 전송 명령 전송
                    writeSocket.writeUTF(cname)    //클라이언트 이름
                    writeSocket.writeUTF(msg)    //메시지 내용
                } catch (e: Exception) {
                    e.printStackTrace()
                    mHandler.obtainMessage(12).apply {
                        sendToTarget()
                    }
                }
            }
        }
    }
 
    //자신의 IP주소를 표시
    class ShowInfo:Thread() {
 
        override fun run() {
            var ip = ""
            val en = NetworkInterface.getNetworkInterfaces()
            while (en.hasMoreElements()) {
                val intf = en.nextElement()
                val enumIpAddr = intf.inetAddresses
                while (enumIpAddr.hasMoreElements()) {
                    val inetAddress = enumIpAddr.nextElement()
                    if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) {
                        @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
                        ip = inetAddress.hostAddress as String
                    }
                }
            }
 
            if (ip == "") {
                mHandler.obtainMessage(19).apply {
                    sendToTarget()
                }
            } else {
                val msg = mHandler.obtainMessage()
                msg.what = 20
                msg.obj = ip
                mHandler.sendMessage(msg)
            }
        }
    }
 
    //서버에 접속한 클라이언트 소켓 제어
    class Client(socket: Socket) : Thread(){
        private lateinit var clientName: String
        private lateinit var clientAddr: String
        private lateinit var cWriteSocket: DataOutputStream
        private val cSocket: Socket=socket
 
        override fun run(){
            cWriteSocket = DataOutputStream(cSocket.getOutputStream())
            val cReadSocket = DataInputStream(cSocket.getInputStream())
 
            cWriteSocket.writeInt(1)    //클라이언트에게 서버의 소켓 생성을 알림
            val socketAddr = socket.remoteSocketAddress as InetSocketAddress
            clientAddr = socketAddr.address.hostAddress as String
 
            mHandler.obtainMessage(13, clientAddr).apply {
                sendToTarget()
            }
            mHandler.obtainMessage(9, clientAddr + "님이 입장하였습니다.").apply {
                sendToTarget()
            }
            Broadcast(cList, 3, clientAddr, "입장").start()
            while (true) {
                val ac = cReadSocket.read()
                clientName = cReadSocket.readUTF().toString()
                if(ac==10){    //클라이언트로부터 소켓 종료 명령 수신
                    mHandler.obtainMessage(16, clientName).apply {
                        sendToTarget()
                    }
                    mHandler.obtainMessage(9"$clientName 님이 퇴장하였습니다.").apply {
                        sendToTarget()
                    }
                    Broadcast(cList, 4, clientName, "퇴장").start()
                    break
                }else if(ac == 2){    //클라이언트로부터 메시지 전송 명령 수신
                    val bac = cReadSocket.readUTF()
                    val input = bac.toString()
                    val recvInput = input.trim()
 
                    val msg = mHandler.obtainMessage()
                    msg.what = 9
                    msg.obj = "$clientName> $recvInput"
                    mHandler.sendMessage(msg)    //핸들러에게 클라이언트로 전달받은 메시지 전송
 
                    Broadcast(cList, 2, clientName, recvInput).start()
                }
            }
            cWriteSocket.close()
            cSocket.close()
        }
 
        fun isClosed(): Boolean {
            return cSocket.isClosed
        }
 
        fun sendMessage(state: Int, cname: String, msg: String){
            try{
                cWriteSocket.writeInt(state)    //메시지 전송 명령 전송
                cWriteSocket.writeUTF(cname)    //클라이언트 이름
                cWriteSocket.writeUTF(msg)    //메시지 내용
            }catch(e:Exception){
                e.printStackTrace()
                mHandler.obtainMessage(12).apply {
                    sendToTarget()
                }
            }
        }
    }
//서버에 접속한 클라이언트에게 메시지 전파
    class Broadcast(private val cList: MutableList<Client>private val state: Int, private val cname: Stringprivate val msg: String):Thread(){
 
        override fun run(){
            if(cList.size>0){
                val cIter = cList.iterator()
                while(cIter.hasNext()){
                    val client = cIter.next()
                    if (!client.isClosed()) {
                        client.sendMessage(state, cname, msg)
                    }
                    else cIter.remove()
                }
            }
        }
    }
 
}
cs

 

 

실행 결과는 다음과 같습니다.

 

※Sever 사용 방법

1. Port에 원하는 포트 번호를 입력합니다.

2. 'SET SERVER' 버튼을 클릭하면 포트를 열 수 있습니다. 만약 다른 프로세스에서 포트를 사용중일 경우 다른 포트 번호를 입력합니다.

3. 인터넷이 연결된 상태에서 앱을 실행시 NAME에 Server의 IP주소가 나타납니다. 이를 Client의 IP란에 입력해주세요.

4. Client와의 연결이 성공하면 Toast로 연결에 성공하였다는 알림이 나옵니다.



※Client 사용 방법

1. IP와 PORT에 서버의 IP주소와 설정 PORT 번호를 입력합니다.

2. 'CONNECT' 버튼을 클릭하여 서버에 접속합니다. 성공시 Toast로 접속에 성공하였다는 메시지를 확인하실 수 있습니다.

3. 'SEND MESSAGE' 버튼을 클릭하면 MESSAGE 칸에 입력한 텍스트 정보를 상대에게 전송할 수 있습니다.


4.  'DISCONNECT' 버튼을 눌러 서버와의 연결을 해제합니다.

 

 

실험을 위해 저희 가족들이 사용하고 있는 폰들을 모두 모아 실험에 사용하였습니다.

 

맨 위의 폰이 Server 역할을 하고 있으며 나머지 3개의 폰은 Client 역할을 하고 있습니다.

 

 

Server에서는 각 클라이언트의 메시지와 접속 상황을 실시간으로 보여줍니다.

 

 

일부 클라이언트 접속 상황이 제대로 반영되지 않은 것으로 보이나 정상적으로 서버와의 통신 및 채팅방 대화가 공유되는 것을 확인하실 수 있습니다!

300x250