본문 바로가기

프로그래밍/자바

[JAVA] 멀티스레드로 채팅 구현하기

대학생 때 수강한 강의에서 멀티 스레드 기반 채팅 게임을 구현했었는데,

이때는 자바를 잘 몰랐고 OS 지식이 조금 미흡했어서 정확히 무엇을 구현하는건지 잘 모르고 했었다.

때문에 이번에 CS 지식도 복습할 겸, 다시 구현해봤다.

 

혹시라도 틀린 내용 있다면, 댓글로 알려주시면 감사하겠습니다. 🙇‍♀️

 

 

https://better-go.tistory.com/45

 

[OS] 프로세스와 스레드

프로그램 : 컴퓨터에서 어떤 작업을 위해 실행할 수 있는 파일 프로세스 프로그램을 실행 시켜 동작하는 상태 메모리에 적재되고, CPU 자원을 할당 받음 코드, 데이터, 스택, 힙 영역으로 구성되

better-go.tistory.com

 

위 게시글을 읽고 프로세스와 스레드의 차이점을 이해하고 구현해보면 좋을 듯 싶다. 😀

 

소켓 통신과 스레드를 다시 공부해봤지만.. 나에겐 난해한 개념이어서 완전하게 정복하진 못했지만 이번 복습을 통해서 이전보단 확실히 코드 이해하기도 쉬웠고, 어떤 원리로 작동하는지 깨달을 수 있었다.

 

실시간 채팅을 위해선 서버와 클라이언트가 연결되어 서로 통신이 가능해야 하는데 소켓 통신을 이용하여 이 부분을 구현 할 수 있다.

구현한 채팅 프로그램은 다중 사용자 간 채팅 프로그램이기 때문에, 멀티 스레드를 사용하였다.

 

소켓 통신은 다음과 같은 흐름으로 동작한다.

 

 

서버 코드

 

스레드 개수를 제한하고 싶어 ThreadPool 을 이용하였는데,

ThreadPool 에 대한 이해도가 부족해 실행되지 않은 스레드(클라이언트)에도 채팅이 broadcasting 되어 sendToAll 함수에서 출력을 스레드 개수만큼만 하게 구현해줬다.. (이 부분은 스레드가 실행되지 않더라도 생성은 되기 때문에, 생성자가 실행되면서 clientWriter 객체에 들어가면서 생기는 오류로 생각된다.)

이 부분은 다시 생각해봐야 할 것 같다.

 

/**
 * [Server]
 * 클라이언트가 접속 했는지 안했는지 확인 용도
 * 클라이언트가 접속 시 Socket.accept()를 하여 요청을 받는다.
 * 스레드풀을 이용하여 접속 가능한 인원을 제한시킨다.
 *
 * !! BufferedWriter 가 아닌 PrintWriter 사용 이유 !!
 *  PrintWriter 의 경우 print(), println(), printf() 와 같은 다양한 출력함수 제공 => 파일 출력 간편함
 *
 * !! 동기화에서의 Vector vs ArrayList !! L.63
 *  ArrayList 사용하는 것이 성능적으로 더 좋음 (Vector는 Obsolete, Deprecated)
 *  => Collections.synchronizedList()를 사용하자
 * 참고 : https://inpa.tistory.com/entry/JCF-%F0%9F%A7%B1-ArrayList-vs-Vector-%EB%8F%99%EA%B8%B0%ED%99%94-%EC%B0%A8%EC%9D%B4-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0
 *
 */

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class Server {
    private static final int PORT_NUM = 8080;
    private static final int THREAD_CNT = 2;


    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    public void start() {
        ServerSocket serverSocket = null;
        Socket socket = null;
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_CNT); // 스레드 풀, 최대 개수 제한

        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
        try {
            serverSocket = new ServerSocket(PORT_NUM);
            while (true) {
                System.out.println("[사용자 접속 대기중...]");
                socket = serverSocket.accept();

                // client가 접속하면 새로운 스레드 생성
                threadPoolExecutor.execute(new ReceievedThreadByClient(socket));
                // System.out.println(threadPoolExecutor);
            }
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                    executorService.shutdown();
                    System.out.println("[서버 종료]");
                } catch (IOException e) {
                    e.printStackTrace();
                    System.out.println("[서버 소켓 통신 에러]");
                }
            }
        }
    }
}

class ReceievedThreadByClient extends Thread {
    static List<PrintWriter> clientWriters = Collections.synchronizedList(new ArrayList<>()); // 접속한 클라이언트 writer 객체 리스트
    Socket socket = null;
    BufferedReader fromClient = null;
    PrintWriter currentClientWriter = null;

    public ReceievedThreadByClient(Socket socket) {
        this.socket = socket;

        // client 소켓 정보로 초기화
        try {
            fromClient = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            currentClientWriter = new PrintWriter(socket.getOutputStream());
            clientWriters.add(currentClientWriter);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
//        System.out.println(clientWriters);
    }

    @Override
    public void run() {
        String name = "";
        try {
            // 첫 라인은 사용자 이름을 받음
            name = fromClient.readLine();
            System.out.println("[" + name + " 연결 생성]");
            sendToAll("[Notice] " + name + "님이 접속하셨습니다.");

            while (fromClient != null) {
                String clientMsg = fromClient.readLine();
                if ("exit".equals(clientMsg)) break;
                sendToAll(name + " : " + clientMsg);
            }
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("[" + name + " 접속 끊김]");
            throw new RuntimeException(e);
        } finally {
            sendToAll("[Notice] " + name + "님이 나가셨습니다.");
            clientWriters.remove(currentClientWriter);
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }
        System.out.println("[" + name + " 연결 종료]");
    }

    private void sendToAll(String msg) {
        int num = 0;
        final int THREAD_CNT = 2;
        for (PrintWriter out : clientWriters) {
            out.println(msg);
            out.flush();
            num++;
            if(num == THREAD_CNT) break;
        }
    }
}

 

코드를 살펴보면 서버 소켓을 열고 클라이언트로부터 접속이 들어올때까지 대기하다가 들어오면 accept 하여 스레드 풀에 넣고 스레드를 실행시켜줬다.

스레드가 실행되면 클라이언트로부터 전달받은 메시지를 sendToAll 함수를 이용하여 broadcast 해줬다.

 

클라이언트 코드
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }

    public void start() {
        Socket socket = null;
        BufferedReader in = null;
        Scanner sc = new Scanner(System.in);

        try {
            socket = new Socket("localhost", 8080);
            System.out.println("[Notice] 서버와 연결 되었습니다.");

            System.out.println("[Notice] 이름을 입력해주세요.");
            String name = sc.nextLine();

            System.out.println("[Notice] 접속 대기중..");

            // 사용자로부터 받은 입력을 서버로 전송하는 스레드 실행
            Thread sendThreadToServer = new SendThreadToServer(socket, name);
            sendThreadToServer.start();

            // 서버로부터 전달받은 메시지 출력, 본인이 나갔을 경우 종료
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            while (in != null) {
                String receivedMsg = in.readLine();
                if (("[Notice] " + name + "님이 나가셨습니다.").equals(receivedMsg)) break;
                System.out.println(receivedMsg);
            }
        } catch (UnknownHostException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }
    }

    class SendThreadToServer extends Thread {
        private Socket socket = null;
        private String name;
        Scanner sc = new Scanner(System.in);

        public SendThreadToServer(Socket socket, String name) {
            this.socket = socket;
            this.name = name;
        }

        @Override
        public void run() {
            try {
                PrintStream out = new PrintStream(socket.getOutputStream()); // 서버 소켓의 outputstream 객체 가져오기

                // 첫 라인은 사용자 이름 전송
                out.println(this.name);
                out.flush();

                // 사용자로부터 입력받은 메시지 전송, exit 입력받으면 종료
                while (true) {
                    String msg = sc.nextLine();
                    out.println(msg);
                    out.flush();

                    if ("exit".equals(msg)) break;
                }

            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }
    }
}

클라이언트 코드에서는 서버 소켓과 연결하여, 성공적으로 연결되면 스레드를 실행시켜줬다.

스레드를 실행시키면 사용자가 입력한 메시지를 서버로 전송 시킨다.

그러고 나서 서버로부터 받아온 메시지를 콘솔에 출력해줬다.

 

실행 결과

[Server]
[Client - user1]
[Client - user2]
[Client - user3], 대기중인 모습, 채팅 내역이 보이지 않는다.

 

 

Code Reference

https://kadosholy.tistory.com/126

Recent Posts
Popular Posts