저번의 간단한 초기 세팅에 이어 본격적으로 채팅을 구현해볼 생각이다.
socket.io는 이벤트 발생 시 행동을 정의하는 on과 이벤트를 발생시키는 emit을 통해 서버와 클라이언트가 통신하게 만들 수 있다.
io.sockets.on('connection', (socket) => {
console.log(`Socket connected : ${socket.id}`)
socket.on('발생할 이벤트', (data) => {
socket.emit('클라이언트 측에 발생시킬 이벤트', '보낼 데이터')
})
}
mSocket.on("발생할 이벤트", args -> mSocket.emit("서버 측에 발생시킬 이벤트", "보낼 데이터");
예를 들어 이런 식으로 말이다.
이를 이용해서 채팅방 입장 / 퇴장 기능을 구현해보자. 먼저 채팅방을 입장 / 퇴장할 때에는 유저의 이름과 방의 정보를 담아 서버에 보내야 한다.
public class RoomData {
private String username;
private String roomNumber;
public RoomData(String username, String roomNumber) {
this.username = username;
this.roomNumber = roomNumber;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getRoomNumber() {
return roomNumber;
}
public void setRoomNumber(String roomNumber) {
this.roomNumber = roomNumber;
}
}
따라서 다음과 같이 서버에 보내줄 RoomData 클래스를 정의해주었다.
mSocket.on(Socket.EVENT_CONNECT, args -> {
mSocket.emit("enter", gson.toJson(new RoomData(username, roomNumber)));
});
소켓이 연결될 때, RoomData를 서버의 enter 이벤트로 전송해준다.
socket.on('enter', (data) => {
const roomData = JSON.parse(data)
const username = roomData.username
const roomNumber = roomData.roomNumber
socket.join(`${roomNumber}`)
console.log(`[Username : ${username}] entered [room number : ${roomNumber}]`)
})
서버에서는 이를 받아 방에 들어가는 join 메소드를 호출해주면 된다.
mSocket.emit("left", gson.toJson(new RoomData(username, roomNumber)));
mSocket.disconnect();
socket.on('left', (data) => {
const roomData = JSON.parse(data)
const username = roomData.username
const roomNumber = roomData.roomNumber
socket.leave(`${roomNumber}`)
console.log(`[Username : ${username}] left [room number : ${roomNumber}]`)
}
퇴장할 때도 같은 맥락으로 하되, join 대신 leave 메소드를 호출해주면 된다.
이제 이 어플에 가장 핵심적인 기능이 되는 채팅 기능의 구현이다. 채팅을 보낼 때 메세지의 정보 또한 서버로 보내야하므로 MessageData 클래스를 정의해주었다.
public class MessageData {
private String type;
private String from;
private String to;
private String content;
private long sendTime;
public MessageData(String type, String from, String to, String content, long sendTime) {
this.type = type;
this.from = from;
this.to = to;
this.content = content;
this.sendTime = sendTime;
}
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getFrom() { return from; }
public void setFrom(String from) { this.from = from; }
public String getTo() { return to; }
public void setTo(String to) { this.to = to; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public long getSendTime() { return sendTime; }
public void setSendTime(long sendTime) { this.sendTime = sendTime; }
}
여기서 서버에 보내기로 한 정보는 다음과 같다.
type : 메세지의 타입 (ENTER, LEFT, MESSAGE)
from : 메세지를 보낸 유저의 이름 (username)
to : 메세지를 보낼 방 (roomNumber)
content : 메세지의 내용
sendTime : 메세지를 보낸 시간
sendTime의 경우 System.currentTimeMillis()로 현재 시간을 long 타입을 보내고, 안드로이드 측에서 화면에 보낸 시각을 띄울 때, 따로 처리를 해줄 것이다.
private void sendMessage() {
mSocket.emit("newMessage", gson.toJson(new MessageData("MESSAGE",
username,
roomNumber,
binding.contentEdit.getText().toString(),
System.currentTimeMillis())));
binding.contentEdit.setText("");
}
안드로이드 측에서는 해당 데이터를 서버 측의 newMessage 이벤트에 전송할 것이며
socket.on('newMessage', (data) => {
const messageData = JSON.parse(data)
console.log(`[Room Number ${messageData.to}] ${messageData.from} : ${messageData.content}`)
io.to(`${messageData.to}`).emit('update', JSON.stringify(messageData))
})
서버에서는 to 메소드를 통해, 해당 방에 있는 사람들에게 update 이벤트에 이 데이터를 전송한다.
mSocket.on("update", args -> {
MessageData data = gson.fromJson(args[0].toString(), MessageData.class);
addChat(data);
});
private void addChat(MessageData data) {
runOnUiThread(() -> {
if (data.getType().equals("ENTER") || data.getType().equals("LEFT")) {
adapter.addItem(new ChatItem(data.getFrom(), data.getContent(), toDate(data.getSendTime()), ChatType.CENTER_CONTENT));
binding.recyclerView.scrollToPosition(adapter.getItemCount() - 1);
} else {
if (username.equals(data.getFrom())) {
adapter.addItem(new ChatItem(data.getFrom(), data.getContent(), toDate(data.getSendTime()), ChatType.RIGHT_CONTENT));
binding.recyclerView.scrollToPosition(adapter.getItemCount() - 1);
} else {
adapter.addItem(new ChatItem(data.getFrom(), data.getContent(), toDate(data.getSendTime()), ChatType.LEFT_CONTENT));
binding.recyclerView.scrollToPosition(adapter.getItemCount() - 1);
}
}
});
}
// System.currentTimeMillis를 몇시:몇분 am/pm 형태의 문자열로 반환
private String toDate(long currentMiliis) {
return new SimpleDateFormat("hh:mm a").format(new Date(currentMiliis));
}
이제 이를 리사이클러뷰에 채팅 메세지를 표시해주면 된다.
socket.on('enter', (data) => {
const roomData = JSON.parse(data)
const username = roomData.username
const roomNumber = roomData.roomNumber
socket.join(`${roomNumber}`)
console.log(`[Username : ${username}] entered [room number : ${roomNumber}]`)
const enterData = {
type : "ENTER",
content : `${username} entered the room`
}
socket.broadcast.to(`${roomNumber}`).emit('update', JSON.stringify(enterData))
})
socket.on('left', (data) => {
const roomData = JSON.parse(data)
const username = roomData.username
const roomNumber = roomData.roomNumber
socket.leave(`${roomNumber}`)
console.log(`[Username : ${username}] left [room number : ${roomNumber}]`)
const leftData = {
type : "LEFT",
content : `${username} left the room`
}
socket.broadcast.to(`${roomNumber}`).emit('update', JSON.stringify(leftData))
})
마찬가지로 안드로이드 측의 update 이벤트에 입장 / 퇴장했을 때의 데이터를 전송해서 메시지를 띄울 수 있다. 또한, 본인에게는 입장 / 퇴장 메시지를 띄울 것이기 때문에 방에 있는 모든 사람에게 전송하는 to 메소드가 아닌 broadcast.to 메소드를 사용하였다.
자세한 코드는 깃허브에 올려놓았다. 추가적으로 이미지 전송 기능도 구현해볼 생각이다.
npm, "socket.io", https://www.npmjs.com/package/socket.io
Joyce Hong, "Building an Android Chat App with socket.io— All source code provided!", https://medium.com/@joycehong0524/simple-android-chatting-app-using-socket-io-all-source-code-provided-7b06bc7b5aff