JAVA

자바 61강. 다대다 통신(2)

JJJAEOoni 2022. 2. 16. 14:04
반응형

클라이언트 소켓과 서버 소켓이

연결된다 -> 바인딩했다!

 

2000번 포트에서 리스너로 대기하고 있다가

클라이언트가 바인딩하면

새로운 소켓을 만든 후 연결해준다.

 

바인드 되는 순간 세션이 만들어지는 것은 아니다.

소켓이 생성되고 선이 만들어지면 세션이 만들어진다.

 

그리고 새로 만들어진 소켓을

고객 리스트에 넣어둔다.

 

보관해두지 않으면

while을 돌 때마다 스택이 날아가버리기 때문이다.

 

전역 변수로 소켓을 선언하면

하나의 소켓이 초기화되어

하나밖에 사용 못하고,

 

지역변수로 선언하면

선언해둔 소켓이

계속해서 날아가버리고

다시 선언되기 때문에

컬렉션을 만들어서 heap에 담아두었다. 

 

-> 까지 메인 스레드가 진행한다.

 

새로운 클래스를 만들어서 소켓을 생성하고

main에서 소켓을 new 하면

같은 클래스이지만 2번 new 되어

2개의 소켓이 heap에 띄워진다.

 

이때 소켓을 리스트에 넣는 게 아닌

클래스를 리스트에 담아두기 때문에

스레드와 소켓 정보 모두 담긴 클래스를

리스트가 가지고 있다.

 

클래스가 아닌 익명 클래스로 만들면

변수 관리에 어렵기 때문에

새로운 클래스를 만들어준 것이다.

 

다른 곳에서 재사용할 일이 없고

내부에서만 쓰이는 클래스이기 때문에

내부 클래스로 만들어주었다.

 


 

만약 클라이언트 1과 2가 동시에 접근하여

컬렉션으로 넣으면

데이터가 섞일 수 있기 때문에

ArrayList가 아닌 Vector를 사용한다.

 

Vector는 동기화가 처리된 ArrayList인 것이다.

일의 순서가 있다는 것이다.

 

for-each문

 

기존 for문은 내가 10바퀴, 20바퀴

원하는 만큼 돌았었는데

for-each는 컬렉션의 크기만큼 돌아간다.

// for문
for (int i = 0; i < 고객리스트.size(); i++) {
	고객리스트.get(i).writer.write(inputData + "\n");
	고객리스트.get(i).writer.flush();
}
// for-each문
for (고객전담스레드 t : 고객리스트) { // 컬렉션타입 : 컬렉션
	t.writer.write(inputData + "\n");
	t.writer.flush();
}

덜도 말고 더도 말고 딱 컬렉션의 크기만큼 돌 때는

for-each를 사용해주는 게 깔끔하다.

 


고객 리스트들은 DB에 INSERT 하여 관리한다.

보통 오류 내용은 파일에 log로 저장하여 관리한다.

 

하지만 오류 내용 또한 DB에 저장해놓으면 관리하기 쉽다.

 

근데 오류가 발생할 때마다 INSERT를 하면

I/O가 너무 많이 일어나게 되니까,

100개 정도 모아놨다가 한 번에 INSERT 하면 된다.

 

이걸 bulk collector라고 한다.

 


 

연결이 끊어진 클라이언트는

더 이상 사용하지 않기 때문에

가비지 컬렉션의 대상이 된다.

 

하지만 가비지 컬렉션은

원래 필요 없어질 때마다 삭제되지 않고,

어느 정도 모아놓았다가 처리하는데

 

가비지 컬렉션을 하기 전에 모아놓는 연산보다

고객 리스트에서 클라이언트를 삭제하고

가비지 컬렉션이 되기 전에

통신을 유지하고 있는 게

메모리의 과부하가 더 크기 때문에

연결을 해제하자마자

강제로 가비지 컬렉션을 해주는 게 좋다.

 

가비지 컬렉션은 cpu와 RAM만 일하지만

통신을 유지하고 있는 것은 

하드디스크에 접근하여

I/O가 발생하기 때문에

과부하가 더 심한 것이다.

 

꼭 강제로 날리지 않아도

나중에 알아서 없어지긴 하지만

클라이언트 수가 많아질수록

가비지 컬렉션을 빨리 해줘야

메모리의 효율이 좋다.

 

그런데 heap에 떠있는 고객 전담 스레드가

가비지 컬렉션 대상이 되어도

컬렉션에서 고객의 주소를 참조하고 있기 때문에

가비지 컬렉션 처리가 되지 않는다.

 

그래서 직접 제거해주어야 한다.

reader.close();
writer.close();
socket.close();

 

package site.metacoding.char_v2;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

public class MyServerSocket {

    // 리스너(연결받기) -> 메인 스레드
    ServerSocket serverSocket;
    List<고객전담스레드> 고객리스트;
    boolean isLogin = true;

    // 서버는 메시지 여러명에게 받아서 보내기(순차적)
    // 새로운 스레드, 클라이언트 수마다
    // 채팅서버는 메시지(요청)를 받을 때만 동작! -> pull 서버

    public MyServerSocket() {
        while (isLogin) {
            try {
                serverSocket = new ServerSocket(2000);
                고객리스트 = new Vector<>(); // 동기화가 처리된 ArrayList

                // while 돌리기
                // 여러사람이 요청할 때마다 소켓이 새로 생성되어야하기 때문에 전역변수로 생성 X

                Socket socket = serverSocket.accept(); // 대기 -> main 스레드가 하는 일
                System.out.println("클라이언트 연결됨");

                // while의 stack이 종료되면 t의 값이 가비지컬렉션 되기 때문에
                // 고객 socket을 기억하기 위해 전역 Vector에 보관하기
                고객전담스레드 t = new 고객전담스레드(socket);
                고객리스트.add(t); // 클라이언트 각각의 socket을 가지고있음
                System.out.println("고객리스트 크기 : " + 고객리스트.size());

                new Thread(t).start();
            } catch (Exception e) {
                System.out.println("오류내용 : " + e.getMessage());
            }
        }
    }

    // 내부 클래스
    class 고객전담스레드 implements Runnable {

        // 소켓 보관 컬렉션
        Socket socket;
        BufferedReader reader;
        BufferedWriter writer;

        public 고객전담스레드(Socket socket) {
            this.socket = socket;
            // new될 때 양끝단에 버퍼달림
            try {
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            while (true) {
                String inputData;
                try {
                    inputData = reader.readLine();
                    System.out.println("From 클라이언트 : " + inputData);

                    // 메시지 받았으니까 List<고객전담스레드> 고객리스트 <== 여기에 담긴
                    // 모든 클라이언트에게 메시지 전송(컬렉션 크기만큼 for문 돌려서!!)
                    // for(int i = 0; i<고객리스트.size(); i++) {
                    // 고객리스트.get(i).writer.write(inputData + "\n");
                    // 고객리스트.get(i).writer.flush();
                    // }

                    // 컬렉션의 0번째 데이터부터 t에 넣는것
                    // 자신이 보낸 메세지는 자신에게 돌아오지 않기 if문
                    for (고객전담스레드 t : 고객리스트) { // 컬렉션타입 : 컬렉션
                        if(t != this) {
                            t.writer.write(inputData + "\n");
                            t.writer.flush();
                        }
                    }

                } catch (Exception e) {
                    // 클라이언트로부터 메세지를 읽는데, 클라이언트가 연결을 해지하면
                    // readline에서 대기하다가 Stream이 끊겨서 catch로 넘어오고
                    // while은 catch만 계속 반복해서 출력된다.
                    System.out.println("오류내용 : " + e.getMessage());
                    // 오류내용 반복출력을 막기위한 상태변수 사용
                    isLogin = false;
                    // 리스트에서 참조중이라서 사라지지 않으니까 heap에 떠있는 자신을 날림
                    고객리스트.remove(this);

                    try {
                        reader.close();
                        writer.close();
                        socket.close();
                    } catch (Exception e1) {

                    }
                }

            }
        }
    }

    public static void main(String[] args) {
        new MyServerSocket();
    }
}
package site.metacoding.char_v3;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;

public class MyClientSocket {

    Socket socket;

    // write 스레드
    Scanner sc;
    BufferedWriter writer;

    // read 스레드
    BufferedReader reader;

    public MyClientSocket() {
        try {
            // localhost = 127.0.0.1 루프백 주소
            // IP = 192.168.0.132
            socket = new Socket("192.168.0.132", 2000); // 연결

            sc = new Scanner(System.in);
            writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            // 새로운 스레드(읽기 전용)
            new Thread(new 읽기전담스레드()).start();

            // 메인 스레드(쓰기 전용)
            while (true) {
                String keyboardInputData = sc.nextLine();

                // 중계자(서버 소켓)에게만 write하면 됨
                // 끝에 "\n" 필수 -> 이거 안쓰려면 PrintWrite 쓰면 됨
                writer.write(keyboardInputData + "\n"); // 내 버퍼에 담기
                writer.flush(); // 버퍼에 담긴 데이터 Stream으로 흘려보내기
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 내부 클래스로 만들면 좋은점
    // MyClientSocket의 전역변수를 모두 new 없이 사용가능
    class 읽기전담스레드 implements Runnable {

        @Override
        public void run() {
            try {
                while (true) {
                    String inputData = reader.readLine();
                    System.out.println("받은 메시지 : " + inputData);
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {
        new MyClientSocket();
    }
}

 

 

 

[출처]

 

https://cafe.naver.com/metacoding

 

메타코딩 : 네이버 카페

코린이들의 궁금증

cafe.naver.com

 

메타 코딩 유튜브

https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9

 

메타코딩

문의사항 : getinthere@naver.com 인스타그램 : https://www.instagram.com/meta4pm 깃헙 : https://github.com/codingspecialist 유료강좌 : https://www.easyupclass.com

www.youtube.com

 

반응형