코로나19와의 사투(1) - 생활치료센터에 입소하다

흔치않은일상 2022. 3. 12. 22:26

2022년 대통령선거 다음날이었던 3월 10일 평소에 비해 목구멍에서 통증이 밀려왔습니다. 지난달에는 환절기 몸살로 앓아누웠었는데 다행히도 그때는 신속항원검사 결과 양성이었고 일상생활도 별로 달라진게 없었는데 말이죠.

혹시나 해서 이번에도 신속항원검사를 해보았는데...

 

 

테스트기에서 두 줄이 뜨는것을 보고야 만것입니다!!

이럴수가... 결국 저에게도 코로나19가 찾아온것인가 싶었습니다. 안그래도 요새 확진자가 30만명에 근접한데다가 주변에서도 확진 판정을 받으신 분들의 소식이 들려오는 마당에 저라고 오죽할까요?

테스트기에서 양성 판정이 나온 순간 잠시 멍하니 바라보고 있었다가 다시 정신을 차리고 신속히 보건소로 달려갑니다.
보건소에 도착했을때 오후 2시 정도 였는데 정말이지 줄이 너무나 길었고 기다리는 도중에 틈틈이 대기자들을 위해 마련된 의자에 앉아있으면서 휴식을 취했지만 줄은 좀처럼 줄어들지 않고...
그렇게 3시간을 기다리고서야 PCR 검사를 받을 수 있었고 혹시 확진될지 모르기에 격리기간동안 먹을 컵밥을 챙기고 귀가하였습니다. 곧바로 회사에 PCR 검사를 받았음을 알렸고 확진 경험이 있으셨던 리더님께서 내일 하루는 집에서 푹 쉬라는 격려도 받았습니다. 요새 유행중인 오미크론의 주요 증상이던 인후통 증세가 점점 심해지고 있었고 저 또한 확진일 것으로 생각하고 있었습니다.

그렇게 다음날 아침이 밝아올 무렵 문자 통보를 받게 되었습니다.

귀하는 코로나바이러스감염증-19로 확진되셨으므로 감염병예방법 제41조 및 제43조 등에 따른 격리 대상임을 통지합니다. 또한 귀하의 동거인이 10일간 준수하여야 할 권고사항을 함께 안내해 드리오니 동거인에게 본 문자를 공유하여 주시기 바랍니다.

결국 올 것이 오고야 말았습니다... 어느 정도 예상을 하고 있었기에 충격적이지는 않았지만 무엇보다도 감염병으로 인한 통증과의 사투가 이제 시작되었다는 것이니까요.

요새는 확진자들이 너무나 많아서 재택치료가 위주였으나 제가 하필이면 집이 아닌 타지에서 일을 하던 도중에 증상이 발현해서 거주지가 아닌 회사에서 제공하는 시설에서 대기하다 확진 통보를 받게 되었고 생활 시설들도 공용으로 사용되는 공간이라 동선 분리가 불가능한 곳이었습니다. 이러한 사정을 보건소에 전달하였고 운 좋게도 생활치료센터에 자리가 생겨 구급차를 지원받아 안동에 있는 생활치료센터에 입소할 수 있었습니다.

 

 구급차에서 내리자마자 방호복을 입은 의료진 분께서 안내하는 대로 부랴부랴 싸온 짐을 들고 이동합니다. 입실 전 엑스레이 촬영을 간단히 하고나서 위의 사진같은 방에 입실하게 되었습니다.

급하게 입소 준비하느라 짐을 부랴부랴 챙겨왔는데 생각보다 생활치료센터에서 제공해주는 키트에서 생필품들 구성이 잘 되어있어 굳이 짐을 많이 싸올 필요는 없어 보입니다.(굳이 필요하다면 실내에서 편하게 입고 다닐 수 있는 옷 정도?) 심지어 격리 기간동안 방 안에서만 지내기 때문에 여분의 옷 한 벌과 약간의 속옷 정도만 있으면 되는 듯 합니다. 

 

 

 예상외로 제공되는 키트에 빨래비누가 있어서 혹시 속옷이 모자랄 때 손빨래로 세탁을 할 수 있게 해주긴 합니다만 다행히도 챙겨온 속옷이 많아서 퇴소 전까지는 챙겨온 속옷으로 갈아입으며 생활할듯합니다. 

 

 당분간은 건강 관리에 최선을 다하면서 회복의 기회로 삼아보고자 합니다. 한동안 블로그에 글을 쓸 소재에 대해 고민이 많았는데 좋은 주제의 글들을 읽으면서 평소 생각해보았던 내용들을 정리해볼까 합니다.

 

 2022년 3월 전국적으로 코로나19 오미크론 변이 확산의 폭풍우 속에 있습니다. 여러분들께서도 최대한 감염되지 않도록 개인 위생 철저히 하면서 지내시길 바랍니다. 어떤 분들은 가벼운 감기 증상처럼 지나갈 것이라고 하였는데 저의 경우 심한 인후통을 겪고 있습니다. 오미크론 변이의 증상이 경미하다고는 하나 그래도 최대한 걸리지 않고 지나가는 것이 가장 좋은것 같습니다.

 

 부디 다음에는 건강하게 생활치료센터를 퇴소하였다는 소식을 전달할 수 있기를 간절히 기도해봅니다. 여러분들도 화이팅!

 

 

300x250

안드로이드 기기 간에 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

KTX-이음을 타고 중부내륙선을 달리다 - 부발역 ~ 충주역[2022.01.01]


지금껏 사라져가는 모습들을 사진으로 남겨왔던 제게 처음으로 개통되는 구간을 가보게 된다는 것이 한편으로는 신기한 경험이었습니다. 없었던 길을 간다는 것은 새로운 길을 개척한다는 것이고 한편으로는 가보지 않은 길을 간다는 의미이기도 합니다.
이번 여행 또한 가보지 않았던 곳을 떠다본다는 설렘과 기대로 2022년 새해의 첫 여행을 시작해보고자 합니다.



경강선 전철을 타고 부발역으로 이동합니다.
부발발 충주행
역 바깥에서 KTX 이음을 바로 볼 수 있습니다.
개통 초기이다 보니 많은 사람들이 보입니다.
새로 개통된 열차를 타보고자 온 사람들인듯 합니다.
충주행 ktx를 타보러 갑니다.
ITX 청춘 열차를 탈 수 있는 곳에서 볼 수 있는 승하차 태그기가 부발역에도 있습니다.
처음으로 탑승해본 KTX 이음
같은 플랫폼에서 전철과 KTX를 모두 이용할 수 있다니
충주에서 열차를 타고 부발에서 바로 판교행 열차를 탈 수 있게 동선이 구성되어 있습니다.
계단 없이 열차에 바로 탈 수 있는게 고상홈의 장점이겠지요
열차에서 바라본 부발역
열차는 가남역을 지나
논밭을 달려 감곡장호원역에 도착합니다.

 

 

희안하게도 역명판이 장호원이라 적혀있네요?
분명 역 이름은 감곡장호원역입니다.
더욱 가관인건 타는곳의 역명판이었습니다.
아무리 역명 정할때 논란이 있었다 하더라고 이렇게 플랫폼에서 조차 이렇게 만들면 행선지로 오인할 수 있을텐데 말이지요
새롭게 개통한 역을 사진으로 남기는 사람들
이제 다시 열차에 올라 충주역으로 갑니다.
드디어 충주역에 도착했습니다.
차후 KTX 역에서도 이렇게 스크린도어가 운영될 듯 합니다.
충주역은 역내 건널목으로 열차를 이용할 수 있는 역이었습니다.
지금은 새 건물을 지어 길을 막아놓았습니다.
KTX 개통을 대비하여 안전하게 에스컬레이터를 설치하였습니다.
도착후 역 바깥으로 나와봅니다.
놀랍게도 기존 역사를 그대로 사용하고 있는군요.
늦은 시간이지만 충주 주변을 여행하고
자고 일어나 충주댐 구경도 하고
맛난 송어비빔회도 먹어보고
다시 충주역으로 돌아옵니다.
역 광장을 택시승강장으로 새로 만들었군요.
기존의 건널목 횡단을 막고 에스컬레이터를 타고 탑승할 수 있게 동선을 만들었습니다.
차후엔 이 곳을 통해 열차를 탑승하게 될 듯 합니다.
충주역에서 만난 KTX 이음
어느덧 충주역에도 어둠이 찾아오고
KTX 타는곳은 새로 지은 승강장을 사용합니다.
저상홈과 KTX 이음
해가 산으로 넘어갈 무렵 도착한 대전행 무궁화호를 타고 충주역을 떠납니다.

 

300x250