[JAVA] 스레드를 이용한 채팅서버

merci·2023년 1월 10일
0

JAVA

목록 보기
7/10

스레드

스레드의 개념을 간단히 정리 하자면..

  • 스레드는 메모리를 공유한다 ( 프로세스는 각자 할당 )

  • 따라서 메모리공간(힙)을 이용해 데이터를 주고 받고 작업을 처리할 수 있다.

  • 메모리를 공유함으로써 동기화 문제가 생길수 있다.
    ( synchronized 붙인 동기화 메소드를 이용하기도 한다 )
    하지만 동기화메소드로 인해서 데드락과같은 문제가 발생할 수 있어 주의가 필요하다.

  • 프로세스 생성에는 많은 자원이 소모 되므로 스레드를 이용한다.

  • 자바에서 멀티스레드는 기본적으로 라운드로빈방식을 이용한다
    -> 슬라이싱된 작업들중에 우선순위가 같으면 가장 빨리 끝나는 것부터 진행

  • cpu는 시분할 프로그래밍을 이용 -> Context Switching 을 따른다
    -> 어디까지 진행했는지 기억함(저장). 다음작업때 이어서 작업

  • 메인스레드는 작업큐를 이용해 라인들을 실행한다
    ( 메소드는 큐에 넣고 변수들은 스택에 넣은뒤 실행 )


동기화

동기화(syncronous)와 비동기화(Asyncronous)

  • 동기화 - 하나의 스레드만 있을때( main ) main스레드는 작업에 순서를 가진다
    ( 주의점 ! - 데이터 관점에서 동기화는 데이터가 일치하다는 뜻 )

  • 비동기 - 멀티스레드에는 작업에 순서가 없다. -> 동시에 진행
    (Context Switching을 이용해서 작업이 진행됨
    -> 매우 빠르게 처리하기 때문에 동시에 처리 되는 것처럼 보인다 )

하지만, 멀티스레드를 이용해도 속도가 더 느려질때가 있다. 그럼에도 사용하는 이유는

  • UX( 사용자경험 ) 를 개선하기 위해 -> 동시에 진행되는것처럼 보이면 사용자가 답답하지 않다.

  • CPU는 자신보다 느린 일처리( 통신 )를 대기해야 하기때문에
    ( 동기화 때문에 의미없이 대기하면 자원낭비 ) -> 대기할 시간에 다른 작업을 처리


간단한 예를 들어보면 프로그램을 설치할때 순서대로 next를 눌러서 설치하는 과정을 동기화라고 표현할수 있고, 여러 파일을 한번에 다운받는것을 비동기라고 표현할수 있다.




먼저 간단하게 서버와 클라이언트가 1:1로 채팅을 계속해서 칠수 있게 만들어 보자.

 // 클라이언트
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class App {
    Socket socket;
    BufferedReader br, keyboard;
    PrintWriter pw;

    public App() {
        try {
            socket = new Socket("192.168.200.175", 10000);

            pw = new PrintWriter(socket.getOutputStream(),true);
            br = new BufferedReader(
            new InputStreamReader(socket.getInputStream()));
            keyboard = new BufferedReader(new InputStreamReader(System.in));

            Thread t1 = new Thread(new Runnable() { // 익명함수
                @Override
                public void run() {
                    // 계속해서 메세지를 수신해서 콘솔에 출력
                    while (true) {
                        try {                            
                            String clientInput = br.readLine();
                            System.out.println(clientInput);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            t1.start();

            Thread t2 = new Thread(()->{  // 람다식
                @Override
                public void run() {
                    // 계속해서 입력받은 메세지 전송
                    while (true) {
                        String keyboardInput;
                        try {
                            keyboardInput = keyboard.readLine();
                            pw.println(keyboardInput);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            t2.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        new App();
    }
}
// 서버
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
// Point to Point, 점대점방식, 1:1 
public class App {
    ServerSocket serverSocket;
    Socket socket;
    BufferedReader br, keyboard;
    PrintWriter pw;

    public App() {
        try {
            serverSocket = new ServerSocket(10000);
            socket = serverSocket.accept();

            pw = new PrintWriter(socket.getOutputStream(), true);
            br = new BufferedReader(
            new InputStreamReader(socket.getInputStream()));
            keyboard = new BufferedReader(new InputStreamReader(System.in));

            // 계속해서 메세지를 수신해서 콘솔에 출력
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {                    
                    while (true) {
                        try {                            
                            String clientInput = br.readLine();
                            System.out.println(clientInput);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            t1.start();

            // 계속해서 입력받은 메세지 전송
            Thread t2 = new Thread(()->{
                @Override
                public void run() {                    
                    while (true) {
                        String keyboardInput;
                        try {
                            keyboardInput = keyboard.readLine();
                            pw.println(keyboardInput);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            t2.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        new App();
    }
}

서버와 클라이언트가 번갈아가지 않고 마음대로 1:1로 채팅을 할수 있다.
서버와 클라이언트 모두 2개의 데몬스레드를 가진다(수신,송신)




서버가 클라이언트들의 채팅을 중계만 한다면

// 서버 작성, 클라이언트는 위 코드 재사용
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Vector;

public class App1 {
    ServerSocket serverSocket; 
    Vector<SocketThread> vc; // 세션저장소로 이용.. 벡터는 동기화 기능 있음

    public App1() {
        try {
            serverSocket = new ServerSocket(10000); // 최초에 한번 실행
            vc = new Vector<>();
            // 메인스레드는 요청에 따른 소켓을 생성하기만 한다
            while (true) {
                Socket socket = serverSocket.accept();
                SocketThread st = new SocketThread(socket); 
                // 벡터에 넣기 위해 밖에서 객체 생성
                Thread t1 = new Thread(st); 
                t1.start();
                vc.add(st);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    class SocketThread implements Runnable {
        Socket socket; // 클라이언트 수만큼 필요
        BufferedReader br;
        PrintWriter pw;

        public SocketThread(Socket socket) {  // 생성자
            try {
                this.socket = socket;
                
                // 소켓을 생성할때마다 두개를 만든다
                pw = new PrintWriter(socket.getOutputStream(), true);
                br = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        @Override
        public void run() {
            try {
                while (true) {
                    for (SocketThread socketThread : vc) {
                        if (socketThread != this) { 
                        	// 모두에게 보내는데 입력한 사람에게는 보내면 안된다.
                            socketThread.pw.println(br.readLine());
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
    new App1();
    }
}

간단하게 작성해서 예외처리도 불완전하고 기능이 별로 없긴 하지만 구현이 되었다

  • while을 이용 -> 계속해서 입력기다림/ Polling(폴링) 방식 ( 자원 낭비 )
    서버가 벡터의 클라이언트들에게 입력된 내용을 Push 방식으로 준다.
    반대로 클라이언트가 서버에 요청하는것을 Pull 방식이라고 한다.

  • 소켓을 생성할때는 시간이 소요된다. 만약 많은 사용자가 서버에 접속하고 나간다면 소켓생성시간 때문에 서버가 느려지게된다.
    -> 서버시작시에 소켓풀을 만들어서 사용자의 요청을 처리후 연결을 끊고 다음 사용자의 요청을 처리하는 과정을 반복하면 전체적인 속도가 개선된다. ( http 는 stateless 이용 )

  • 이러한 풀을 이용하는 방식을 Pooling(풀링) 이라고 한다.
    ( 한정된 자원 재활용, 오버헤드 줄임 )



profile
작은것부터

0개의 댓글