검색결과 리스트
글
안드로이드 기기 간에 Wifi Socket 통신하기(Kotlin)
오랜만에 안드로이드를 다루어보는 기회가 생겼는데 생각보다 많은 변화가 있었습니다. 특히 올해부터는 구글에서 새로 만든 프로그래밍 언어인 Kotlin이 도입되면서 Java 위주로 설계된 안드로이드 생태계에 거대한 변화가 있을 것으로 보입니다. 이러한 안드로이드의 환경 변화에 맞추어 제가 기존에 다루었던 프로그램들을 Kotlin으로 다시 한 번 설계해보려고 합니다.
이전에 안드로이드 기기간의 Wi-Fi 통신 프로그래밍을 설계하였던 적이 있었는데 Kotlin으로 다시 짜보는 김에 프로그램을 최적화 해서 다시 만들어보았습니다.
AndroidMenifest.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 | <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.elecs.interandroidcommunication"> <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/AppTheme"> <activity android:name=".MainActivity"> <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">IAC</string> <string name="hello_world">Hello world!</string> <string name="action_settings">Settings</string> <string name="ip">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_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">View my info</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 | <?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="wrap_content" 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="wrap_content" 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/msg" /> <EditText android:id="@+id/et_msg" android:layout_width="match_parent" android:layout_height="wrap_content" 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_info" 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> <TextView android:id="@+id/text_status" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout> | cs |
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 | package com.elecs.interandroidcommunication 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 androidx.appcompat.app.AppCompatActivity import kotlinx.android.synthetic.main.activity_main.* import java.io.DataInputStream import java.io.DataOutputStream import java.net.* class MainActivity : AppCompatActivity() { companion object{ var socket = Socket() var server = ServerSocket() lateinit var writeSocket: DataOutputStream lateinit var readSocket: DataInputStream lateinit var cManager: ConnectivityManager var ip = "192.168.0.1" var port = 2222 var mHandler = Handler() var closed = false } 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() 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_info.setOnClickListener { //자기자신의 연결 정보(IP 주소)확인 ShowInfo().start() } button_msg.setOnClickListener { //상대에게 메시지 전송 if(socket.isClosed){ Toast.makeText(this@MainActivity, "연결이 되어있지 않습니다.", Toast.LENGTH_SHORT).show() }else { val mThread = SendMessage() mThread.setMsg(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 = msg.obj as String 11->Toast.makeText(this@MainActivity, "서버에 접속하였습니다.", Toast.LENGTH_SHORT).show() 12->Toast.makeText(this@MainActivity, "메시지 전송에 실패하였습니다.", Toast.LENGTH_SHORT).show() 13->Toast.makeText(this@MainActivity, "클라이언트와 연결되었습니다.",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, "클라이언트와의 연결을 종료합니다.", Toast.LENGTH_SHORT).show() 17->Toast.makeText(this@MainActivity, "포트가 이미 닫혀있습니다.", Toast.LENGTH_SHORT).show() 18->Toast.makeText(this@MainActivity, "서버와의 연결이 끊어졌습니다.", Toast.LENGTH_SHORT).show() } } } } class Connect:Thread(){ override fun run(){ try{ socket = Socket(ip, port) writeSocket = DataOutputStream(socket.getOutputStream()) readSocket = DataInputStream(socket.getInputStream()) val b = readSocket.read() if(b==1){ //서버로부터 접속이 확인되었을 때 mHandler.obtainMessage(11).apply { sendToTarget() } ClientSocket().start() }else{ //서버 접속에 성공하였으나 서버가 응답을 하지 않았을 때 mHandler.obtainMessage(14).apply { sendToTarget() } socket.close() } }catch(e:Exception){ //연결 실패 val state = 1 mHandler.obtainMessage(state).apply { sendToTarget() } socket.close() } } } class ClientSocket:Thread(){ override fun run() { try{ while (true) { val ac = readSocket.read() if(ac == 2) { //서버로부터 메시지 수신 명령을 받았을 때 val bac = readSocket.readUTF() val input = bac.toString() val recvInput = input.trim() val msg = mHandler.obtainMessage() msg.what = 3 msg.obj = recvInput mHandler.sendMessage(msg) }else if(ac == 10){ //서버로부터 접속 종료 명령을 받았을 때 mHandler.obtainMessage(18).apply { sendToTarget() } socket.close() break } } }catch(e:SocketException){ //소켓이 닫혔을 때 mHandler.obtainMessage(15).apply { sendToTarget() } } } } class Disconnect:Thread(){ override fun run() { try{ writeSocket.write(10) //서버에게 접속 종료 명령 전송 socket.close() }catch(e:Exception){ } } } class SetServer:Thread(){ override fun run(){ try{ server = ServerSocket(port) //포트 개방 mHandler.obtainMessage(2, "").apply { sendToTarget() } while(true) { socket = server.accept() writeSocket = DataOutputStream(socket.getOutputStream()) readSocket = DataInputStream(socket.getInputStream()) writeSocket.write(1) //클라이언트에게 서버의 소켓 생성을 알림 mHandler.obtainMessage(13).apply { sendToTarget() } while (true) { val ac = readSocket.read() if(ac==10){ //클라이언트로부터 소켓 종료 명령 수신 mHandler.obtainMessage(16).apply { sendToTarget() } break }else if(ac == 2){ //클라이언트로부터 메시지 전송 명령 수신 val bac = readSocket.readUTF() val input = bac.toString() val recvInput = input.trim() val msg = mHandler.obtainMessage() msg.what = 3 msg.obj = recvInput mHandler.sendMessage(msg) //핸들러에게 클라이언트로 전달받은 메시지 전송 } } } }catch(e:BindException) { //이미 개방된 포트를 개방하려 시도하였을때 mHandler.obtainMessage(5).apply { sendToTarget() } }catch(e:SocketException){ //소켓이 닫혔을 때 mHandler.obtainMessage(7).apply { sendToTarget() } } catch(e:Exception){ if(!closed) { mHandler.obtainMessage(6).apply { sendToTarget() } }else{ closed = false } } } } class CloseServer:Thread(){ override fun run(){ try{ closed = true writeSocket.write(10) //클라이언트에게 서버가 종료되었음을 알림 socket.close() server.close() }catch(e:Exception){ e.printStackTrace() mHandler.obtainMessage(8).apply { sendToTarget() } } } } class SendMessage:Thread(){ private lateinit var msg:String fun setMsg(m:String){ msg = m } override fun run() { try{ writeSocket.writeInt(2) //메시지 전송 명령 전송 writeSocket.writeUTF(msg) //메시지 내용 }catch(e:Exception){ e.printStackTrace() mHandler.obtainMessage(12).apply { sendToTarget() } } } } class ShowInfo:Thread(){ override fun run(){ lateinit var ip:String var breakLoop = false 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){ ip = inetAddress.hostAddress.toString() breakLoop = true break } } if(breakLoop){ break } } val msg = mHandler.obtainMessage() msg.what = 9 msg.obj = ip mHandler.sendMessage(msg) } } } | cs |
위의 코드를 실행화면 다음과 같은 화면이 나타납니다.
※Sever 사용 방법
1. Port에 원하는 포트 번호를 입력합니다.
2. 'SET SERVER' 버튼을 클릭하면 포트를 열 수 있습니다. 만약 다른 프로세스에서 포트를 사용중일 경우 다른 포트 번호를 입력합니다.
3. 'VIEW MY INFO' 버튼을 클릭하면 현재 접속중인 IP주소를 알 수 있습니다. 이를 Client의 IP란에 입력해주세요.
4. Client와의 연결이 성공하면 Toast로 연결에 성공하였다는 알림이 나옵니다.
※Client 사용 방법
1. IP와 PORT에 서버의 IP주소와 설정 PORT 번호를 입력합니다.
2. 'CONNECT' 버튼을 클릭하여 서버에 접속합니다. 성공시 Toast로 접속에 성공하였다는 메시지를 확인하실 수 있습니다.
3. 'SEND MESSAGE' 버튼을 클릭하면 MESSAGE 칸에 입력한 텍스트 정보를 상대에게 전송할 수 있습니다.
4. 'DISCONNECT' 버튼을 눌러 서버와의 연결을 해제합니다.