[client] <-------------------> [server]
|---- 접속 ------------------->>|
|<<------ 환영 메시지 ------------|
|---- 사용자 입력 전송 ---------->>|
|<<------ 응답 메시지 ------------|
| 요청/응답 반복 |
|---- "quit" ---------------->>|
|<<------ 연결 끊기 ------------|
요청 메시지: // client -> server
한 덩어리의 문자열
응답 메시지: // server -> client
한 덩어리의 문자열
ServerApp class
public class ServerApp {
//메인 메뉴 목록 준비
static String[] menus = {"게시판","회원"};
public static void main(String[] args) {
try(ServerSocket serverSocket = new ServerSocket(8888)){
System.out.println("서버 실행중 ...");
// 핸들러를 담을 컬렉션을 준비한다.
ArrayList<Handler> handlers = new ArrayList<>();
handlers.add(new BoardHandler(null));
handlers.add(new MemberHandler(null));
while(true) {
Socket socket = serverSocket.accept();
new Thread(() -> { // Runnable interface 익명 클래스
// 스레드를 시작하는 순간 별도의 실행 흐름에서 병행으로 실행된다.
try(
DataOutputStream out = new DataOutputStream(socket.getOutputStream()); // 실제 client에게 출력하기 위해서는 이 out을 이용해야한다.
DataInputStream in = new DataInputStream(socket.getInputStream())
)
{
System.out.println("클라이언트 접속!");
// client가 접속한 순간, 접속한 클라이언트의 이동 경로를 보관할 breadcrumb 객체 준비 -> 스레드마다 각각 생성해야한다.
BreadCrumb breadcrumb =new BreadCrumb(); // 현재 스레드 보관소에 저장된다.
breadcrumb.put("메인");
boolean first = true;
String errorMessage = null;
while(true) {
// 접속 후 환영 메시지와 메인 메뉴를 출력한다.
try(
StringWriter strOut = new StringWriter(); // 여기에 buffer로 다 쌓여있다가 밑에 toString()으로 다 쏟아낸다.
PrintWriter tempOut = new PrintWriter(strOut)
) // try()
{
if(first) { // 최초 접속이면 환영 메시지도 출력한다.
welcome(tempOut);
first = false;
}
if(errorMessage != null) {
tempOut.println(errorMessage);
errorMessage = null;
}
// breadcrumb 메뉴출력
tempOut.println(breadcrumb.toString());
// main menu 출력 (게시판, 회원)
printMainMenus(tempOut);
out.writeUTF(strOut.toString()); // client로 전송
} //try(){}
// 클라이언트가 보낸 요청을 읽는다.
String request = in.readUTF();
if(request.equals("quit")) break;
try {
int mainMenuNo = Integer.parseInt(request);
if (mainMenuNo >= 1 && mainMenuNo <= menus.length) { // 메뉴 번호가 유효한 경우
// 핸들러에 들어가기 전에 breadcrumb 메뉴에 하위 메뉴 이름을 추가한다.
breadcrumb.put(menus[mainMenuNo-1]);
// 메뉴 번호로 Handler 객체를 찾아 실행한다.
handlers.get(mainMenuNo-1).execute(in, out); // client로 전송할 정보들을 tempOut으로 담으라고 파라미터로 전달해준다.
// 다시 메인 메뉴로 돌아 왔다면, breadcrumb 메뉴에서 한 단계 위로 올라간다.
breadcrumb.pickUp();
}else {
throw new Exception("해당 번호의 메뉴가 없습니다!");
}
}catch(Exception e) {
errorMessage = String.format("실행 오류: %s\n", e.getMessage());
} // try{} - catch{}
} // while()
System.out.println("클라이언트와 접속 종료!");
} catch (Exception e) {
System.out.println("클라이언트와 통신하는 중 오류 발생!");
e.printStackTrace();
} // Socket.accept() try(){}
}).start(); // Thread()
} // while()
// System.out.println("서버 종료!");
}catch (Exception e) {
System.out.println("서버 실행 중 오류 발생!");
e.printStackTrace();
} // ServerSocket try(){}
} // main()
static void welcome(PrintWriter out) throws Exception {
out.println("[게시판 애플리케이션]");
out.println();
out.println( "환영합니다!");
out.println();
}
static void printMainMenus(PrintWriter out) {
// 메뉴 목록 출력
for (int i = 0; i < menus.length; i++) {
out.printf(" %d: %s\n", i + 1, menus[i]);
}
// 메뉴 번호 입력을 요구하는 문장 출력
out.printf("메뉴를 선택하세요[1..%d](quit: 종료) ", menus.length);
}
}
Handler interface
// 사용자 요청을 다룰 객체의 사용법을 정의한다.
//
public interface Handler {
// Handler가 클라이언트와 통신할 수 있도록 입출력 스트림을 파라미터로 전달한다.
void execute(DataInputStream in, DataOutputStream out) throws Exception;
}
execute() method in AbstractHandler class
public void execute(DataInputStream in, DataOutputStream out) throws Exception {
// 현재 스레드를 위해 보관된 BreadCrumb 객체를 꺼낸다.
BreadCrumb breadCrumb = BreadCrumb.getBreadCrumbOfCurrentThread();
// 핸들러의 메뉴를 클라이언트에게 보낸다.
try(StringWriter strOut = new StringWriter();
PrintWriter tempOut = new PrintWriter(strOut);
){
tempOut.println(breadCrumb.toString());
printMenus(tempOut);
out.writeUTF(strOut.toString());
}
while (true) {
// 클라이언트가 보낸 요청을 읽는다.
String request = in.readUTF();
if(request.equals("0")) break;
// 클라이언트에게 출력
try(StringWriter strOut = new StringWriter();
PrintWriter tempOut = new PrintWriter(strOut);
){
tempOut.println("해당 메뉴를 준비 중 입니다.");
printBlankLine(tempOut);
tempOut.println(breadCrumb.toString());
printMenus(tempOut);
out.writeUTF(strOut.toString());
}
BreadCrumb class
public class BreadCrumb {
public Stack<String> menuStack = new Stack<>(); // client마다 현재 있는 목록 위치가 다르므로 각각 관리해주어야 하므로 static이 아니라 instance로 생성해준다.
// Thread마다 BreadCrumb 객체를 따로 관리해주는 관리자를 준비한다.
// -> 현재 스레드의 이름으로 저장하고 꺼낸다. -> 스레드를 구분해서 관리하고 싶을 때 사용한다.
static ThreadLocal<BreadCrumb> localManager = new ThreadLocal<>();
public static BreadCrumb getBreadCrumbOfCurrentThread() {
// 스레드 로컬 관리자를 통해 현재 스레드 보관소에 저장되어 있는
// BreadCrumb 객체를 달라고 요청한다.
return localManager.get(); // 현재 스레드의 이름으로 꺼내기
}
public BreadCrumb() {
// 스레드 로컬 관리자에게 현재 스레드 전용 보관소에
// BreadCrumb 객체를 보관해 달라고 요청한다.
localManager.set(this); // 현재 스레드의 이름으로 저장
}
public void put(String menu) {
menuStack.push(menu);
}
public void pickUp() {
menuStack.pop();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
for(String title: menuStack) {
if(!builder.isEmpty()) {
builder.append(" > ");
}
builder.append(title);
}
return builder.toString();
}// toString()
}
ClientApp class
public class ClientApp {
public static void main(String[] args) {
System.out.println("[게시글 관리 클라이언트]");
try(
Socket socket = new Socket("localhost", 8888);
DataInputStream in = new DataInputStream(socket.getInputStream()); // Decorator pattern을 사용하면 기능 변경이 쉽다.
DataOutputStream out = new DataOutputStream(socket.getOutputStream())
){
String response = null;
while(true) {
response = in.readUTF();
System.out.println(response);
// 사용자의 입력값 서버에 전송
String input = Prompt.inputString("> ");
out.writeUTF(input);
if(input.equals("quit")) break;
}//while()
} /*try(){}*/ catch(Exception e){
System.out.println("서버와 통신 중 오류 발생");
}
}
우왕 동글이다