티스토리 뷰
1. 스레드 병렬처리의 필요성
- 블로킹( 대기상태 )이 되는 메소드 :
- (1) accept( ) (2) connect( ) (3) read( ) (4) write( )
- 이때, 스레드가 블로킹이 되면, 다른 작업을 수행하지 못하게 된다.
- 그렇기에, 입/출력할 동안 다른 클라이언트의 연결 요청 수락이 불가능해지며
- 입/출력할 동안 다른 클라이언트의 입/출력이 불가능해진다.
- 그렇기에 accept( ), connect( ), read( ), write( )는 별도의 작업 스레드로 생성해야 된다. ( 이때 서버는 중개의 역할만을 하는 것이 좋다. )
2. 데이터 베이스
- Database는 OS와 APP 사이에 존재하기에, Middleware라고 부른다.
3. @Log4j2 – 로그의 심각도
- log.trace("TRACE"); <- 가장 낮은 단계
- log.debug("DEBUG");
- log.info("INFO");
- log.warn("WARN");
- log.error("ERROR");
- log.fatal("FATAL"); <- 가장 높은 단계
더보기
01. Multi Chat - Server구현
package tcp_practice;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import lombok.extern.log4j.Log4j2;
@Log4j2 // 롬복은 static에서만 사용이 가능하다.
// 그렇기에 lombok annotation을 중첩 클래스에 적용하기 위해서는 static을 붙여줘야 한다.
public class S10_MultiChatServer {
private static final int port = 7777;
private static final Charset charset = Charset.forName("UTF-8"); // 다국어 처리를 위해 Charset을 UTF-8로 지정
// private static final Charset charset = Charset.defaultCharset(); // XX
// Multicast 기반의 체팅을 구현하기 위해서는
// 서버의 역할은 오로지 중개의 역할을 잘 해주면 된다!
// 즉, 서버가 직접 자신의 메시지를 보내거나 받는 것이 아니라
// 이 서버에 연결된 클라이언트들 끼리, 보내고 / 받는 메시지 처리만 중앙에서 잘 처리하면 된다.
// 그래서, 서버에 연결된 모든 클라이언트 정보를 담은 컬렉션이 필요하다.
// 그렇기 때문에 이 컬렉션을 Map <K,V> 로 하기로 하였다.
private static Map <String,Socket> clients; // (***)
// =================================================================================
public S10_MultiChatServer () {
log.trace("Default constructor invoked.");
clients = new HashMap<>(); // HashMap 객체 생성
// HashMap은 멀티스레드 환경에서 Thread-Unsafe하기 때문에, 이를 다시
// Thread-safe하게 조작할 수 있도록 만들어줘야 하며, 이를 해주는 것이 바로 아래의 제네릭 정적 메소드이다.
clients = Collections.<String,Socket> synchronizedMap (clients);
log.debug("synchronized client list created. type : {}", clients.getClass().getName()); // Log4j2
} //constructor
// =================================================================================
private void start () { // 서버를 시작하는 메소드
log.trace("start() invoked.");
try( ServerSocket serverSocket = new ServerSocket(port); ) { // 서버소켓을 특정 포트번호인 7777와 묶어서 만든다. ( 터널 형성 )
log.debug("Server started."); //서버의 터널이 형성되었음을 알려줌.
while(true) { // 서버는 계속 돌아가야하기 때문에, while(true)로 구현한다.
log.debug("Listen on {} ......", serverSocket); // 요청받을 준비가 되었음을 알려준다.
Socket sock = serverSocket.accept(); // 연결요청을 받는다.
log.debug("Client connected From {}",sock); //어디 클라이언트와 연결되었는지 알려준다.
// ============================================================
// 별도의 스레드로 다수의 클라이언트가 보낸 메시지를 수신할 수 있는 Task를 처리하도록 함.
// 새로운 클라이언트가 연결될때 마다 반복하게 된다.
ServerReceiver receiver = new ServerReceiver(sock); // Worker 스레드 객체
receiver.start(); // Worker 스레드 시작
} // while
} catch ( IOException e) {
e.printStackTrace();
} finally {
log.info("Server finished.");
}// try with : serverSocket
} // start
// =================================================================================
// ServerReceiver 클래스 생성 ( Worker 스레드 ) - 중첩 class
// 중첩 클래스는 위 클래스와 밀접한 관계에 있고, 위 클래스 내에서만 사용이 될 때 사용한다.
// 메소드 블록 안에서 선언된 클래스는 "로컬Class"라고 부른다.
// 이 worker Thread는 새로이 연결된 클라이언트 Socket당 ServerReceiver를 하나씩 생성한다.
@Log4j2
static class ServerReceiver extends Thread { // 서버에 받은 것을 그대로 넘기는 클래스 ( 진짜 중개역할을 하는 서버측 스레드 ) (***)
final Socket sock; // Socket을 구현해야 한다.(****)
final String clientKey; // 새로운 클라이언트의 닉네임
// =================================================================================
ServerReceiver( Socket sock ){
log.trace("Construcotr ({}) invoked.",sock);
this.sock = sock;
this.clientKey = sock.getRemoteSocketAddress().toString(); // 어느 IP주소와 Port번호인지
// 서버가 중개역할에 충실하려면, 새롭게 연결된 클라이언트 Socket을 컬랙션으로 관리하게 하고,
// 그 후에 publish하기 위해서 컬렉션에 저장해야 한다.
clients.put(clientKey, sock);
log.debug("Clents : {}", clients);
// =================================================================================
try {
String WELCOME = String.format("<<< Client Key : %s Entered. Welcome!!", this.clientKey); // Welcome 메세지
// 현재 연결된 모든 클라이언트에게 동일 메세지를 전송해주는 메소드 호출(***)
publish("SERVER",WELCOME,true);
} catch (IOException e) { // 오류가 발생한다면...
// 통신( 입/출력 )에 문제가 발생한 클라이언트는 Map에서 삭제처리
clients.remove(this.clientKey);
log.debug("clients : {}", clients);
try {
String BYE = String.format(">>> Client Key : %s Exited. Bye!!!", this.clientKey);
// 현재 연결된 모든 클라이언트에게 동일한 메세지를 전송해주는 메소드 호출
publish("SERVER",BYE,true); // (***)
} catch(IOException e1 ) { ;; } // inner try - finnally
} // try catch - outter
} // ServerReceiver - constructor
// =================================================================================
@Override
public void run() {
log.trace("run() invoked.");
try( this.sock; ){
InputStream is = this.sock.getInputStream(); // 입력스트림
// 이 보조 스트림은 기본 스트림에 연결해서 사용하는 보조 스트림이 아니라.
// JVM 메모리에 바이트 데이터를 축척하기 위해서 사용하는 보조스트림이다. ( 마치 금고처럼 ) (***)
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try( is; baos; ){ // 보조스트림을 먼저 닫아야 한다. 닫히는 순서는 왼쪽에서 오른쪽으로 진행된다.
// =========================================
// 1. Receive Message From All Clients (***)
// =========================================
int CR =13, LF = 10; // Enter Key Code
int ch; // ch = char(캐릭터)
String RECV = null; // 받는 메시지
while( (ch = is.read()) != -1) { // EOF
if( ( ch != CR) & ( ch != LF)) {
baos.write(ch); // 엔터키를 치기 전까지는 계속 입력을 받는다.
} else {
if( ch == LF) { // 엔터키를 쳤다면
RECV = baos.toString(charset); // baos에 저장된 문자열을 charset을 통해 UTF-8로 받아들인다.
log.info("RECV from {} : {} ", this.clientKey, RECV); // 받은 문자열 줄력
baos.reset(); // baos에서 저장되었던 문자열을 지우고, 새로운 문자를 받을 준비를 한다.
// =========================================
// 2. Publish Message To All Clients (***)
// =========================================
publish(this.sock.getRemoteSocketAddress().toString(), RECV, false);
} // inner if
}// if - else
} // while : EOF
} // try : is, boas
} catch ( IOException e) {
e.printStackTrace();
} finally { // 어떤 식으로든 끊어져 더 이상 메시지를 교환할 수 없는 클라이언트는 Map에서 제거하자!
log.info("Client {} Disconnected", this.sock);
clients.remove(this.clientKey, this.sock);
log.debug("clients: {}", clients);
try {
String BYE = String.format(">>> Client Key: %s Exited. Bye !!!", this.clientKey);
publish("SERVER", BYE, true); // (*****)
} catch(IOException e) { ;; } // try catch
} // try : this.sock
} // run
} // end class - ServerReceiver
// =================================================================================
// 현재 연결되어 있는 Connected All Clients에게 메시지 전달
private static void publish (String fromKey, String message, boolean toSelf) throws IOException {
// 이렇게 예외를 지정하게 되면, 하나의 클라이언트에서 예외가 발생하면 다른 클라이언트도 멈춰보리기에 따로 처리해야 한다.
log.trace("publish({}, {}) invoked.", fromKey, message);
Set<String> keys = clients.keySet(); // HashMap에서 key값에 keys 값을 입력한다.
for(String key : keys) { // Map 객체의 Key를 traverse하는 중
log.debug("publish to the client key: {}", key);
// 메세지를 받은 클라이언트와 key가 동일하지 않거나, to-self 매개변수가 true일때만( 메세지를 보낸 자신한테도 보낼 것인가)
// 받은 메세지를 전송해라! false일 때에는 자신한테는 메세지를 보내지 않게 만들어라!
if( !key.equals(fromKey) | toSelf ) {
int CR=13, LF=10; // Enter Key Code
Socket sock = clients.get(key); // HashMap에서 key값을 가져온다.
OutputStream os = sock.getOutputStream(); // 출력 스트림
os.write((fromKey+" : "+message).getBytes(charset)); // 보낸 이 : 메세지를 UTF-8로 출력한다.
os.write(CR);
os.write(LF); // 메세지를 다 읽어들이면 엔터키 치게 한다.
os.flush(); // 출력 스트림이기에 flush는 필수이다.
// log.info("SENT: {}", message);
log.info("SENT from {} : {}", fromKey, message); // 누구로부터 어떤 메세지가 왔는지 표시
} else {
log.debug("Excluding client itself key: {} to publish.", fromKey);
} // if-else
} // enhanced for
} // publish
// =================================================================================
public static void main(String[] args) {
log.trace("main({}) invoked", Arrays.toString(args));
// Charset.availableCharsets().forEach((key, cs) -> {
// log.debug("{}", key);
// });
new S10_MultiChatServer().start();
} // main
// =================================================================================
// 출력
// 11:00:50.179 TRACE --- [main ] t.S10_MultiChatServer.main - main([]) invoked
// 11:00:50.182 TRACE --- [main ] t.S10_MultiChatServer.<init> - Default constructor invoked.
// 11:00:50.183 DEBUG --- [main ] t.S10_MultiChatServer.<init> - synchronized client list created. type : java.util.Collections$SynchronizedMap
// 11:00:50.183 TRACE --- [main ] t.S10_MultiChatServer.start - start() invoked.
// 11:00:50.192 DEBUG --- [main ] t.S10_MultiChatServer.start - Server started.
// 11:00:50.192 DEBUG --- [main ] t.S10_MultiChatServer.start - Listen on ServerSocket[addr=0.0.0.0/0.0.0.0,localport=7777] ......
// =================================================================================
} // end class
더보기
02. Multi Chat - Client구현
package tcp_practice;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.Scanner;
import lombok.extern.log4j.Log4j2;
@Log4j2
public class S10_MultiChatClient {
// 채팅 서버의 연결정보 선언 ( 상수 )
private static final String host = "localhost"; // Loopback address = 127.0.0.1
private static final int port = 7777; // Port Number
// =================================================================
public static void main(String[] args) throws IOException, InterruptedException {
log.trace("main({}) invoked.", Arrays.toString(args));
try ( Socket sock = new Socket(host, port); ) { // 채팅서버 연결
log.info("Connected to the server addr: {}, port: {}", host, port);
Thread R = new Receiver(sock.getInputStream()); // 메세지 수신 스레드 생성 및 시작
R.start();
Thread S = new Sender(sock.getOutputStream()); // 메세지 송신 스레드 생성 및 시작
S.start();
// =================================================================
R.join(); // R ( 수신 스레드 ) 스레드가 종료될 때까지 기다린다.
S.join(); // S ( 송신 스레드 ) 스레드가 종료될 때까지 기다린다.
// =================================================================
} finally {
log.info("Disconnected.");
} // try-with-resources : sock
} // main
// =================================================================
// "멤버 클래스"라고 부른다. ( - Sender클래스는 default인 상태이다. )
// 맴버 클래스는 이 클래스와 밀접한 관계가 있고, 이 클래스 블록 안에서만 사용할 때 사용된다.
static class Sender extends Thread { // 메세지 송신
private final OutputStream os;
// =================================================================
Sender(OutputStream os) {
log.trace("constructor({}) invoked.", os);
this.os = os;
} // constructor
// =================================================================
@Override
public void run() {
log.trace("run() invoked.");
Scanner scanner = new Scanner(System.in);
try ( this.os; scanner; ) { // 스캐너부터 닫아줘야 한다.
log.debug("Sender started.");
int CR=13, LF=10; // Enter Key Code
String line = null;
while(( line = scanner.nextLine() ) != null) { // EOF ( 스캐너에서 EOF는 null이다. )
// log.info("line: {}", line);
String message = line.trim(); // 가운데에 있는 공백 외의 공백을 제거해준다. ( 양쪽 끝단에 있는 공백을 제거 )
os.write(message.getBytes("UTF-8")); // 인코딩 -> String을 Byte로 변환해준다.
// Sent CRLF (***) - 엔터키를 활용해 메세지를 구분해줘야 한다. ( 메세지의 끝임을 강제로 정했다. )
os.write(CR);
os.write(LF);
os.flush(); // 출력 스트림 강제 flush해주기
// log.info("SENT: {}", message);
log.info(message); // 메세지를 출력한다.
} // while
} catch(IOException | IllegalStateException | NoSuchElementException e) {
e.printStackTrace();
} finally {
log.debug("Sender stopped.");
} // try-with-resources
} // run
} // end class : Sender
// =================================================================
static class Receiver extends Thread { // 메세지 수신
private final InputStream is;
Receiver(InputStream is) {
log.trace("constructor({}) invoked.", is);
this.is = is;
} // constructor
// =================================================================
@Override
public void run() {
log.trace("run() invoked."); // run 메소드가 실행되었다.
ByteArrayOutputStream baos = new ByteArrayOutputStream(); // (***)
// =================================================================
try (this.is; baos;) { // 보조 스트림부터 닫아주고 입력스트림을 닫아준다.
log.debug("Receiver started.");
// =================================================================
int ch; // char ( 캐릭터 )
int CR=13, LF=10; // Enter Key Code
// =================================================================
while((ch=is.read()) != -1) { // EOF
// log.info("ch: {}", ch);
if(ch != CR && ch != LF) {
baos.write(ch);
} else {
// log.info("\t+ excluded: {}", ch);
if(ch == LF) {
String recv = baos.toString("UTF-8");
// log.info("RECV: {}", recv);
log.info(recv);
baos.reset();
} // inner - if
} // Outer if-else
} // while
// =================================================================
} catch(IOException e) {
e.printStackTrace();
} finally {
log.debug("Receiver stopped.");
} // try-with-resources
} // run
// =================================================================
} // end class : Receiver
} // end class
더보기
03. 과제 - Annagrams
package hackers_rank_practice;
import java.util.Arrays;
import java.util.Scanner;
public class Anagrams {
static boolean isAnagram(String a, String b) {
// 소문자로 변환
String lowerStringA = a.toLowerCase();
String lowerStringB = b.toLowerCase();
// String A 한글자씩 배열로 생성 - arr1
char [] arr1 = lowerStringA.toCharArray();
for ( int i = 0; i < arr1.length; i++){
arr1[i] = lowerStringB.charAt(i);
} // enhance for - A
// String B 한글자씩 배열로 생성 - arr2
char [] arr2 = lowerStringB.toCharArray();
for ( int i = 0; i < arr2.length; i++){
arr2[i] = lowerStringB.charAt(i);
} // enhance for - B
// 오름차순으로 배열 정리
Arrays.sort(arr1);
Arrays.sort(arr2);
if ( Arrays.toString(arr1).equals( Arrays.toString(arr2) ) ){ // 꼭 Arrays.toString하기!! (**)
return true;
} else {
return false;
} // if
} // isAnagram
public static void main(String[] args) {
// 스캐너
Scanner scan = new Scanner(System.in);
String a = scan.next();
String b = scan.next();
scan.close();
// isAnagram 메소드를 활용하여 블린 타입으로 판단하여 결과 반환
boolean ret = isAnagram(a, b);
System.out.println( (ret) ? "Anagrams" : "Not Anagrams" );
} // main
} //end class
728x90
'KH 정보교육원 [ Java ]' 카테고리의 다른 글
KH 33일차 - 자바 표준 API (0) | 2022.04.13 |
---|---|
KH 32일차 - 데이터 베이스 ( Oracle SQL Developer ) (0) | 2022.04.12 |
KH 30일차 - TCP 네트워킹 및 멀티 스레드 (0) | 2022.04.08 |
KH 29일차 - TCP 네트워킹 (0) | 2022.04.07 |
KH 28일차 - 네트워킹 (0) | 2022.04.06 |
댓글