졸업 작품에서 모바일(안드로이드)과 메인 스테이션(데스크탑 컴퓨터)간의 통신을 구현해야해서 자바로 서버 코드를 작성하게 되었다. 그래서 한번 처음부터 끝까지 개발 일지 형식으로 글로 써서 기록으로 남기고자 한다.
코드 전체 구조는 이런 형식으로 되어 있다. 대체적으로 인프런의 Rookiss 강사님의 서버 강의를 수강하고 나름대로 재해석하여 만들었다. 다만 내가 강의의 모든 부분을 이해한 것이 아니고 언어(C#) 자체도 다르다(JAVA) 보니 완전히 같은 구조로 갈 수가 없었다.
따라서 모든 코드는 자바에 맞게 다시 작성하였고, 서버의 구조자체도 어느정도 변경이 필요하였다.
서버동작이 시작하게 되면 서버는 3개의 Listener 객체를 생성하여 클라이언트의 접속을 기다린다. Listener의 개수를 늘릴 수록 한번에 많은 수의 클라이언트가 접속을 할 수 있다. 다만 졸업 작품에서는 가정에서 사용하는 서버이므로 많은 수의 클라이언트가 동시에 접속할 경우가 거의 없으므로 3개로 고정하였다.
Listener객체가 클라이언트의 접속을 accept하면, Listener는 ThreadManager로 부터 ThreadPool에서 쓰레드를 가져온 후, 접속한 클라이언트에 대한 정보인 ClientSession객체를 새로 생성하여 클라이언트와 연결을 유지할 쓰레드 에게 넘기고 쓰레드를 실행시킨다.
그 후 Listener는 다시 클라이언트의 접속을 기다리는 상태로 돌아가게 되며 통신자체는 ClientConnectionThread라는 별도의 쓰레드가 담당하게 된다.
- ThreadManager.java
package ServerCore;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ThreadManager {
private static final int MaxThread = 10;
static ThreadPoolExecutor _threadPool =
(ThreadPoolExecutor) Executors.newFixedThreadPool(MaxThread);
public void startThread(Thread thread)
{
_threadPool.submit(thread);
}
}
ThreadManager의 경우 위와 같이 되어 있고 현재는 10개의 쓰레드가 포함된 풀을 관리하며, 아직까지는 단순히 할일을 받으면 startThread 메소드를 통해 대기중인 쓰레드에 할 일을 시키는 식으로 구현하였다. 사실 떠오르는 역할이 별로 없어서 이걸 별도의 클래스로 빼줘야 하는 지 고민을 많이 했지만 다른 클래스와 역할이 확실히 달라서 분리해줬다.
이번에 코드를 작성하면서 알게된 사실이지만 자바의 경우 ThreadPoolExecutor라는 클래스에 의해 쓰레드풀을 손쉽게 사용할 수 있었다. 물론 다른 언어도 마찬가지로 쓰레드 풀을 이용하게 해주는 기능들이 있으나 이름이 다르다.
또한 기본적으로 ThreadPoolExcutor의 submit이라는 메소드를 통해 쓰레드에게 할 일을 할당하는데, 통신 종료 후 쓰레드가 해야할 일이 끝나면 자동으로 다시 쓰레드풀로 복귀시킨다는 특징이 있어 별다른 구현을 하지 않고도 쓰레드 풀을 편리하게 사용할 수 있다는 특징이 있었다.
- ClientSession.java
package Server;
import java.io.IOException;
import java.net.Socket;
import Packet.DisconnectPacket;
import Packet.SyncPacket;
import ServerCore.Define;
import ServerCore.Managers;
import ServerCore.Session;
public class ClientSession extends Session{
String _sessionId;
public ClientSession(Socket socket) throws IOException {
_socket = socket;
}
@Override
public void OnConnected(ClientSession session) {
System.out.println("Log - " + "[" + Define.PacketId.Conc.getValue() + "]" +
" - " + session._socket.getInetAddress().getHostAddress() + " connected");
_sessionId = Managers.getSessionManager().addSession(session);
}
@Override
public boolean OnRecvPacket(byte[] data) {
try {
if(data[1] == 0) // sync
{
SyncPacket packet = new SyncPacket(data[2], data[3], data[4]);
packet.printLog();
return true;
}
else if(data[1] == 2) // disc
{
DisconnectPacket packet = new DisconnectPacket(_socket);
packet.printLog();
return false;
}
// else if...
}
catch(Exception e)
{
System.out.println(e.toString());
}
return true;
}
@Override
public void OnSendPacket() {
// TODO Auto-generated method stub
}
@Override
public void OnDisconnected(ClientSession session) {
Managers.getSessionManager().removeSession(_sessionId);
try{
if(_ois != null) { _ois.close();}
if(_socket != null){_socket.close();}
}
catch(IOException e){
System.out.print(e.toString());
}
}
}
통신을 담당하는 ClientSession 객체는 위와 같은 방식으로 작성하였다. 각각 연결되었을 때, 패킷을 받았을 때, 패킷을 전송할 때, 연결이 종료 되었을 때에 수행하는 메서드들이다. 참고로 아직까진 패킷을 보낼 때 처리를 안해두었는데, 이는 졸업 작품에서 사용하는 서버자체는 클라이언트에게 패킷을 보낼일이 없어서 그렇다. 나중에 추가해야할 일이 생기면 구현하는 방식으로 하기위해 빈칸으로 두었다.
OnRecvPacket 같은 경우 byte 배열 형태의 데이터를 받는 것을 알 수 있는데 이는 아직 역직렬화를 거치지 않는 바이트 배열을 받기 때문이다. 사실 이 부분도 패킷이 깨져서 올 경우를 좀더 걸러내야 하는데 아직 테스트 단계이므로 코드를 추가 하진 않았다. 패킷 자체에 첫번째 데이터로 packetSize를 기록하기 때문에 이를 이용해서 할 계획이다.
OnRecvPacket 에서 패킷의 종류별로 다른 처리를 하는 것을 알 수 있는데, 지금은 동기화 패킷과 통신 종료 패킷 두 가지만 존재하므로, if문 형태로 단순하게 구현만 해놓았다. 이 부분은 강의에서도 들었지만, 이런 식으로 if else 문으로 구현하기 보다는 핸들러를 데이터와 함께 넘기는 식으로 구현해서 핸들러를 invoke하는 방식으로 하는 게 좋다고 판단했으나, 아직은 패킷의 종류가 2가지 밖에 되지 않으므로 if else문을 통해 구현하였다.
package ServerCore;
/*
-------------0 Sync-----------------------
0번 - 동기화 패킷(5바이트)
패킷 Len[0] - 1바이트
패킷 ID(종류)[1] - 1바이트
[2] [3] [4] 각각 rssi 값 1바이트 씩 총합 5바이트 패킷
------------------------------------------
-------------2 Disc-----------------------
2번 - disconnect 패킷(2바이트), 1번은 연결완료의미이나 패킷은 필요없음
패킷 Len(0) - 1바이트
패킷 ID(종류)[1] - 1바이트
------------------------------------------
*/
public class Define {
public static enum PacketId {
Sync(0),
Conc(1),
Disc(2),;
private final int value;
PacketId(int value) {
this.value = value;
}
public int getValue() { return value; }
}
public static enum PacketSize {
// TODO
SyncSize(5),
DiscSize(2),;
private final int value;
PacketSize(int value) {
this.value = value;
}
public int getValue() { return value; }
}
}
패킷의 구조는 위의 주석과 같이 정의하였으며, enum 타입으로 패킷의 id와 size를 정의하여 놓았다. 패킷은 2가지만 정의해 놓았고, 플젝이 진행됨에 따라 패킷의 종류는 늘어날 예정이다.
라즈베리파이에서 서버를 띄우기 전, 내 랩탑에서 한번 시험용으로 돌려봤다. 위의 loopback 주소로 찍힌 세개의 클라이언트들은 성능 테스트 용으로 등록한 3개의 dummy client이고, 아래의 사설 주소로 찍힌 로그가 내 핸드폰에서 보낸 메시지 이다.
현재 클라이언트들은 접속 -> 메시지 전송 -> 통신 종료 패킷 전송 -> 통신 종료 의 순서로 진행되게 해 놓았고, 위처럼 접속 후에 테스트 메시지를 보내고 통신을 정상적으로 종료시키는 것까지 확인할 수 있다.
'네트워크 > 예제' 카테고리의 다른 글
[Network] - Java로 구현한 소켓 통신 서버(2) (0) | 2023.02.27 |
---|---|
[예제] - multiplexing 기반 서버 select/epoll (0) | 2022.07.06 |