node js와 안드로이드(kotlin) 연결하는데 약 12시간 정도 삽질하다가 성공하고 나중에 연결할 때 써먹으려고 쓰는 글.
socket.io-client(java)튜토리얼을 보면 아예 아래와 같이 socket.io 서버 라이브러리와 호환 가능한 버전들이 나와 있다.
nodejs 서버: package.json
{
"name": "damda-server",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node app.js"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"dotenv": "10.0.0",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"mysql2": "2.2.5",
"sequelize": "6.6.5",
**"socket.io": "4.1.3"**
}
}
"socket.io": "4.1.3"
으로 설치해 줌
build.gradle(Module: SocketToy.app)
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
**implementation ('io.socket:socket.io-client:2.0.0') {
// excluding org.json which is provided by Android
exclude group: 'org.json', module: 'json'
}**
}
socket.io-client
을 2.0.0
으로 설치해 줌
app.js
// node_modules 에 있는 express 관련 파일을 가져온다.
const express = require("express");
// express 는 함수이므로, 반환값을 변수에 저장한다.
const app = express();
const http = require("http");
const server = http.createServer(app);
const socketIo = require("socket.io");
const io = socketIo(server);
// db 연결
const { sequelize, User } = require("./models");
sequelize
.sync()
.then(() => {
console.log("DB connection success");
})
.catch((err) => {
console.error(err);
});
// http request 에러 방지: Origin [링크] is not allowed by Access-Control-Allow-Origin.
var allowCrossDomain = function (req, res, next) {
// Website you wish to allow to connect
res.setHeader("Access-Control-Allow-Origin", "*");
// Request methods you wish to allow
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, OPTIONS, PUT, PATCH, DELETE"
);
// Request headers you wish to allow
res.setHeader(
"Access-Control-Allow-Headers",
"X-Requested-With,content-type"
);
// Set to true if you need the website to include cookies in the requests sent
// to the API (e.g. in case you use sessions)
res.setHeader("Access-Control-Allow-Credentials", true);
next();
};
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(allowCrossDomain);
// 테스트 코드
app.get("/", (req, res) => {
console.log("hello this is get api");
res.status(200).send({ hi: 1 });
});
// 소켓 일단 간단하게
io.on("connection", (socket) => {
console.log(`Socket connected ${socket.id}`);
socket.on("roomjoin", (userid) => {
console.log(userid);
// socket.join(userid);
});
socket.on("message", (obj) => {
// 클라이언트에서 message라는 이름의 이벤트를 받았을 경우 호출
console.log("server received data");
console.log(obj);
});
socket.on("disconnect", () => {
// 클라이언트의 연결이 끊어졌을 때 호출
console.log(`Socket disconnected : ${socket.id}`);
});
});
// 3000 포트로 서버 오픈
server.listen(3000, function () {
console.log("start! express server on port 3000");
});
<uses-permission android:name="android.permission.INTERNET" />
꼭 해줘야 함.android:usesCleartextTraffic="true"
application 안에 넣어줘야 함.<activity android:name=".MainActivity"> ... </activity>
부분 넣어주기AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.sockettoy">
**<uses-permission android:name="android.permission.INTERNET" />**
<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.SocketToy"
**android:usesCleartextTraffic="true"**
>
**<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>
이 부분 때문에 소켓 연결 실패의 굴레에서 10시간을 굴렀는데, 다음과 같은 과정을 거쳐서 코드를 만져주자.
lateinit
으로 mainactivity
에서 socket
으로 선언해주었다(자바에서는 static
변수로 선언).companion object
로 작성해 봤더니 정말 잘되었다(아래 코드 참고).삽질 내역
참고로, 해당 이슈에서 companion object로 구현하지 않으면 try catch문으로 에러가 안 잡히고, socket.connected() 를 통해 connect 여부를 확인했다(물론 false로 떴다).
이때 특이했던 건, socket.toString()
을 호출했을 때 로그에 socket@abcdef
이렇게 null 값이 아닌 소켓 객체가 적혔다는 것이었다.
그래서 서버가 문제인가 싶어서 wireshark를 깔고 WebSocket으로 필터 설정 후 계속 봤는데 아무리 봐도 소켓 연결 시도 자체가 없었다(정적 그자체...).
아래 사진이 wireshark 사진. (react로 socket 커넥트 시도하면 잘 되는 걸 볼 수 있었다.) 그런데 이제 companion object를 설정해주니까 잘 되더라.. 너무 잘 되어서 내 눈을 의심하다가 한번 더 확인하고 엄마한테 쿵쾅쿵쾅 달려가서 자랑했다. 후후.
앞서 말했듯이 companion object로 안했던 게 문제가 아니고, 두번째 인자로 잘못된 options를 넣어준게 문제였던 것 같다. 다음에 options 커스텀할 때 이건 시도해 보는 걸로...
[uri]부분
은 "http://X.X.X.X:3000"
꼴로 넣어주는 게 좋다.SocketApplication.kt
package com.example.sockettoy
import io.socket.client.IO
import io.socket.client.Socket
import java.net.URISyntaxException
class SocketApplication {
companion object {
private lateinit var socket : Socket
fun get(): Socket {
try {
// [uri]부분은 "http://X.X.X.X:3000" 꼴로 넣어주는 게 좋다.
socket = IO.socket("[uri]")
} catch (e: URISyntaxException) {
e.printStackTrace()
}
return socket
}
}
}
MainActivity.kt
package com.example.sockettoy
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import io.socket.client.Socket
import io.socket.emitter.Emitter
import org.json.JSONObject
class MainActivity : AppCompatActivity() {
lateinit var mSocket: Socket
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mSocket = SocketApplication.get()
mSocket.connect()
val edittext: EditText = findViewById<EditText>(R.id.edittext)
val sendbutton: Button = findViewById(R.id.sendbutton)
sendbutton.setOnClickListener{
mSocket.emit("message", "hello")
Log.d("send socket11", edittext.text.toString())
}
mSocket.on("get message", onMessage)
// mSocket.connect()
}
var onMessage = Emitter.Listener { args ->
val sendtext: TextView = findViewById(R.id.sendtext) as TextView
val obj = JSONObject(args[0].toString())
val a = sendtext.text.toString()
Log.d("main activity", obj.toString())
Thread(object : Runnable{
override fun run() {
runOnUiThread(Runnable {
kotlin.run {
sendtext.text = a + "\n" + obj.get("name") + ": " + obj.get("message")
}
})
}
}).start()
}
}
activity-main.xml
<?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"
>
<TextView
android:id="@+id/sendtext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toTopOf="@+id/edittext"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/sendbutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="107dp"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edittext" />
<EditText
android:id="@+id/edittext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:text="TextView"
app:layout_constraintBottom_toTopOf="@+id/sendbutton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sendtext" />
</androidx.constraintlayout.widget.ConstraintLayout>
GitHub - jinusong/Android-Socket: Android Socket 통신을 공부합니다.
Android Node.js Socket.io not connecting
socket.io xhr post error on slow connection (3G mobile network)
좋은 정보 감사합니다.
잘 쓰겠습니다.