* try-catch 문법
try-catch 안에
try-catch가 존재하더라도 당황할 것 없다 !
① 첫번째 try 문법에서 오류가 발생하는 것은 socket 오류이기때문에
서버통신오류이고,
② 두번째 try 문법에서 오류가 발생하는 것은 연산에서 발생한 오류이기때문에
계산식이 옳지 않다는 오류를 도출한다.
어느 과정에서 오류가 발생했는지 알려주기 위해서
Exception을 상속받은 ExpressionParseExcepiton을 사용한다.
* stateful vs stateless
* stateful
클라이언트-서버 관계에서 서버가 클라이언트의 상태에 대해 추적하고 저장함.
* stateful - 서버 및 클라이언트 실행 순서
[서버 실행 순서]
서버 소켓 생성 및 대기:
서버는 ServerSocket을 사용하여 특정 포트
(예: 8888)에서 클라이언트의 연결을 대기합니다.
클라이언트 연결 수락:
ServerSocket의 accept() 메서드를 호출하여 클라이언트의 연결을 기다립니다.
클라이언트가 연결되면 Socket 객체를 생성하여 클라이언트와 통신할 수 있는 소켓을 얻습니다.
클라이언트 요청 처리:
서버는 processRequest() 메서드를 호출하여 클라이언트의 요청을 처리합니다.
연산자와 값을 전달받아 계산을 수행하고, 계산 결과를 클라이언트에게 응답합니다.
클라이언트와의 상호작용이 끝날 때까지 이 과정을 반복합니다.
[클라이언트 실행 순서]
소켓 생성 및 서버 연결:
클라이언트는 Socket을 사용하여 서버에 연결합니다.
서버의 IP 주소와 포트 번호를 지정하여 Socket 객체를 생성합니다.
서버에 요청 전송:
사용자로부터 연산자와 값을 입력받습니다.
DataOutputStream을 사용하여 연산자와 값을 서버로 전송합니다.
서버의 응답 수신 및 출력:
Scanner를 사용하여 서버로부터 응답을 읽어옵니다.
서버에서 보낸 계산 결과나 오류 메시지를 출력합니다.
응답이 "Goodbye!"인 경우 클라이언트 종료로 인식하여 반복문을 종료합니다.
자원 해제:
클라이언트 실행이 종료되면 입력 스트림, 출력 스트림, 소켓을 닫아 자원을 해제합니다.
* stateful - CalcServer
// stateful 방식의 이점 활용 - 계산기 서버 만들기
package com.eomcs.net.ex04.stateful2;
import java.io.DataInputStream;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
public class CalcServer {
public static void main(String[] args) throws Exception {
System.out.println("서버 실행 중...");
ServerSocket ss = new ServerSocket(8888);
while (true) {
// stateful을 사용할 때 이점:
// => 연결되어 있는 동안 클라이언트의 작업 결과를 계속 유지할 수 있다.
try (Socket socket = ss.accept()) {
processRequest(socket);
} catch (Exception e) {
System.out.println("클라이언트 요청 처리 중 오류 발생!");
System.out.println("다음 클라이언트의 요청을 처리합니다.");
}
}
// ss.close();
}
static void processRequest(Socket socket) throws Exception {
try (Socket socket2 = socket;
DataInputStream in = new DataInputStream(socket.getInputStream());
PrintStream out = new PrintStream(socket.getOutputStream());) {
// 작업 결과를 유지할 변수
int result = 0;
loop: while (true) {
String op = in.readUTF();
int a = in.readInt();
switch (op) {
case "+":
result += a;
break;
case "-":
result -= a;
break;
case "*":
result *= a;
break;
case "/":
result /= a;
break;
case "quit":
break loop;
default:
out.println("해당 연산을 지원하지 않습니다.");
continue;
}
out.printf("계산 결과: %d\n", result);
}
out.println("Goodbye!");
}
}
}
* stateful - CalcClient
// stateful 방식의 이점 활용 - 계산기 클라이언트 만들기
package com.eomcs.net.ex04.stateful2;
import java.io.DataOutputStream;
import java.net.Socket;
import java.util.Scanner;
public class CalcClient {
public static void main(String[] args) throws Exception {
Scanner keyScan = new Scanner(System.in);
Socket socket = new Socket("localhost", 8888);
Scanner in = new Scanner(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
while (true) {
System.out.print("연산자? ");
out.writeUTF(keyScan.nextLine());
System.out.print("값? ");
out.writeInt(Integer.parseInt(keyScan.nextLine()));
String str = in.nextLine();
System.out.println(str);
if (str.equals("Goodbye!"))
break;
}
in.close();
out.close();
socket.close();
keyScan.close();
}
}
실행예시)
Server
Client
* stateless
클라이언트-서버 관계에서 서버가 클라이언트의 상태에 대해 추적하거나 보존하지 않음.
* stateless - 서버 및 클라이언트 실행 순서
[서버 실행 순서]
서버 소켓 생성 및 대기:
서버는 ServerSocket을 사용하여 특정 포트 (예: 8888)에서 클라이언트의 연결을 대기합니다.
클라이언트 연결 수락:
ServerSocket의 accept() 메서드를 호출하여 클라이언트의 연결을 기다립니다.
클라이언트가 연결되면 Socket 객체를 생성하여 클라이언트와 통신할 수 있는 소켓을 얻습니다.
클라이언트 요청 처리:
서버는 processRequest() 메서드를 호출하여 클라이언트의 요청을 처리합니다.
클라이언트로부터 요청 데이터를 읽어와서 처리하고, 응답을 생성합니다.
응답을 클라이언트에게 전송합니다.
클라이언트와의 상호작용이 끝나면 연결을 종료합니다.
연결 종료:
클라이언트와의 연결이 종료되면 해당 클라이언트에 대한 리소스를 해제합니다.
서버는 다음 클라이언트의 연결을 기다리기 위해 다시 클라이언트 연결 수락 단계로 돌아갑니다.
[클라이언트 실행 순서]
소켓 생성 및 서버 연결:
클라이언트는 Socket을 사용하여 서버에 연결합니다.
서버의 IP 주소와 포트 번호를 지정하여 Socket 객체를 생성합니다.
서버에 요청 전송:
클라이언트는 사용자로부터 요청 데이터를 입력받습니다.
요청 데이터를 서버로 전송합니다.
서버의 응답 수신 및 출력:
클라이언트는 서버로부터 응답을 받아옵니다.
받아온 응답을 출력합니다.
상호작용 종료:
클라이언트는 필요한 경우 추가 요청을 생성하고,
서버로 전송하여 응답을 받아올 수 있습니다.
클라이언트가 상호작용을 종료하려면 특정 조건을 만족하는 경우 반복문을 종료합니다.
연결 종료:
클라이언트는 서버와의 연결을 종료합니다.
클라이언트 실행이 완료되면 자원을 해제합니다.
* stateless - CalcServer
// stateless 방식에서 클라이언트를 구분하고 작업 결과를 유지하는 방법
package com.eomcs.net.ex04.stateless2;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class CalcServer {
// 각 클라이언트의 작업 결과를 보관할 맵 객체
// => Map<clientID, result>
static Map<Long, Integer> resultMap = new HashMap<>();
public static void main(String[] args) throws Exception {
System.out.println("서버 실행 중...");
ServerSocket ss = new ServerSocket(8888);
while (true) {
Socket socket = ss.accept();
System.out.println("클라이언트 요청 처리!");
try {
processRequest(socket);
} catch (Exception e) {
System.out.println("클라이언트 요청 처리 중 오류 발생!");
System.out.println("다음 클라이언트의 요청을 처리합니다.");
}
}
// ss.close();
}
static void processRequest(Socket socket) throws Exception {
try (Socket socket2 = socket;
DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());) {
// 클라이언트를 구분하기 위한 아이디
// => 0: 아직 클라이언트 아이디가 없다는 의미
// => x: 서버가 클라이언트에게 아이디를 부여했다는 의미
long clientId = in.readLong();
// 연산자와 값을 입력 받는다.
String op = in.readUTF();
int value = in.readInt();
// 클라이언트를 위한 기존 값 꺼내기
Integer obj = resultMap.get(clientId);
int result = 0;
if (obj != null) {
System.out.printf("%d 기존 고객 요청 처리!\n", clientId);
result = obj; // auto-unboxing
} else {
// 맵에 해당 클라이언트 ID로 저장된 값이 없다는 것은
// 한 번도 서버에 접속한 적이 없다는 의미다.
// 따라서 새 클라이언트 아이디를 발급한다.
// => 예제를 간단히 하기 위해 현재 실행 시점의 밀리초를 사용한다.
clientId = System.currentTimeMillis();
System.out.printf("%d 신규 고객 요청 처리!\n", clientId);
}
String message = null;
switch (op) {
case "+":
result += value;
break;
case "-":
result -= value;
break;
case "*":
result *= value;
break;
case "/":
Thread.sleep(30000);
result /= value;
break;
default:
message = "해당 연산을 지원하지 않습니다.";
}
// 계산 결과를 resultMap에 보관한다.
resultMap.put(clientId, result);
// 클라이언트로 응답할 때 항상 클라이언트 아이디와 결과를 출력한다.
// => 클라이언트 아이디 출력
out.writeLong(clientId);
// => 계산 결과 출력
if (message == null) {
message = String.format("계산 결과: %d", result);
}
out.writeUTF(message);
out.flush();
}
}
}
* stateless - CalcClient
// stateless 방식에서 클라이언트를 구분하고 작업 결과를 유지하는 방법
package com.eomcs.net.ex04.stateless2;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.util.Scanner;
public class CalcClient {
public static void main(String[] args) throws Exception {
Scanner keyScan = new Scanner(System.in);
// 서버에서 이 클라이언트를 구분할 때 사용하는 번호이다.
// => 0 번으로 서버에 요청하면, 서버가 신규 번호를 발급해 줄 것이다.
long clientId = 0;
while (true) {
System.out.print("연산자? ");
String op = keyScan.nextLine();
System.out.print("값? ");
int value = Integer.parseInt(keyScan.nextLine());
try (Socket socket = new Socket("localhost", 8888);
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
DataInputStream in = new DataInputStream(socket.getInputStream())) {
// => 서버에 클라이언트 아이디를 보낸다.
out.writeLong(clientId);
// => 서버에 연산자와 값을 보낸다.
out.writeUTF(op);
out.writeInt(value);
out.flush();
// => 서버에서 보낸 클라이언트 아이디를 읽는다.
clientId = in.readLong();
// => 서버에서 보낸 결과를 읽는다.
System.out.println(in.readUTF());
} catch (Exception e) {
System.out.println("서버와 통신 중 오류 발생!");
}
System.out.print("계속하시겠습니까?(Y/n)");
if (keyScan.nextLine().equalsIgnoreCase("n")) {
break;
}
}
keyScan.close();
}
}
실행예시)
sever
client
* stateful + Thread
stateful + Thread 방식의 장점 ?
클라이언트와 통신하는 부분을 별도의 스레드(실행흐름)로 분리하여
독립적으로 실행하게 한다.
스레드란 ?
스레드는 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다.
일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만,
프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다.
이러한 실행 방식을 멀티스레드라고 한다.
정리하자면, stateful 의 장점은 서버가 클라이언트의 상태에 대해 추적하고 저장하며
스레드를 통해서 다른 Client가 Server에 접속하여 프로그램을 사용해도기존의 사용자는 프로그램을 계속 유지하며 사용할 수 있게 만드는 것이다.
* stateful + Thread - 서버 및 클라이언트 실행 순서
[서버 실행 순서]
서버 소켓 생성 및 대기:
서버는 ServerSocket을 사용하여 특정 포트(예: 8888)에서 클라이언트의 연결을 대기합니다.
ServerSocket ss = new ServerSocket(8888);
클라이언트 연결 수락:
서버는 accept() 메서드를 호출하여 클라이언트의 연결을 기다립니다.
클라이언트가 연결되면 Socket 객체를 생성하여 클라이언트와 통신할 수 있는 소켓을 얻습니다.
Socket socket = ss.accept();
클라이언트 요청 처리:
서버는 processRequest() 메서드를 호출하여 클라이언트의 요청을 처리합니다.
클라이언트와의 통신은 별도의 스레드로 분리되어 독립적으로 실행됩니다.
스레드는 클라이언트와의 상호작용을 담당하며, 작업 결과를 유지합니다.
processRequest(socket);
클라이언트와의 통신 종료:
클라이언트와의 통신이 끝나면 해당 클라이언트의 소켓 및 연관된 자원을 정리하고 닫습니다.
socket.close();
다음 클라이언트 연결 대기:
서버는 다음 클라이언트의 연결을 기다리기 위해 다시 클라이언트 연결 수락 단계로 돌아갑니다.
while (true) { ... }
[클라이언트 실행 순서]
소켓 생성 및 서버 연결:
클라이언트는 Socket을 사용하여 서버에 연결합니다.
서버의 IP 주소와 포트 번호를 지정하여 Socket 객체를 생성합니다.
Socket socket = new Socket("localhost", 8888);
서버에 요청 전송:
사용자로부터 연산자와 값을 입력받습니다.
DataOutputStream을 사용하여 연산자와 값을 서버로 전송합니다.
out.writeUTF(operator);
out.writeInt(value);
서버의 응답 수신 및 출력:
서버로부터 응답을 받기 위해 DataInputStream을 사용하여 읽어옵니다.
서버에서 보낸 계산 결과나 오류 메시지를 출력합니다.
String response = in.readUTF();
System.out.println(response);
서버와의 통신 종료:
서버의 응답이 "Goodbye!"인 경우, 클라이언트는 통신을 종료합니다.
클라이언트의 소켓 및 연관된 자원을 정리하고 닫습니다.
in.close();
out.close();
socket.close();
프로그램 종료:
클라이언트의 실행이 끝나면 프로그램을 종료합니다.
System.exit(0);
위와 같이 stateful + Thread 방식에서는 서버는 클라이언트와의 통신을 별도의 스레드로 분리하여 실행하고, 각 클라이언트는 서버와의 통신을 담당하는 스레드와 독립적으로 작동합니다. 이를 통해 서버는 동시에 여러 클라이언트와 상호작용하고, 클라이언트는 서버의 상태 추적 및 작업 결과를 유지할 수 있습니다.
* stateless + Thread - CalcServer
// stateful 방식 - 다중 클라이언트의 요청 처리 시 문제점과 해결책
package com.eomcs.net.ex04.stateful3;
import java.io.DataInputStream;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class CalcServer {
// 클라이언트와 통신하는 부분을 별도의 스레드(실행흐름)로 분리하여
// 독립적으로 실행하게 한다.
static class RequestHandler extends Thread {
Socket socket;
public RequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
// main() 메서드 호출과 분리하여 별도로 실행할 코드가 있다면
// 이 메서드에 둔다.
try (Socket socket2 = socket;
DataInputStream in = new DataInputStream(socket.getInputStream());
PrintStream out = new PrintStream(socket.getOutputStream());) {
// 작업 결과를 유지할 변수
int result = 0;
loop: while (true) {
String op = in.readUTF();
int a = in.readInt();
switch (op) {
case "+":
result += a;
break;
case "-":
result -= a;
break;
case "*":
result *= a;
break;
case "/":
result /= a;
break;
case "quit":
break loop;
default:
out.println("해당 연산을 지원하지 않습니다.");
continue;
}
out.printf("계산 결과: %d\n", result);
}
out.println("quit");
} catch (Exception e) {
System.out.println("클라이언트 요청 처리 중 오류 발생!");
} finally {
System.out.println("클라이언트 연결 종료!");
}
}
}
public static void main(String[] args) throws Exception {
System.out.println("서버 실행 중...");
ServerSocket ss = new ServerSocket(8888);
while (true) {
System.out.println("클라이언트의 연결을 기다림!");
Socket socket = ss.accept();
InetSocketAddress remoteAddr = (InetSocketAddress) socket.getRemoteSocketAddress();
System.out.printf("클라이언트(%s:%d)가 연결되었음!\n", //
remoteAddr.getAddress(), remoteAddr.getPort());
// 연결된 클라이언트가 연결을 끊기 전까지는
// 대기하고 있는 다른 클라이언트의 요청을 처리할 수 없다.
// 이것이 스레드를 사용하기 전의 문제점이다.
// 해결책?
// 클라이언트와 대화하는 부분을 스레드로 분리하여 실행하라!
RequestHandler requestHandler = new RequestHandler(socket);
requestHandler.start();
// 스레드를 실행하려면 start() 를 호출하라.
// start() 내부에서 run()을 호출할 것이다.
// start() 호출한 후에 즉시 리턴한다.
System.out.printf("%s 클라이언트와의 대화를 별도의 스레드에서 담당합니다!\n", //
remoteAddr.getAddress());
}
// ss.close();
}
}
* stateless + Thread - CalcClient
// stateful 방식 - 다중 클라이언트의 요청 처리 시 문제점과 해결책
package com.eomcs.net.ex04.stateful3;
import java.io.DataOutputStream;
import java.net.Socket;
import java.util.Scanner;
public class CalcClient {
public static void main(String[] args) throws Exception {
Scanner keyScan = new Scanner(System.in);
Socket socket = new Socket("localhost", 8888);
Scanner in = new Scanner(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
while (true) {
System.out.print("연산자? ");
out.writeUTF(keyScan.nextLine());
System.out.print("값1? ");
out.writeInt(Integer.parseInt(keyScan.nextLine()));
String str = in.nextLine();
System.out.println(str);
if (str.equals("quit"))
break;
}
in.close();
out.close();
socket.close();
keyScan.close();
}
}
실행예시)
Server
Client
* Concurrent 예제1 - 코드 동시 실행
// main() 메서드를 실행하는 기본 실행 흐름에서 새로운 실행 흐름으로 분기하고 싶다면,
// Thread 클래스를 정의할 때 분기해서 실행할 코드를 담으면 된다.
// 그러면 두 개의 실행 흐름이 서로 왔다 갔다 하면서 실행된다.
//
// ## 멀티태스킹(multi-tasking)
// - 한 개의 CPU가 여러 코드를 동시(?)에 실행하는 것.
// - 실제는 일정한 시간을 쪼개 이 코드와 저 코드를 왔다갔다 하면서 실행한다.
// - 그럼에도 불구하고 외부에서 봤을 때는 명령어가 동시에 실행되는 것 처럼 보인다.
// - 왜? CPU 속도 워낙 빠르기 때문이다.
//
// ## CPU의 실행 시간을 쪼개서 배분하는 정책 : CPU Scheculing 또는 프로세스 스케줄링
// - CPU의 실행 시간을 쪼개 코드를 실행하는 방법이다.
// 1) Round-Robin 방식
// - Windows OS에서 사용하는 방식
// - CPU 실행 시간을 일정하게 쪼개서 각 프로세스에 분배하는 방식
// 2) Priority 방식
// - Unix, Linux 에서 사용하는 방식
// - 우선 순위가 높은 프로세스에 더 많은 실행 시간을 배정하는 방식
// - 문제점:
// - 우선 순위가 낮은 프로그램인 경우 CPU 시간을 배정 받지 못하는 문제가 발생했다.
// - 그래서 몇 년이 지나도록 실행되지 않는 경우가 나타났다.
// - 해결책?
// - CPU 시간을 배정 받지 못할 때 마다
// 즉 다른 프로세스에 밀릴 대 마다 우선 순위를 높여서
// 언젠가는 실행되게 만들었다.
// - 이런 방식을 "에이징(aging) 기법"이라 부른다.
//
// ## 멀티 태스킹을 구현하는 방법
// 1) 멀티 프로세싱
// - 프로세스(실행 중인 프로그램)를 복제하여 분기한다.
// - 그리고 분기된 프로세스를 실행시켜서 작업을 동시에 진행하게 한다.
// - 장점:
// - 분기하기가 쉽다. fork() 호출.
// - 즉 구현(프로그래밍)하기가 쉽다.
// - 단점:
// - 프로세스를 그대로 복제하기 때문에
// 프로세스가 사용하는 메모리도 그대로 복제된다.
// - 메모리 낭비가 심하다.
// - 복제된 프로세스는 독립적이기 때문에
// 실행 종료할 때도 일일이 종료해야 한다.
//
// 2) 멀티 스레딩
// - 특정 코드만 분리하여 실행한다.
// - 따라서 프로세스가 사용하는 메모리를 공유한다.
// - 장점:
// - 프로세스의 힙 메모리를 공유하기 때문에 메모리 낭비가 적다.
// - 모든 스레드는 프로세스에 종속되기 때문에 프로세스를 종료하면
// 스레드도 자동 종료된다.
// - 단점:
// - 프로세스 복제 방식에 비해 코드 구현이 복잡하다.
//
// ## 컨텍스트 스위칭(context switching)
// - CPU의 실행 시간을 쪼개 이 코드 저 코드를 실행할 때 마다
// 실행 위치 및 정보(context)를 저장하고 로딩하는 과정이 필요하다.
// - 이 과정을 '컨텍스트 스위칭'이라 부른다.
//
// ## 스레드(thread)
// - '실'이라는 뜻을 갖고 있다.
// - 한 실행 흐름을 가리킨다.
// - 하나의 실은 끊기지 않은 하나의 실행 흐름을 의미한다.
//
// ## 스레드 생성
// - 새 실을 만든다는 것이다.
// - 즉 새 실행 흐름을 시작하겠다는 의미다.
// - CPU는 스레드를 프로세스와 마찬가지로 동일한 자격을 부여하여
// 스케줄링에 참여시킨다.
// - 즉 프로세스에 종속된 스레드라고 취급하여
// 한 프로세스에 부여된 실행 시간을 다시 쪼개 스레드에 나눠주는 방식이 아니다.
// - 그냥 단독적인 프로세스처럼 동일한 실행 시간을 부여한다.
//
* Concurrent 예제2 - 실행 중인 스레드 알아내기