기타(framework)
2018.06.29 / 21:41

[JAVA / 네트워크] 멀티룸 구조에 대하여(게임&채팅 방 여러개)

aichatbot
추천 수 45

멀티 룸 구조라는 것에 대해 이해는 하고 있지만 구현에 애를 먹었던 기억이 있다.


이는 채팅 이라던지, 게임에 통용되는 개념으로


하나의 방을 가지는 게임과 채팅이라면 상관은 없으나, 여러개의 방을 가져야하는 프로그램에서는 구현을 해주어야 한다.


일단 접속하는 각각의 유저마다 소켓을 가지고 있을 것이다.


서버에서는 여러개의 소켓을 생성하여 클라이언트를 할당하게 되고,


그 소켓을 방(Room) 개념에 맞게 분배해주면 간단하게 구현이 된다.


예를 들어 메인 클래스가 있다고 했을 때 멀티룸 구조를 위해서 두개의 클래스가 더 필요하게 된다.


편의상 간단하게


RoomManager, GameRoom


이라는 이름으로 정한다고 가정하자.


구조는 다음과 같다.


그림이 많이 심플하긴한데.. 전달하고자 하는 의미는 전달되었으리라 믿음.


클래스는 기본적으로 총 3개가 필요하다


게임서버를 기준으로 잡았을 때(채팅서버나 게임서버나 사실 그냥 이름만 다를뿐)


RoomManager

GameRoom

GameUser


이정도가 필요하겠다.


각 클래스의 역할은 다음과 같다.


RoomManager : Room의 생성/삭제를 관리하는 클래스이다. Room을 여러개 가질 수 있음

GameRoom : 게임 내의 로직(게임 진행 관련)을 처리하기 위한 클래스이다.  GameUser를 여러개 가질 수 있음

GameUser : 클라이언트의 고유한 정보(닉네임, 아이템, 플레이어 정보 등)를 가지는 클래스이며, 중요한 것은 "소켓" 을 가지고 있어야 한다.


예를 들어 A, B , C라는 방이 있을 때


각 방에서 일어나는 일은 다른 방에서 알 필요가 없다.


A에서 두 사용자가 채팅을 하고 있을 때 A방에 속한 사용자들 끼리만 데이터를 주고받으면 된다.


이 때 채팅의 단계는 다음과 같이 이루어진다.



(유저A , 유저B, 유저 C)가 룸 A에 속해있다.


유저 A : Hello World 전송하고자 함


유저 A -> 서버로 Hello world 문자열 송신 -> 서버에서는 A가 속한 룸 클래스에 그 정보를 전달하게 됨 -> 전달받은 문자열을 A룸에 속한 사용자들에게 Broadcast(유저들이 가지고 있는 소켓 정보를 참조하면 바로 보낼 수 있다)


이런식으로 구성하게 된다면 각 방에 속한 사용자들 끼리만 데이터를 주고 받을 수 있다.


클래스를 직접 작성해서 올리는 건 다음에 하도록하고..


간단하게 이런식이면 될거같다


RoomManager


List<GameRoom> roomList;


로 게임룸의 리스트를 만들고 관리하며 방을 생성하고 지우는 함수를 구현한다.


GameRoom


List<GameUser> userList;

로 유저 리스트를 관리하고 유저가 나갔을 때 리스트에서 빼고, 접속했을 때 리스트에 넣어주면 된다.


GameUser


이 클래스에는 로그인 정보 및 닉네임 등을 가지고 있으면 된다. 게임이라면 아이템 정보들이, 채팅이라면 부가적인 채팅관련 변수들이 존재


 



그래서 서버에서 유저가 접속해서 방에 들어가는 과정을 간략하게  나타내본다면


--메인클래스--


Socket Accept 이후


Socket 객체를 GameUser 클래스로 생성을 해준다.


GameUser user = new GameUser(socket);


이후 사용자가 원하는 방에 입장하고자 입장처리를 요청하면


user.enterRoom(room); 이런식으로 룸에 입장처리를 한다.


enterRoom의 함수에는


void enterRoom(GameRoom room){

this.gameRoom = room;

}


이런식으로 GameUser가 가지고 있는 GameRoom객체를 복사해준다.


혹은 새로운 방을 생성하고자 하는 요청을 받는다면


--메인클래스--

RoomManager roomManger = new RoomManager(); // 클래스 시작 시 한번만 생성해야 한다.


GameUser user = new GameUser(socket);

GameRoom room = new GameRoom();


user.enterRoom(room);

room.enterUser(user);


roomManager.createRoom(room);


이런식으로 만들면 될 것 같다.


중요한점은 Room <-> User 간 서로 상호참조를 하고 있어야 한다는 점이고,

RoomManager는 Room의 리스트를,

GameRoom은 User의 리스트를,


가지고 있어야 한다는 것이다.


예제코드를 보도록 하자.


이런식으로 간단하게 세가지 케이스를 대상으로 샘플코드를 만들어봤다.


위에 언급한대로, Room과 User간 서로 상호참조를 하고 있으면 쉽게 유저와 룸에 접근할 수가 있다.


샘플 프로젝트의 구성은 네개의 클래스로 구성을 했다.



이런식으로 역시 한 패키지 내에 묶어서 작성을 하면 된다.


매우 기초적인 기능만 담았기 때문에 필요한 기능이 있다면 추가해서 사용하면 될 것 같다


이제 게임룸 구조, 프로토콜 구조에 대한 포스팅을 완료했기 때문에


추후에 작성될 포스팅은 "비동기 JAVA 네트워크" 에 대해 포스팅 하고자 한다.


그리고 최종적으로 앞선 포스팅들을 모두 이용한 간단한 샘플 네트워크 프로그램을 만드는 것이 목적이다.




접기


/*
 * Author : Gompang
 * Desc : 네트워크 게임에서 사용되는(채팅도 포함) 방 개념 클래스
 * Blog : http://gompangs.tistory.com/
 */
package GameRoomPkg;

import java.util.ArrayList;
import java.util.List;

public class GameRoom {

    private int id; // 룸 ID
    private List userList;
    private GameUser roomOwner; // 방장
    private String roomName; // 방 이름

    public GameRoom(int roomId) { // 아무도 없는 방을 생성할 때
        this.id = roomId;
        userList = new ArrayList();
    }

    public GameRoom(GameUser user) { // 유저가 방을 만들때
        userList = new ArrayList();
        user.enterRoom(this);
        userList.add(user); // 유저를 추가시킨 후
        this.roomOwner = user; // 방장을 유저로 만든다.
    }

    public GameRoom(List users) { // 유저 리스트가 방을 생성할
        this.userList = users; // 유저리스트 복사

        // 룸 입장
        for(GameUser user : users){
            user.enterRoom(this);
        }

        this.roomOwner = userList.get(0); // 첫번째 유저를 방장으로 설정
    }

    public void enterUser(GameUser user) {
        user.enterRoom(this);
        userList.add(user);
    }

    public void enterUser(List users) {
        for(GameUser gameUser : users){
            gameUser.enterRoom(this);
        }
        userList.addAll(users);
    }

    /**
     * 해당 유저를 방에서 내보냄
     * @param user 내보낼 유저
     */
    public void exitUser(GameUser user) {
        user.exitRoom(this);
        userList.remove(user); // 해당 유저를 방에서 내보냄

        if (userList.size() < 1) { // 모든 인원이 다 방을 나갔다면
            RoomManager.removeRoom(this); // 이 방을 제거한다.
            return;
        }

        if (userList.size() < 2) { // 방에 남은 인원이 1명 이하라면
            this.roomOwner = userList.get(0); // 리스트의 첫번째 유저가 방장이 된다.
            return;
        }
    }

    /**
     * 해당 룸의 유저를 다 퇴장시키고 삭제함
     */
    public void close() {
        for (GameUser user : userList) {
            user.exitRoom(this);
        }
        this.userList.clear();
        this.userList = null;
    }

    // 게임 로직

    /**
     * 해당 byte 배열을 방의 모든 유저에게 전송
     * @param data 보낼 data
     */
    public void broadcast(byte[] data) {
        for (GameUser user : userList) { // 방에 속한 유저의 수만큼 반복
            // 각 유저에게 데이터를 전송하는 메서드 호출~
            // ex) user.SendData(data);

//			try {
//				user.sock.getOutputStream().write(data); // 이런식으로 바이트배열을 보낸다.
//			} catch (IOException e) {
//				// TODO Auto-generated catch block
//				e.printStackTrace();
//			}
        }
    }

    public void setOwner(GameUser gameUser) {
        this.roomOwner = gameUser; // 특정 사용자를 방장으로 변경한다.
    }

    public void setRoomName(String name) { // 방 이름을 설정
        this.roomName = name;
    }

    public GameUser getUserByNickName(String nickName) { // 닉네임을 통해서 방에 속한 유저를 리턴함

        for (GameUser user : userList) {
            if (user.getNickName().equals(nickName)) {
                return user; // 유저를 찾았다면
            }
        }
        return null; // 찾는 유저가 없다면
    }

    public GameUser getUser(GameUser gameUser) { // GameUser 객체로 get

        int idx = userList.indexOf(gameUser);

        // 유저가 존재한다면(gameUser의 equals로 비교)
        if(idx > 0){
            return userList.get(idx);
        }
        else{
            // 유저가 없다면
            return null;
        }
    }

    public String getRoomName() { // 방 이름을 가져옴
        return roomName;
    }

    public int getUserSize() { // 유저의 수를 리턴
        return userList.size();
    }

    public GameUser getOwner() { // 방장을 리턴
        return roomOwner;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public List getUserList() {
        return userList;
    }

    public void setUserList(List userList) {
        this.userList = userList;
    }

    public GameUser getRoomOwner() {
        return roomOwner;
    }

    public void setRoomOwner(GameUser roomOwner) {
        this.roomOwner = roomOwner;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        GameRoom gameRoom = (GameRoom) o;

        return id == gameRoom.id;
    }

    @Override
    public int hashCode() {
        return id;
    }
}

접기


접기


/*
 * Author : Gompang
 * Desc : 네트워크 게임에서 사용되는(채팅도 포함) 방 개념 클래스
 * Blog : http://gompangs.tistory.com/
 */
package GameRoomPkg;

import java.net.Socket;

// 실제로 게임을 플레이하는 유저의 클래스이다.

public class GameUser {

	private int id; 			// Unique ID
	private GameRoom room; 		// 유저가 속한 룸이다.
	private Socket sock; 		// 소켓 object
	private String nickName;	// 닉네임

	// 게임에 관련된 변수 설정
	// ...
	//

	private PlayerGameInfo.Location playerLocation; // 게임 정보
	private PlayerGameInfo.Status playerStatus; // 게임 정보

	public GameUser() { // 아무런 정보가 없는 깡통 유저를 만들 때
	}

    /**
     * 유저 생성
     * @param nickName 닉네임
     */
    public GameUser(String nickName) { // 닉네임 정보만 가지고 생성
		this.nickName = nickName;
	}

    /**
     * 유저 생성
     * @param id ID
     * @param nickName 닉네임
     */
    public GameUser(int id, String nickName) { // UID, 닉네임 정보를 가지고 생성
		this.id = id;
		this.nickName = nickName;
	}

    /**
     * 방에 입장시킴
     * @param room  입장할 방
     */
    public void enterRoom(GameRoom room) {
		room.enterUser(this); // 룸에 입장시킨 후
		this.room = room; // 유저가 속한 방을 룸으로 변경한다.(중요)
	}

    /**
     * 방에서 퇴장
     * @param room 퇴장할 방
     */
    public void exitRoom(GameRoom room){
        this.room = null;
        // 퇴장처리(화면에 메세지를 준다는 등)
        // ...
    }

	public void setPlayerStatus(PlayerGameInfo.Status status) { // 유저의 상태를 설정
		this.playerStatus = status;
	}

	public void setPlayerLocation(PlayerGameInfo.Location location) { // 유저의 위치를 설정
		this.playerLocation = location;
	}

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public GameRoom getRoom() {
        return room;
    }

    public void setRoom(GameRoom room) {
        this.room = room;
    }

    public Socket getSock() {
        return sock;
    }

    public void setSock(Socket sock) {
        this.sock = sock;
    }

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }

    public PlayerGameInfo.Location getPlayerLocation() {
        return playerLocation;
    }

    public PlayerGameInfo.Status getPlayerStatus() {
        return playerStatus;
    }

    /*
        equals와 hashCode를 override 해줘야, 동일유저를 비교할 수 있다
        비교할 때 -> gameUser 간 equals 비교, list에서 find 등
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        GameUser gameUser = (GameUser) o;

        return id == gameUser.id;
    }

    @Override
    public int hashCode() {
        return id;
    }
}


접기


접기



/*
 * Author : Gompang
 * Desc : 네트워크 게임에서 사용되는(채팅도 포함) 방 개념 클래스
 * Blog : http://gompangs.tistory.com/
 */
package GameRoomPkg;

// 유저의 상태 및 현재 위치하고 있는 장소를 지정하기 위한 Enum Class
public class PlayerGameInfo {

	enum Location {
		MAP_1, MAP_2, MAP_3, MAP_4, MAP_5
	};

	enum Status {
		IDLE, BATTLE, DEAD
	};
}


접기


접기



/*
 * Author : Gompang
 * Desc : 네트워크 게임에서 사용되는(채팅도 포함) 방 개념 클래스
 * Blog : http://gompangs.tistory.com/
 */
package GameRoomPkg;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class RoomManager {

    private static List roomList; // 방의 리스트
    private static AtomicInteger atomicInteger;

    static {
        roomList = new ArrayList();
        atomicInteger = new AtomicInteger();
    }

    public RoomManager() {

    }

    /**
     * 빈 룸을 생성
     * @return GameRoom
     */
    public static GameRoom createRoom() { // 룸을 새로 생성(빈 방)
        int roomId = atomicInteger.incrementAndGet();// room id 채번
        GameRoom room = new GameRoom(roomId);
        roomList.add(room);
        System.out.println("Room Created!");
        return room;
    }

    /**
     * 방을 생성함과 동시에 방장을 만들어줌
     * @param owner 방장
     * @return GameRoom
     */
    public static GameRoom createRoom(GameUser owner) { // 유저가 방을 생성할 때 사용(유저가 방장으로 들어감)
        int roomId = atomicInteger.incrementAndGet();// room id 채번

        GameRoom room = new GameRoom(roomId);
        room.enterUser(owner);
        room.setOwner(owner);

        roomList.add(room);
        System.out.println("Room Created!");
        return room;
    }

    /**
     * 유저 리스트로 방을 생성
     * @param users 입장시킬 유저 리스트
     * @return GameRoom
     */
    public static GameRoom createRoom(List users) {
        int roomId = atomicInteger.incrementAndGet();// room id 채번

        GameRoom room = new GameRoom(roomId);
        room.enterUser(users);

        roomList.add(room);
        System.out.println("Room Created!");
        return room;
    }

    public static GameRoom getRoom(GameRoom gameRoom){

        int idx = roomList.indexOf(gameRoom);

        if(idx > 0){
            return roomList.get(idx);
        }
        else{
            return null;
        }
    }

    /**
     * 전달받은 룸을 제거
     * @param room 제거할 룸
     */
    public static void removeRoom(GameRoom room) {
        room.close();
        roomList.remove(room); // 전달받은 룸을 제거한다.
        System.out.println("Room Deleted!");
    }

    /**
     * 방의 현재 크기를 리턴
     * @return 현재 size
     */
    public static int roomCount() {
        return roomList.size();
    }
}

접기


접기


package game.room.test;


import GameRoomPkg.GameRoom;
import GameRoomPkg.GameUser;
import GameRoomPkg.RoomManager;

public class GameRoomTest {

    public void roomCreateTest(){

        // #1. 유저 로그인
        GameUser gameUser = new GameUser(1, "gompang");
        GameUser gameUser2 = new GameUser(2, "apple");
        GameUser gameUser3 = new GameUser(3, "banana");

        // #2. 특정 유저가 방을 만드려고 함
        GameRoom gameRoom = RoomManager.createRoom(gameUser);

        // #3. 그 방에 나머지 유저가 들어가려고 함
        gameRoom.enterUser(gameUser2);
        gameRoom.enterUser(gameUser3);

        // -- 로직 진행~ 게임, 채팅 등 --

        // #4. 유저가 방에서 나감
        gameRoom.exitUser(gameUser2);

        // #5. 방장이 방에서 나감 -> gameUser3이 방장이 됨
        gameRoom.exitUser(gameUser);

        // #6. gameUser3이 방에서 나감 -> size가 0이 되면서 방이 없어짐
        gameRoom.exitUser(gameUser3);

        // #7. 아래 결과는 null 이겠지
        GameRoom room = RoomManager.getRoom(gameRoom);

        if(room != null){
            System.out.println("방이 아직 있네");
        }
        else{
            System.out.println("방이 없어졌네");
        }
    }

    public void roomTest(){
        // #1. 유저 로그인
        GameUser gameUser = new GameUser(1, "gompang");
        GameUser gameUser2 = new GameUser(2, "apple");
        GameUser gameUser3 = new GameUser(3, "banana");

        // #2. 특정 유저가 방을 만드려고 함
        GameRoom gameRoom = RoomManager.createRoom(gameUser);

        // #3. 그 방에 나머지 유저가 들어가려고 함
        gameRoom.enterUser(gameUser2);
        gameRoom.enterUser(gameUser3);

        // -- 방 모든유저에게 데이터 전송 --
        byte[] data = "방 유저에게 broadcast할 데이터".getBytes();
        gameRoom.broadcast(data);

        // #4. 누군가가 gompang이라는 닉네임으로 방의 유저를 검색함(귓속말, 거래 등의 목적으로)
        GameUser gompang = gameRoom.getUserByNickName("gompang");

        // #5. 서버에서 무언가의 이유로 gameRoom을 삭제함(모든 유저 퇴장처리)
        RoomManager.removeRoom(gameRoom);
    }

}


접기


* p.s

2017.11.30 수정

 - 많은 분들이 test class를 좀 달라고 하셔서.. 간만에 소스코드 좀 보고 파라미터와 클래스 등을 정리해서 수정.

 - test class 추가

 - 세상에나 코드가 아주 쓰레기였습니다. (죄송합니다) 이젠 완벽하진 못해도 기본기능은 하는 코드니 참고하시기 바래요